diff --git a/cmd/dev/dev_commit.go b/cmd/dev/dev_commit.go index 3507cfe6..7037f9c2 100644 --- a/cmd/dev/dev_commit.go +++ b/cmd/dev/dev_commit.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "path/filepath" "github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/pkg/git" @@ -37,8 +38,14 @@ func addCommitCommand(parent *cobra.Command) { func runCommit(registryPath string, all bool) error { ctx := context.Background() + cwd, _ := os.Getwd() - // Find or use provided registry, fall back to directory scan + // Check if current directory is a git repo (single-repo mode) + if registryPath == "" && isGitRepo(cwd) { + return runCommitSingleRepo(ctx, cwd, all) + } + + // Multi-repo mode: find or use provided registry var reg *repos.Registry var err error @@ -57,8 +64,7 @@ func runCommit(registryPath string, all bool) error { } fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath) } else { - // Fallback: scan current directory - cwd, _ := os.Getwd() + // Fallback: scan current directory for repos reg, err = repos.ScanDirectory(cwd) if err != nil { return fmt.Errorf("failed to scan directory: %w", err) @@ -154,3 +160,66 @@ func runCommit(registryPath string, all bool) error { return nil } + +// isGitRepo checks if a directory is a git repository. +func isGitRepo(path string) bool { + gitDir := path + "/.git" + info, err := os.Stat(gitDir) + return err == nil && info.IsDir() +} + +// runCommitSingleRepo handles commit for a single repo (current directory). +func runCommitSingleRepo(ctx context.Context, repoPath string, all bool) error { + repoName := filepath.Base(repoPath) + + // Get status + statuses := git.Status(ctx, git.StatusOptions{ + Paths: []string{repoPath}, + Names: map[string]string{repoPath: repoName}, + }) + + if len(statuses) == 0 || statuses[0].Error != nil { + if len(statuses) > 0 && statuses[0].Error != nil { + return statuses[0].Error + } + return fmt.Errorf("failed to get repo status") + } + + s := statuses[0] + if !s.IsDirty() { + fmt.Println(i18n.T("cmd.dev.no_changes")) + return nil + } + + // Show status + fmt.Printf("%s: ", repoNameStyle.Render(s.Name)) + if s.Modified > 0 { + fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.modified", map[string]interface{}{"Count": s.Modified}))) + } + if s.Untracked > 0 { + fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.untracked", map[string]interface{}{"Count": s.Untracked}))) + } + if s.Staged > 0 { + fmt.Printf("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged}))) + } + fmt.Println() + + // Confirm unless --all + if !all { + fmt.Println() + if !shared.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) { + fmt.Println(i18n.T("cli.aborted")) + return nil + } + } + + fmt.Println() + + // Commit + if err := claudeCommit(ctx, repoPath, repoName, ""); err != nil { + fmt.Printf(" %s %s\n", errorStyle.Render("x"), err) + return err + } + fmt.Printf(" %s %s\n", successStyle.Render("v"), i18n.T("cmd.dev.committed")) + return nil +} diff --git a/cmd/dev/dev_push.go b/cmd/dev/dev_push.go index 6a401c9b..e262bee4 100644 --- a/cmd/dev/dev_push.go +++ b/cmd/dev/dev_push.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "path/filepath" "github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/pkg/git" @@ -37,8 +38,14 @@ func addPushCommand(parent *cobra.Command) { func runPush(registryPath string, force bool) error { ctx := context.Background() + cwd, _ := os.Getwd() - // Find or use provided registry, fall back to directory scan + // Check if current directory is a git repo (single-repo mode) + if registryPath == "" && isGitRepo(cwd) { + return runPushSingleRepo(ctx, cwd, force) + } + + // Multi-repo mode: find or use provided registry var reg *repos.Registry var err error @@ -57,8 +64,7 @@ func runPush(registryPath string, force bool) error { } fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath) } else { - // Fallback: scan current directory - cwd, _ := os.Getwd() + // Fallback: scan current directory for repos reg, err = repos.ScanDirectory(cwd) if err != nil { return fmt.Errorf("failed to scan directory: %w", err) @@ -185,3 +191,110 @@ func runPush(registryPath string, force bool) error { return nil } + +// runPushSingleRepo handles push for a single repo (current directory). +func runPushSingleRepo(ctx context.Context, repoPath string, force bool) error { + repoName := filepath.Base(repoPath) + + // Get status + statuses := git.Status(ctx, git.StatusOptions{ + Paths: []string{repoPath}, + Names: map[string]string{repoPath: repoName}, + }) + + if len(statuses) == 0 { + return fmt.Errorf("failed to get repo status") + } + + s := statuses[0] + if s.Error != nil { + return s.Error + } + + if !s.HasUnpushed() { + // Check if there are uncommitted changes + if s.IsDirty() { + fmt.Printf("%s: ", repoNameStyle.Render(s.Name)) + if s.Modified > 0 { + fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.modified", map[string]interface{}{"Count": s.Modified}))) + } + if s.Untracked > 0 { + fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.untracked", map[string]interface{}{"Count": s.Untracked}))) + } + if s.Staged > 0 { + fmt.Printf("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged}))) + } + fmt.Println() + fmt.Println() + if shared.Confirm(i18n.T("cmd.dev.push.uncommitted_changes_commit")) { + fmt.Println() + // Use edit-enabled commit if only untracked files (may need .gitignore fix) + var err error + if s.Modified == 0 && s.Staged == 0 && s.Untracked > 0 { + err = claudeEditCommit(ctx, repoPath, repoName, "") + } else { + err = runCommitSingleRepo(ctx, repoPath, false) + } + if err != nil { + return err + } + // Re-check - only push if Claude created commits + newStatuses := git.Status(ctx, git.StatusOptions{ + Paths: []string{repoPath}, + Names: map[string]string{repoPath: repoName}, + }) + if len(newStatuses) > 0 && newStatuses[0].HasUnpushed() { + return runPushSingleRepo(ctx, repoPath, force) + } + } + return nil + } + fmt.Println(i18n.T("cmd.dev.push.all_up_to_date")) + return nil + } + + // Show commits to push + fmt.Printf("%s: %s\n", repoNameStyle.Render(s.Name), + aheadStyle.Render(i18n.T("cmd.dev.push.commits_count", map[string]interface{}{"Count": s.Ahead}))) + + // Confirm unless --force + if !force { + fmt.Println() + if !shared.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": s.Ahead, "Repos": 1})) { + fmt.Println(i18n.T("cli.aborted")) + return nil + } + } + + fmt.Println() + + // Push + err := git.Push(ctx, repoPath) + if err != nil { + if git.IsNonFastForward(err) { + fmt.Printf(" %s %s: %s\n", warningStyle.Render("!"), repoName, i18n.T("cmd.dev.push.diverged")) + fmt.Println() + fmt.Printf("%s\n", i18n.T("cmd.dev.push.diverged_help")) + if shared.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) { + fmt.Println() + fmt.Printf(" %s %s...\n", dimStyle.Render("↓"), repoName) + if pullErr := git.Pull(ctx, repoPath); pullErr != nil { + fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), repoName, pullErr) + return pullErr + } + fmt.Printf(" %s %s...\n", dimStyle.Render("↑"), repoName) + if pushErr := git.Push(ctx, repoPath); pushErr != nil { + fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), repoName, pushErr) + return pushErr + } + fmt.Printf(" %s %s\n", successStyle.Render("v"), repoName) + return nil + } + } + fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), repoName, err) + return err + } + + fmt.Printf(" %s %s\n", successStyle.Render("v"), repoName) + return nil +} diff --git a/cmd/dev/dev_work.go b/cmd/dev/dev_work.go index 75429115..8bba6dac 100644 --- a/cmd/dev/dev_work.go +++ b/cmd/dev/dev_work.go @@ -5,11 +5,11 @@ import ( "fmt" "os" "os/exec" - "path/filepath" "sort" "strings" "github.com/host-uk/core/cmd/shared" + "github.com/host-uk/core/pkg/agentic" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" @@ -302,15 +302,7 @@ func printStatusTable(statuses []git.RepoStatus) { } func claudeCommit(ctx context.Context, repoPath, repoName, registryPath string) error { - // Load AGENTS.md context if available - agentsPath := filepath.Join(filepath.Dir(registryPath), "AGENTS.md") - var agentContext string - if data, err := os.ReadFile(agentsPath); err == nil { - agentContext = string(data) + "\n\n" - } - - prompt := agentContext + "Review the uncommitted changes and create an appropriate commit. " + - "Use Co-Authored-By: Claude Opus 4.5 . Be concise." + prompt := agentic.Prompt("commit") cmd := exec.CommandContext(ctx, "claude", "-p", prompt, "--allowedTools", "Bash,Read,Glob,Grep") cmd.Dir = repoPath @@ -320,3 +312,15 @@ func claudeCommit(ctx context.Context, repoPath, repoName, registryPath string) return cmd.Run() } + +func claudeEditCommit(ctx context.Context, repoPath, repoName, registryPath string) error { + prompt := agentic.Prompt("commit") + + cmd := exec.CommandContext(ctx, "claude", "-p", prompt, "--allowedTools", "Bash,Read,Write,Edit,Glob,Grep") + cmd.Dir = repoPath + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + return cmd.Run() +} diff --git a/pkg/agentic/embed.go b/pkg/agentic/embed.go new file mode 100644 index 00000000..4dd88862 --- /dev/null +++ b/pkg/agentic/embed.go @@ -0,0 +1,19 @@ +package agentic + +import ( + "embed" + "strings" +) + +//go:embed prompts/*.md +var promptsFS embed.FS + +// Prompt returns the content of an embedded prompt file. +// Name should be without the .md extension (e.g., "commit"). +func Prompt(name string) string { + data, err := promptsFS.ReadFile("prompts/" + name + ".md") + if err != nil { + return "" + } + return strings.TrimSpace(string(data)) +} diff --git a/pkg/agentic/prompts/commit.md b/pkg/agentic/prompts/commit.md new file mode 100644 index 00000000..58d6d7b3 --- /dev/null +++ b/pkg/agentic/prompts/commit.md @@ -0,0 +1,44 @@ +# Commit Instructions + +## Context + +When asked to commit changes in these repositories: + +1. **All changes are pre-approved** - Both tracked (modified) and untracked files have been reviewed and approved for commit +2. **Include everything** - Commit ALL modified files AND all untracked files (including new directories) +3. **Don't skip files** - Do not leave out untracked files assuming they need separate review +4. **Single commit preferred** - Combine all changes into one cohesive commit unless explicitly told otherwise + +The user has already validated these changes. Proceed with confidence. + +## Handling Ignored Files + +If you see untracked directories that should typically be ignored (like `node_modules/`, `vendor/`, `.cache/`, `dist/`, `build/`): + +1. **Fix the .gitignore** - Create or update `.gitignore` to exclude these directories +2. **Commit the .gitignore** - Include this fix in your commit +3. **Don't ask** - Just fix it and commit + +Common patterns to add to .gitignore: +``` +node_modules/ +vendor/ +.cache/ +dist/ +build/ +*.log +.env +.DS_Store +``` + +## Commit Message Style + +- Use conventional commit format: `type(scope): description` +- Common types: `refactor`, `feat`, `fix`, `docs`, `chore` +- Keep the first line under 72 characters +- Add body for complex changes explaining the "why" +- Include `Co-Authored-By: Claude Opus 4.5 ` + +## Task + +Review the uncommitted changes and create an appropriate commit. Be concise. diff --git a/pkg/framework/core/core.go b/pkg/framework/core/core.go new file mode 100644 index 00000000..27deb9e6 --- /dev/null +++ b/pkg/framework/core/core.go @@ -0,0 +1,290 @@ +package framework + +import ( + "context" + "embed" + "errors" + "fmt" + "reflect" + "strings" +) + +// New initialises a Core instance using the provided options and performs the necessary setup. +// It is the primary entry point for creating a new Core application. +// +// Example: +// +// core, err := core.New( +// core.WithService(&MyService{}), +// core.WithAssets(assets), +// ) +func New(opts ...Option) (*Core, error) { + c := &Core{ + services: make(map[string]any), + Features: &Features{}, + } + for _, o := range opts { + if err := o(c); err != nil { + return nil, err + } + } + + if c.serviceLock { + c.servicesLocked = true + } + return c, nil +} + +// WithService creates an Option that registers a service. It automatically discovers +// the service name from its package path and registers its IPC handler if it +// implements a method named `HandleIPCEvents`. +// +// Example: +// +// // In myapp/services/calculator.go +// package services +// +// type Calculator struct{} +// +// func (s *Calculator) Add(a, b int) int { return a + b } +// +// // In main.go +// import "myapp/services" +// +// core.New(core.WithService(services.NewCalculator)) +func WithService(factory func(*Core) (any, error)) Option { + return func(c *Core) error { + serviceInstance, err := factory(c) + + if err != nil { + return fmt.Errorf("core: failed to create service: %w", err) + } + + // --- Service Name Discovery --- + typeOfService := reflect.TypeOf(serviceInstance) + if typeOfService.Kind() == reflect.Ptr { + typeOfService = typeOfService.Elem() + } + pkgPath := typeOfService.PkgPath() + parts := strings.Split(pkgPath, "/") + name := strings.ToLower(parts[len(parts)-1]) + + // --- IPC Handler Discovery --- + instanceValue := reflect.ValueOf(serviceInstance) + handlerMethod := instanceValue.MethodByName("HandleIPCEvents") + if handlerMethod.IsValid() { + if handler, ok := handlerMethod.Interface().(func(*Core, Message) error); ok { + c.RegisterAction(handler) + } + } + + return c.RegisterService(name, serviceInstance) + } +} + +// WithName creates an option that registers a service with a specific name. +// This is useful when the service name cannot be inferred from the package path, +// such as when using anonymous functions as factories. +// Note: Unlike WithService, this does not automatically discover or register +// IPC handlers. If your service needs IPC handling, implement HandleIPCEvents +// and register it manually. +func WithName(name string, factory func(*Core) (any, error)) Option { + return func(c *Core) error { + serviceInstance, err := factory(c) + if err != nil { + return fmt.Errorf("core: failed to create service '%s': %w", name, err) + } + return c.RegisterService(name, serviceInstance) + } +} + +// WithApp creates an Option that injects the GUI runtime (e.g., Wails App) into the Core. +// This is essential for services that need to interact with the GUI runtime. +func WithApp(app any) Option { + return func(c *Core) error { + c.App = app + return nil + } +} + +// WithAssets creates an Option that registers the application's embedded assets. +// This is necessary for the application to be able to serve its frontend. +func WithAssets(fs embed.FS) Option { + return func(c *Core) error { + c.assets = fs + return nil + } +} + +// WithServiceLock creates an Option that prevents any further services from being +// registered after the Core has been initialized. This is a security measure to +// prevent late-binding of services that could have unintended consequences. +func WithServiceLock() Option { + return func(c *Core) error { + c.serviceLock = true + return nil + } +} + +// --- Core Methods --- + +// ServiceStartup is the entry point for the Core service's startup lifecycle. +// It is called by the GUI runtime when the application starts. +func (c *Core) ServiceStartup(ctx context.Context, options any) error { + c.serviceMu.RLock() + startables := append([]Startable(nil), c.startables...) + c.serviceMu.RUnlock() + + var agg error + for _, s := range startables { + if err := s.OnStartup(ctx); err != nil { + agg = errors.Join(agg, err) + } + } + + if err := c.ACTION(ActionServiceStartup{}); err != nil { + agg = errors.Join(agg, err) + } + + return agg +} + +// ServiceShutdown is the entry point for the Core service's shutdown lifecycle. +// It is called by Wails when the application shuts down. +func (c *Core) ServiceShutdown(ctx context.Context) error { + var agg error + if err := c.ACTION(ActionServiceShutdown{}); err != nil { + agg = errors.Join(agg, err) + } + + c.serviceMu.RLock() + stoppables := append([]Stoppable(nil), c.stoppables...) + c.serviceMu.RUnlock() + + for i := len(stoppables) - 1; i >= 0; i-- { + if err := stoppables[i].OnShutdown(ctx); err != nil { + agg = errors.Join(agg, err) + } + } + + return agg +} + +// ACTION dispatches a message to all registered IPC handlers. +// This is the primary mechanism for services to communicate with each other. +func (c *Core) ACTION(msg Message) error { + c.ipcMu.RLock() + handlers := append([]func(*Core, Message) error(nil), c.ipcHandlers...) + c.ipcMu.RUnlock() + + var agg error + for _, h := range handlers { + if err := h(c, msg); err != nil { + agg = fmt.Errorf("%w; %v", agg, err) + } + } + return agg +} + +// RegisterAction adds a new IPC handler to the Core. +func (c *Core) RegisterAction(handler func(*Core, Message) error) { + c.ipcMu.Lock() + c.ipcHandlers = append(c.ipcHandlers, handler) + c.ipcMu.Unlock() +} + +// RegisterActions adds multiple IPC handlers to the Core. +func (c *Core) RegisterActions(handlers ...func(*Core, Message) error) { + c.ipcMu.Lock() + c.ipcHandlers = append(c.ipcHandlers, handlers...) + c.ipcMu.Unlock() +} + +// RegisterService adds a new service to the Core. +func (c *Core) RegisterService(name string, api any) error { + if c.servicesLocked { + return fmt.Errorf("core: service %q is not permitted by the serviceLock setting", name) + } + if name == "" { + return errors.New("core: service name cannot be empty") + } + c.serviceMu.Lock() + defer c.serviceMu.Unlock() + if _, exists := c.services[name]; exists { + return fmt.Errorf("core: service %q already registered", name) + } + c.services[name] = api + + if s, ok := api.(Startable); ok { + c.startables = append(c.startables, s) + } + if s, ok := api.(Stoppable); ok { + c.stoppables = append(c.stoppables, s) + } + + return nil +} + +// Service retrieves a registered service by name. +// It returns nil if the service is not found. +func (c *Core) Service(name string) any { + c.serviceMu.RLock() + api, ok := c.services[name] + c.serviceMu.RUnlock() + if !ok { + return nil + } + return api +} + +// ServiceFor retrieves a registered service by name and asserts its type to the given interface T. +func ServiceFor[T any](c *Core, name string) (T, error) { + var zero T + raw := c.Service(name) + if raw == nil { + return zero, fmt.Errorf("service '%s' not found", name) + } + typed, ok := raw.(T) + if !ok { + return zero, fmt.Errorf("service '%s' is of type %T, but expected %T", name, raw, zero) + } + return typed, nil +} + +// MustServiceFor retrieves a registered service by name and asserts its type to the given interface T. +// It panics if the service is not found or cannot be cast to T. +func MustServiceFor[T any](c *Core, name string) T { + svc, err := ServiceFor[T](c, name) + if err != nil { + panic(err) + } + return svc +} + +// App returns the global application instance. +// It panics if the Core has not been initialized. +func App() any { + if instance == nil { + panic("framework.App() called before framework.Setup() was successfully initialized") + } + return instance.App +} + +// Config returns the registered Config service. +func (c *Core) Config() Config { + cfg := MustServiceFor[Config](c, "config") + return cfg +} + +// Display returns the registered Display service. +func (c *Core) Display() Display { + d := MustServiceFor[Display](c, "display") + return d +} + +func (c *Core) Core() *Core { return c } + +// Assets returns the embedded filesystem containing the application's assets. +func (c *Core) Assets() embed.FS { + return c.assets +} diff --git a/pkg/framework/core/core_extra_test.go b/pkg/framework/core/core_extra_test.go new file mode 100644 index 00000000..ebde048e --- /dev/null +++ b/pkg/framework/core/core_extra_test.go @@ -0,0 +1,43 @@ +package framework + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type MockServiceWithIPC struct { + MockService + handled bool +} + +func (m *MockServiceWithIPC) HandleIPCEvents(c *Core, msg Message) error { + m.handled = true + return nil +} + +func TestCore_WithService_IPC(t *testing.T) { + svc := &MockServiceWithIPC{MockService: MockService{Name: "ipc-service"}} + factory := func(c *Core) (any, error) { + return svc, nil + } + c, err := New(WithService(factory)) + assert.NoError(t, err) + + // Trigger ACTION to verify handler was registered + err = c.ACTION(nil) + assert.NoError(t, err) + assert.True(t, svc.handled) +} + +func TestCore_ACTION_Bad(t *testing.T) { + c, err := New() + assert.NoError(t, err) + errHandler := func(c *Core, msg Message) error { + return assert.AnError + } + c.RegisterAction(errHandler) + err = c.ACTION(nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), assert.AnError.Error()) +} diff --git a/pkg/framework/core/core_lifecycle_test.go b/pkg/framework/core/core_lifecycle_test.go new file mode 100644 index 00000000..219be431 --- /dev/null +++ b/pkg/framework/core/core_lifecycle_test.go @@ -0,0 +1,163 @@ +package framework + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +type MockStartable struct { + started bool + err error +} + +func (m *MockStartable) OnStartup(ctx context.Context) error { + m.started = true + return m.err +} + +type MockStoppable struct { + stopped bool + err error +} + +func (m *MockStoppable) OnShutdown(ctx context.Context) error { + m.stopped = true + return m.err +} + +type MockLifecycle struct { + MockStartable + MockStoppable +} + +func TestCore_LifecycleInterfaces(t *testing.T) { + c, err := New() + assert.NoError(t, err) + + startable := &MockStartable{} + stoppable := &MockStoppable{} + lifecycle := &MockLifecycle{} + + // Register services + err = c.RegisterService("startable", startable) + assert.NoError(t, err) + err = c.RegisterService("stoppable", stoppable) + assert.NoError(t, err) + err = c.RegisterService("lifecycle", lifecycle) + assert.NoError(t, err) + + // Startup + err = c.ServiceStartup(context.Background(), nil) + assert.NoError(t, err) + assert.True(t, startable.started) + assert.True(t, lifecycle.started) + assert.False(t, stoppable.stopped) + + // Shutdown + err = c.ServiceShutdown(context.Background()) + assert.NoError(t, err) + assert.True(t, stoppable.stopped) + assert.True(t, lifecycle.stopped) +} + +type MockLifecycleWithLog struct { + id string + log *[]string +} + +func (m *MockLifecycleWithLog) OnStartup(ctx context.Context) error { + *m.log = append(*m.log, "start-"+m.id) + return nil +} + +func (m *MockLifecycleWithLog) OnShutdown(ctx context.Context) error { + *m.log = append(*m.log, "stop-"+m.id) + return nil +} + +func TestCore_LifecycleOrder(t *testing.T) { + c, err := New() + assert.NoError(t, err) + + var callOrder []string + + s1 := &MockLifecycleWithLog{id: "1", log: &callOrder} + s2 := &MockLifecycleWithLog{id: "2", log: &callOrder} + + err = c.RegisterService("s1", s1) + assert.NoError(t, err) + err = c.RegisterService("s2", s2) + assert.NoError(t, err) + + // Startup + err = c.ServiceStartup(context.Background(), nil) + assert.NoError(t, err) + assert.Equal(t, []string{"start-1", "start-2"}, callOrder) + + // Reset log + callOrder = nil + + // Shutdown + err = c.ServiceShutdown(context.Background()) + assert.NoError(t, err) + assert.Equal(t, []string{"stop-2", "stop-1"}, callOrder) +} + +func TestCore_LifecycleErrors(t *testing.T) { + c, err := New() + assert.NoError(t, err) + + s1 := &MockStartable{err: assert.AnError} + s2 := &MockStoppable{err: assert.AnError} + + c.RegisterService("s1", s1) + c.RegisterService("s2", s2) + + err = c.ServiceStartup(context.Background(), nil) + assert.Error(t, err) + assert.ErrorIs(t, err, assert.AnError) + + err = c.ServiceShutdown(context.Background()) + assert.Error(t, err) + assert.ErrorIs(t, err, assert.AnError) +} + +func TestCore_LifecycleErrors_Aggregated(t *testing.T) { + c, err := New() + assert.NoError(t, err) + + // Register action that fails + c.RegisterAction(func(c *Core, msg Message) error { + if _, ok := msg.(ActionServiceStartup); ok { + return errors.New("startup action error") + } + if _, ok := msg.(ActionServiceShutdown); ok { + return errors.New("shutdown action error") + } + return nil + }) + + // Register service that fails + s1 := &MockStartable{err: errors.New("startup service error")} + s2 := &MockStoppable{err: errors.New("shutdown service error")} + + err = c.RegisterService("s1", s1) + assert.NoError(t, err) + err = c.RegisterService("s2", s2) + assert.NoError(t, err) + + // Startup + err = c.ServiceStartup(context.Background(), nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "startup action error") + assert.Contains(t, err.Error(), "startup service error") + + // Shutdown + err = c.ServiceShutdown(context.Background()) + assert.Error(t, err) + assert.Contains(t, err.Error(), "shutdown action error") + assert.Contains(t, err.Error(), "shutdown service error") +} diff --git a/pkg/framework/core/core_test.go b/pkg/framework/core/core_test.go new file mode 100644 index 00000000..6b566d8f --- /dev/null +++ b/pkg/framework/core/core_test.go @@ -0,0 +1,297 @@ +package framework + +import ( + "embed" + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +// mockApp is a simple mock for testing app injection +type mockApp struct{} + +func TestCore_New_Good(t *testing.T) { + c, err := New() + assert.NoError(t, err) + assert.NotNil(t, c) +} + +// Mock service for testing +type MockService struct { + Name string +} + +func (m *MockService) GetName() string { + return m.Name +} + +func TestCore_WithService_Good(t *testing.T) { + factory := func(c *Core) (any, error) { + return &MockService{Name: "test"}, nil + } + c, err := New(WithService(factory)) + assert.NoError(t, err) + svc := c.Service("core") + assert.NotNil(t, svc) + mockSvc, ok := svc.(*MockService) + assert.True(t, ok) + assert.Equal(t, "test", mockSvc.GetName()) +} + +func TestCore_WithService_Bad(t *testing.T) { + factory := func(c *Core) (any, error) { + return nil, assert.AnError + } + _, err := New(WithService(factory)) + assert.Error(t, err) + assert.ErrorIs(t, err, assert.AnError) +} + +type MockConfigService struct{} + +func (m *MockConfigService) Get(key string, out any) error { return nil } +func (m *MockConfigService) Set(key string, v any) error { return nil } + +type MockDisplayService struct{} + +func (m *MockDisplayService) OpenWindow(opts ...WindowOption) error { return nil } + +func TestCore_Services_Good(t *testing.T) { + c, err := New() + assert.NoError(t, err) + + err = c.RegisterService("config", &MockConfigService{}) + assert.NoError(t, err) + + err = c.RegisterService("display", &MockDisplayService{}) + assert.NoError(t, err) + + assert.NotNil(t, c.Config()) + assert.NotNil(t, c.Display()) +} + +func TestCore_Services_Ugly(t *testing.T) { + c, err := New() + assert.NoError(t, err) + + assert.Panics(t, func() { + c.Config() + }) + assert.Panics(t, func() { + c.Display() + }) +} + +func TestCore_App_Good(t *testing.T) { + app := &mockApp{} + c, err := New(WithApp(app)) + assert.NoError(t, err) + + // To test the global App() function, we need to set the global instance. + originalInstance := instance + instance = c + defer func() { instance = originalInstance }() + + assert.Equal(t, app, App()) +} + +func TestCore_App_Ugly(t *testing.T) { + // This test ensures that calling App() before the core is initialized panics. + originalInstance := instance + instance = nil + defer func() { instance = originalInstance }() + assert.Panics(t, func() { + App() + }) +} + +func TestCore_Core_Good(t *testing.T) { + c, err := New() + assert.NoError(t, err) + assert.Equal(t, c, c.Core()) +} + +func TestFeatures_IsEnabled_Good(t *testing.T) { + c, err := New() + assert.NoError(t, err) + + c.Features.Flags = []string{"feature1", "feature2"} + + assert.True(t, c.Features.IsEnabled("feature1")) + assert.True(t, c.Features.IsEnabled("feature2")) + assert.False(t, c.Features.IsEnabled("feature3")) +} + +type startupMessage struct{} +type shutdownMessage struct{} + +func TestCore_ServiceLifecycle_Good(t *testing.T) { + c, err := New() + assert.NoError(t, err) + + var messageReceived Message + handler := func(c *Core, msg Message) error { + messageReceived = msg + return nil + } + c.RegisterAction(handler) + + // Test Startup + _ = c.ServiceStartup(nil, nil) + _, ok := messageReceived.(ActionServiceStartup) + assert.True(t, ok, "expected ActionServiceStartup message") + + // Test Shutdown + _ = c.ServiceShutdown(nil) + _, ok = messageReceived.(ActionServiceShutdown) + assert.True(t, ok, "expected ActionServiceShutdown message") +} + +func TestCore_WithApp_Good(t *testing.T) { + app := &mockApp{} + c, err := New(WithApp(app)) + assert.NoError(t, err) + assert.Equal(t, app, c.App) +} + +//go:embed testdata +var testFS embed.FS + +func TestCore_WithAssets_Good(t *testing.T) { + c, err := New(WithAssets(testFS)) + assert.NoError(t, err) + assets := c.Assets() + file, err := assets.Open("testdata/test.txt") + assert.NoError(t, err) + defer file.Close() + content, err := io.ReadAll(file) + assert.NoError(t, err) + assert.Equal(t, "hello from testdata\n", string(content)) +} + +func TestCore_WithServiceLock_Good(t *testing.T) { + c, err := New(WithServiceLock()) + assert.NoError(t, err) + err = c.RegisterService("test", &MockService{}) + assert.Error(t, err) +} + +func TestCore_RegisterService_Good(t *testing.T) { + c, err := New() + assert.NoError(t, err) + err = c.RegisterService("test", &MockService{Name: "test"}) + assert.NoError(t, err) + svc := c.Service("test") + assert.NotNil(t, svc) + mockSvc, ok := svc.(*MockService) + assert.True(t, ok) + assert.Equal(t, "test", mockSvc.GetName()) +} + +func TestCore_RegisterService_Bad(t *testing.T) { + c, err := New() + assert.NoError(t, err) + err = c.RegisterService("test", &MockService{}) + assert.NoError(t, err) + err = c.RegisterService("test", &MockService{}) + assert.Error(t, err) + err = c.RegisterService("", &MockService{}) + assert.Error(t, err) +} + +func TestCore_ServiceFor_Good(t *testing.T) { + c, err := New() + assert.NoError(t, err) + err = c.RegisterService("test", &MockService{Name: "test"}) + assert.NoError(t, err) + svc, err := ServiceFor[*MockService](c, "test") + assert.NoError(t, err) + assert.Equal(t, "test", svc.GetName()) +} + +func TestCore_ServiceFor_Bad(t *testing.T) { + c, err := New() + assert.NoError(t, err) + _, err = ServiceFor[*MockService](c, "nonexistent") + assert.Error(t, err) + err = c.RegisterService("test", "not a service") + assert.NoError(t, err) + _, err = ServiceFor[*MockService](c, "test") + assert.Error(t, err) +} + +func TestCore_MustServiceFor_Good(t *testing.T) { + c, err := New() + assert.NoError(t, err) + err = c.RegisterService("test", &MockService{Name: "test"}) + assert.NoError(t, err) + svc := MustServiceFor[*MockService](c, "test") + assert.Equal(t, "test", svc.GetName()) +} + +func TestCore_MustServiceFor_Ugly(t *testing.T) { + c, err := New() + assert.NoError(t, err) + assert.Panics(t, func() { + MustServiceFor[*MockService](c, "nonexistent") + }) + err = c.RegisterService("test", "not a service") + assert.NoError(t, err) + assert.Panics(t, func() { + MustServiceFor[*MockService](c, "test") + }) +} + +type MockAction struct { + handled bool +} + +func (a *MockAction) Handle(c *Core, msg Message) error { + a.handled = true + return nil +} + +func TestCore_ACTION_Good(t *testing.T) { + c, err := New() + assert.NoError(t, err) + action := &MockAction{} + c.RegisterAction(action.Handle) + err = c.ACTION(nil) + assert.NoError(t, err) + assert.True(t, action.handled) +} + +func TestCore_RegisterActions_Good(t *testing.T) { + c, err := New() + assert.NoError(t, err) + action1 := &MockAction{} + action2 := &MockAction{} + c.RegisterActions(action1.Handle, action2.Handle) + err = c.ACTION(nil) + assert.NoError(t, err) + assert.True(t, action1.handled) + assert.True(t, action2.handled) +} + +func TestCore_WithName_Good(t *testing.T) { + factory := func(c *Core) (any, error) { + return &MockService{Name: "test"}, nil + } + c, err := New(WithName("my-service", factory)) + assert.NoError(t, err) + svc := c.Service("my-service") + assert.NotNil(t, svc) + mockSvc, ok := svc.(*MockService) + assert.True(t, ok) + assert.Equal(t, "test", mockSvc.GetName()) +} + +func TestCore_WithName_Bad(t *testing.T) { + factory := func(c *Core) (any, error) { + return nil, assert.AnError + } + _, err := New(WithName("my-service", factory)) + assert.Error(t, err) + assert.ErrorIs(t, err, assert.AnError) +} diff --git a/pkg/framework/core/docs/site/404.html b/pkg/framework/core/docs/site/404.html new file mode 100644 index 00000000..e0fae56f --- /dev/null +++ b/pkg/framework/core/docs/site/404.html @@ -0,0 +1,707 @@ + + + + + + + + + + + + + + + + + + + + + + Core.Help + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ + + + + + + + + +
+ +

404 - Not found

+ +
+
+ + + + + +
+ + + +
+ +
+ + +
+ +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/pkg/framework/core/docs/site/assets/external/fonts.googleapis.com/css.49ea35f2.css b/pkg/framework/core/docs/site/assets/external/fonts.googleapis.com/css.49ea35f2.css new file mode 100644 index 00000000..d5c0c148 --- /dev/null +++ b/pkg/framework/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/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkAnkaWzU.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkAnkaWzU.woff2 new file mode 100644 index 00000000..ab38fd54 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkAnkaWzU.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaWzU.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaWzU.woff2 new file mode 100644 index 00000000..db658495 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaWzU.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnka.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnka.woff2 new file mode 100644 index 00000000..7c9cbed6 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnka.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaWzU.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaWzU.woff2 new file mode 100644 index 00000000..e0aa3939 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaWzU.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaWzU.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaWzU.woff2 new file mode 100644 index 00000000..b6771301 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaWzU.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaWzU.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaWzU.woff2 new file mode 100644 index 00000000..669ba793 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaWzU.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCnkaWzU.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCnkaWzU.woff2 new file mode 100644 index 00000000..6cc1de8c Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCnkaWzU.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaWzU.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaWzU.woff2 new file mode 100644 index 00000000..ded8a41e Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaWzU.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaWzU.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaWzU.woff2 new file mode 100644 index 00000000..dbac4817 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaWzU.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2 new file mode 100644 index 00000000..8e0eec69 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2 new file mode 100644 index 00000000..0ddf16c6 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2 new file mode 100644 index 00000000..7bd3c2ef Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2 new file mode 100644 index 00000000..8e43aa42 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2 new file mode 100644 index 00000000..2c6ba19b Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2 new file mode 100644 index 00000000..2f8b493b Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2 new file mode 100644 index 00000000..7c16c79f Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2 new file mode 100644 index 00000000..c2788c74 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2 new file mode 100644 index 00000000..528b3bf4 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/roboto/v49/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2 new file mode 100644 index 00000000..2c06834b Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2 new file mode 100644 index 00000000..532a888a Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2 new file mode 100644 index 00000000..b02e2d6c Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2 new file mode 100644 index 00000000..ae2f9eb0 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2 new file mode 100644 index 00000000..bfa169c3 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2 new file mode 100644 index 00000000..8a15f5c1 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2 new file mode 100644 index 00000000..d1ee097f Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2 new file mode 100644 index 00000000..c8e6ed44 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2 new file mode 100644 index 00000000..1debc1b4 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2 new file mode 100644 index 00000000..43f75160 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2 new file mode 100644 index 00000000..227f3624 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2 b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2 new file mode 100644 index 00000000..10a65a78 Binary files /dev/null and b/pkg/framework/core/docs/site/assets/external/fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2 differ diff --git a/pkg/framework/core/docs/site/assets/external/unpkg.com/iframe-worker/shim.js b/pkg/framework/core/docs/site/assets/external/unpkg.com/iframe-worker/shim.js new file mode 100644 index 00000000..5f1e2321 --- /dev/null +++ b/pkg/framework/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/framework/core/docs/site/core/crypt.html b/pkg/framework/core/docs/site/core/crypt.html new file mode 100644 index 00000000..7c4fbfee --- /dev/null +++ b/pkg/framework/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/framework/core/docs/site/core/display.html b/pkg/framework/core/docs/site/core/display.html new file mode 100644 index 00000000..85d104c5 --- /dev/null +++ b/pkg/framework/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/framework/core/docs/site/core/docs.html b/pkg/framework/core/docs/site/core/docs.html new file mode 100644 index 00000000..dc90d1f6 --- /dev/null +++ b/pkg/framework/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/framework/core/docs/site/core/index.html b/pkg/framework/core/docs/site/core/index.html new file mode 100644 index 00000000..38c575dd --- /dev/null +++ b/pkg/framework/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/framework/core/docs/site/core/io.html b/pkg/framework/core/docs/site/core/io.html new file mode 100644 index 00000000..4485a50c --- /dev/null +++ b/pkg/framework/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/framework/core/docs/site/core/workspace.html b/pkg/framework/core/docs/site/core/workspace.html new file mode 100644 index 00000000..72bbc031 --- /dev/null +++ b/pkg/framework/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/framework/core/docs/site/images/cross-platform.jpeg b/pkg/framework/core/docs/site/images/cross-platform.jpeg new file mode 100644 index 00000000..8de2288e Binary files /dev/null and b/pkg/framework/core/docs/site/images/cross-platform.jpeg differ diff --git a/pkg/framework/core/docs/site/images/decentralised-vpn.jpg b/pkg/framework/core/docs/site/images/decentralised-vpn.jpg new file mode 100644 index 00000000..df1f487d Binary files /dev/null and b/pkg/framework/core/docs/site/images/decentralised-vpn.jpg differ diff --git a/pkg/framework/core/docs/site/images/favicon.ico b/pkg/framework/core/docs/site/images/favicon.ico new file mode 100644 index 00000000..8bc8ebbe Binary files /dev/null and b/pkg/framework/core/docs/site/images/favicon.ico differ diff --git a/pkg/framework/core/docs/site/images/illustration.png b/pkg/framework/core/docs/site/images/illustration.png new file mode 100644 index 00000000..69f739c0 Binary files /dev/null and b/pkg/framework/core/docs/site/images/illustration.png differ diff --git a/pkg/framework/core/docs/site/images/lethean-logo.png b/pkg/framework/core/docs/site/images/lethean-logo.png new file mode 100644 index 00000000..591019d5 Binary files /dev/null and b/pkg/framework/core/docs/site/images/lethean-logo.png differ diff --git a/pkg/framework/core/docs/site/images/private-transaction-net.png b/pkg/framework/core/docs/site/images/private-transaction-net.png new file mode 100644 index 00000000..1eee17a0 Binary files /dev/null and b/pkg/framework/core/docs/site/images/private-transaction-net.png differ diff --git a/pkg/framework/core/docs/site/images/secure-data-storage.jpg b/pkg/framework/core/docs/site/images/secure-data-storage.jpg new file mode 100644 index 00000000..395a8ae1 Binary files /dev/null and b/pkg/framework/core/docs/site/images/secure-data-storage.jpg differ diff --git a/pkg/framework/core/docs/site/index.html b/pkg/framework/core/docs/site/index.html new file mode 100644 index 00000000..a956691d --- /dev/null +++ b/pkg/framework/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/framework/core/docs/site/search/search_index.js b/pkg/framework/core/docs/site/search/search_index.js new file mode 100644 index 00000000..193f0500 --- /dev/null +++ b/pkg/framework/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/framework/core/docs/site/search/search_index.json b/pkg/framework/core/docs/site/search/search_index.json new file mode 100644 index 00000000..323cc074 --- /dev/null +++ b/pkg/framework/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/framework/core/docs/site/sitemap.xml b/pkg/framework/core/docs/site/sitemap.xml new file mode 100644 index 00000000..a0633589 --- /dev/null +++ b/pkg/framework/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/framework/core/docs/site/sitemap.xml.gz b/pkg/framework/core/docs/site/sitemap.xml.gz new file mode 100644 index 00000000..c4e06d97 Binary files /dev/null and b/pkg/framework/core/docs/site/sitemap.xml.gz differ diff --git a/pkg/framework/core/docs/site/stylesheets/extra.css b/pkg/framework/core/docs/site/stylesheets/extra.css new file mode 100644 index 00000000..8a89327b --- /dev/null +++ b/pkg/framework/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/framework/core/e.go b/pkg/framework/core/e.go new file mode 100644 index 00000000..9001f77d --- /dev/null +++ b/pkg/framework/core/e.go @@ -0,0 +1,59 @@ +// Package e provides a standardized error handling mechanism for the Core library. +// It allows for wrapping errors with contextual information, making it easier to +// trace the origin of an error and provide meaningful feedback. +// +// The design of this package is influenced by the need for a simple, yet powerful +// way to handle errors that can occur in different layers of the application, +// from low-level file operations to high-level service interactions. +// +// The key features of this package are: +// - Error wrapping: The Op and an optional Msg field provide context about +// where and why an error occurred. +// - Stack traces: By wrapping errors, we can build a logical stack trace +// that is more informative than a raw stack trace. +// - Consistent error handling: Encourages a uniform approach to error +// handling across the entire codebase. +package framework + +import ( + "fmt" +) + +// Error represents a standardized error with operational context. +type Error struct { + // Op is the operation being performed, e.g., "config.Load". + Op string + // Msg is a human-readable message explaining the error. + Msg string + // Err is the underlying error that was wrapped. + Err error +} + +// E is a helper function to create a new Error. +// This is the primary way to create errors that will be consumed by the system. +// For example: +// +// return e.E("config.Load", "failed to load config file", err) +// +// The 'op' parameter should be in the format of 'package.function' or 'service.method'. +// The 'msg' parameter should be a human-readable message that can be displayed to the user. +// The 'err' parameter is the underlying error that is being wrapped. +func E(op, msg string, err error) error { + if err == nil { + return &Error{Op: op, Msg: msg} + } + return &Error{Op: op, Msg: msg, Err: err} +} + +// Error returns the string representation of the error. +func (e *Error) Error() string { + if e.Err != nil { + return fmt.Sprintf("%s: %s: %v", e.Op, e.Msg, e.Err) + } + return fmt.Sprintf("%s: %s", e.Op, e.Msg) +} + +// Unwrap provides compatibility for Go's errors.Is and errors.As functions. +func (e *Error) Unwrap() error { + return e.Err +} \ No newline at end of file diff --git a/pkg/framework/core/e_test.go b/pkg/framework/core/e_test.go new file mode 100644 index 00000000..1d5c8b8a --- /dev/null +++ b/pkg/framework/core/e_test.go @@ -0,0 +1,29 @@ +package framework + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestE_Good(t *testing.T) { + err := E("test.op", "test message", assert.AnError) + assert.Error(t, err) + assert.Equal(t, "test.op: test message: assert.AnError general error for testing", err.Error()) + + err = E("test.op", "test message", nil) + assert.Error(t, err) + assert.Equal(t, "test.op: test message", err.Error()) +} + +func TestE_Unwrap(t *testing.T) { + originalErr := errors.New("original error") + err := E("test.op", "test message", originalErr) + + assert.True(t, errors.Is(err, originalErr)) + + var eErr *Error + assert.True(t, errors.As(err, &eErr)) + assert.Equal(t, "test.op", eErr.Op) +} diff --git a/pkg/framework/core/interfaces.go b/pkg/framework/core/interfaces.go new file mode 100644 index 00000000..a5001e50 --- /dev/null +++ b/pkg/framework/core/interfaces.go @@ -0,0 +1,101 @@ +package framework + +import ( + "context" + "embed" + "sync" +) + +// This file defines the public API contracts (interfaces) for the services +// in the Core framework. Services depend on these interfaces, not on +// concrete implementations. + +// Contract specifies the operational guarantees that the Core and its services must adhere to. +// This is used for configuring panic handling and other resilience features. +type Contract struct { + // DontPanic, if true, instructs the Core to recover from panics and return an error instead. + DontPanic bool + // DisableLogging, if true, disables all logging from the Core and its services. + DisableLogging bool +} + +// Features provides a way to check if a feature is enabled. +// This is used for feature flagging and conditional logic. +type Features struct { + // Flags is a list of enabled feature flags. + Flags []string +} + +// IsEnabled returns true if the given feature is enabled. +func (f *Features) IsEnabled(feature string) bool { + for _, flag := range f.Flags { + if flag == feature { + return true + } + } + return false +} + +// Option is a function that configures the Core. +// This is used to apply settings and register services during initialization. +type Option func(*Core) error + +// Message is the interface for all messages that can be sent through the Core's IPC system. +// Any struct can be a message, allowing for structured data to be passed between services. +type Message interface{} + +// Startable is an interface for services that need to perform initialization. +type Startable interface { + OnStartup(ctx context.Context) error +} + +// Stoppable is an interface for services that need to perform cleanup. +type Stoppable interface { + OnShutdown(ctx context.Context) error +} + +// Core is the central application object that manages services, assets, and communication. +type Core struct { + once sync.Once + initErr error + App any // GUI runtime (e.g., Wails App) - set by WithApp option + assets embed.FS + Features *Features + serviceLock bool + ipcMu sync.RWMutex + ipcHandlers []func(*Core, Message) error + serviceMu sync.RWMutex + services map[string]any + servicesLocked bool + startables []Startable + stoppables []Stoppable +} + +var instance *Core + +// Config provides access to application configuration. +type Config interface { + // Get retrieves a configuration value by key and stores it in the 'out' variable. + Get(key string, out any) error + // Set stores a configuration value by key. + Set(key string, v any) error +} + +// WindowOption is an interface for applying configuration options to a window. +type WindowOption interface { + Apply(any) +} + +// Display provides access to windowing and visual elements. +type Display interface { + // OpenWindow creates a new window with the given options. + OpenWindow(opts ...WindowOption) error +} + +// ActionServiceStartup is a message sent when the application's services are starting up. +// This provides a hook for services to perform initialization tasks. +type ActionServiceStartup struct{} + +// ActionServiceShutdown is a message sent when the application is shutting down. +// This allows services to perform cleanup tasks, such as saving state or closing resources. +type ActionServiceShutdown struct{} diff --git a/pkg/framework/core/runtime_pkg.go b/pkg/framework/core/runtime_pkg.go new file mode 100644 index 00000000..8278c84b --- /dev/null +++ b/pkg/framework/core/runtime_pkg.go @@ -0,0 +1,113 @@ +package framework + +import ( + "context" + "fmt" + "sort" +) + +// ServiceRuntime is a helper struct embedded in services to provide access to the core application. +// It is generic and can be parameterized with a service-specific options struct. +type ServiceRuntime[T any] struct { + core *Core + opts T +} + +// NewServiceRuntime creates a new ServiceRuntime instance for a service. +// This is typically called by a service's constructor. +func NewServiceRuntime[T any](c *Core, opts T) *ServiceRuntime[T] { + return &ServiceRuntime[T]{ + core: c, + opts: opts, + } +} + +// Core returns the central core instance, providing access to all registered services. +func (r *ServiceRuntime[T]) Core() *Core { + return r.core +} + +// Config returns the registered Config service from the core application. +// This is a convenience method for accessing the application's configuration. +func (r *ServiceRuntime[T]) Config() Config { + return r.core.Config() +} + +// Runtime is the container that holds all instantiated services. +// Its fields are the concrete types, allowing GUI runtimes to bind them directly. +// This struct is the primary entry point for the application. +type Runtime struct { + app any // GUI runtime (e.g., Wails App) + Core *Core +} + +// ServiceFactory defines a function that creates a service instance. +// This is used to decouple the service creation from the runtime initialization. +type ServiceFactory func() (any, error) + +// NewWithFactories creates a new Runtime instance using the provided service factories. +// This is the most flexible way to create a new Runtime, as it allows for +// the registration of any number of services. +func NewWithFactories(app any, factories map[string]ServiceFactory) (*Runtime, error) { + services := make(map[string]any) + coreOpts := []Option{ + WithApp(app), + } + + names := make([]string, 0, len(factories)) + for name := range factories { + names = append(names, name) + } + sort.Strings(names) + + for _, name := range names { + factory := factories[name] + svc, err := factory() + if err != nil { + return nil, fmt.Errorf("failed to create service %s: %w", name, err) + } + services[name] = svc + svcCopy := svc + coreOpts = append(coreOpts, WithName(name, func(c *Core) (any, error) { return svcCopy, nil })) + } + + coreInstance, err := New(coreOpts...) + if err != nil { + return nil, err + } + + // --- Type Assertions --- + + rt := &Runtime{ + app: app, + Core: coreInstance, + } + + return rt, nil +} + +// NewRuntime creates and wires together all application services. +// This is the simplest way to create a new Runtime, but it does not allow for +// the registration of any custom services. +func NewRuntime(app any) (*Runtime, error) { + return NewWithFactories(app, map[string]ServiceFactory{}) +} + +// ServiceName returns the name of the service. This is used by GUI runtimes to identify the service. +func (r *Runtime) ServiceName() string { + return "Core" +} + +// ServiceStartup is called by the GUI runtime at application startup. +// This is where the Core's startup lifecycle is initiated. +func (r *Runtime) ServiceStartup(ctx context.Context, options any) { + r.Core.ServiceStartup(ctx, options) +} + +// ServiceShutdown is called by Wails at application shutdown. +// This is where the Core's shutdown lifecycle is initiated. +func (r *Runtime) ServiceShutdown(ctx context.Context) { + if r.Core != nil { + r.Core.ServiceShutdown(ctx) + } +} diff --git a/pkg/framework/core/runtime_pkg_extra_test.go b/pkg/framework/core/runtime_pkg_extra_test.go new file mode 100644 index 00000000..df29ee79 --- /dev/null +++ b/pkg/framework/core/runtime_pkg_extra_test.go @@ -0,0 +1,18 @@ +package framework + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewWithFactories_EmptyName(t *testing.T) { + factories := map[string]ServiceFactory{ + "": func() (any, error) { + return &MockService{Name: "test"}, nil + }, + } + _, err := NewWithFactories(nil, factories) + assert.Error(t, err) + assert.Contains(t, err.Error(), "service name cannot be empty") +} diff --git a/pkg/framework/core/runtime_pkg_test.go b/pkg/framework/core/runtime_pkg_test.go new file mode 100644 index 00000000..ada309b0 --- /dev/null +++ b/pkg/framework/core/runtime_pkg_test.go @@ -0,0 +1,127 @@ +package framework + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewRuntime(t *testing.T) { + testCases := []struct { + name string + app any + factories map[string]ServiceFactory + expectErr bool + expectErrStr string + checkRuntime func(*testing.T, *Runtime) + }{ + { + name: "Good path", + app: nil, + factories: map[string]ServiceFactory{}, + expectErr: false, + checkRuntime: func(t *testing.T, rt *Runtime) { + assert.NotNil(t, rt) + assert.NotNil(t, rt.Core) + }, + }, + { + name: "With non-nil app", + app: &mockApp{}, + factories: map[string]ServiceFactory{}, + expectErr: false, + checkRuntime: func(t *testing.T, rt *Runtime) { + assert.NotNil(t, rt) + assert.NotNil(t, rt.Core) + assert.NotNil(t, rt.Core.App) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rt, err := NewRuntime(tc.app) + + if tc.expectErr { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectErrStr) + assert.Nil(t, rt) + } else { + assert.NoError(t, err) + if tc.checkRuntime != nil { + tc.checkRuntime(t, rt) + } + } + }) + } +} + +func TestNewWithFactories_Good(t *testing.T) { + factories := map[string]ServiceFactory{ + "test": func() (any, error) { + return &MockService{Name: "test"}, nil + }, + } + rt, err := NewWithFactories(nil, factories) + assert.NoError(t, err) + assert.NotNil(t, rt) + svc := rt.Core.Service("test") + assert.NotNil(t, svc) + mockSvc, ok := svc.(*MockService) + assert.True(t, ok) + assert.Equal(t, "test", mockSvc.Name) +} + +func TestNewWithFactories_Bad(t *testing.T) { + factories := map[string]ServiceFactory{ + "test": func() (any, error) { + return nil, assert.AnError + }, + } + _, err := NewWithFactories(nil, factories) + assert.Error(t, err) + assert.ErrorIs(t, err, assert.AnError) +} + +func TestNewWithFactories_Ugly(t *testing.T) { + factories := map[string]ServiceFactory{ + "test": nil, + } + assert.Panics(t, func() { + _, _ = NewWithFactories(nil, factories) + }) +} + +func TestRuntime_Lifecycle_Good(t *testing.T) { + rt, err := NewRuntime(nil) + assert.NoError(t, err) + assert.NotNil(t, rt) + + // ServiceName + assert.Equal(t, "Core", rt.ServiceName()) + + // ServiceStartup & ServiceShutdown + // These are simple wrappers around the core methods, which are tested in core_test.go. + // We call them here to ensure coverage. + rt.ServiceStartup(nil, nil) + rt.ServiceShutdown(nil) + + // Test shutdown with nil core + rt.Core = nil + rt.ServiceShutdown(nil) +} + +func TestNewServiceRuntime_Good(t *testing.T) { + c, err := New() + assert.NoError(t, err) + + sr := NewServiceRuntime(c, "test options") + assert.NotNil(t, sr) + assert.Equal(t, c, sr.Core()) + + // We can't directly test sr.Config() without a registered config service, + // but we can ensure it doesn't panic. We'll test the panic case separately. + assert.Panics(t, func() { + sr.Config() + }) +} diff --git a/pkg/framework/core/testdata/test.txt b/pkg/framework/core/testdata/test.txt new file mode 100644 index 00000000..4f04564f --- /dev/null +++ b/pkg/framework/core/testdata/test.txt @@ -0,0 +1 @@ +hello from testdata diff --git a/pkg/i18n/locales/en_GB.json b/pkg/i18n/locales/en_GB.json index bcefb8ba..3d2f5a33 100644 --- a/pkg/i18n/locales/en_GB.json +++ b/pkg/i18n/locales/en_GB.json @@ -187,6 +187,10 @@ "cmd.dev.push.commits_count": "{{.Count}} commit(s)", "cmd.dev.push.confirm_push": "Push {{.Commits}} commit(s) to {{.Repos}} repo(s)?", "cmd.dev.push.done_pushed": "Done: {{.Count}} pushed", + "cmd.dev.push.diverged": "branch has diverged from remote", + "cmd.dev.push.diverged_help": "Some repos have diverged (local and remote have different commits).", + "cmd.dev.push.pull_and_retry": "Pull changes and retry push?", + "cmd.dev.push.uncommitted_changes_commit": "You have uncommitted changes. Commit with Claude first?", "cmd.dev.pull.all_up_to_date": "All repos up to date. Nothing to pull.", "cmd.dev.pull.pulling_repos": "Pulling {{.Count}} repo(s):",