diff --git a/README.md b/README.md index 01b039f..6b1374d 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,22 @@ Core is a Web3 Framework, written in Go using Wails.io to replace Electron and the bloat of browsers that, at their core, still live in their mum's basement. -More to come, follow us on Discord http://discord.dappco.re +- Discord: http://discord.dappco.re +- Repo: https://github.com/Snider/Core +## Vision -Repo: https://github.com/Snider/Core +Core is an **opinionated Web3 desktop application framework** providing: -## Quick start +1. **Service-Oriented Architecture** - Pluggable services with dependency injection +2. **Encrypted Workspaces** - Each workspace gets its own PGP keypair, files are obfuscated +3. **Cross-Platform Storage** - Abstract storage backends (local, SFTP, WebDAV) behind a `Medium` interface +4. **Multi-Brand Support** - Same codebase powers different "hub" apps (AdminHub, ServerHub, GatewayHub, DeveloperHub, ClientHub) +5. **Built-in Crypto** - PGP encryption/signing, hashing, checksums as first-class citizens + +**Mental model:** A secure, encrypted workspace manager where each "workspace" is a cryptographically isolated environment. The framework handles windows, menus, trays, config, and i18n. + +## Quick Start ```go import core "github.com/Snider/Core" @@ -17,119 +27,322 @@ app := core.New( ) ``` -## Development Workflow - -This project follows a Test-Driven Development (TDD) approach. We use [Task](https://taskfile.dev/) for task automation to streamline the development process. - -The recommended workflow is: - -1. **Generate Tests**: For any changes to the public API, first generate the necessary test stubs. - - ```bash - task test-gen - ``` - -2. **Run Tests (and watch them fail)**: Verify that the new tests fail as expected. - - ```bash - task test - ``` - -3. **Implement Your Feature**: Write the code to make the tests pass. - -4. **Run Tests Again**: Ensure all tests now pass. - - ```bash - task test - ``` - -5. **Submit for Review**: Once your changes are complete and tests are passing, submit them for a CodeRabbit review. - - ```bash - task review - ``` - -## Project Structure - -The project is organized into the following main directories: - -- `pkg/`: Contains the core Go packages that make up the framework. -- `cmd/`: Contains the entry points for the two main applications: - - `core-gui/`: The Wails-based GUI application. - - `core/`: The command-line interface (CLI) application. - ## Prerequisites -- [Go](https://go.dev/) +- [Go](https://go.dev/) 1.25+ - [Node.js](https://nodejs.org/) -- [Wails](https://wails.io/) +- [Wails](https://wails.io/) v3 - [Task](https://taskfile.dev/) -## Building and Running - -### GUI Application - -To run the GUI application in development mode: +## Development Workflow (TDD) ```bash -task gui:dev +task test-gen # 1. Generate test stubs +task test # 2. Run tests (watch them fail) +# 3. Implement your feature +task test # 4. Run tests (watch them pass) +task review # 5. CodeRabbit review ``` -To build the final application for your platform: +## Building & Running ```bash -task gui:build +# GUI (Wails) +task gui:dev # Development with hot-reload +task gui:build # Production build + +# CLI +task cli:build # Build to cmd/core/bin/core +task cli:run # Build and run ``` -### CLI Application +## All Tasks -To build the CLI application: +| Task | Description | +|------|-------------| +| `task test` | Run all Go tests | +| `task test-gen` | Generate test stubs for public API | +| `task check` | go mod tidy + tests + review | +| `task review` | CodeRabbit review | +| `task cov` | Generate coverage.txt | +| `task cov-view` | Open HTML coverage report | +| `task sync` | Update public API Go files | + +--- + +## Architecture + +### Project Structure + +``` +. +├── core.go # Facade re-exporting pkg/core +├── pkg/ +│ ├── core/ # Service container, DI, Runtime[T] +│ ├── config/ # JSON persistence, XDG paths +│ ├── display/ # Windows, tray, menus (Wails) +│ ├── crypt/ # Hashing, checksums, PGP +│ │ └── openpgp/ # Full PGP implementation +│ ├── io/ # Medium interface + backends +│ ├── workspace/ # Encrypted workspace management +│ ├── help/ # In-app documentation +│ └── i18n/ # Internationalization +├── cmd/ +│ ├── core/ # CLI application +│ └── core-gui/ # Wails GUI application +└── go.work # Links root, cmd/core, cmd/core-gui +``` + +### Service Pattern (Dual-Constructor DI) + +Every service follows this pattern: + +```go +// Static DI - standalone use/testing (no core.Runtime) +func New() (*Service, error) + +// Dynamic DI - for core.WithService() registration +func Register(c *core.Core) (any, error) +``` + +Services embed `*core.Runtime[Options]` for access to `Core()` and `Config()`. + +### IPC/Action System + +Services implement `HandleIPCEvents(c *core.Core, msg core.Message) error` - auto-discovered via reflection. Handles typed actions like `core.ActionServiceStartup`. + +--- + +## Wails v3 Frontend Bindings + +Core uses [Wails v3](https://v3alpha.wails.io/) to expose Go methods to a WebView2 browser runtime. Wails automatically generates TypeScript bindings for registered services. + +**Documentation:** [Wails v3 Method Bindings](https://v3alpha.wails.io/features/bindings/methods/) + +### How It Works + +1. **Go services** with exported methods are registered with Wails +2. Run `wails3 generate bindings` (or `wails3 dev` / `wails3 build`) +3. **TypeScript SDK** is generated in `frontend/bindings/` +4. Frontend calls Go methods with full type safety, no HTTP overhead + +### Current Binding Architecture + +```go +// cmd/core-gui/main.go +app.RegisterService(application.NewService(coreService)) // Only Core is registered +``` + +**Problem:** Only `Core` is registered with Wails. Sub-services (crypt, workspace, display, etc.) are internal to Core's service map - their methods aren't directly exposed to JS. + +**Currently exposed** (see `cmd/core-gui/public/bindings/`): +```typescript +// From frontend: +import { ACTION, Config, Service } from './bindings/github.com/Snider/Core/pkg/core' + +ACTION(msg) // Broadcast IPC message +Config() // Get config service reference +Service("workspace") // Get service by name (returns any) +``` + +**NOT exposed:** Direct calls like `workspace.CreateWorkspace()` or `crypt.Hash()`. + +### The IPC Bridge Pattern (Chosen Architecture) + +Sub-services are accessed via Core's **IPC/ACTION system**, not direct Wails bindings: + +```typescript +// Frontend calls Core.ACTION() with typed messages +import { ACTION } from './bindings/github.com/Snider/Core/pkg/core' + +// Open a window +ACTION({ action: "display.open_window", name: "settings", options: { Title: "Settings", Width: 800 } }) + +// Switch workspace +ACTION({ action: "workspace.switch_workspace", name: "myworkspace" }) +``` + +Each service implements `HandleIPCEvents(c *core.Core, msg core.Message)` to process these messages: + +```go +// pkg/display/display.go +func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { + switch m := msg.(type) { + case map[string]any: + if action, ok := m["action"].(string); ok && action == "display.open_window" { + return s.handleOpenWindowAction(m) + } + } + return nil +} +``` + +**Why this pattern:** +- Single Wails service (Core) = simpler binding generation +- Services remain decoupled from Wails +- Centralized message routing via `ACTION()` +- Services can communicate internally using same pattern + +**Current gap:** Not all service methods have IPC handlers yet. See `HandleIPCEvents` in each service to understand what's wired up. + +### Generating Bindings ```bash -task cli:build +cd cmd/core-gui +wails3 generate bindings # Regenerate after Go changes ``` -The executable will be located in the `cmd/core/bin` directory. +Bindings output to `cmd/core-gui/public/bindings/github.com/Snider/Core/` mirroring Go package structure. -## Available Tasks +--- -To run any of the following tasks, open your terminal in the project's root directory and execute the `task` command. +### Service Interfaces (`pkg/core/interfaces.go`) -### General Tasks +```go +type Config interface { + Get(key string, out any) error + Set(key string, v any) error +} -- `task test`: Runs all Go tests recursively for the entire project. -- `task test-gen`: Generates tests for the public API. -- `task check`: A comprehensive check that runs `go mod tidy`, the full test suite, and a CodeRabbit review. -- `task review`: Submits the current changes for a CodeRabbit review. -- `task cov`: Generates a test coverage profile (`coverage.txt`). -- `task cov-view`: Opens the HTML coverage report in your browser. -- `task sync`: Updates the public API Go files to match the exported interface of the modules. +type Display interface { + OpenWindow(opts ...WindowOption) error +} -### GUI Application (`cmd/core-gui`) +type Workspace interface { + CreateWorkspace(identifier, password string) (string, error) + SwitchWorkspace(name string) error + WorkspaceFileGet(filename string) (string, error) + WorkspaceFileSet(filename, content string) error +} -These tasks are run from the root directory and operate on the GUI application. +type Crypt interface { + EncryptPGP(writer io.Writer, recipientPath, data string, ...) (string, error) + DecryptPGP(recipientPath, message, passphrase string, ...) (string, error) +} +``` -- `task gui:build`: Builds the GUI application. -- `task gui:package`: Packages a production build of the GUI application. -- `task gui:run`: Runs the GUI application. -- `task gui:dev`: Runs the GUI application in development mode, with hot-reloading enabled. +--- -### CLI Application (`cmd/core`) +## Current State (Prototype) -These tasks are run from the root directory and operate on the CLI application. +### Working -- `task cli:build`: Builds the CLI application. -- `task cli:build:dev`: Builds the CLI application for development. -- `task cli:run`: Builds and runs the CLI application. -- `task cli:sync`: Updates the public API Go files. -- `task cli:test-gen`: Generates tests for the public API. +| Package | Notes | +|---------|-------| +| `pkg/core` | Service container, DI, thread-safe - solid | +| `pkg/config` | JSON persistence, XDG paths - solid | +| `pkg/crypt` | Hashing, checksums, PGP - solid, well-tested | +| `pkg/help` | Embedded docs, Show/ShowAt - solid | +| `pkg/i18n` | Multi-language with go-i18n - solid | +| `pkg/io` | Medium interface + local backend - solid | +| `pkg/workspace` | Workspace creation, switching, file ops - functional | -## Docs (MkDocs) -The help site and in‑app docs are built with MkDocs Material and live under `pkg/v1/core/docs`. +### Partial -- Live preview: from `pkg/v1/core/docs` run - - `pip install -r requirements.txt` - - `mkdocs serve -o -c` (or `task dev` if you use Task) -- Build static site: `mkdocs build --clean -d public` (or `task build`) +| Package | Issues | +|---------|--------| +| `pkg/display` | Window creation works; menu/tray handlers are TODOs | -The demo app embeds the built docs from `public/` and can open specific sections in new windows using stable, short headings. +--- + +## Priority Work Items + +### 1. IMPLEMENT: System Tray Brand Support + +`pkg/display/tray.go:52-63` - Commented brand-specific menu items need implementation. + +### 2. ADD: Integration Tests + +| Package | Notes | +|---------|-------| +| `pkg/display` | Integration tests requiring Wails runtime (27% unit coverage) | + +--- + +## Package Deep Dives + +### pkg/workspace - The Core Feature + +Each workspace is: +1. Identified by LTHN hash of user identifier +2. Has directory structure: `config/`, `log/`, `data/`, `files/`, `keys/` +3. Gets a PGP keypair generated on creation +4. Files accessed via obfuscated paths + +The `workspaceList` maps workspace IDs to public keys. + +### pkg/crypt/openpgp + +Full PGP using `github.com/ProtonMail/go-crypto`: +- `CreateKeyPair(name, passphrase)` - RSA-4096 with revocation cert +- `EncryptPGP()` - Encrypt + optional signing +- `DecryptPGP()` - Decrypt + optional signature verification + +### pkg/io - Storage Abstraction + +```go +type Medium interface { + Read(path string) (string, error) + Write(path, content string) error + EnsureDir(path string) error + IsFile(path string) bool + FileGet(path string) (string, error) + FileSet(path, content string) error +} +``` + +Implementations: `local/`, `sftp/`, `webdav/` + +--- + +## Future Work + +### Phase 1: Core Stability +- [x] ~~Fix workspace medium injection (critical blocker)~~ +- [x] ~~Initialize `io.Local` global~~ +- [x] ~~Clean up dead code (orphaned vars, broken wrappers)~~ +- [x] ~~Wire up IPC handlers for all services (config, crypt, display, help, i18n, workspace)~~ +- [x] ~~Complete display menu handlers (New/List workspace)~~ +- [x] ~~Tray icon setup with asset embedding~~ +- [x] ~~Test coverage for io packages~~ +- [ ] System tray brand-specific menus + +### Phase 2: Multi-Brand Support +- [ ] Define brand configuration system (config? build flags?) +- [ ] Implement brand-specific tray menus (AdminHub, ServerHub, GatewayHub, DeveloperHub, ClientHub) +- [ ] Brand-specific theming/assets +- [ ] Per-brand default workspace configurations + +### Phase 3: Remote Storage +- [ ] Complete SFTP backend (`pkg/io/sftp/`) +- [ ] Complete WebDAV backend (`pkg/io/webdav/`) +- [ ] Workspace sync across storage backends +- [ ] Conflict resolution for multi-device access + +### Phase 4: Enhanced Crypto +- [ ] Key management UI (import/export, key rotation) +- [ ] Multi-recipient encryption +- [ ] Hardware key support (YubiKey, etc.) +- [ ] Encrypted workspace backup/restore + +### Phase 5: Developer Experience +- [ ] TypeScript types for IPC messages (codegen from Go structs) +- [ ] Hot-reload for service registration +- [ ] Plugin system for third-party services +- [ ] CLI tooling for workspace management + +### Phase 6: Distribution +- [ ] Auto-update mechanism +- [ ] Platform installers (DMG, MSI, AppImage) +- [ ] Signing and notarization +- [ ] Crash reporting integration + +--- + +## For New Contributors + +1. Run `task test` to verify all tests pass +2. Follow TDD: `task test-gen` creates stubs, implement to pass +3. The dual-constructor pattern is intentional: `New(deps)` for tests, `Register()` for runtime +4. See `cmd/core-gui/main.go` for how services wire together +5. IPC handlers in each service's `HandleIPCEvents()` are the frontend bridge diff --git a/pkg/config/config.go b/pkg/config/config.go index eba202e..a34b52b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -13,6 +13,20 @@ import ( "github.com/adrg/xdg" ) +// HandleIPCEvents processes IPC messages for the config service. +func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { + switch msg.(type) { + case core.ActionServiceStartup: + // Config initializes during Register(), no additional startup needed. + return nil + default: + if c.App != nil && c.App.Logger != nil { + c.App.Logger.Debug("Config: Unhandled message type", "type", fmt.Sprintf("%T", msg)) + } + } + return nil +} + const appName = "lethean" const configFileName = "config.json" diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 6ca52f2..3fe6d92 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -129,4 +129,39 @@ func TestConfigService(t *testing.T) { t.Errorf("Expected value '%s', got '%s'", expectedValue, actualValue) } }) + + t.Run("HandleIPCEvents with ActionServiceStartup", func(t *testing.T) { + _, cleanup := setupTestEnv(t) + defer cleanup() + + c := newTestCore(t) + serviceAny, err := Register(c) + if err != nil { + t.Fatalf("Register() failed: %v", err) + } + + s := serviceAny.(*Service) + err = s.HandleIPCEvents(c, core.ActionServiceStartup{}) + if err != nil { + t.Errorf("HandleIPCEvents(ActionServiceStartup) should not error, got: %v", err) + } + }) + + t.Run("HandleIPCEvents with unknown message type", func(t *testing.T) { + _, cleanup := setupTestEnv(t) + defer cleanup() + + c := newTestCore(t) + serviceAny, err := Register(c) + if err != nil { + t.Fatalf("Register() failed: %v", err) + } + + s := serviceAny.(*Service) + // Pass an arbitrary type as unknown message + err = s.HandleIPCEvents(c, "unknown message") + if err != nil { + t.Errorf("HandleIPCEvents(unknown) should not error, got: %v", err) + } + }) } diff --git a/pkg/core/docs/site/404.html b/pkg/core/docs/site/404.html new file mode 100644 index 0000000..e0fae56 --- /dev/null +++ b/pkg/core/docs/site/404.html @@ -0,0 +1,707 @@ + + + + + + + + + + + + + + + + + + + + + + Core.Help + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ + + + + + + + + +
+ +

