feat(nebula): add core middleware, mount, and config packages
This commit is contained in:
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))
|
||||
}
|
||||
176
cmd/server/routes.go
Normal file
176
cmd/server/routes.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"nebula/models"
|
||||
"nebula/views"
|
||||
)
|
||||
|
||||
func registerRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /", handleWelcome)
|
||||
mux.HandleFunc("GET /welcome", handleWelcome)
|
||||
mux.HandleFunc("GET /welcome/step/{step}", handleWelcomeStep)
|
||||
|
||||
mux.HandleFunc("GET /register", handleRegister)
|
||||
mux.HandleFunc("GET /register/step/{step}", handleRegisterStep)
|
||||
mux.HandleFunc("GET /register/capabilities", handleRegisterCapabilities)
|
||||
mux.HandleFunc("POST /register/verify-code", handleRegisterVerifyCode)
|
||||
|
||||
mux.HandleFunc("GET /login", handleLogin)
|
||||
mux.HandleFunc("GET /login/step/{step}", handleLoginStep)
|
||||
mux.HandleFunc("GET /login/qr-status", handleLoginQRStatus)
|
||||
|
||||
mux.HandleFunc("GET /authorize", handleAuthorize)
|
||||
mux.HandleFunc("POST /authorize/approve", handleAuthorizeApprove)
|
||||
mux.HandleFunc("POST /authorize/deny", handleAuthorizeDeny)
|
||||
|
||||
mux.HandleFunc("GET /dashboard", handleDashboard)
|
||||
|
||||
mux.HandleFunc("GET /settings", handleSettings)
|
||||
}
|
||||
|
||||
// handleWelcome renders the full welcome page at step 1
|
||||
func handleWelcome(w http.ResponseWriter, r *http.Request) {
|
||||
views.WelcomePage(1).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
// handleWelcomeStep handles HTMX partial updates for step navigation
|
||||
func handleWelcomeStep(w http.ResponseWriter, r *http.Request) {
|
||||
stepStr := r.PathValue("step")
|
||||
step, err := strconv.Atoi(stepStr)
|
||||
if err != nil || step < 1 || step > 3 {
|
||||
step = 1
|
||||
}
|
||||
|
||||
// Check if this is an HTMX request
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
// Return step content with OOB stepper update (HTMX 4 pattern)
|
||||
views.WelcomeStepWithStepper(step).Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
|
||||
views.WelcomePage(step).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
state := models.RegisterState{Step: 1}
|
||||
views.RegisterPage(state).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func handleRegisterStep(w http.ResponseWriter, r *http.Request) {
|
||||
stepStr := r.PathValue("step")
|
||||
step, err := strconv.Atoi(stepStr)
|
||||
if err != nil || step < 1 || step > 3 {
|
||||
step = 1
|
||||
}
|
||||
|
||||
method := r.URL.Query().Get("method")
|
||||
if method == "" {
|
||||
method = "passkey"
|
||||
}
|
||||
|
||||
state := models.RegisterState{Step: step, Method: method}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
views.RegisterStepWithStepper(state).Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
|
||||
views.RegisterPage(state).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func handleRegisterCapabilities(w http.ResponseWriter, r *http.Request) {
|
||||
caps := models.DeviceCapabilities{
|
||||
Platform: true,
|
||||
CrossPlatform: true,
|
||||
Conditional: true,
|
||||
}
|
||||
views.CapabilitiesResult(caps).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func handleRegisterVerifyCode(w http.ResponseWriter, r *http.Request) {
|
||||
state := models.RegisterState{Step: 3}
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
views.RegisterStepWithStepper(state).Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
views.RegisterPage(state).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
state := models.LoginState{Step: "1"}
|
||||
views.LoginPage(state).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func handleLoginStep(w http.ResponseWriter, r *http.Request) {
|
||||
step := r.PathValue("step")
|
||||
if step == "" {
|
||||
step = "1"
|
||||
}
|
||||
|
||||
state := models.LoginState{Step: step}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
views.LoginStepWithOOB(state).Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
|
||||
views.LoginPage(state).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
var qrPollCount = 0
|
||||
|
||||
func handleLoginQRStatus(w http.ResponseWriter, r *http.Request) {
|
||||
qrPollCount++
|
||||
if qrPollCount >= 3 {
|
||||
qrPollCount = 0
|
||||
views.QRStatusSuccess().Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
views.QRStatusWaiting().Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func handleAuthorize(w http.ResponseWriter, r *http.Request) {
|
||||
reqType := r.URL.Query().Get("type")
|
||||
req := models.DefaultAuthRequest(reqType)
|
||||
views.AuthorizePage(req).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func handleAuthorizeApprove(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
actionType := r.FormValue("type")
|
||||
if actionType == "" {
|
||||
actionType = "connect"
|
||||
}
|
||||
views.AuthResultSuccess(actionType).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func handleAuthorizeDeny(w http.ResponseWriter, r *http.Request) {
|
||||
views.AuthResultDenied().Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
tab := r.URL.Query().Get("tab")
|
||||
if tab == "" {
|
||||
tab = "overview"
|
||||
}
|
||||
data := models.DefaultDashboardData()
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
views.DashboardContent(data, tab).Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
|
||||
views.DashboardPage(data, tab).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func handleSettings(w http.ResponseWriter, r *http.Request) {
|
||||
tab := r.URL.Query().Get("tab")
|
||||
if tab == "" {
|
||||
tab = "profile"
|
||||
}
|
||||
data := models.DefaultSettingsData()
|
||||
views.SettingsPage(data, tab).Render(r.Context(), w)
|
||||
}
|
||||
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")
|
||||
}
|
||||
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