Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fcbe848b8a | |||
| 6b0db4616a | |||
| c77650659e | |||
| 6b9935ddbe | |||
| 51b5d57222 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
nebula
|
||||
.osgrep
|
||||
|
||||
7
Makefile
7
Makefile
@@ -4,10 +4,13 @@ all: gen start open
|
||||
gen:
|
||||
@templ generate
|
||||
|
||||
build:
|
||||
@go build -o nebula ./cmd/server
|
||||
|
||||
start:
|
||||
@go run main.go
|
||||
@go run ./cmd/server
|
||||
|
||||
open:
|
||||
@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 (
|
||||
"net/http"
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"nebula/views"
|
||||
)
|
||||
|
||||
func RegisterRoutes(mux *http.ServeMux) {
|
||||
func registerRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /", handleWelcome)
|
||||
mux.HandleFunc("GET /welcome", handleWelcome)
|
||||
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
|
||||
|
||||
// Base provides the HTML document structure for all pages
|
||||
import "nebula/pkg/config"
|
||||
|
||||
templ Base(title string) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="wa-cloak wa-theme-dark">
|
||||
<html lang="en" class={ "wa-cloak", config.FromContext(ctx).ThemeClass() }>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<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://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>
|
||||
<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>
|
||||
@baseStyles()
|
||||
</head>
|
||||
<body>
|
||||
{ children... }
|
||||
</body>
|
||||
</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 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 {
|
||||
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
|
||||
@@ -30,20 +31,86 @@ func Base(title string) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
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))
|
||||
var templ_7745c5c3_Var2 = []any{"wa-cloak", config.FromContext(ctx).ThemeClass()}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -51,7 +118,36 @@ func Base(title string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
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