Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1deb0c8a27 | |||
| fcbe848b8a | |||
| 6b0db4616a | |||
| c77650659e | |||
| 6b9935ddbe | |||
| 51b5d57222 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
nebula
|
nebula
|
||||||
|
.osgrep
|
||||||
|
|||||||
292
AGENTS.md
292
AGENTS.md
@@ -1,227 +1,133 @@
|
|||||||
# AGENTS.md - Nebula Project Guidelines
|
# NEBULA PROJECT KNOWLEDGE BASE
|
||||||
|
|
||||||
## Project Overview
|
**Generated:** 2026-01-11 | **Commit:** fcbe848 | **Branch:** feat/pkg
|
||||||
|
|
||||||
Nebula is a Go/Templ wallet application with HTMX 4 for server-driven UI and WebAwesome (wa-*) web components.
|
## OVERVIEW
|
||||||
|
|
||||||
**Stack**: Go 1.25+ | templ v0.3.977 | htmx 4.0.0-alpha5 | WebAwesome | SQLite WASM
|
Go/Templ wallet UI framework using HTMX 4 for server-driven interactions and WebAwesome (`wa-*`) web components. Hybrid: root is a library (`package nebula`), `cmd/server/` is the reference app.
|
||||||
|
|
||||||
## Build & Run Commands
|
## STRUCTURE
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate Go code from .templ files (REQUIRED after any .templ changes)
|
|
||||||
templ generate
|
|
||||||
|
|
||||||
# Build the project
|
|
||||||
go build ./...
|
|
||||||
|
|
||||||
# Run development server (serves on :8080)
|
|
||||||
go run main.go
|
|
||||||
|
|
||||||
# Watch mode for templ files
|
|
||||||
templ generate --watch
|
|
||||||
|
|
||||||
# Full build + run + open browser
|
|
||||||
make all
|
|
||||||
```
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
```
|
||||||
nebula/
|
nebula/
|
||||||
├── models/ # Data types (MVC models)
|
├── cmd/server/ # App entry: main.go + routes.go (handlers HERE, not handlers/)
|
||||||
├── views/ # Page templates (.templ)
|
├── views/ # Page templates (.templ) - XxxPage, XxxContent, XxxWithStepper
|
||||||
├── layouts/ # Base layouts (DashboardLayout, CenteredLayout)
|
├── components/ # Reusable UI (sidebar, stepper, navbar, charts)
|
||||||
├── components/ # Reusable UI components
|
├── layouts/ # Base (HTML shell), App (dashboard), Centered (auth)
|
||||||
├── handlers/ # HTTP route handlers
|
├── models/ # Data structs + DefaultXxxData() factories
|
||||||
├── _migrate/ # HTML prototypes to convert
|
├── htmx/ # HTMX 4 context/request/response helpers
|
||||||
├── HTMX.md # htmx 4 patterns documentation
|
├── pkg/config/ # Config models for middleware injection
|
||||||
└── SQLC.md # Database schema documentation
|
├── _migrate/ # HTML prototypes pending conversion (technical debt)
|
||||||
|
├── middleware.go # Injects HTMX + Config into request context
|
||||||
|
├── mount.go # Mount() helper for handler registration
|
||||||
|
├── options.go # WithXxx() functional options
|
||||||
|
└── config.go # DefaultConfig() + Config struct
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code Style
|
## WHERE TO LOOK
|
||||||
|
|
||||||
### Go Files
|
| Task | Location | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| Add route | `cmd/server/routes.go` | Go 1.22+ patterns: `GET /path/{param}` |
|
||||||
|
| Add page | `views/` + `models/` | Create XxxPage + model, register in routes.go |
|
||||||
|
| HTMX patterns | `HTMX.md` | `<hx-partial>`, morphing, SSE, OOB updates |
|
||||||
|
| Modify layout | `layouts/base.templ` | CDN scripts, htmx-config meta tag |
|
||||||
|
| WebAwesome ref | [webawesome.com](https://webawesome.com) | All `wa-*` components |
|
||||||
|
| Migrate HTML | `_migrate/` -> `views/` | See MIGRATION.md |
|
||||||
|
|
||||||
- Standard `gofmt` formatting
|
## CONVENTIONS
|
||||||
- Package-level types in `models/` directory
|
|
||||||
- Handlers in `handlers/routes.go` with pattern: `handleX(w http.ResponseWriter, r *http.Request)`
|
|
||||||
- Use `r.Header.Get("HX-Request") == "true"` to detect HTMX requests
|
|
||||||
- Return partials for HTMX, full pages for direct navigation
|
|
||||||
|
|
||||||
### Templ Files (.templ)
|
### Naming
|
||||||
|
|
||||||
- One main page template per file: `PageName(data ModelType) templ.Component`
|
| Type | Pattern | Example |
|
||||||
- Helper functions lowercase: `helperName()`
|
|------|---------|---------|
|
||||||
- CSS-in-templ using `templ css` blocks or `<style>` tags
|
| Page template | `XxxPage(data)` | `DashboardPage(data, tab)` |
|
||||||
- Component parameters use Go types from `models/` package
|
| Partial (HTMX) | `XxxContent`, `XxxTab` | `DashboardContent(data, tab)` |
|
||||||
|
| OOB update | `XxxWithStepper`, `XxxWithOOB` | `WelcomeStepWithStepper(step)` |
|
||||||
|
| Model | `XxxData`, `XxxState` | `DashboardData`, `LoginState` |
|
||||||
|
| Factory | `DefaultXxxData()` | `DefaultDashboardData()` |
|
||||||
|
| Handler | `handleXxx` | `handleDashboard` |
|
||||||
|
|
||||||
### Naming Conventions
|
### HTMX 4 Request Pattern
|
||||||
|
|
||||||
| Type | Convention | Example |
|
|
||||||
|------|------------|---------|
|
|
||||||
| Page template | `XxxPage` | `SettingsPage`, `DashboardPage` |
|
|
||||||
| Partial template | `XxxContent`, `XxxTab` | `DashboardContent`, `ProfileTab` |
|
|
||||||
| OOB update | `XxxWithOOB`, `XxxWithStepper` | `LoginStepWithOOB` |
|
|
||||||
| Model struct | PascalCase | `SettingsData`, `ProfileSettings` |
|
|
||||||
| Handler func | `handleXxx` | `handleSettings`, `handleDashboard` |
|
|
||||||
|
|
||||||
## HTMX 4 Patterns
|
|
||||||
|
|
||||||
### Partial Updates
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div hx-get="/endpoint" hx-target="#target" hx-swap="innerHTML">
|
|
||||||
```
|
|
||||||
|
|
||||||
### Out-of-Band Updates
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
templ ComponentWithOOB() {
|
func handleXxx(w http.ResponseWriter, r *http.Request) {
|
||||||
@MainContent()
|
data := models.DefaultXxxData()
|
||||||
<div id="sidebar" hx-swap-oob="true">
|
if r.Header.Get("HX-Request") == "true" {
|
||||||
@UpdatedSidebar()
|
views.XxxContent(data).Render(r.Context(), w) // Partial
|
||||||
|
return
|
||||||
|
}
|
||||||
|
views.XxxPage(data).Render(r.Context(), w) // Full page
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### OOB Updates (Multi-Target)
|
||||||
|
|
||||||
|
```templ
|
||||||
|
templ XxxStepWithStepper(step int) {
|
||||||
|
@XxxStepContent(step)
|
||||||
|
<div id="stepper-container" hx-swap-oob="innerHTML">
|
||||||
|
@components.Stepper(step, steps)
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### HTMX Request Detection
|
### Templ Syntax Quick Ref
|
||||||
|
|
||||||
```go
|
```templ
|
||||||
if r.Header.Get("HX-Request") == "true" {
|
if cond { } // Conditional
|
||||||
views.PartialContent(data).Render(r.Context(), w)
|
for _, item := range items { } // Loop
|
||||||
return
|
<input disabled?={ isDisabled } /> // Bool attr
|
||||||
}
|
<div class={ "base " + extra }> // String interpolation
|
||||||
views.FullPage(data).Render(r.Context(), w)
|
@ChildComponent(props) // Composition
|
||||||
|
@templ.Raw(html) // Raw HTML (avoid)
|
||||||
```
|
```
|
||||||
|
|
||||||
## WebAwesome Components
|
## ANTI-PATTERNS (THIS PROJECT)
|
||||||
|
|
||||||
Use `wa-*` web components for UI:
|
| DO NOT | Why |
|
||||||
|
|--------|-----|
|
||||||
|
| Edit `*_templ.go` files | Generated by `templ generate` - changes purged |
|
||||||
|
| Use separate `htmx-ext-*` packages | HTMX 4 bundles extensions at `/dist/ext/` |
|
||||||
|
| Rely on implicit inheritance | Use `:inherited` modifier (`hx-target:inherited`) |
|
||||||
|
| Use `TopBar` or `TopBarStyles` | DEPRECATED - use `wa-page` slots instead |
|
||||||
|
| Manual DOM manipulation | Use `hx-*` attributes; server controls state |
|
||||||
|
| Put handlers in `handlers/` | Docs outdated - use `cmd/server/routes.go` |
|
||||||
|
|
||||||
```html
|
## COMMANDS
|
||||||
<wa-button variant="brand">Click me</wa-button>
|
|
||||||
<wa-card>Content</wa-card>
|
|
||||||
<wa-tab-group>
|
|
||||||
<wa-tab panel="one">Tab 1</wa-tab>
|
|
||||||
<wa-tab-panel name="one">Panel 1</wa-tab-panel>
|
|
||||||
</wa-tab-group>
|
|
||||||
<wa-icon name="user"></wa-icon>
|
|
||||||
<wa-avatar initials="JD"></wa-avatar>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Route Registration
|
|
||||||
|
|
||||||
All routes in `handlers/routes.go`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func RegisterRoutes(mux *http.ServeMux) {
|
|
||||||
mux.HandleFunc("GET /path", handlePath)
|
|
||||||
mux.HandleFunc("POST /path/action", handleAction)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Go 1.22+ routing patterns:
|
|
||||||
- `GET /path` - method prefix
|
|
||||||
- `GET /path/{param}` - path parameters via `r.PathValue("param")`
|
|
||||||
- `GET /path/{param...}` - catch-all
|
|
||||||
|
|
||||||
## Model Pattern
|
|
||||||
|
|
||||||
Models in `models/` package with `DefaultXxxData()` factory functions:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// models/example.go
|
|
||||||
package models
|
|
||||||
|
|
||||||
type ExampleData struct {
|
|
||||||
Field1 string
|
|
||||||
Field2 int
|
|
||||||
Items []Item
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultExampleData() ExampleData {
|
|
||||||
return ExampleData{
|
|
||||||
Field1: "default",
|
|
||||||
Items: []Item{...},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Pattern (HTML to Templ)
|
|
||||||
|
|
||||||
When converting `_migrate/*.html` files:
|
|
||||||
|
|
||||||
1. Create model types in `models/xxx.go`
|
|
||||||
2. Create view in `views/xxx.templ`
|
|
||||||
3. Add route in `handlers/routes.go`
|
|
||||||
4. Update sidebar active state in `components/sidebar.templ`
|
|
||||||
|
|
||||||
### Templ Syntax Reference
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Conditionals
|
|
||||||
if condition {
|
|
||||||
<div>shown</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loops
|
|
||||||
for _, item := range items {
|
|
||||||
<div>{ item.Name }</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Boolean attributes
|
|
||||||
<input disabled?={ isDisabled } />
|
|
||||||
<wa-tab active?={ tab == "profile" }>
|
|
||||||
|
|
||||||
// String interpolation
|
|
||||||
<div class={ "base " + extraClass }>
|
|
||||||
<div data-id={ item.ID }>
|
|
||||||
|
|
||||||
// Raw HTML (use sparingly)
|
|
||||||
@templ.Raw(htmlString)
|
|
||||||
|
|
||||||
// Component composition
|
|
||||||
@ChildComponent(props)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Generate templ (REQUIRED after .templ changes)
|
||||||
go test ./...
|
templ generate
|
||||||
|
|
||||||
# Run with verbose
|
# Dev server on :8080
|
||||||
go test -v ./...
|
go run ./cmd/server
|
||||||
|
# OR
|
||||||
|
make start
|
||||||
|
|
||||||
# Run specific package
|
# Full cycle: generate + start + open browser
|
||||||
go test ./handlers/...
|
make all
|
||||||
|
|
||||||
|
# Build binary
|
||||||
|
make build # outputs ./nebula
|
||||||
|
|
||||||
|
# Watch mode
|
||||||
|
templ generate --watch
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Tasks
|
## STACK VERSIONS
|
||||||
|
|
||||||
### Add New Page
|
- **Go**: 1.25.5 (cutting-edge)
|
||||||
|
- **templ**: v0.3.977
|
||||||
|
- **htmx**: 4.0.0-alpha5 (CDN)
|
||||||
|
- **WebAwesome**: Kit 47c7425b971f443c (CDN)
|
||||||
|
- **D3.js**: v7 (charts)
|
||||||
|
|
||||||
1. Create `models/newpage.go` with data types
|
## NOTES
|
||||||
2. Create `views/newpage.templ` with page template
|
|
||||||
3. Add handler in `handlers/routes.go`
|
|
||||||
4. Add nav item in `components/sidebar.templ` if needed
|
|
||||||
|
|
||||||
### Add New Component
|
- **No tests exist** - `go test ./...` returns "no test files"
|
||||||
|
- **No CI/CD** - No `.github/workflows/`
|
||||||
1. Create in `components/newcomp.templ`
|
- **SQLite WASM** mentioned in docs but not implemented
|
||||||
2. Import in views: `import "nebula/components"`
|
- **Documentation references `handlers/`** - doesn't exist; handlers are in `cmd/server/routes.go`
|
||||||
3. Use: `@components.NewComp(props)`
|
- Config split: root `config.go` (app config) vs `pkg/config/` (HTMX feature flags)
|
||||||
|
- Middleware chain: `nebula.Middleware(opts...)(mux)` injects context for templates
|
||||||
### Modify Layout
|
|
||||||
|
|
||||||
Layouts in `layouts/`:
|
|
||||||
- `base.templ` - HTML document wrapper with htmx CDN
|
|
||||||
- `app.templ` - Dashboard app shell with sidebar
|
|
||||||
- `centered.templ` - Centered auth pages
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- **templ**: Template engine - `go install github.com/a-h/templ/cmd/templ@latest`
|
|
||||||
- **htmx 4**: Loaded from CDN in `layouts/base.templ`
|
|
||||||
- **WebAwesome**: Loaded from CDN in `layouts/base.templ`
|
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -4,10 +4,13 @@ all: gen start open
|
|||||||
gen:
|
gen:
|
||||||
@templ generate
|
@templ generate
|
||||||
|
|
||||||
|
build:
|
||||||
|
@go build -o nebula ./cmd/server
|
||||||
|
|
||||||
start:
|
start:
|
||||||
@go run main.go
|
@go run ./cmd/server
|
||||||
|
|
||||||
open:
|
open:
|
||||||
@open http://localhost:8080
|
@open http://localhost:8080
|
||||||
|
|
||||||
.PHONY: gen start
|
.PHONY: gen build start open all
|
||||||
|
|||||||
29
cmd/server/main.go
Normal file
29
cmd/server/main.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"nebula"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// Register application routes
|
||||||
|
registerRoutes(mux)
|
||||||
|
|
||||||
|
// Apply nebula middleware
|
||||||
|
handler := nebula.Middleware(
|
||||||
|
nebula.WithBasePath("/"),
|
||||||
|
nebula.WithAppTitle("Sonr Wallet"),
|
||||||
|
nebula.WithTheme("dark"),
|
||||||
|
nebula.WithTransitions(true),
|
||||||
|
nebula.WithPreload(true),
|
||||||
|
)(mux)
|
||||||
|
|
||||||
|
addr := ":8080"
|
||||||
|
fmt.Printf("Starting server at http://localhost%s\n", addr)
|
||||||
|
log.Fatal(http.ListenAndServe(addr, handler))
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handlers
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"nebula/views"
|
"nebula/views"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterRoutes(mux *http.ServeMux) {
|
func registerRoutes(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("GET /", handleWelcome)
|
mux.HandleFunc("GET /", handleWelcome)
|
||||||
mux.HandleFunc("GET /welcome", handleWelcome)
|
mux.HandleFunc("GET /welcome", handleWelcome)
|
||||||
mux.HandleFunc("GET /welcome/step/{step}", handleWelcomeStep)
|
mux.HandleFunc("GET /welcome/step/{step}", handleWelcomeStep)
|
||||||
19
config.go
Normal file
19
config.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"nebula/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config = config.Config
|
||||||
|
type HTMXConfig = config.HTMXConfig
|
||||||
|
type SSEConfig = config.SSEConfig
|
||||||
|
|
||||||
|
func DefaultConfig() Config {
|
||||||
|
return config.Default()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigFromContext(ctx context.Context) *Config {
|
||||||
|
return config.FromContext(ctx)
|
||||||
|
}
|
||||||
36
htmx/context.go
Normal file
36
htmx/context.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package htmx
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const htmxContextKey contextKey = "htmx"
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
IsHTMX bool
|
||||||
|
IsPartial bool
|
||||||
|
IsBoosted bool
|
||||||
|
Target string
|
||||||
|
Trigger string
|
||||||
|
TriggerID string
|
||||||
|
CurrentURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithContext(ctx context.Context, hc *Context) context.Context {
|
||||||
|
return context.WithValue(ctx, htmxContextKey, hc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromContext(ctx context.Context) *Context {
|
||||||
|
if hc, ok := ctx.Value(htmxContextKey).(*Context); ok {
|
||||||
|
return hc
|
||||||
|
}
|
||||||
|
return &Context{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsHTMXRequest(ctx context.Context) bool {
|
||||||
|
return FromContext(ctx).IsHTMX
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsPartialRequest(ctx context.Context) bool {
|
||||||
|
return FromContext(ctx).IsPartial
|
||||||
|
}
|
||||||
51
htmx/request.go
Normal file
51
htmx/request.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package htmx
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func IsRequest(r *http.Request) bool {
|
||||||
|
return r.Header.Get("HX-Request") == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsPartial(r *http.Request) bool {
|
||||||
|
return r.Header.Get("HX-Request-Type") == "partial"
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsBoosted(r *http.Request) bool {
|
||||||
|
return r.Header.Get("HX-Boosted") == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsHistoryRestore(r *http.Request) bool {
|
||||||
|
return r.Header.Get("HX-History-Restore-Request") == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTarget(r *http.Request) string {
|
||||||
|
return r.Header.Get("HX-Target")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTrigger(r *http.Request) string {
|
||||||
|
return r.Header.Get("HX-Trigger")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTriggerName(r *http.Request) string {
|
||||||
|
return r.Header.Get("HX-Trigger-Name")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCurrentURL(r *http.Request) string {
|
||||||
|
return r.Header.Get("HX-Current-URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPromptResponse(r *http.Request) string {
|
||||||
|
return r.Header.Get("HX-Prompt")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractContext(r *http.Request) *Context {
|
||||||
|
return &Context{
|
||||||
|
IsHTMX: IsRequest(r),
|
||||||
|
IsPartial: IsPartial(r),
|
||||||
|
IsBoosted: IsBoosted(r),
|
||||||
|
Target: GetTarget(r),
|
||||||
|
Trigger: GetTrigger(r),
|
||||||
|
TriggerID: GetTrigger(r),
|
||||||
|
CurrentURL: GetCurrentURL(r),
|
||||||
|
}
|
||||||
|
}
|
||||||
103
htmx/response.go
Normal file
103
htmx/response.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package htmx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResponse(w http.ResponseWriter) *Response {
|
||||||
|
return &Response{w: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Header() http.Header {
|
||||||
|
return r.w.Header()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Location(url string) *Response {
|
||||||
|
r.w.Header().Set("HX-Location", url)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) LocationWithTarget(url, target string) *Response {
|
||||||
|
data, _ := json.Marshal(map[string]string{"path": url, "target": target})
|
||||||
|
r.w.Header().Set("HX-Location", string(data))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) PushURL(url string) *Response {
|
||||||
|
r.w.Header().Set("HX-Push-Url", url)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) ReplaceURL(url string) *Response {
|
||||||
|
r.w.Header().Set("HX-Replace-Url", url)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Redirect(url string) *Response {
|
||||||
|
r.w.Header().Set("HX-Redirect", url)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Refresh() *Response {
|
||||||
|
r.w.Header().Set("HX-Refresh", "true")
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Reswap(strategy string) *Response {
|
||||||
|
r.w.Header().Set("HX-Reswap", strategy)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) ReswapWithModifiers(strategy string) *Response {
|
||||||
|
r.w.Header().Set("HX-Reswap", strategy)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Retarget(selector string) *Response {
|
||||||
|
r.w.Header().Set("HX-Retarget", selector)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Reselect(selector string) *Response {
|
||||||
|
r.w.Header().Set("HX-Reselect", selector)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Trigger(event string) *Response {
|
||||||
|
r.w.Header().Set("HX-Trigger", event)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) TriggerWithData(event string, data any) *Response {
|
||||||
|
payload, _ := json.Marshal(map[string]any{event: data})
|
||||||
|
r.w.Header().Set("HX-Trigger", string(payload))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) TriggerAfterSwap(event string) *Response {
|
||||||
|
r.w.Header().Set("HX-Trigger-After-Swap", event)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) TriggerAfterSettle(event string) *Response {
|
||||||
|
r.w.Header().Set("HX-Trigger-After-Settle", event)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) StopPolling() *Response {
|
||||||
|
r.w.WriteHeader(286)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) NoContent() {
|
||||||
|
r.w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) PreventSwap() *Response {
|
||||||
|
return r.Reswap("none")
|
||||||
|
}
|
||||||
@@ -1,36 +1,44 @@
|
|||||||
package layouts
|
package layouts
|
||||||
|
|
||||||
// Base provides the HTML document structure for all pages
|
import "nebula/pkg/config"
|
||||||
|
|
||||||
templ Base(title string) {
|
templ Base(title string) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="wa-cloak wa-theme-dark">
|
<html lang="en" class={ "wa-cloak", config.FromContext(ctx).ThemeClass() }>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<meta name="color-scheme" content="dark light"/>
|
<meta name="color-scheme" content="dark light"/>
|
||||||
<title>{ title } - Sonr Motr Wallet</title>
|
<meta name="htmx-config" content={ config.FromContext(ctx).HTMXConfigJSON() }/>
|
||||||
|
<title>{ title } - { config.FromContext(ctx).AppTitle }</title>
|
||||||
<script src="https://kit.webawesome.com/47c7425b971f443c.js" crossorigin="anonymous"></script>
|
<script src="https://kit.webawesome.com/47c7425b971f443c.js" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/htmx.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/htmx.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/ext/hx-preload.js"></script>
|
if config.FromContext(ctx).Preload {
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/ext/hx-preload.js"></script>
|
||||||
|
}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
||||||
<style>
|
@baseStyles()
|
||||||
:root {
|
|
||||||
--wa-color-primary: #17c2ff;
|
|
||||||
--sidebar-width: 64px;
|
|
||||||
}
|
|
||||||
html, body {
|
|
||||||
min-height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
wa-button, a, [onclick], [hx-get], [hx-post] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{ children... }
|
{ children... }
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ baseStyles() {
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--wa-color-primary: #17c2ff;
|
||||||
|
--sidebar-width: 64px;
|
||||||
|
}
|
||||||
|
html, body {
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
wa-button, a, [onclick], [hx-get], [hx-post] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ package layouts
|
|||||||
import "github.com/a-h/templ"
|
import "github.com/a-h/templ"
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
// Base provides the HTML document structure for all pages
|
import "nebula/pkg/config"
|
||||||
|
|
||||||
func Base(title string) templ.Component {
|
func Base(title string) templ.Component {
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
@@ -30,20 +31,86 @@ func Base(title string) templ.Component {
|
|||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"wa-cloak wa-theme-dark\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"color-scheme\" content=\"dark light\"><title>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 = []any{"wa-cloak", config.FromContext(ctx).ThemeClass()}
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `layouts/base.templ`, Line: 11, Col: 17}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Sonr Motr Wallet</title><script src=\"https://kit.webawesome.com/47c7425b971f443c.js\" crossorigin=\"anonymous\"></script><script src=\"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/htmx.min.js\"></script><script src=\"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/ext/hx-preload.js\"></script><script src=\"https://cdn.jsdelivr.net/npm/d3@7\"></script><style>\n\t\t\t\t:root {\n\t\t\t\t\t--wa-color-primary: #17c2ff;\n\t\t\t\t\t--sidebar-width: 64px;\n\t\t\t\t}\n\t\t\t\thtml, body {\n\t\t\t\t\tmin-height: 100%;\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tcursor: default;\n\t\t\t\t}\n\t\t\t\twa-button, a, [onclick], [hx-get], [hx-post] {\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t}\n\t\t\t</style></head><body>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<html lang=\"en\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `layouts/base.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"color-scheme\" content=\"dark light\"><meta name=\"htmx-config\" content=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(config.FromContext(ctx).HTMXConfigJSON())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `layouts/base.templ`, Line: 12, Col: 78}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"><title>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `layouts/base.templ`, Line: 13, Col: 17}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " - ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(config.FromContext(ctx).AppTitle)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `layouts/base.templ`, Line: 13, Col: 56}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</title><script src=\"https://kit.webawesome.com/47c7425b971f443c.js\" crossorigin=\"anonymous\"></script><script src=\"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/htmx.min.js\"></script>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if config.FromContext(ctx).Preload {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<script src=\"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/ext/hx-preload.js\"></script>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<script src=\"https://cdn.jsdelivr.net/npm/d3@7\"></script>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = baseStyles().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</head><body>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -51,7 +118,36 @@ func Base(title string) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</body></html>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func baseStyles() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var7 == nil {
|
||||||
|
templ_7745c5c3_Var7 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<style>\n\t\t:root {\n\t\t\t--wa-color-primary: #17c2ff;\n\t\t\t--sidebar-width: 64px;\n\t\t}\n\t\thtml, body {\n\t\t\tmin-height: 100%;\n\t\t\tpadding: 0;\n\t\t\tmargin: 0;\n\t\t\tcursor: default;\n\t\t}\n\t\twa-button, a, [onclick], [hx-get], [hx-post] {\n\t\t\tcursor: pointer;\n\t\t}\n\t</style>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
18
main.go
18
main.go
@@ -1,18 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"nebula/handlers"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
handlers.RegisterRoutes(mux)
|
|
||||||
|
|
||||||
addr := ":8080"
|
|
||||||
fmt.Printf("Starting server at http://localhost%s\n", addr)
|
|
||||||
log.Fatal(http.ListenAndServe(addr, mux))
|
|
||||||
}
|
|
||||||
38
middleware.go
Normal file
38
middleware.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"nebula/htmx"
|
||||||
|
"nebula/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Middleware(opts ...Option) func(http.Handler) http.Handler {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
hxCtx := htmx.ExtractContext(r)
|
||||||
|
ctx := htmx.WithContext(r.Context(), hxCtx)
|
||||||
|
ctx = config.WithContext(ctx, &cfg)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SSEMiddleware() func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Header.Get("Accept") == "text/event-stream" {
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
40
mount.go
Normal file
40
mount.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler creates a new http.Handler with nebula middleware applied.
|
||||||
|
// The handler parameter is wrapped with HTMX context and config middleware.
|
||||||
|
func Handler(handler http.Handler, opts ...Option) http.Handler {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var h http.Handler = handler
|
||||||
|
h = Middleware(opts...)(h)
|
||||||
|
|
||||||
|
if cfg.SSE.Enabled {
|
||||||
|
h = SSEMiddleware()(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount registers a handler at the configured base path on the provided mux.
|
||||||
|
// Use this to mount your application routes with nebula middleware.
|
||||||
|
func Mount(mux *http.ServeMux, handler http.Handler, opts ...Option) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := Handler(handler, opts...)
|
||||||
|
|
||||||
|
if cfg.BasePath == "/" {
|
||||||
|
mux.Handle("/", h)
|
||||||
|
} else {
|
||||||
|
mux.Handle(cfg.BasePath+"/", http.StripPrefix(cfg.BasePath, h))
|
||||||
|
}
|
||||||
|
}
|
||||||
78
options.go
Normal file
78
options.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package nebula
|
||||||
|
|
||||||
|
type Option func(*Config)
|
||||||
|
|
||||||
|
func WithBasePath(path string) Option {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.BasePath = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAppTitle(title string) Option {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.AppTitle = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTheme(theme string) Option {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.Theme = theme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTransitions(enabled bool) Option {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.HTMX.Transitions = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithImplicitInheritance(enabled bool) Option {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.HTMX.ImplicitInheritance = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSelfRequestsOnly(enabled bool) Option {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.HTMX.SelfRequestsOnly = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTimeout(ms int) Option {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.HTMX.Timeout = ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithNoSwap(codes ...string) Option {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.HTMX.NoSwap = codes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSSE(enabled bool) Option {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.SSE.Enabled = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSSEReconnect(enabled bool, delay, maxDelay, maxAttempts int) Option {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.SSE.Reconnect = enabled
|
||||||
|
c.SSE.ReconnectDelay = delay
|
||||||
|
c.SSE.ReconnectMaxDelay = maxDelay
|
||||||
|
c.SSE.ReconnectMaxAttempts = maxAttempts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPreload(enabled bool) Option {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.Preload = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMorphing(enabled bool) Option {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.Morphing = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
106
pkg/config/config.go
Normal file
106
pkg/config/config.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey struct{}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
BasePath string
|
||||||
|
AppTitle string
|
||||||
|
Theme string
|
||||||
|
HTMX HTMXConfig
|
||||||
|
SSE SSEConfig
|
||||||
|
Preload bool
|
||||||
|
Morphing bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTMXConfig struct {
|
||||||
|
Transitions bool
|
||||||
|
ImplicitInheritance bool
|
||||||
|
SelfRequestsOnly bool
|
||||||
|
Timeout int
|
||||||
|
NoSwap []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SSEConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Reconnect bool
|
||||||
|
ReconnectDelay int
|
||||||
|
ReconnectMaxDelay int
|
||||||
|
ReconnectMaxAttempts int
|
||||||
|
ReconnectJitter float64
|
||||||
|
PauseInBackground bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Default() Config {
|
||||||
|
return Config{
|
||||||
|
BasePath: "/",
|
||||||
|
AppTitle: "Sonr Wallet",
|
||||||
|
Theme: "dark",
|
||||||
|
HTMX: HTMXConfig{
|
||||||
|
Transitions: true,
|
||||||
|
ImplicitInheritance: false,
|
||||||
|
SelfRequestsOnly: true,
|
||||||
|
Timeout: 0,
|
||||||
|
NoSwap: []string{"204", "304"},
|
||||||
|
},
|
||||||
|
SSE: SSEConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Reconnect: true,
|
||||||
|
ReconnectDelay: 500,
|
||||||
|
ReconnectMaxDelay: 60000,
|
||||||
|
ReconnectMaxAttempts: 10,
|
||||||
|
ReconnectJitter: 0.3,
|
||||||
|
PauseInBackground: false,
|
||||||
|
},
|
||||||
|
Preload: true,
|
||||||
|
Morphing: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithContext(ctx context.Context, cfg *Config) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKey{}, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromContext(ctx context.Context) *Config {
|
||||||
|
if cfg, ok := ctx.Value(contextKey{}).(*Config); ok {
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
defaultCfg := Default()
|
||||||
|
return &defaultCfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) HTMXConfigJSON() string {
|
||||||
|
cfg := map[string]any{
|
||||||
|
"transitions": c.HTMX.Transitions,
|
||||||
|
"implicitInheritance": c.HTMX.ImplicitInheritance,
|
||||||
|
"selfRequestsOnly": c.HTMX.SelfRequestsOnly,
|
||||||
|
"timeout": c.HTMX.Timeout,
|
||||||
|
"noSwap": c.HTMX.NoSwap,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.SSE.Enabled {
|
||||||
|
cfg["streams"] = map[string]any{
|
||||||
|
"reconnect": c.SSE.Reconnect,
|
||||||
|
"reconnectDelay": c.SSE.ReconnectDelay,
|
||||||
|
"reconnectMaxDelay": c.SSE.ReconnectMaxDelay,
|
||||||
|
"reconnectMaxAttempts": c.SSE.ReconnectMaxAttempts,
|
||||||
|
"reconnectJitter": c.SSE.ReconnectJitter,
|
||||||
|
"pauseInBackground": c.SSE.PauseInBackground,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ThemeClass() string {
|
||||||
|
return fmt.Sprintf("wa-theme-%s", c.Theme)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user