404 - Not found

+ +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/pkg/core/docs/site/assets/external/fonts.googleapis.com/css.49ea35f2.css b/pkg/core/docs/site/assets/external/fonts.googleapis.com/css.49ea35f2.css new file mode 100644 index 0000000..d5c0c14 --- /dev/null +++ b/pkg/core/docs/site/assets/external/fonts.googleapis.com/css.49ea35f2.css @@ -0,0 +1,756 @@ +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaWzU.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkAnkaWzU.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCnkaWzU.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaWzU.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* math */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaWzU.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaWzU.woff2) format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaWzU.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaWzU.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnka.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaWzU.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkAnkaWzU.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCnkaWzU.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaWzU.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* math */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaWzU.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaWzU.woff2) format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaWzU.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaWzU.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnka.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaWzU.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkAnkaWzU.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCnkaWzU.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaWzU.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* math */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaWzU.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaWzU.woff2) format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaWzU.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaWzU.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnka.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* math */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2) format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* math */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2) format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* math */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2) format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: fallback; + src: url(../fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 400; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 400; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 400; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 400; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 400; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 400; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 700; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 700; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 700; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 700; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 700; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 700; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 700; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 700; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 700; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 700; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 700; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 700; + font-display: fallback; + src: url(../fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkAnkaWzU.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkAnkaWzU.woff2 new file mode 100644 index 0000000..ab38fd5 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkAnkaWzU.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaWzU.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaWzU.woff2 new file mode 100644 index 0000000..db65849 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaWzU.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnka.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnka.woff2 new file mode 100644 index 0000000..7c9cbed Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnka.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaWzU.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaWzU.woff2 new file mode 100644 index 0000000..e0aa393 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaWzU.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaWzU.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaWzU.woff2 new file mode 100644 index 0000000..b677130 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaWzU.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaWzU.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaWzU.woff2 new file mode 100644 index 0000000..669ba79 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaWzU.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCnkaWzU.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCnkaWzU.woff2 new file mode 100644 index 0000000..6cc1de8 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCnkaWzU.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaWzU.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaWzU.woff2 new file mode 100644 index 0000000..ded8a41 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaWzU.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaWzU.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaWzU.woff2 new file mode 100644 index 0000000..dbac481 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaWzU.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2 new file mode 100644 index 0000000..8e0eec6 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2 new file mode 100644 index 0000000..0ddf16c Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2 new file mode 100644 index 0000000..7bd3c2e Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2 new file mode 100644 index 0000000..8e43aa4 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2 new file mode 100644 index 0000000..2c6ba19 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2 new file mode 100644 index 0000000..2f8b493 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2 new file mode 100644 index 0000000..7c16c79 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2 new file mode 100644 index 0000000..c2788c7 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2 new file mode 100644 index 0000000..528b3bf Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2 new file mode 100644 index 0000000..2c06834 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2 new file mode 100644 index 0000000..532a888 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2 new file mode 100644 index 0000000..b02e2d6 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2 new file mode 100644 index 0000000..ae2f9eb Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2 new file mode 100644 index 0000000..bfa169c Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2 new file mode 100644 index 0000000..8a15f5c Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2 new file mode 100644 index 0000000..d1ee097 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2 new file mode 100644 index 0000000..c8e6ed4 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2 new file mode 100644 index 0000000..1debc1b Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2 new file mode 100644 index 0000000..43f7516 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2 new file mode 100644 index 0000000..227f362 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2 differ diff --git a/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2 b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2 new file mode 100644 index 0000000..10a65a7 Binary files /dev/null and b/pkg/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2 differ diff --git a/pkg/core/docs/site/assets/external/unpkg.com/iframe-worker/shim.js b/pkg/core/docs/site/assets/external/unpkg.com/iframe-worker/shim.js new file mode 100644 index 0000000..5f1e232 --- /dev/null +++ b/pkg/core/docs/site/assets/external/unpkg.com/iframe-worker/shim.js @@ -0,0 +1 @@ +"use strict";(()=>{function c(s,n){parent.postMessage(s,n||"*")}function d(...s){return s.reduce((n,e)=>n.then(()=>new Promise(r=>{let t=document.createElement("script");t.src=e,t.onload=r,document.body.appendChild(t)})),Promise.resolve())}var o=class extends EventTarget{constructor(e){super();this.url=e;this.m=e=>{e.source===this.w&&(this.dispatchEvent(new MessageEvent("message",{data:e.data})),this.onmessage&&this.onmessage(e))};this.e=(e,r,t,i,m)=>{if(r===`${this.url}`){let a=new ErrorEvent("error",{message:e,filename:r,lineno:t,colno:i,error:m});this.dispatchEvent(a),this.onerror&&this.onerror(a)}};let r=document.createElement("iframe");r.hidden=!0,document.body.appendChild(this.iframe=r),this.w.document.open(),this.w.document.write(` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ + + + + + + + + +
+ + + + + + + +

Core.Config

+

Short: App config and UI state persistence.

+

Overview

+

Stores and retrieves configuration, including window positions/sizes and user prefs.

+

Setup

+
package main
+
+import (
+  core "github.com/Snider/Core"
+  config "github.com/Snider/Core/config"
+)
+
+app := core.New(
+  core.WithService(config.Register),
+  core.WithServiceLock(),
+)
+
+

Use

+
    +
  • Persist UI state automatically when using Core.Display.
  • +
  • Read/write your own settings via the config API.
  • +
+

API

+
    +
  • Register(c *core.Core) error
  • +
  • Get(path string, out any) error
  • +
  • Set(path string, v any) error
  • +
+ + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/pkg/core/docs/site/core/crypt.html b/pkg/core/docs/site/core/crypt.html new file mode 100644 index 0000000..7c4fbfe --- /dev/null +++ b/pkg/core/docs/site/core/crypt.html @@ -0,0 +1,934 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Core.Crypt - Core.Help + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ + + + + + + + + +
+ + + + + + + +

Core.Crypt

+

Short: Keys, encrypt/decrypt, sign/verify.

+

Overview

+

Simple wrappers around OpenPGP for common crypto tasks.

+

Setup

+
import (
+  core "github.com/Snider/Core"
+  crypt "github.com/Snider/Core/crypt"
+)
+
+app := core.New(
+  core.WithService(crypt.Register),
+  core.WithServiceLock(),
+)
+
+

Use

+
    +
  • Generate keys
  • +
  • Encrypt/decrypt data
  • +
  • Sign/verify messages
  • +
+

API

+
    +
  • Register(c *core.Core) error
  • +
  • GenerateKey(opts ...Option) (*Key, error)
  • +
  • Encrypt(pub *Key, data []byte) ([]byte, error)
  • +
  • Decrypt(priv *Key, data []byte) ([]byte, error)
  • +
  • Sign(priv *Key, data []byte) ([]byte, error)
  • +
  • Verify(pub *Key, data, sig []byte) error
  • +
+

Notes

+ + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/pkg/core/docs/site/core/display.html b/pkg/core/docs/site/core/display.html new file mode 100644 index 0000000..85d104c --- /dev/null +++ b/pkg/core/docs/site/core/display.html @@ -0,0 +1,936 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Core.Display - Core.Help + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ + + + + + + + + +
+ + + + + + + +

Core.Display

+

Short: Windows, tray, and window state.

+

Overview

+

Manages Wails windows, remembers positions/sizes, exposes JS bindings, and integrates with Core.Config for persistence.

+

Setup

+
import (
+  core "github.com/Snider/Core"
+  display "github.com/Snider/Core/display"
+)
+
+app := core.New(
+  core.WithService(display.Register),
+  core.WithServiceLock(),
+)
+
+

Use

+
    +
  • Open a window: OpenWindow(OptName("main"), ...)
  • +
  • Get a window: Window("main")
  • +
  • Save/restore state automatically when Core.Config is present
  • +
+

API

+
    +
  • Register(c *core.Core) error
  • +
  • OpenWindow(opts ...Option) *Window
  • +
  • Window(name string) *Window
  • +
  • Options: OptName, OptWidth, OptHeight, OptURL, OptTitle
  • +
+

Example

+
func (d *API) ServiceStartup(ctx context.Context, _ application.ServiceOptions) error {
+  d.OpenWindow(
+    OptName("main"), OptWidth(1280), OptHeight(900), OptURL("/"), OptTitle("Core"),
+  )
+  return nil
+}
+
+ + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/pkg/core/docs/site/core/docs.html b/pkg/core/docs/site/core/docs.html new file mode 100644 index 0000000..dc90d1f --- /dev/null +++ b/pkg/core/docs/site/core/docs.html @@ -0,0 +1,932 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Core.Docs - Core.Help + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ + + + + + + + + +
+ + + + + + + +

Core.Docs

+

Short: In‑app help and deep‑links.

+

Overview

+

Renders MkDocs content inside your app. Opens specific sections in new windows for contextual help.

+

Setup

+
import (
+  core "github.com/Snider/Core"
+  docs "github.com/Snider/Core/docs"
+)
+
+app := core.New(
+  core.WithService(docs.Register),
+  core.WithServiceLock(),
+)
+
+

Use

+
    +
  • Open docs home in a window: docs.Open()
  • +
  • Open a section: docs.OpenAt("core/display#setup")
  • +
  • Use short, descriptive headings to create stable anchors.
  • +
+

API

+
    +
  • Register(c *core.Core) error
  • +
  • Open() — show docs home
  • +
  • OpenAt(anchor string) — open specific section
  • +
+

Notes

+
    +
  • Docs are built with MkDocs Material and included in the demo app assets.
  • +
  • You are viewing Core.Docs right now, this Website is bundled into the app binary by default.
  • +
+ + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/pkg/core/docs/site/core/index.html b/pkg/core/docs/site/core/index.html new file mode 100644 index 0000000..38c575d --- /dev/null +++ b/pkg/core/docs/site/core/index.html @@ -0,0 +1,901 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Core - Core.Help + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ + + + + + + + + +
+ + + + + + + +

Core

+

Short: Framework bootstrap and service container.

+

What it is

+

Core wires modules together, provides lifecycle hooks, and locks the service graph for clarity and safety.

+

Setup

+
import "github.com/Snider/Core"
+
+app := core.New(
+    core.WithServiceLock(),
+)
+
+

Use

+
    +
  • Register a module: core.RegisterModule(name, module)
  • +
  • Access a module: core.Mod[T](c, name)
  • +
  • Lock services: core.WithServiceLock()
  • +
+

API

+
    +
  • New(opts ...) *core.Core
  • +
  • RegisterModule(name string, m any) error
  • +
  • Mod[T any](c *core.Core, name ...string) *T
  • +
+ + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/pkg/core/docs/site/core/io.html b/pkg/core/docs/site/core/io.html new file mode 100644 index 0000000..4485a50 --- /dev/null +++ b/pkg/core/docs/site/core/io.html @@ -0,0 +1,932 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Core.IO - Core.Help + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ + + + + + + + + +
+ + + + + + + +

Core.IO

+

Short: Local/remote filesystem helpers.

+

Overview

+

Abstracts filesystems (local, SFTP, WebDAV) behind a unified API for reading/writing and listing.

+

Setup

+
import (
+  core "github.com/Snider/Core"
+  ioapi "github.com/Snider/Core/filesystem"
+)
+
+app := core.New(
+  core.WithService(ioapi.Register),
+  core.WithServiceLock(),
+)
+
+

Use

+
    +
  • Open a filesystem: fs := ioapi.Local() or ioapi.SFTP(cfg)
  • +
  • Read/write files: fs.Read(path), fs.Write(path, data)
  • +
  • List directories: fs.List(path)
  • +
+

API

+
    +
  • Register(c *core.Core) error
  • +
  • Local() FS
  • +
  • SFTP(cfg Config) (FS, error)
  • +
  • WebDAV(cfg Config) (FS, error)
  • +
+

Notes

+
    +
  • See package pkg/v1/core/filesystem/* for drivers.
  • +
+ + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/pkg/core/docs/site/core/workspace.html b/pkg/core/docs/site/core/workspace.html new file mode 100644 index 0000000..72bbc03 --- /dev/null +++ b/pkg/core/docs/site/core/workspace.html @@ -0,0 +1,930 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Core.Workspace - Core.Help + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ + + + + + + + + +
+ + + + + + + +

Core.Workspace

+

Short: Projects and paths.

+

Overview

+

Provides a consistent way to resolve app/project directories, temp/cache locations, and user data paths across platforms.

+

Setup

+
import (
+  core "github.com/Snider/Core"
+  workspace "github.com/Snider/Core/workspace"
+)
+
+app := core.New(
+  core.WithService(workspace.Register),
+  core.WithServiceLock(),
+)
+
+

Use

+
    +
  • Get app data dir: ws.DataDir()
  • +
  • Get cache dir: ws.CacheDir()
  • +
  • Resolve project path: ws.Project("my-app")
  • +
+

API

+
    +
  • Register(c *core.Core) error
  • +
  • DataDir() string
  • +
  • CacheDir() string
  • +
  • Project(name string) string
  • +
+

Notes

+
    +
  • Follows OS directory standards (AppData, ~/Library, XDG, etc.).
  • +
+ + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/pkg/core/docs/site/images/cross-platform.jpeg b/pkg/core/docs/site/images/cross-platform.jpeg new file mode 100644 index 0000000..8de2288 Binary files /dev/null and b/pkg/core/docs/site/images/cross-platform.jpeg differ diff --git a/pkg/core/docs/site/images/decentralised-vpn.jpg b/pkg/core/docs/site/images/decentralised-vpn.jpg new file mode 100644 index 0000000..df1f487 Binary files /dev/null and b/pkg/core/docs/site/images/decentralised-vpn.jpg differ diff --git a/pkg/core/docs/site/images/favicon.ico b/pkg/core/docs/site/images/favicon.ico new file mode 100644 index 0000000..8bc8ebb Binary files /dev/null and b/pkg/core/docs/site/images/favicon.ico differ diff --git a/pkg/core/docs/site/images/illustration.png b/pkg/core/docs/site/images/illustration.png new file mode 100644 index 0000000..69f739c Binary files /dev/null and b/pkg/core/docs/site/images/illustration.png differ diff --git a/pkg/core/docs/site/images/lethean-logo.png b/pkg/core/docs/site/images/lethean-logo.png new file mode 100644 index 0000000..591019d Binary files /dev/null and b/pkg/core/docs/site/images/lethean-logo.png differ diff --git a/pkg/core/docs/site/images/private-transaction-net.png b/pkg/core/docs/site/images/private-transaction-net.png new file mode 100644 index 0000000..1eee17a Binary files /dev/null and b/pkg/core/docs/site/images/private-transaction-net.png differ diff --git a/pkg/core/docs/site/images/secure-data-storage.jpg b/pkg/core/docs/site/images/secure-data-storage.jpg new file mode 100644 index 0000000..395a8ae Binary files /dev/null and b/pkg/core/docs/site/images/secure-data-storage.jpg differ diff --git a/pkg/core/docs/site/index.html b/pkg/core/docs/site/index.html new file mode 100644 index 0000000..a956691 --- /dev/null +++ b/pkg/core/docs/site/index.html @@ -0,0 +1,939 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Core.Help - Core.Help + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ + + + + + + + + +
+ + + + + + + +

Overview

+

Core is an opinionated framework for building Go desktop apps with Wails, providing a small set of focused modules you can mix into your app. It ships with sensible defaults and a demo app that doubles as in‑app help.

+
    +
  • Site: https://dappco.re
  • +
  • Repo: https://github.com/Snider/Core
  • +
+

Modules

+
    +
  • Core — framework bootstrap and service container
  • +
  • Core.Config — app and UI state persistence
  • +
  • Core.Crypt — keys, encrypt/decrypt, sign/verify
  • +
  • Core.Display — windows, tray, window state
  • +
  • Core.Docs — in‑app help and deep‑links
  • +
  • Core.IO — local/remote filesystem helpers
  • +
  • Core.Workspace — projects and paths
  • +
+

Quick start

+
package main
+
+import (
+    core "github.com/Snider/Core"
+)
+
+func main() {
+    app := core.New(
+        core.WithServiceLock(),
+    )
+    _ = app // start via Wails in your main package
+}
+
+

Services

+
package demo
+
+import (
+    core "github.com/Snider/Core"
+)
+
+// Register your service
+func Register(c *core.Core) error {
+    return c.RegisterModule("demo", &Demo{core: c})
+}
+
+

Display example

+
package display
+
+import (
+    "context"
+    "github.com/wailsapp/wails/v3/pkg/application"
+)
+
+// Open a window on startup
+func (d *API) ServiceStartup(ctx context.Context, _ application.ServiceOptions) error {
+    d.OpenWindow(
+        OptName("main"),
+        OptHeight(900),
+        OptWidth(1280),
+        OptURL("/"),
+        OptTitle("Core"),
+    )
+    return nil
+}
+
+

See the left nav for detailed pages on each module.

+ + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/pkg/core/docs/site/search/search_index.js b/pkg/core/docs/site/search/search_index.js new file mode 100644 index 0000000..193f050 --- /dev/null +++ b/pkg/core/docs/site/search/search_index.js @@ -0,0 +1 @@ +var __index = {"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"index.html","title":"Overview","text":"

Core is an opinionated framework for building Go desktop apps with Wails, providing a small set of focused modules you can mix into your app. It ships with sensible defaults and a demo app that doubles as in\u2011app help.

"},{"location":"index.html#modules","title":"Modules","text":""},{"location":"index.html#quick-start","title":"Quick start","text":"
package main\nimport (\ncore \"github.com/Snider/Core\"\n)\nfunc main() {\napp := core.New(\ncore.WithServiceLock(),\n)\n_ = app // start via Wails in your main package\n}\n
"},{"location":"index.html#services","title":"Services","text":"
package demo\nimport (\ncore \"github.com/Snider/Core\"\n)\n// Register your service\nfunc Register(c *core.Core) error {\nreturn c.RegisterModule(\"demo\", &Demo{core: c})\n}\n
"},{"location":"index.html#display-example","title":"Display example","text":"
package display\nimport (\n\"context\"\n\"github.com/wailsapp/wails/v3/pkg/application\"\n)\n// Open a window on startup\nfunc (d *API) ServiceStartup(ctx context.Context, _ application.ServiceOptions) error {\nd.OpenWindow(\nOptName(\"main\"),\nOptHeight(900),\nOptWidth(1280),\nOptURL(\"/\"),\nOptTitle(\"Core\"),\n)\nreturn nil\n}\n

See the left nav for detailed pages on each module.

"},{"location":"core/index.html","title":"Core","text":"

Short: Framework bootstrap and service container.

"},{"location":"core/index.html#what-it-is","title":"What it is","text":"

Core wires modules together, provides lifecycle hooks, and locks the service graph for clarity and safety.

"},{"location":"core/index.html#setup","title":"Setup","text":"
import \"github.com/Snider/Core\"\napp := core.New(\ncore.WithServiceLock(),\n)\n
"},{"location":"core/index.html#use","title":"Use","text":""},{"location":"core/index.html#api","title":"API","text":""},{"location":"core/config.html","title":"Core.Config","text":"

Short: App config and UI state persistence.

"},{"location":"core/config.html#overview","title":"Overview","text":"

Stores and retrieves configuration, including window positions/sizes and user prefs.

"},{"location":"core/config.html#setup","title":"Setup","text":"
package main\nimport (\ncore \"github.com/Snider/Core\"\nconfig \"github.com/Snider/Core/config\"\n)\napp := core.New(\ncore.WithService(config.Register),\ncore.WithServiceLock(),\n)\n
"},{"location":"core/config.html#use","title":"Use","text":""},{"location":"core/config.html#api","title":"API","text":""},{"location":"core/crypt.html","title":"Core.Crypt","text":"

Short: Keys, encrypt/decrypt, sign/verify.

"},{"location":"core/crypt.html#overview","title":"Overview","text":"

Simple wrappers around OpenPGP for common crypto tasks.

"},{"location":"core/crypt.html#setup","title":"Setup","text":"
import (\ncore \"github.com/Snider/Core\"\ncrypt \"github.com/Snider/Core/crypt\"\n)\napp := core.New(\ncore.WithService(crypt.Register),\ncore.WithServiceLock(),\n)\n
"},{"location":"core/crypt.html#use","title":"Use","text":""},{"location":"core/crypt.html#api","title":"API","text":""},{"location":"core/crypt.html#notes","title":"Notes","text":""},{"location":"core/display.html","title":"Core.Display","text":"

Short: Windows, tray, and window state.

"},{"location":"core/display.html#overview","title":"Overview","text":"

Manages Wails windows, remembers positions/sizes, exposes JS bindings, and integrates with Core.Config for persistence.

"},{"location":"core/display.html#setup","title":"Setup","text":"
import (\ncore \"github.com/Snider/Core\"\ndisplay \"github.com/Snider/Core/display\"\n)\napp := core.New(\ncore.WithService(display.Register),\ncore.WithServiceLock(),\n)\n
"},{"location":"core/display.html#use","title":"Use","text":""},{"location":"core/display.html#api","title":"API","text":""},{"location":"core/display.html#example","title":"Example","text":"
func (d *API) ServiceStartup(ctx context.Context, _ application.ServiceOptions) error {\nd.OpenWindow(\nOptName(\"main\"), OptWidth(1280), OptHeight(900), OptURL(\"/\"), OptTitle(\"Core\"),\n)\nreturn nil\n}\n
"},{"location":"core/docs.html","title":"Core.Docs","text":"

Short: In\u2011app help and deep\u2011links.

"},{"location":"core/docs.html#overview","title":"Overview","text":"

Renders MkDocs content inside your app. Opens specific sections in new windows for contextual help.

"},{"location":"core/docs.html#setup","title":"Setup","text":"
import (\ncore \"github.com/Snider/Core\"\ndocs \"github.com/Snider/Core/docs\"\n)\napp := core.New(\ncore.WithService(docs.Register),\ncore.WithServiceLock(),\n)\n
"},{"location":"core/docs.html#use","title":"Use","text":""},{"location":"core/docs.html#api","title":"API","text":""},{"location":"core/docs.html#notes","title":"Notes","text":""},{"location":"core/io.html","title":"Core.IO","text":"

Short: Local/remote filesystem helpers.

"},{"location":"core/io.html#overview","title":"Overview","text":"

Abstracts filesystems (local, SFTP, WebDAV) behind a unified API for reading/writing and listing.

"},{"location":"core/io.html#setup","title":"Setup","text":"
import (\ncore \"github.com/Snider/Core\"\nioapi \"github.com/Snider/Core/filesystem\"\n)\napp := core.New(\ncore.WithService(ioapi.Register),\ncore.WithServiceLock(),\n)\n
"},{"location":"core/io.html#use","title":"Use","text":""},{"location":"core/io.html#api","title":"API","text":""},{"location":"core/io.html#notes","title":"Notes","text":""},{"location":"core/workspace.html","title":"Core.Workspace","text":"

Short: Projects and paths.

"},{"location":"core/workspace.html#overview","title":"Overview","text":"

Provides a consistent way to resolve app/project directories, temp/cache locations, and user data paths across platforms.

"},{"location":"core/workspace.html#setup","title":"Setup","text":"
import (\ncore \"github.com/Snider/Core\"\nworkspace \"github.com/Snider/Core/workspace\"\n)\napp := core.New(\ncore.WithService(workspace.Register),\ncore.WithServiceLock(),\n)\n
"},{"location":"core/workspace.html#use","title":"Use","text":""},{"location":"core/workspace.html#api","title":"API","text":""},{"location":"core/workspace.html#notes","title":"Notes","text":""}]} \ No newline at end of file diff --git a/pkg/core/docs/site/search/search_index.json b/pkg/core/docs/site/search/search_index.json new file mode 100644 index 0000000..323cc07 --- /dev/null +++ b/pkg/core/docs/site/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"index.html","title":"Overview","text":"

Core is an opinionated framework for building Go desktop apps with Wails, providing a small set of focused modules you can mix into your app. It ships with sensible defaults and a demo app that doubles as in\u2011app help.

"},{"location":"index.html#modules","title":"Modules","text":""},{"location":"index.html#quick-start","title":"Quick start","text":"
package main\nimport (\ncore \"github.com/Snider/Core\"\n)\nfunc main() {\napp := core.New(\ncore.WithServiceLock(),\n)\n_ = app // start via Wails in your main package\n}\n
"},{"location":"index.html#services","title":"Services","text":"
package demo\nimport (\ncore \"github.com/Snider/Core\"\n)\n// Register your service\nfunc Register(c *core.Core) error {\nreturn c.RegisterModule(\"demo\", &Demo{core: c})\n}\n
"},{"location":"index.html#display-example","title":"Display example","text":"
package display\nimport (\n\"context\"\n\"github.com/wailsapp/wails/v3/pkg/application\"\n)\n// Open a window on startup\nfunc (d *API) ServiceStartup(ctx context.Context, _ application.ServiceOptions) error {\nd.OpenWindow(\nOptName(\"main\"),\nOptHeight(900),\nOptWidth(1280),\nOptURL(\"/\"),\nOptTitle(\"Core\"),\n)\nreturn nil\n}\n

See the left nav for detailed pages on each module.

"},{"location":"core/index.html","title":"Core","text":"

Short: Framework bootstrap and service container.

"},{"location":"core/index.html#what-it-is","title":"What it is","text":"

Core wires modules together, provides lifecycle hooks, and locks the service graph for clarity and safety.

"},{"location":"core/index.html#setup","title":"Setup","text":"
import \"github.com/Snider/Core\"\napp := core.New(\ncore.WithServiceLock(),\n)\n
"},{"location":"core/index.html#use","title":"Use","text":""},{"location":"core/index.html#api","title":"API","text":""},{"location":"core/config.html","title":"Core.Config","text":"

Short: App config and UI state persistence.

"},{"location":"core/config.html#overview","title":"Overview","text":"

Stores and retrieves configuration, including window positions/sizes and user prefs.

"},{"location":"core/config.html#setup","title":"Setup","text":"
package main\nimport (\ncore \"github.com/Snider/Core\"\nconfig \"github.com/Snider/Core/config\"\n)\napp := core.New(\ncore.WithService(config.Register),\ncore.WithServiceLock(),\n)\n
"},{"location":"core/config.html#use","title":"Use","text":""},{"location":"core/config.html#api","title":"API","text":""},{"location":"core/crypt.html","title":"Core.Crypt","text":"

Short: Keys, encrypt/decrypt, sign/verify.

"},{"location":"core/crypt.html#overview","title":"Overview","text":"

Simple wrappers around OpenPGP for common crypto tasks.

"},{"location":"core/crypt.html#setup","title":"Setup","text":"
import (\ncore \"github.com/Snider/Core\"\ncrypt \"github.com/Snider/Core/crypt\"\n)\napp := core.New(\ncore.WithService(crypt.Register),\ncore.WithServiceLock(),\n)\n
"},{"location":"core/crypt.html#use","title":"Use","text":""},{"location":"core/crypt.html#api","title":"API","text":""},{"location":"core/crypt.html#notes","title":"Notes","text":""},{"location":"core/display.html","title":"Core.Display","text":"

Short: Windows, tray, and window state.

"},{"location":"core/display.html#overview","title":"Overview","text":"

Manages Wails windows, remembers positions/sizes, exposes JS bindings, and integrates with Core.Config for persistence.

"},{"location":"core/display.html#setup","title":"Setup","text":"
import (\ncore \"github.com/Snider/Core\"\ndisplay \"github.com/Snider/Core/display\"\n)\napp := core.New(\ncore.WithService(display.Register),\ncore.WithServiceLock(),\n)\n
"},{"location":"core/display.html#use","title":"Use","text":""},{"location":"core/display.html#api","title":"API","text":""},{"location":"core/display.html#example","title":"Example","text":"
func (d *API) ServiceStartup(ctx context.Context, _ application.ServiceOptions) error {\nd.OpenWindow(\nOptName(\"main\"), OptWidth(1280), OptHeight(900), OptURL(\"/\"), OptTitle(\"Core\"),\n)\nreturn nil\n}\n
"},{"location":"core/docs.html","title":"Core.Docs","text":"

Short: In\u2011app help and deep\u2011links.

"},{"location":"core/docs.html#overview","title":"Overview","text":"

Renders MkDocs content inside your app. Opens specific sections in new windows for contextual help.

"},{"location":"core/docs.html#setup","title":"Setup","text":"
import (\ncore \"github.com/Snider/Core\"\ndocs \"github.com/Snider/Core/docs\"\n)\napp := core.New(\ncore.WithService(docs.Register),\ncore.WithServiceLock(),\n)\n
"},{"location":"core/docs.html#use","title":"Use","text":""},{"location":"core/docs.html#api","title":"API","text":""},{"location":"core/docs.html#notes","title":"Notes","text":""},{"location":"core/io.html","title":"Core.IO","text":"

Short: Local/remote filesystem helpers.

"},{"location":"core/io.html#overview","title":"Overview","text":"

Abstracts filesystems (local, SFTP, WebDAV) behind a unified API for reading/writing and listing.

"},{"location":"core/io.html#setup","title":"Setup","text":"
import (\ncore \"github.com/Snider/Core\"\nioapi \"github.com/Snider/Core/filesystem\"\n)\napp := core.New(\ncore.WithService(ioapi.Register),\ncore.WithServiceLock(),\n)\n
"},{"location":"core/io.html#use","title":"Use","text":""},{"location":"core/io.html#api","title":"API","text":""},{"location":"core/io.html#notes","title":"Notes","text":""},{"location":"core/workspace.html","title":"Core.Workspace","text":"

Short: Projects and paths.

"},{"location":"core/workspace.html#overview","title":"Overview","text":"

Provides a consistent way to resolve app/project directories, temp/cache locations, and user data paths across platforms.

"},{"location":"core/workspace.html#setup","title":"Setup","text":"
import (\ncore \"github.com/Snider/Core\"\nworkspace \"github.com/Snider/Core/workspace\"\n)\napp := core.New(\ncore.WithService(workspace.Register),\ncore.WithServiceLock(),\n)\n
"},{"location":"core/workspace.html#use","title":"Use","text":""},{"location":"core/workspace.html#api","title":"API","text":""},{"location":"core/workspace.html#notes","title":"Notes","text":""}]} \ No newline at end of file diff --git a/pkg/core/docs/site/sitemap.xml b/pkg/core/docs/site/sitemap.xml new file mode 100644 index 0000000..a063358 --- /dev/null +++ b/pkg/core/docs/site/sitemap.xml @@ -0,0 +1,35 @@ + + + + https://dappco.re/index.html + 2025-10-25 + + + https://dappco.re/core/index.html + 2025-10-25 + + + https://dappco.re/core/config.html + 2025-10-25 + + + https://dappco.re/core/crypt.html + 2025-10-25 + + + https://dappco.re/core/display.html + 2025-10-25 + + + https://dappco.re/core/docs.html + 2025-10-25 + + + https://dappco.re/core/io.html + 2025-10-25 + + + https://dappco.re/core/workspace.html + 2025-10-25 + + \ No newline at end of file diff --git a/pkg/core/docs/site/sitemap.xml.gz b/pkg/core/docs/site/sitemap.xml.gz new file mode 100644 index 0000000..c4e06d9 Binary files /dev/null and b/pkg/core/docs/site/sitemap.xml.gz differ diff --git a/pkg/core/docs/site/stylesheets/extra.css b/pkg/core/docs/site/stylesheets/extra.css new file mode 100644 index 0000000..8a89327 --- /dev/null +++ b/pkg/core/docs/site/stylesheets/extra.css @@ -0,0 +1,367 @@ +[data-md-color-scheme="lethean"] { + --md-primary-fg-color: #0F131C; +} + +.hero-section { + background: linear-gradient(135deg, #0F131C 0%, #1a237e 100%); + color: white; + padding: 4rem 2rem; + text-align: center; + margin-bottom: 3rem; +} + +.hero-content { + max-width: 800px; + margin: 0 auto; +} + +.hero-content h1 { + font-size: 2.5rem; + margin-bottom: 1rem; + color: white; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.hero-subtitle { + font-size: 1.25rem; + margin-bottom: 2rem; + opacity: 0.9; +} + +.hero-badges { + margin-bottom: 2rem; +} + +.badge { + background: rgba(255, 255, 255, 0.1); + padding: 0.5rem 1rem; + border-radius: 20px; + margin: 0 0.5rem; + font-size: 0.9rem; +} + +.cta-button { + display: inline-block; + background: #4A90E2; + color: white; + padding: 0.8rem 2rem; + border-radius: 4px; + text-decoration: none; + font-weight: 500; + transition: all 0.3s; +} + +.cta-button:hover { + background: #357ABD; + color: white; + transform: translateY(-2px); +} + +.cta-button.secondary { + background: transparent; + border: 2px solid #4A90E2; + color: #4A90E2; +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 0.2rem; + padding: 0.2rem; + margin-bottom: 3rem; +} + +.feature-card { + background: white; + border-radius: 8px; + padding: 1.0rem; + border: 2px solid #e2e8f0; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: all 0.3s; +} + +[data-md-color-scheme="slate"] .feature-card { + background: #2d3748; + border-color: #4a5568; + color: #e2e8f0; +} + +.feature-card:hover { + transform: translateY(-5px); + box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); +} + +.feature-card img { + width: 100%; + height: 150px; + object-fit: cover; + border-radius: 4px; + margin-bottom: 1rem; +} + +.feature-card h3 { + margin: 1rem 0; + color: #0F131C; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +[data-md-color-scheme="slate"] .feature-card h3 { + color: #e2e8f0; +} + +.get-started { + color: #4A90E2; + text-decoration: none; + font-weight: 500; +} + +.benefits-section { + background: #f5f5f5; + padding: 0.4rem 0.2rem; + text-align: center; + margin-bottom: 3rem; +} + +.benefits-section h2 { + font-size: 1.5rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 0.5rem; + margin-top: 0.8rem; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +[data-md-color-scheme="slate"] .benefits-section { + background: #1a202c; + color: #e2e8f0; +} + +.benefits-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 0.2rem; + padding: 0.2rem; + margin: 0.2rem auto; +} + +.benefit-card { + background: white; + padding: 0.5rem; + border-radius: 8px; + border: 2px solid #e2e8f0; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + text-align: left; +} + +[data-md-color-scheme="slate"] .benefit-card { + background: #2d3748; + border-color: #4a5568; + color: #e2e8f0; +} + +.roadmap-section { + padding: 0.4rem 0.2rem; + max-width: 1200px; + margin: 0 auto; +} + +.timeline { + position: relative; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 2rem; + margin: 2rem 0; +} + +.timeline-item { + background: white; + padding: 1.5rem; + border-radius: 8px; + border: 2px solid #e2e8f0; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + position: relative; + transition: all 0.3s; +} + +.timeline-item.completed { + grid-column: span 2; +} + +[data-md-color-scheme="slate"] .timeline-item { + background: #2d3748; + border-color: #4a5568; + color: #e2e8f0; +} + +.timeline-item:hover { + transform: translateY(-2px); + box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); +} + +.timeline-marker { + width: 20px; + height: 20px; + border-radius: 50%; + position: absolute; + top: -10px; + left: 50%; + transform: translateX(-50%); +} + +.timeline-item.planning .timeline-marker { + background: #718096; +} + +.timeline-item.in-progress .timeline-marker { + background: #4A90E2; +} + +.timeline-item.completed .timeline-marker { + background: #48BB78; +} + +.timeline-item ul { + list-style: none; + padding: 0; +} + +.timeline-item li { + margin: 0.5rem 0; + padding-left: 24px; + position: relative; +} + +.timeline-item li::before { + content: ""; + width: 12px; + height: 12px; + border-radius: 50%; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); +} + +.timeline-item li.planned::before { + background: #718096; +} + +.timeline-item li.active::before { + background: #4A90E2; +} + +.timeline-item li.completed::before { + background: #48BB78; +} + +.timeline-item li ul { + margin-top: 0.5rem; + margin-left: 1rem; +} + +.timeline-item li ul li { + font-size: 0.9rem; + margin: 0.25rem 0; +} + +.timeline-item li ul li::before { + width: 8px; + height: 8px; + background: #a0aec0; +} + +.timeline-item li ul li a { + color: #4A90E2; + text-decoration: none; + font-weight: 500; +} + +.timeline-item li ul li a:hover { + color: #357ABD; + text-decoration: underline; +} + +[data-md-color-scheme="slate"] .timeline-item li ul li a { + color: #63b3ed; +} + +[data-md-color-scheme="slate"] .timeline-item li ul li a:hover { + color: #90cdf4; +} + +.date { + font-size: 0.8rem; + color: #718096; + margin-left: 0.5rem; +} + +[data-md-color-scheme="slate"] .date { + color: #a0aec0; +} + +.cta-section { + background: #0F131C; + color: white; + padding: 4rem 2rem; + text-align: center; + margin-bottom: 3rem; +} + +.cta-buttons { + display: flex; + gap: 1rem; + justify-content: center; + margin-top: 2rem; +} + +.community-section { + padding: 4rem 2rem; + text-align: center; +} + +.community-links { + display: flex; + gap: 2rem; + justify-content: center; + margin-top: 2rem; +} + +.community-link { + color: #4A90E2; + text-decoration: none; + font-weight: 500; + transition: all 0.3s; +} + +.community-link:hover { + color: #357ABD; + transform: translateY(-2px); +} + +@media (max-width: 768px) { + .hero-content h1 { + font-size: 2rem; + } + + .timeline { + grid-template-columns: 1fr; + } + + .timeline-item.completed { + grid-column: auto; + } + + .features-grid { + grid-template-columns: 1fr; + } + + .cta-buttons { + flex-direction: column; + } + + .community-links { + flex-direction: column; + gap: 1rem; + } +} \ No newline at end of file diff --git a/pkg/crypt/crypt.go b/pkg/crypt/crypt.go index 4484ea9..fb25bdd 100644 --- a/pkg/crypt/crypt.go +++ b/pkg/crypt/crypt.go @@ -8,6 +8,7 @@ import ( "crypto/sha512" "encoding/binary" "encoding/hex" + "fmt" "io" "strconv" "strings" @@ -17,6 +18,20 @@ import ( "github.com/Snider/Core/pkg/crypt/openpgp" ) +// HandleIPCEvents processes IPC messages for the crypt service. +func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { + switch msg.(type) { + case core.ActionServiceStartup: + // Crypt is stateless, no startup needed. + return nil + default: + if c.App != nil && c.App.Logger != nil { + c.App.Logger.Debug("Crypt: Unhandled message type", "type", fmt.Sprintf("%T", msg)) + } + } + return nil +} + // Options holds configuration for the crypt service. type Options struct{} diff --git a/pkg/crypt/crypt_test.go b/pkg/crypt/crypt_test.go index 2cde507..3707f26 100644 --- a/pkg/crypt/crypt_test.go +++ b/pkg/crypt/crypt_test.go @@ -1,20 +1,366 @@ package crypt import ( + "bytes" "testing" + "github.com/Snider/Core/pkg/core" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestHash(t *testing.T) { - s := &Service{} - payload := "hello" - hash := s.Hash(LTHN, payload) - assert.NotEmpty(t, hash) +// --- Constructor Tests --- + +func TestNew(t *testing.T) { + t.Run("creates service successfully", func(t *testing.T) { + service, err := New() + assert.NoError(t, err) + assert.NotNil(t, service) + }) + + t.Run("returns independent instances", func(t *testing.T) { + service1, err1 := New() + service2, err2 := New() + assert.NoError(t, err1) + assert.NoError(t, err2) + assert.NotSame(t, service1, service2) + }) } -func TestLuhn(t *testing.T) { - s := &Service{} - assert.True(t, s.Luhn("79927398713")) - assert.False(t, s.Luhn("79927398714")) +func TestRegister(t *testing.T) { + t.Run("registers with core successfully", func(t *testing.T) { + coreInstance, err := core.New() + require.NoError(t, err) + + service, err := Register(coreInstance) + require.NoError(t, err) + assert.NotNil(t, service) + }) + + t.Run("returns Service type with Runtime", func(t *testing.T) { + coreInstance, err := core.New() + require.NoError(t, err) + + service, err := Register(coreInstance) + require.NoError(t, err) + + cryptService, ok := service.(*Service) + assert.True(t, ok) + assert.NotNil(t, cryptService.Runtime) + }) +} + +// --- Hash Tests --- + +func TestHash(t *testing.T) { + s, _ := New() + + t.Run("LTHN hash", func(t *testing.T) { + hash := s.Hash(LTHN, "hello") + assert.NotEmpty(t, hash) + // LTHN hash should be consistent + hash2 := s.Hash(LTHN, "hello") + assert.Equal(t, hash, hash2) + }) + + t.Run("SHA512 hash", func(t *testing.T) { + hash := s.Hash(SHA512, "hello") + // Known SHA512 hash for "hello" + expected := "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043" + assert.Equal(t, expected, hash) + }) + + t.Run("SHA256 hash", func(t *testing.T) { + hash := s.Hash(SHA256, "hello") + // Known SHA256 hash for "hello" + expected := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + assert.Equal(t, expected, hash) + }) + + t.Run("SHA1 hash", func(t *testing.T) { + hash := s.Hash(SHA1, "hello") + // Known SHA1 hash for "hello" + expected := "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d" + assert.Equal(t, expected, hash) + }) + + t.Run("MD5 hash", func(t *testing.T) { + hash := s.Hash(MD5, "hello") + // Known MD5 hash for "hello" + expected := "5d41402abc4b2a76b9719d911017c592" + assert.Equal(t, expected, hash) + }) + + t.Run("default falls back to SHA256", func(t *testing.T) { + hash := s.Hash("unknown", "hello") + sha256Hash := s.Hash(SHA256, "hello") + assert.Equal(t, sha256Hash, hash) + }) + + t.Run("empty string hash", func(t *testing.T) { + hash := s.Hash(SHA256, "") + // Known SHA256 hash for empty string + expected := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + assert.Equal(t, expected, hash) + }) + + t.Run("hash with special characters", func(t *testing.T) { + hash := s.Hash(SHA256, "hello!@#$%^&*()") + assert.NotEmpty(t, hash) + assert.Len(t, hash, 64) // SHA256 produces 64 hex chars + }) + + t.Run("hash with unicode", func(t *testing.T) { + hash := s.Hash(SHA256, "你好世界") + assert.NotEmpty(t, hash) + assert.Len(t, hash, 64) + }) + + t.Run("hash consistency", func(t *testing.T) { + payload := "test payload for consistency check" + for _, hashType := range []HashType{LTHN, SHA512, SHA256, SHA1, MD5} { + hash1 := s.Hash(hashType, payload) + hash2 := s.Hash(hashType, payload) + assert.Equal(t, hash1, hash2, "Hash should be consistent for %s", hashType) + } + }) +} + +// --- Luhn Tests --- + +func TestLuhn(t *testing.T) { + s, _ := New() + + t.Run("valid Luhn numbers", func(t *testing.T) { + validNumbers := []string{ + "79927398713", + "4532015112830366", // Visa test number + "6011514433546201", // Discover test number + "371449635398431", // Amex test number + "30569309025904", // Diners Club test number + } + for _, num := range validNumbers { + assert.True(t, s.Luhn(num), "Expected %s to be valid", num) + } + }) + + t.Run("invalid Luhn numbers", func(t *testing.T) { + invalidNumbers := []string{ + "79927398714", + "1234567890", + "1111111111", + "1234567891", + } + for _, num := range invalidNumbers { + assert.False(t, s.Luhn(num), "Expected %s to be invalid", num) + } + }) + + t.Run("all zeros is valid", func(t *testing.T) { + // All zeros: each digit contributes 0, sum=0, 0%10==0 + assert.True(t, s.Luhn("0000000000")) + }) + + t.Run("handles spaces", func(t *testing.T) { + // Same number with and without spaces should give same result + assert.True(t, s.Luhn("7992 7398 713")) + assert.True(t, s.Luhn("4532 0151 1283 0366")) + }) + + t.Run("non-digit characters return false", func(t *testing.T) { + assert.False(t, s.Luhn("1234abcd5678")) + assert.False(t, s.Luhn("12-34-56-78")) + assert.False(t, s.Luhn("1234.5678")) + }) + + t.Run("empty string", func(t *testing.T) { + // Empty string: sum=0, 0%10==0, so it returns true + assert.True(t, s.Luhn("")) + }) + + t.Run("single digit", func(t *testing.T) { + assert.True(t, s.Luhn("0")) + assert.False(t, s.Luhn("1")) + }) +} + +// --- Fletcher Checksum Tests --- + +func TestFletcher16(t *testing.T) { + s, _ := New() + + t.Run("basic checksum", func(t *testing.T) { + checksum := s.Fletcher16("hello") + assert.NotZero(t, checksum) + }) + + t.Run("empty string", func(t *testing.T) { + checksum := s.Fletcher16("") + assert.Equal(t, uint16(0), checksum) + }) + + t.Run("consistency", func(t *testing.T) { + checksum1 := s.Fletcher16("test data") + checksum2 := s.Fletcher16("test data") + assert.Equal(t, checksum1, checksum2) + }) + + t.Run("different inputs produce different checksums", func(t *testing.T) { + checksum1 := s.Fletcher16("hello") + checksum2 := s.Fletcher16("world") + assert.NotEqual(t, checksum1, checksum2) + }) + + t.Run("known value", func(t *testing.T) { + // "abcde" has a known Fletcher-16 checksum + checksum := s.Fletcher16("abcde") + assert.NotZero(t, checksum) + }) +} + +func TestFletcher32(t *testing.T) { + s, _ := New() + + t.Run("basic checksum", func(t *testing.T) { + checksum := s.Fletcher32("hello") + assert.NotZero(t, checksum) + }) + + t.Run("empty string", func(t *testing.T) { + checksum := s.Fletcher32("") + assert.Equal(t, uint32(0), checksum) + }) + + t.Run("consistency", func(t *testing.T) { + checksum1 := s.Fletcher32("test data") + checksum2 := s.Fletcher32("test data") + assert.Equal(t, checksum1, checksum2) + }) + + t.Run("different inputs produce different checksums", func(t *testing.T) { + checksum1 := s.Fletcher32("hello") + checksum2 := s.Fletcher32("world") + assert.NotEqual(t, checksum1, checksum2) + }) + + t.Run("handles odd-length input", func(t *testing.T) { + // Odd length input should be padded + checksum := s.Fletcher32("abc") + assert.NotZero(t, checksum) + }) + + t.Run("handles even-length input", func(t *testing.T) { + checksum := s.Fletcher32("abcd") + assert.NotZero(t, checksum) + }) +} + +func TestFletcher64(t *testing.T) { + s, _ := New() + + t.Run("basic checksum", func(t *testing.T) { + checksum := s.Fletcher64("hello") + assert.NotZero(t, checksum) + }) + + t.Run("empty string", func(t *testing.T) { + checksum := s.Fletcher64("") + assert.Equal(t, uint64(0), checksum) + }) + + t.Run("consistency", func(t *testing.T) { + checksum1 := s.Fletcher64("test data") + checksum2 := s.Fletcher64("test data") + assert.Equal(t, checksum1, checksum2) + }) + + t.Run("different inputs produce different checksums", func(t *testing.T) { + checksum1 := s.Fletcher64("hello") + checksum2 := s.Fletcher64("world") + assert.NotEqual(t, checksum1, checksum2) + }) + + t.Run("handles various input lengths", func(t *testing.T) { + // Test padding for different lengths + for i := 1; i <= 8; i++ { + input := string(make([]byte, i)) + checksum := s.Fletcher64(input) + // Just verify it doesn't panic + _ = checksum + } + }) + + t.Run("long input", func(t *testing.T) { + // Use actual text content, not null bytes + longInput := "" + for i := 0; i < 100; i++ { + longInput += "test data " + } + checksum := s.Fletcher64(longInput) + assert.NotZero(t, checksum) + }) +} + +// --- HashType Constants Tests --- + +func TestHashTypeConstants(t *testing.T) { + t.Run("constants have expected values", func(t *testing.T) { + assert.Equal(t, HashType("lthn"), LTHN) + assert.Equal(t, HashType("sha512"), SHA512) + assert.Equal(t, HashType("sha256"), SHA256) + assert.Equal(t, HashType("sha1"), SHA1) + assert.Equal(t, HashType("md5"), MD5) + }) +} + +// --- PGP Tests (basic, detailed tests in openpgp package) --- + +func TestEncryptPGP(t *testing.T) { + t.Run("requires valid key paths", func(t *testing.T) { + s, _ := New() + var buf bytes.Buffer + + // Should fail with invalid path + _, err := s.EncryptPGP(&buf, "/nonexistent/path", "test data", nil, nil) + assert.Error(t, err) + }) +} + +func TestDecryptPGP(t *testing.T) { + t.Run("requires valid key paths", func(t *testing.T) { + s, _ := New() + + // Should fail with invalid path + _, err := s.DecryptPGP("/nonexistent/path", "encrypted data", "passphrase", nil) + assert.Error(t, err) + }) +} + +// --- HandleIPCEvents Tests --- + +func TestHandleIPCEvents(t *testing.T) { + t.Run("handles ActionServiceStartup", func(t *testing.T) { + coreInstance, err := core.New() + require.NoError(t, err) + + serviceAny, err := Register(coreInstance) + require.NoError(t, err) + + s := serviceAny.(*Service) + err = s.HandleIPCEvents(coreInstance, core.ActionServiceStartup{}) + assert.NoError(t, err) + }) + + t.Run("handles unknown message type", func(t *testing.T) { + coreInstance, err := core.New() + require.NoError(t, err) + + serviceAny, err := Register(coreInstance) + require.NoError(t, err) + + s := serviceAny.(*Service) + // Pass an arbitrary type as unknown message + err = s.HandleIPCEvents(coreInstance, "unknown message") + assert.NoError(t, err) + }) } diff --git a/pkg/display/assets/apptray.png b/pkg/display/assets/apptray.png new file mode 100644 index 0000000..0778fc6 Binary files /dev/null and b/pkg/display/assets/apptray.png differ diff --git a/pkg/display/display_test.go b/pkg/display/display_test.go index 9f135f0..b70b956 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -6,39 +6,298 @@ import ( "github.com/Snider/Core/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/wailsapp/wails/v3/pkg/application" ) // newTestCore creates a new core instance with essential services for testing. func newTestCore(t *testing.T) *core.Core { - // We need a real wails app for the display service to function. - // This setup will be more complex than for other services. - // For now, we can use a simplified core instance. coreInstance, err := core.New() require.NoError(t, err) return coreInstance } func TestNew(t *testing.T) { - service, err := New() - assert.NoError(t, err) - assert.NotNil(t, service, "New() should return a non-nil service instance") + t.Run("creates service successfully", func(t *testing.T) { + service, err := New() + assert.NoError(t, err) + assert.NotNil(t, service, "New() should return a non-nil service instance") + }) + + t.Run("returns independent instances", func(t *testing.T) { + service1, err1 := New() + service2, err2 := New() + assert.NoError(t, err1) + assert.NoError(t, err2) + assert.NotSame(t, service1, service2, "New() should return different instances") + }) } func TestRegister(t *testing.T) { - coreInstance := newTestCore(t) - service, err := Register(coreInstance) - require.NoError(t, err) - assert.NotNil(t, service, "Register() should return a non-nil service instance") -} + t.Run("registers with core successfully", func(t *testing.T) { + coreInstance := newTestCore(t) + service, err := Register(coreInstance) + require.NoError(t, err) + assert.NotNil(t, service, "Register() should return a non-nil service instance") + }) -func TestOpenWindow(t *testing.T) { - // This test is complex to set up properly without a running Wails application. - // A true functional test would require a more elaborate test harness that - // can initialize the Wails runtime. + t.Run("returns Service type", func(t *testing.T) { + coreInstance := newTestCore(t) + service, err := Register(coreInstance) + require.NoError(t, err) - // For now, we can perform a basic smoke test. - t.Run("basic window open smoke test", func(t *testing.T) { - // Skipping this test for now as it requires a running app instance. - t.Skip("Skipping OpenWindow test as it requires a running Wails application instance.") + displayService, ok := service.(*Service) + assert.True(t, ok, "Register() should return *Service type") + assert.NotNil(t, displayService.Runtime, "Runtime should be initialized") + }) +} + +func TestServiceName(t *testing.T) { + service, err := New() + require.NoError(t, err) + + name := service.ServiceName() + assert.Equal(t, "github.com/Snider/Core/display", name) +} + +// --- Window Option Tests --- + +func TestWindowName(t *testing.T) { + t.Run("sets window name", func(t *testing.T) { + opt := WindowName("test-window") + window := &Window{} + + err := opt(window) + assert.NoError(t, err) + assert.Equal(t, "test-window", window.Name) + }) + + t.Run("sets empty name", func(t *testing.T) { + opt := WindowName("") + window := &Window{} + + err := opt(window) + assert.NoError(t, err) + assert.Equal(t, "", window.Name) + }) +} + +func TestWindowTitle(t *testing.T) { + t.Run("sets window title", func(t *testing.T) { + opt := WindowTitle("My Application") + window := &Window{} + + err := opt(window) + assert.NoError(t, err) + assert.Equal(t, "My Application", window.Title) + }) + + t.Run("sets title with special characters", func(t *testing.T) { + opt := WindowTitle("App - v1.0 (Beta)") + window := &Window{} + + err := opt(window) + assert.NoError(t, err) + assert.Equal(t, "App - v1.0 (Beta)", window.Title) + }) +} + +func TestWindowURL(t *testing.T) { + t.Run("sets window URL", func(t *testing.T) { + opt := WindowURL("/dashboard") + window := &Window{} + + err := opt(window) + assert.NoError(t, err) + assert.Equal(t, "/dashboard", window.URL) + }) + + t.Run("sets full URL", func(t *testing.T) { + opt := WindowURL("https://example.com/page") + window := &Window{} + + err := opt(window) + assert.NoError(t, err) + assert.Equal(t, "https://example.com/page", window.URL) + }) +} + +func TestWindowWidth(t *testing.T) { + t.Run("sets window width", func(t *testing.T) { + opt := WindowWidth(1024) + window := &Window{} + + err := opt(window) + assert.NoError(t, err) + assert.Equal(t, 1024, window.Width) + }) + + t.Run("sets zero width", func(t *testing.T) { + opt := WindowWidth(0) + window := &Window{} + + err := opt(window) + assert.NoError(t, err) + assert.Equal(t, 0, window.Width) + }) + + t.Run("sets large width", func(t *testing.T) { + opt := WindowWidth(3840) + window := &Window{} + + err := opt(window) + assert.NoError(t, err) + assert.Equal(t, 3840, window.Width) + }) +} + +func TestWindowHeight(t *testing.T) { + t.Run("sets window height", func(t *testing.T) { + opt := WindowHeight(768) + window := &Window{} + + err := opt(window) + assert.NoError(t, err) + assert.Equal(t, 768, window.Height) + }) + + t.Run("sets zero height", func(t *testing.T) { + opt := WindowHeight(0) + window := &Window{} + + err := opt(window) + assert.NoError(t, err) + assert.Equal(t, 0, window.Height) + }) +} + +func TestApplyOptions(t *testing.T) { + t.Run("applies no options", func(t *testing.T) { + window := applyOptions() + assert.NotNil(t, window) + assert.Equal(t, "", window.Name) + assert.Equal(t, "", window.Title) + assert.Equal(t, 0, window.Width) + assert.Equal(t, 0, window.Height) + }) + + t.Run("applies single option", func(t *testing.T) { + window := applyOptions(WindowTitle("Test")) + assert.NotNil(t, window) + assert.Equal(t, "Test", window.Title) + }) + + t.Run("applies multiple options", func(t *testing.T) { + window := applyOptions( + WindowName("main"), + WindowTitle("My App"), + WindowURL("/home"), + WindowWidth(1280), + WindowHeight(720), + ) + + assert.NotNil(t, window) + assert.Equal(t, "main", window.Name) + assert.Equal(t, "My App", window.Title) + assert.Equal(t, "/home", window.URL) + assert.Equal(t, 1280, window.Width) + assert.Equal(t, 720, window.Height) + }) + + t.Run("handles nil options slice", func(t *testing.T) { + window := applyOptions(nil...) + assert.NotNil(t, window) + }) + + t.Run("applies options in order", func(t *testing.T) { + // Later options should override earlier ones + window := applyOptions( + WindowTitle("First"), + WindowTitle("Second"), + ) + + assert.NotNil(t, window) + assert.Equal(t, "Second", window.Title) + }) +} + +// --- ActionOpenWindow Tests --- + +func TestActionOpenWindow(t *testing.T) { + t.Run("creates action with options", func(t *testing.T) { + action := ActionOpenWindow{ + WebviewWindowOptions: application.WebviewWindowOptions{ + Name: "test", + Title: "Test Window", + Width: 800, + Height: 600, + }, + } + + assert.Equal(t, "test", action.Name) + assert.Equal(t, "Test Window", action.Title) + assert.Equal(t, 800, action.Width) + assert.Equal(t, 600, action.Height) + }) +} + +// --- Integration Tests (require Wails runtime) --- + +func TestOpenWindow(t *testing.T) { + t.Run("requires Wails runtime", func(t *testing.T) { + t.Skip("Skipping OpenWindow test - requires running Wails application instance") + }) +} + +func TestNewWithStruct(t *testing.T) { + t.Run("requires Wails runtime", func(t *testing.T) { + t.Skip("Skipping NewWithStruct test - requires running Wails application instance") + }) +} + +func TestNewWithOptions(t *testing.T) { + t.Run("requires Wails runtime", func(t *testing.T) { + t.Skip("Skipping NewWithOptions test - requires running Wails application instance") + }) +} + +func TestNewWithURL(t *testing.T) { + t.Run("requires Wails runtime", func(t *testing.T) { + t.Skip("Skipping NewWithURL test - requires running Wails application instance") + }) +} + +func TestServiceStartup(t *testing.T) { + t.Run("requires Wails runtime", func(t *testing.T) { + t.Skip("Skipping ServiceStartup test - requires running Wails application instance") + }) +} + +func TestSelectDirectory(t *testing.T) { + t.Run("requires Wails runtime", func(t *testing.T) { + t.Skip("Skipping SelectDirectory test - requires running Wails application instance") + }) +} + +func TestShowEnvironmentDialog(t *testing.T) { + t.Run("requires Wails runtime", func(t *testing.T) { + t.Skip("Skipping ShowEnvironmentDialog test - requires running Wails application instance") + }) +} + +func TestHandleIPCEvents(t *testing.T) { + t.Run("requires Wails runtime for full test", func(t *testing.T) { + t.Skip("Skipping HandleIPCEvents test - requires running Wails application instance") + }) +} + +func TestBuildMenu(t *testing.T) { + t.Run("requires Wails runtime", func(t *testing.T) { + t.Skip("Skipping buildMenu test - requires running Wails application instance") + }) +} + +func TestSystemTray(t *testing.T) { + t.Run("requires Wails runtime", func(t *testing.T) { + t.Skip("Skipping systemTray test - requires running Wails application instance") }) } diff --git a/pkg/display/menu.go b/pkg/display/menu.go index 63bb0d1..b1238d1 100644 --- a/pkg/display/menu.go +++ b/pkg/display/menu.go @@ -1,7 +1,9 @@ package display import ( + "fmt" "runtime" + "strings" "github.com/wailsapp/wails/v3/pkg/application" ) @@ -17,8 +19,12 @@ func (s *Service) buildMenu() { appMenu.AddRole(application.EditMenu) workspace := appMenu.AddSubmenu("Workspace") - workspace.Add("New").OnClick(func(ctx *application.Context) { /* TODO */ }) - workspace.Add("List").OnClick(func(ctx *application.Context) { /* TODO */ }) + workspace.Add("New...").OnClick(func(ctx *application.Context) { + s.handleNewWorkspace() + }) + workspace.Add("List").OnClick(func(ctx *application.Context) { + s.handleListWorkspaces() + }) // Add brand-specific menu items //if s.brand == DeveloperHub { @@ -30,3 +36,56 @@ func (s *Service) buildMenu() { s.Core().App.Menu.Set(appMenu) } + +// handleNewWorkspace opens a window for creating a new workspace. +func (s *Service) handleNewWorkspace() { + // Open a dedicated window for workspace creation + // The frontend at /workspace/new handles the form + opts := application.WebviewWindowOptions{ + Name: "workspace-new", + Title: "New Workspace", + Width: 500, + Height: 400, + URL: "/workspace/new", + } + s.Core().App.Window.NewWithOptions(opts) +} + +// handleListWorkspaces shows a dialog with available workspaces. +func (s *Service) handleListWorkspaces() { + // Get workspace service from core + ws := s.Core().Service("workspace") + if ws == nil { + dialog := s.Core().App.Dialog.Warning() + dialog.SetTitle("Workspace") + dialog.SetMessage("Workspace service not available") + dialog.Show() + return + } + + // Type assert to access ListWorkspaces method + lister, ok := ws.(interface{ ListWorkspaces() []string }) + if !ok { + dialog := s.Core().App.Dialog.Warning() + dialog.SetTitle("Workspace") + dialog.SetMessage("Unable to list workspaces") + dialog.Show() + return + } + + workspaces := lister.ListWorkspaces() + + var message string + if len(workspaces) == 0 { + message = "No workspaces found.\n\nUse Workspace → New to create one." + } else { + message = fmt.Sprintf("Available Workspaces (%d):\n\n%s", + len(workspaces), + strings.Join(workspaces, "\n")) + } + + dialog := s.Core().App.Dialog.Info() + dialog.SetTitle("Workspaces") + dialog.SetMessage(message) + dialog.Show() +} diff --git a/pkg/display/tray.go b/pkg/display/tray.go index b96f2ef..697bb63 100644 --- a/pkg/display/tray.go +++ b/pkg/display/tray.go @@ -1,26 +1,33 @@ package display import ( - _ "embed" + "embed" + "runtime" "github.com/wailsapp/wails/v3/pkg/application" ) -// setupTray configures and creates the system tray icon and menu. +//go:embed assets/apptray.png +var assets embed.FS + +// systemTray configures and creates the system tray icon and menu. func (s *Service) systemTray() { systray := s.Core().App.SystemTray.New() systray.SetTooltip("Core") systray.SetLabel("Core") - //appTrayIcon, _ := d.assets.ReadFile("assets/apptray.png") - // - //if runtime.GOOS == "darwin" { - // systray.SetTemplateIcon(appTrayIcon) - //} else { - // // Support for light/dark mode icons - // systray.SetDarkModeIcon(appTrayIcon) - // systray.SetIcon(appTrayIcon) - //} + + // Load and set tray icon + appTrayIcon, err := assets.ReadFile("assets/apptray.png") + if err == nil { + if runtime.GOOS == "darwin" { + systray.SetTemplateIcon(appTrayIcon) + } else { + // Support for light/dark mode icons + systray.SetDarkModeIcon(appTrayIcon) + systray.SetIcon(appTrayIcon) + } + } // Create a hidden window for the system tray menu to interact with trayWindow, _ := s.NewWithStruct(&Window{ Name: "system-tray", diff --git a/pkg/display/window.go b/pkg/display/window.go index 6dc0a8a..fe788e5 100644 --- a/pkg/display/window.go +++ b/pkg/display/window.go @@ -87,7 +87,3 @@ func (s *Service) SelectDirectory() (string, error) { dialog.SetTitle("Select Project Directory") return dialog.PromptForSingleSelection() } - -var instance *Window - -func (s *Service) Window() *Window { return instance } diff --git a/pkg/i18n/i18n_test.go b/pkg/i18n/i18n_test.go new file mode 100644 index 0000000..a3889a0 --- /dev/null +++ b/pkg/i18n/i18n_test.go @@ -0,0 +1,185 @@ +package i18n + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/language" +) + +func TestNew(t *testing.T) { + t.Run("creates service successfully", func(t *testing.T) { + service, err := New() + require.NoError(t, err) + assert.NotNil(t, service) + assert.NotNil(t, service.bundle) + assert.NotEmpty(t, service.availableLangs) + }) + + t.Run("loads all available languages", func(t *testing.T) { + service, err := New() + require.NoError(t, err) + + // Should have loaded multiple languages from locales/ + assert.GreaterOrEqual(t, len(service.availableLangs), 2) + }) +} + +func TestSetLanguage(t *testing.T) { + t.Run("sets English successfully", func(t *testing.T) { + service, err := New() + require.NoError(t, err) + + err = service.SetLanguage("en") + assert.NoError(t, err) + assert.NotNil(t, service.localizer) + }) + + t.Run("sets Spanish successfully", func(t *testing.T) { + service, err := New() + require.NoError(t, err) + + err = service.SetLanguage("es") + assert.NoError(t, err) + assert.NotNil(t, service.localizer) + }) + + t.Run("sets German successfully", func(t *testing.T) { + service, err := New() + require.NoError(t, err) + + err = service.SetLanguage("de") + assert.NoError(t, err) + assert.NotNil(t, service.localizer) + }) + + t.Run("handles language variants", func(t *testing.T) { + service, err := New() + require.NoError(t, err) + + // en-US should match to en + err = service.SetLanguage("en-US") + assert.NoError(t, err) + }) + + t.Run("handles unknown language by matching closest", func(t *testing.T) { + service, err := New() + require.NoError(t, err) + + // Unknown languages may fall back to a default match + // The matcher uses confidence levels, so many tags will match something + err = service.SetLanguage("tlh") // Klingon + // May or may not error depending on matcher confidence + if err != nil { + assert.Contains(t, err.Error(), "unsupported language") + } + }) +} + +func TestTranslate(t *testing.T) { + t.Run("translates English message", func(t *testing.T) { + service, err := New() + require.NoError(t, err) + require.NoError(t, service.SetLanguage("en")) + + result := service.Translate("menu.settings") + assert.Equal(t, "Settings", result) + }) + + t.Run("translates Spanish message", func(t *testing.T) { + service, err := New() + require.NoError(t, err) + require.NoError(t, service.SetLanguage("es")) + + result := service.Translate("menu.settings") + assert.Equal(t, "Ajustes", result) + }) + + t.Run("returns message ID for missing translation", func(t *testing.T) { + service, err := New() + require.NoError(t, err) + require.NoError(t, service.SetLanguage("en")) + + result := service.Translate("nonexistent.message.id") + assert.Equal(t, "nonexistent.message.id", result) + }) + + t.Run("translates multiple messages correctly", func(t *testing.T) { + service, err := New() + require.NoError(t, err) + require.NoError(t, service.SetLanguage("en")) + + assert.Equal(t, "Dashboard", service.Translate("menu.dashboard")) + assert.Equal(t, "Help", service.Translate("menu.help")) + assert.Equal(t, "Search", service.Translate("app.core.ui.search")) + }) + + t.Run("language switch changes translations", func(t *testing.T) { + service, err := New() + require.NoError(t, err) + + // Start with English + require.NoError(t, service.SetLanguage("en")) + assert.Equal(t, "Search", service.Translate("app.core.ui.search")) + + // Switch to Spanish + require.NoError(t, service.SetLanguage("es")) + assert.Equal(t, "Buscar", service.Translate("app.core.ui.search")) + + // Switch back to English + require.NoError(t, service.SetLanguage("en")) + assert.Equal(t, "Search", service.Translate("app.core.ui.search")) + }) +} + +func TestGetAvailableLanguages(t *testing.T) { + t.Run("returns available languages", func(t *testing.T) { + langs, err := getAvailableLanguages() + require.NoError(t, err) + assert.NotEmpty(t, langs) + + // Should include at least English + langStrings := make([]string, len(langs)) + for i, l := range langs { + langStrings[i] = l.String() + } + assert.Contains(t, langStrings, "en") + }) +} + +func TestDetectLanguage(t *testing.T) { + t.Run("returns empty for empty LANG env", func(t *testing.T) { + // Save and clear LANG + t.Setenv("LANG", "") + + service, err := New() + require.NoError(t, err) + + detected, err := detectLanguage(service.availableLangs) + assert.NoError(t, err) + assert.Empty(t, detected) + }) + + t.Run("returns empty for empty supported list", func(t *testing.T) { + t.Setenv("LANG", "en_US.UTF-8") + + detected, err := detectLanguage([]language.Tag{}) + assert.NoError(t, err) + assert.Empty(t, detected) + }) + + t.Run("detects language from LANG env", func(t *testing.T) { + t.Setenv("LANG", "es_ES.UTF-8") + + service, err := New() + require.NoError(t, err) + + detected, err := detectLanguage(service.availableLangs) + assert.NoError(t, err) + // Should detect Spanish or a close variant + if detected != "" { + assert.Contains(t, detected, "es") + } + }) +} diff --git a/pkg/io/client_test.go b/pkg/io/client_test.go index ad1f4d9..4434420 100644 --- a/pkg/io/client_test.go +++ b/pkg/io/client_test.go @@ -6,6 +6,77 @@ import ( "github.com/stretchr/testify/assert" ) +// --- MockMedium Tests --- + +func TestNewMockMedium(t *testing.T) { + m := NewMockMedium() + assert.NotNil(t, m) + assert.NotNil(t, m.Files) + assert.NotNil(t, m.Dirs) + assert.Empty(t, m.Files) + assert.Empty(t, m.Dirs) +} + +func TestMockMedium_Read(t *testing.T) { + t.Run("reads existing file", func(t *testing.T) { + m := NewMockMedium() + m.Files["test.txt"] = "hello world" + content, err := m.Read("test.txt") + assert.NoError(t, err) + assert.Equal(t, "hello world", content) + }) + + t.Run("returns error for non-existent file", func(t *testing.T) { + m := NewMockMedium() + _, err := m.Read("nonexistent.txt") + assert.Error(t, err) + }) +} + +func TestMockMedium_Write(t *testing.T) { + m := NewMockMedium() + err := m.Write("test.txt", "content") + assert.NoError(t, err) + assert.Equal(t, "content", m.Files["test.txt"]) + + // Overwrite existing file + err = m.Write("test.txt", "new content") + assert.NoError(t, err) + assert.Equal(t, "new content", m.Files["test.txt"]) +} + +func TestMockMedium_EnsureDir(t *testing.T) { + m := NewMockMedium() + err := m.EnsureDir("/path/to/dir") + assert.NoError(t, err) + assert.True(t, m.Dirs["/path/to/dir"]) +} + +func TestMockMedium_IsFile(t *testing.T) { + m := NewMockMedium() + m.Files["exists.txt"] = "content" + + assert.True(t, m.IsFile("exists.txt")) + assert.False(t, m.IsFile("nonexistent.txt")) +} + +func TestMockMedium_FileGet(t *testing.T) { + m := NewMockMedium() + m.Files["test.txt"] = "content" + content, err := m.FileGet("test.txt") + assert.NoError(t, err) + assert.Equal(t, "content", content) +} + +func TestMockMedium_FileSet(t *testing.T) { + m := NewMockMedium() + err := m.FileSet("test.txt", "content") + assert.NoError(t, err) + assert.Equal(t, "content", m.Files["test.txt"]) +} + +// --- Wrapper Function Tests --- + func TestRead(t *testing.T) { m := NewMockMedium() m.Files["test.txt"] = "hello" @@ -21,11 +92,55 @@ func TestWrite(t *testing.T) { assert.Equal(t, "hello", m.Files["test.txt"]) } -func TestCopy(t *testing.T) { - source := NewMockMedium() - dest := NewMockMedium() - source.Files["test.txt"] = "hello" - err := Copy(source, "test.txt", dest, "test.txt") +func TestEnsureDir(t *testing.T) { + m := NewMockMedium() + err := EnsureDir(m, "/my/dir") assert.NoError(t, err) - assert.Equal(t, "hello", dest.Files["test.txt"]) + assert.True(t, m.Dirs["/my/dir"]) +} + +func TestIsFile(t *testing.T) { + m := NewMockMedium() + m.Files["exists.txt"] = "content" + + assert.True(t, IsFile(m, "exists.txt")) + assert.False(t, IsFile(m, "nonexistent.txt")) +} + +func TestCopy(t *testing.T) { + t.Run("copies file between mediums", func(t *testing.T) { + source := NewMockMedium() + dest := NewMockMedium() + source.Files["test.txt"] = "hello" + err := Copy(source, "test.txt", dest, "test.txt") + assert.NoError(t, err) + assert.Equal(t, "hello", dest.Files["test.txt"]) + }) + + t.Run("copies to different path", func(t *testing.T) { + source := NewMockMedium() + dest := NewMockMedium() + source.Files["original.txt"] = "content" + err := Copy(source, "original.txt", dest, "copied.txt") + assert.NoError(t, err) + assert.Equal(t, "content", dest.Files["copied.txt"]) + }) + + t.Run("returns error for non-existent source", func(t *testing.T) { + source := NewMockMedium() + dest := NewMockMedium() + err := Copy(source, "nonexistent.txt", dest, "dest.txt") + assert.Error(t, err) + }) +} + +// --- Local Global Tests --- + +func TestLocalGlobal(t *testing.T) { + // io.Local should be initialized by init() + assert.NotNil(t, Local, "io.Local should be initialized") + + // Should be able to use it as a Medium + var m Medium = Local + assert.NotNil(t, m) } diff --git a/pkg/io/io.go b/pkg/io/io.go index df02342..d268586 100644 --- a/pkg/io/io.go +++ b/pkg/io/io.go @@ -1,5 +1,9 @@ package io +import ( + "github.com/Snider/Core/pkg/io/local" +) + // Medium defines the standard interface for a storage backend. // This allows for different implementations (e.g., local disk, S3, SFTP) // to be used interchangeably. @@ -23,5 +27,15 @@ type Medium interface { FileSet(path, content string) error } -// Pre-initialized, sandboxed medium for the local filesystem. +// Local is a pre-initialized medium for the local filesystem. +// It uses "/" as root, providing unsandboxed access to the filesystem. +// For sandboxed access, create a new local.Medium with a specific root path. var Local Medium + +func init() { + var err error + Local, err = local.New("/") + if err != nil { + panic("io: failed to initialize Local medium: " + err.Error()) + } +} diff --git a/pkg/io/local/client_test.go b/pkg/io/local/client_test.go index ff3dce7..329eba5 100644 --- a/pkg/io/local/client_test.go +++ b/pkg/io/local/client_test.go @@ -152,3 +152,43 @@ func TestIsFile(t *testing.T) { // Test with path traversal attempt assert.False(t, medium.IsFile("../bad_file.txt")) } + +func TestFileGetFileSet(t *testing.T) { + testRoot, err := os.MkdirTemp("", "local_fileget_fileset_test") + assert.NoError(t, err) + defer os.RemoveAll(testRoot) + + medium, err := New(testRoot) + assert.NoError(t, err) + + fileName := "data.txt" + content := "Hello, FileGet/FileSet!" + + // Test FileSet + err = medium.FileSet(fileName, content) + assert.NoError(t, err) + + // Verify file was written + readContent, err := os.ReadFile(filepath.Join(testRoot, fileName)) + assert.NoError(t, err) + assert.Equal(t, content, string(readContent)) + + // Test FileGet + gotContent, err := medium.FileGet(fileName) + assert.NoError(t, err) + assert.Equal(t, content, gotContent) + + // Test FileGet on non-existent file + _, err = medium.FileGet("nonexistent.txt") + assert.Error(t, err) + + // Test FileSet with path traversal attempt + err = medium.FileSet("../bad.txt", "malicious") + assert.Error(t, err) + assert.Contains(t, err.Error(), "path traversal attempt detected") + + // Test FileGet with path traversal attempt + _, err = medium.FileGet("../bad.txt") + assert.Error(t, err) + assert.Contains(t, err.Error(), "path traversal attempt detected") +} diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 85cab14..0f492f7 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -9,6 +9,7 @@ import ( "github.com/Snider/Core/pkg/display" "github.com/Snider/Core/pkg/help" "github.com/Snider/Core/pkg/i18n" + "github.com/Snider/Core/pkg/io" "github.com/Snider/Core/pkg/workspace" // Import the ABSTRACT contracts (interfaces). "github.com/Snider/Core/pkg/core" @@ -99,6 +100,6 @@ func New() (*Runtime, error) { "help": func() (any, error) { return help.New() }, "crypt": func() (any, error) { return crypt.New() }, "i18n": func() (any, error) { return i18n.New() }, - "workspace": func() (any, error) { return workspace.New() }, + "workspace": func() (any, error) { return workspace.New(io.Local) }, }) } diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index 8829051..495c776 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -10,6 +10,7 @@ import ( "github.com/Snider/Core/pkg/crypt" "github.com/Snider/Core/pkg/display" "github.com/Snider/Core/pkg/help" + "github.com/Snider/Core/pkg/io" "github.com/Snider/Core/pkg/workspace" ) @@ -62,7 +63,7 @@ func TestNewServiceInitializationError(t *testing.T) { "help": func() (any, error) { return help.New() }, "crypt": func() (any, error) { return crypt.New() }, "i18n": func() (any, error) { return nil, errors.New("i18n service failed to initialize") }, // This factory will fail - "workspace": func() (any, error) { return workspace.New() }, + "workspace": func() (any, error) { return workspace.New(io.Local) }, } runtime, err := newWithFactories(factories) diff --git a/pkg/workspace/local.go b/pkg/workspace/local.go deleted file mode 100644 index 27769f7..0000000 --- a/pkg/workspace/local.go +++ /dev/null @@ -1,41 +0,0 @@ -package workspace - -import "github.com/Snider/Core/pkg/io" - -// localMedium implements the Medium interface for the local disk. -type localMedium struct{} - -// NewLocalMedium creates a new instance of the local storage medium. -func NewLocalMedium() io.Medium { - return &localMedium{} -} - -// FileGet reads a file from the local disk. -func (m *localMedium) FileGet(path string) (string, error) { - return io.Read(io.Local, path) -} - -// FileSet writes a file to the local disk. -func (m *localMedium) FileSet(path, content string) error { - return io.Write(io.Local, path, content) -} - -// Read reads a file from the local disk. -func (m *localMedium) Read(path string) (string, error) { - return io.Read(io.Local, path) -} - -// Write writes a file to the local disk. -func (m *localMedium) Write(path, content string) error { - return io.Write(io.Local, path, content) -} - -// EnsureDir creates a directory on the local disk. -func (m *localMedium) EnsureDir(path string) error { - return io.EnsureDir(io.Local, path) -} - -// IsFile checks if a path exists and is a file on the local disk. -func (m *localMedium) IsFile(path string) bool { - return io.IsFile(io.Local, path) -} diff --git a/pkg/workspace/workspace.go b/pkg/workspace/workspace.go index 8de7b46..f14b0e3 100644 --- a/pkg/workspace/workspace.go +++ b/pkg/workspace/workspace.go @@ -10,6 +10,7 @@ import ( "github.com/Snider/Core/pkg/crypt/lthn" "github.com/Snider/Core/pkg/crypt/openpgp" "github.com/Snider/Core/pkg/io" + "github.com/Snider/Core/pkg/io/local" "github.com/wailsapp/wails/v3/pkg/application" ) @@ -46,18 +47,13 @@ func newWorkspaceService() (*Service, error) { // New is the constructor for static dependency injection. // It creates a Service instance without initializing the core.Runtime field. -// Dependencies are passed directly here. -func New() (*Service, error) { +// The medium parameter is required for file operations. +func New(medium io.Medium) (*Service, error) { s, err := newWorkspaceService() if err != nil { return nil, err } - //s.medium = medium - // Initialize the service after creation. - // Note: ServiceStartup will now get config from s.Runtime.Config() - //if err := s.ServiceStartup(context.Background(), application.ServiceOptions{}); err != nil { - // return nil, fmt.Errorf("workspace service startup failed: %w", err) - //} + s.medium = medium return s, nil } @@ -70,6 +66,18 @@ func Register(c *core.Core) (any, error) { return nil, err } s.Runtime = core.NewRuntime(c, Options{}) + + // Initialize the local medium for file operations + var workspaceDir string + if err := c.Config().Get("workspaceDir", &workspaceDir); err != nil { + return nil, fmt.Errorf("workspace: failed to get workspaceDir from config: %w", err) + } + medium, err := local.New(workspaceDir) + if err != nil { + return nil, fmt.Errorf("workspace: failed to create local medium: %w", err) + } + s.medium = medium + return s, nil } @@ -99,25 +107,24 @@ func (s *Service) getWorkspaceDir() (string, error) { // ServiceStartup initializes the service, loading the workspace list. func (s *Service) ServiceStartup(context.Context, application.ServiceOptions) error { - var err error workspaceDir, err := s.getWorkspaceDir() if err != nil { return err } + // Load existing workspace list if it exists listPath := filepath.Join(workspaceDir, listFile) - if listPath != "" { + if s.medium.IsFile(listPath) { + content, err := s.medium.FileGet(listPath) + if err != nil { + return fmt.Errorf("failed to read workspace list: %w", err) + } + if err := json.Unmarshal([]byte(content), &s.workspaceList); err != nil { + // Log warning but continue with empty list + fmt.Printf("Warning: could not parse workspace list: %v\n", err) + s.workspaceList = make(map[string]string) + } } - //if s.medium.IsFile(listPath) { - // content, err := s.medium.FileGet(listPath) - // if err != nil { - // return fmt.Errorf("failed to read workspace list: %w", err) - // } - // if err := json.Unmarshal([]byte(content), &s.workspaceList); err != nil { - // fmt.Printf("Warning: could not parse workspace list: %v\n", err) - // s.workspaceList = make(map[string]string) - // } - //} return s.SwitchWorkspace(defaultWorkspace) } @@ -187,9 +194,9 @@ func (s *Service) SwitchWorkspace(name string) error { } path := filepath.Join(workspaceDir, name) - //if err := s.medium.EnsureDir(path); err != nil { - // return fmt.Errorf("failed to ensure workspace directory exists: %w", err) - //} + if err := s.medium.EnsureDir(path); err != nil { + return fmt.Errorf("failed to ensure workspace directory exists: %w", err) + } s.activeWorkspace = &Workspace{ Name: name, @@ -216,3 +223,17 @@ func (s *Service) WorkspaceFileSet(filename, content string) error { path := filepath.Join(s.activeWorkspace.Path, filename) return s.medium.FileSet(path, content) } + +// ListWorkspaces returns the list of workspace IDs. +func (s *Service) ListWorkspaces() []string { + workspaces := make([]string, 0, len(s.workspaceList)) + for id := range s.workspaceList { + workspaces = append(workspaces, id) + } + return workspaces +} + +// ActiveWorkspace returns the currently active workspace, or nil if none is active. +func (s *Service) ActiveWorkspace() *Workspace { + return s.activeWorkspace +} diff --git a/pkg/workspace/workspace_test.go b/pkg/workspace/workspace_test.go index 3af293f..2826c1a 100644 --- a/pkg/workspace/workspace_test.go +++ b/pkg/workspace/workspace_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/Snider/Core/pkg/core" + "github.com/Snider/Core/pkg/io" "github.com/stretchr/testify/assert" "github.com/wailsapp/wails/v3/pkg/application" ) @@ -37,64 +38,19 @@ func (m *mockConfig) Set(key string, v any) error { return nil } -// MockMedium implements the Medium interface for testing purposes. -type MockMedium struct { - Files map[string]string - Dirs map[string]bool -} - -func NewMockMedium() *MockMedium { - return &MockMedium{ - Files: make(map[string]string), - Dirs: make(map[string]bool), - } -} - -func (m *MockMedium) FileGet(path string) (string, error) { - content, ok := m.Files[path] - if !ok { - return "", assert.AnError // Simulate file not found error - } - return content, nil -} - -func (m *MockMedium) FileSet(path, content string) error { - m.Files[path] = content - return nil -} - -func (m *MockMedium) EnsureDir(path string) error { - m.Dirs[path] = true - return nil -} - -func (m *MockMedium) IsFile(path string) bool { - _, exists := m.Files[path] - return exists -} - -func (m *MockMedium) Read(path string) (string, error) { - return m.FileGet(path) -} - -func (m *MockMedium) Write(path, content string) error { - return m.FileSet(path, content) -} - // newTestService creates a workspace service instance with mocked dependencies. -func newTestService(t *testing.T, workspaceDir string) (*Service, *MockMedium) { +func newTestService(t *testing.T, workspaceDir string) (*Service, *io.MockMedium) { coreInstance, err := core.New() assert.NoError(t, err) mockCfg := &mockConfig{values: map[string]interface{}{"workspaceDir": workspaceDir}} coreInstance.RegisterService("config", mockCfg) - service, err := New() + mockMedium := io.NewMockMedium() + service, err := New(mockMedium) assert.NoError(t, err) service.Runtime = core.NewRuntime(coreInstance, Options{}) - mockMedium := NewMockMedium() - service.medium = mockMedium return service, mockMedium }