mirror of
https://codeberg.org/readeck/readeck.git
synced 2026-05-19 11:00:36 +00:00
Templ sunrise
- Updated makefile so "make serve" watches for .templ files and
rebuild files upon changes
- Added a "templ" make target
- Added a top level "components" module
- Added a "components/forms" module
- Added server.RenderComponent method
- Added server.RenderTurboStreamComponent
- Preferences lazy loading
We're now ready to roll!
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
*_templ.go linguist-generated
|
||||
*_templ.go binary
|
||||
@@ -32,6 +32,7 @@ export GOARCH?=
|
||||
SITECONFIG_SRC=./ftr-site-config
|
||||
SITECONFIG_DEST=pkg/extract/contentscripts/assets/site-config
|
||||
|
||||
TEMPL_PKG ?= github.com/a-h/templ/cmd/templ@latest
|
||||
GOLANGCI_PKG ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.9.0
|
||||
AIR_PKG ?= github.com/air-verse/air@v1.64.5
|
||||
SLOC_PKG ?= github.com/boyter/scc/v3@v3.6.0
|
||||
@@ -69,6 +70,9 @@ setup:
|
||||
$(GO) mod download
|
||||
${MAKE} -C web setup
|
||||
|
||||
templ:
|
||||
$(GO) run $(TEMPL_PKG) generate
|
||||
|
||||
# Build the server
|
||||
.PHONY: build
|
||||
build:
|
||||
@@ -155,41 +159,68 @@ update-site-config:
|
||||
# These targets provide helper for autoreload during development.
|
||||
# `make dev` starts all the needed watch/autoreload and is only
|
||||
# needed when working on web/* or docs/src/*.
|
||||
# When working only on go files, `make serve` is enough.
|
||||
# When working only on go and templ files, `make serve` is enough.
|
||||
|
||||
# Starts the HTTP server
|
||||
# It runs air watching the source files and the assets. It builds and reloads
|
||||
# the server on any change.
|
||||
.PHONY: serve
|
||||
serve:
|
||||
${MAKE} -j2 watch/templ watch/server
|
||||
|
||||
# Starts 3 watchers/reloaders for a full autoreload
|
||||
# dev server.
|
||||
# The initial errors during startup are normal
|
||||
.PHONY: dev
|
||||
dev:
|
||||
${MAKE} -j3 docs-watch web-watch serve
|
||||
${MAKE} -j3 watch/docs watch/web serve
|
||||
|
||||
# Starts the HTTP server
|
||||
# It runs air watching the source files and the assets. It builds and reloads
|
||||
# the server on any change.
|
||||
.PHONY: serve
|
||||
serve: SERVE_CMD ?= serve
|
||||
serve:
|
||||
.PHONY: watch/server
|
||||
watch/server: SERVE_CMD ?= serve
|
||||
watch/server:
|
||||
$(GO) run $(AIR_PKG) \
|
||||
--tmp_dir "dist" \
|
||||
--build.log "" \
|
||||
--build.cmd "${MAKE} DATE= build" \
|
||||
--build.bin "dist/readeck" \
|
||||
--build.args_bin "$(SERVE_CMD)" \
|
||||
--build.delay 2000 \
|
||||
--build.exclude_dir "" \
|
||||
--build.include_dir "assets,configs,docs/api,docs/assets,locales,internal,pkg" \
|
||||
--build.include_dir "assets,components,configs,docs/assets,locales,internal,pkg" \
|
||||
--build.include_ext "go,html,json,js,mo,tmpl,toml,xsl" \
|
||||
--build.delay 2000
|
||||
--build.kill_delay "10s" \
|
||||
--build.stop_on_error "false" \
|
||||
--misc.clean_on_exit false
|
||||
|
||||
# Rebuild components on change.
|
||||
# It runs air watching the .templ files and rebuilds them on any change.
|
||||
watch/templ:
|
||||
$(GO) run $(AIR_PKG) \
|
||||
--tmp_dir "dist" \
|
||||
--build.log "" \
|
||||
--build.cmd "$(GO) run $(TEMPL_PKG) generate -lazy" \
|
||||
--build.bin "/usr/bin/true" \
|
||||
--build.delay 1000 \
|
||||
--build.include_dir "components,internal" \
|
||||
--build.include_ext "templ" \
|
||||
--build.exclude_regex ".*_templ.go" \
|
||||
--build.kill_delay "0s" \
|
||||
--build.send_interrupt "false" \
|
||||
--build.stop_on_error "true" \
|
||||
--misc.clean_on_exit false
|
||||
|
||||
# Watch the docs/src folder and rebuild the documentation
|
||||
# on changes.
|
||||
.PHONY: docs-watch
|
||||
docs-watch:
|
||||
.PHONY: watch/docs
|
||||
watch/docs:
|
||||
$(GO) run $(AIR_PKG) \
|
||||
--tmp_dir "dist" \
|
||||
--build.log "" \
|
||||
--build.cmd "${MAKE} docs-build" \
|
||||
--build.bin "" \
|
||||
--build.bin "/usr/bin/true" \
|
||||
--build.exclude_dir "" \
|
||||
--build.include_file "CHANGELOG.md" \
|
||||
--build.include_dir "docs/src/en,docs/translations,docs/api" \
|
||||
@@ -197,8 +228,8 @@ docs-watch:
|
||||
--build.delay 100
|
||||
|
||||
# Starts the watcher on the web folder.
|
||||
.PHONY: web-watch
|
||||
web-watch:
|
||||
.PHONY: watch/web
|
||||
watch/web:
|
||||
@$(MAKE) -C web watch
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ SPDX-License-Identifier = "CC-BY-4.0"
|
||||
[[annotations]]
|
||||
path = [
|
||||
".dockerignore",
|
||||
".gitattributes",
|
||||
".gitignore",
|
||||
".go-version",
|
||||
".golangci.yml",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
TODO: these notes should move to a main CONTRIB.md file, when we have one.
|
||||
---
|
||||
|
||||
# Readeck templates
|
||||
|
||||
Readeck uses [Templ](https://templ.guide/) as a template/component engine.
|
||||
|
||||
Templ provides type safe templates and very fast execution. Here are some conventions used in Readeck when it comes to templates.
|
||||
|
||||
## Views and Components
|
||||
|
||||
A view is a page rendered by an `http.Handler`.
|
||||
|
||||
- Views don't have global variables but always have a context.
|
||||
- Views are functions that take (typed) arguments.
|
||||
- Views live in the same package as their handler and, therefore, have access to all the package's members (exported or not).
|
||||
- `.templ` files are always prefixed with `x-` (only for file organization)
|
||||
|
||||
To avoid crowding a package with a lot of functions, a package providing views **must** declare
|
||||
empty structs and add methods for each view and/or components.
|
||||
|
||||
The scructs cannot contain anything so they occupy no storage space and all have the same address.
|
||||
|
||||
```templ
|
||||
package something
|
||||
|
||||
import . "codeberg.org/readeck/readeck/components"
|
||||
|
||||
type Views struct{}
|
||||
|
||||
templ (_ Views) menu() {
|
||||
@SideMenuTitle() {
|
||||
Cookbook
|
||||
}
|
||||
<menu class="mt-4">
|
||||
// menu items...
|
||||
</menu>
|
||||
}
|
||||
|
||||
templ (v Views) base(title string) {
|
||||
@SideMenuLayout(title, v.menu()) {
|
||||
{ children... }
|
||||
}
|
||||
}
|
||||
|
||||
templ (v Views) somePage() {
|
||||
@v.base("a title") {
|
||||
// content...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then a handler can call the view like this:
|
||||
|
||||
```go
|
||||
server.RenderComponent(w, r, 200, Views{}.somePage())
|
||||
```
|
||||
|
||||
On complex views and components, it's best to split `Views{}` and `Components{}` in their respective structs.
|
||||
|
||||
## Component helpers
|
||||
|
||||
The package `codeberg.org/readeck/readeck/components` provides helper functions to work with components.
|
||||
|
||||
It can be imported globally as:
|
||||
|
||||
```go
|
||||
import (
|
||||
. "codeberg.org/readeck/readeck/components"
|
||||
)
|
||||
```
|
||||
|
||||
Form helpers are in `codeberg.org/readeck/readeck/components/forms`, which is usually imported with
|
||||
a shortcut:
|
||||
|
||||
```go
|
||||
import (
|
||||
F "codeberg.org/readeck/readeck/components/forms"
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
import "codeberg.org/readeck/readeck/pkg/ctxr"
|
||||
|
||||
type ctxBreadcrumbKey struct{}
|
||||
|
||||
// WithBreadcrumb adds [BreadcrumbList] to the context.
|
||||
var WithBreadcrumb, checkBreadcrumb = ctxr.WithChecker[[][2]string](ctxBreadcrumbKey{})
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
templ Breadcrumbs() {
|
||||
{{ items, _ := checkBreadcrumb(ctx) }}
|
||||
if len(items) > 0 {
|
||||
<nav class="breadcrumbs">
|
||||
<ol>
|
||||
<li>
|
||||
<a href={ URL(ctx, "/") } title={ L(ctx).Gettext("Home Page") }>
|
||||
@Icon("o-home")
|
||||
</a>
|
||||
</li>
|
||||
for _, item := range items {
|
||||
<li>
|
||||
if item[1] != "" {
|
||||
<a href={ item[1] }>{ item[0] }</a>
|
||||
} else {
|
||||
{ item[0] }
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
Generated
+139
@@ -0,0 +1,139 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1001
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
|
||||
//
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func Breadcrumbs() 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_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
items, _ := checkBreadcrumb(ctx)
|
||||
if len(items) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<nav class=\"breadcrumbs\"><ol><li><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, "/"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/breadcrumbs.templ`, Line: 13, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" title=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Home Page"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/breadcrumbs.templ`, Line: 13, Col: 66}
|
||||
}
|
||||
_, 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, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("o-home").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range items {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item[1] != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 templ.SafeURL
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(item[1])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/breadcrumbs.templ`, Line: 20, Col: 24}
|
||||
}
|
||||
_, 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, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item[0])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/breadcrumbs.templ`, Line: 20, Col: 36}
|
||||
}
|
||||
_, 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, 8, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item[0])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/breadcrumbs.templ`, Line: 22, Col: 16}
|
||||
}
|
||||
_, 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, 9, "</li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</ol></nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,138 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Package components contains the shared templ components.
|
||||
package components
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
|
||||
"codeberg.org/readeck/readeck/internal/profile/preferences"
|
||||
"codeberg.org/readeck/readeck/internal/server"
|
||||
"codeberg.org/readeck/readeck/internal/server/urls"
|
||||
"codeberg.org/readeck/readeck/locales"
|
||||
"codeberg.org/readeck/readeck/pkg/glob"
|
||||
"codeberg.org/readeck/readeck/pkg/strftime"
|
||||
)
|
||||
|
||||
// S is a shortcut to [fmt.Sprintf].
|
||||
var S = fmt.Sprintf
|
||||
|
||||
// L returns the context's current [locales.Locale].
|
||||
func L(ctx context.Context) *locales.Locale {
|
||||
return server.LocaleContext(ctx)
|
||||
}
|
||||
|
||||
// URL returns a [templ.SafeURL] (path only) for a given internal URL.
|
||||
func URL(ctx context.Context, args ...string) templ.SafeURL {
|
||||
return templ.URL(urls.PathOnly(urls.AbsoluteURLContext(ctx, args...)))
|
||||
}
|
||||
|
||||
// Asset returns an asset path.
|
||||
func Asset(ctx context.Context, name string) templ.SafeURL {
|
||||
return templ.URL(urls.PathOnly(urls.AssetURLContext(ctx, name)))
|
||||
}
|
||||
|
||||
// CSPNonce returns the CSP nonce for stylesheets and scripts.
|
||||
func CSPNonce(ctx context.Context) string {
|
||||
s, _ := server.GetCSPNonce(ctx)
|
||||
return s
|
||||
}
|
||||
|
||||
// CurrentPath returns the current path without the app prefix.
|
||||
func CurrentPath(ctx context.Context) string {
|
||||
return urls.CurrentPath(server.GetRequest(ctx))
|
||||
}
|
||||
|
||||
// PathIs returns whether one of the given paths p matches the current
|
||||
// request's path (query parameters excluded).
|
||||
func PathIs(ctx context.Context, p ...string) bool {
|
||||
cp := "/" + strings.TrimPrefix(server.GetRequest(ctx).URL.Path, urls.Prefix())
|
||||
for _, x := range p {
|
||||
if glob.Glob(x, cp) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasPermission returns whether the current user is granted permissions.
|
||||
func HasPermission(ctx context.Context, obj, act string) bool {
|
||||
return server.GetUser(ctx).HasPermission(obj, act)
|
||||
}
|
||||
|
||||
// IsAnonymous returns whether the current user is anonymous.
|
||||
func IsAnonymous(ctx context.Context) bool {
|
||||
return server.GetUser(ctx).IsAnonymous()
|
||||
}
|
||||
|
||||
// Strftime calls [strftime.Formatter.Strftime] with the user's locale.
|
||||
func Strftime(ctx context.Context, t time.Time, f string) string {
|
||||
return strftime.New(L(ctx)).Strftime(f, t)
|
||||
}
|
||||
|
||||
// Preferences returns the user's preferences.
|
||||
// Preferences are loaded only once, the first time they are needed.
|
||||
func Preferences(ctx context.Context) *preferences.Preferences {
|
||||
p := server.GetPreferences(ctx)
|
||||
if !p.IsLoaded() {
|
||||
p2 := preferences.New(server.GetUser(ctx), server.GetSession(server.GetRequest(ctx)))
|
||||
*p = *p2
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Tern is a poor man's ternary operator.
|
||||
// Never to be used outside a component!
|
||||
func Tern[T any](t func() bool, whenTrue T, whenFalse T) T {
|
||||
if t() {
|
||||
return whenTrue
|
||||
}
|
||||
return whenFalse
|
||||
}
|
||||
|
||||
// JSON returns a string of the marshaled data into JSON.
|
||||
// The result is not HTML escaped and can be indented if needed.
|
||||
func JSON(data any, indent bool) string {
|
||||
buf := new(bytes.Buffer)
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
if indent {
|
||||
enc.SetIndent("", " ")
|
||||
}
|
||||
enc.Encode(data) //nolint:errcheck
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// HTML copies directly the input [io.Reader] to the response writer.
|
||||
func HTML(r io.Reader) templ.Component {
|
||||
return templ.ComponentFunc(func(_ context.Context, w io.Writer) error {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := io.Copy(w, r)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// JetTemplate renders a Jet template.
|
||||
// TODO: remove after migration.
|
||||
func JetTemplate(name string, tc any) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
|
||||
t, err := server.GetTemplate(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.Execute(w, server.TemplateVars(server.GetRequest(ctx)), tc)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Package forms provide form related components.
|
||||
package forms
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
|
||||
"codeberg.org/readeck/readeck/pkg/forms"
|
||||
)
|
||||
|
||||
type baseWidget struct {
|
||||
name string
|
||||
value any
|
||||
label string
|
||||
help string
|
||||
required bool
|
||||
classes templ.CSSClasses
|
||||
controllAttrs templ.Attributes
|
||||
inputType string
|
||||
inputClasses templ.CSSClasses
|
||||
inputAttrs templ.Attributes
|
||||
}
|
||||
|
||||
// FieldOption is a function to set [baseWidget] properties.
|
||||
type FieldOption func(f *baseWidget)
|
||||
|
||||
type fieldWidget struct {
|
||||
forms.Field
|
||||
baseWidget
|
||||
}
|
||||
|
||||
func widget(field forms.Field, options ...FieldOption) fieldWidget {
|
||||
f := fieldWidget{
|
||||
Field: field,
|
||||
baseWidget: baseWidget{
|
||||
name: field.Name(),
|
||||
value: field.Value(),
|
||||
classes: templ.CSSClasses{},
|
||||
controllAttrs: templ.Attributes{},
|
||||
inputClasses: templ.CSSClasses{},
|
||||
inputAttrs: templ.Attributes{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(&f.baseWidget)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *fieldWidget) ariaAttrs() (attrs templ.Attributes) {
|
||||
attrs = templ.Attributes{}
|
||||
|
||||
if f.help != "" {
|
||||
attrs["aria-describedby"] = "description-" + f.name
|
||||
}
|
||||
if len(f.Errors()) > 0 {
|
||||
attrs["aria-errormessage"] = "errors-" + f.name
|
||||
attrs["aria-invalid"] = "true"
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
// Value set the field's value.
|
||||
func Value(v any) FieldOption {
|
||||
return func(f *baseWidget) {
|
||||
f.value = v
|
||||
}
|
||||
}
|
||||
|
||||
// Name sets the field's name.
|
||||
func Name(v string) FieldOption {
|
||||
return func(f *baseWidget) {
|
||||
f.name = v
|
||||
}
|
||||
}
|
||||
|
||||
// Label sets the field's label.
|
||||
func Label(v string) FieldOption {
|
||||
return func(f *baseWidget) {
|
||||
f.label = v
|
||||
}
|
||||
}
|
||||
|
||||
// Help sets the field's help.
|
||||
func Help(v string) FieldOption {
|
||||
return func(f *baseWidget) {
|
||||
f.help = v
|
||||
}
|
||||
}
|
||||
|
||||
// Required sets the field's required flag.
|
||||
func Required(v bool) FieldOption {
|
||||
return func(f *baseWidget) {
|
||||
f.required = v
|
||||
}
|
||||
}
|
||||
|
||||
// Classes sets the field's classes.
|
||||
func Classes(args ...any) FieldOption {
|
||||
return func(f *baseWidget) {
|
||||
f.classes = templ.Classes(args...)
|
||||
}
|
||||
}
|
||||
|
||||
// ControlAttrs sets the field's control attributes.
|
||||
func ControlAttrs(attrs templ.Attributes) FieldOption {
|
||||
return func(f *baseWidget) {
|
||||
maps.Copy(f.controllAttrs, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
// InputType sets the field's input type.
|
||||
// Works only for <input> elements.
|
||||
func InputType(v string) FieldOption {
|
||||
return func(f *baseWidget) {
|
||||
f.inputType = v
|
||||
}
|
||||
}
|
||||
|
||||
// InputClasses sets the field's input classes.
|
||||
func InputClasses(args ...any) FieldOption {
|
||||
return func(f *baseWidget) {
|
||||
f.inputClasses = templ.Classes(args...)
|
||||
}
|
||||
}
|
||||
|
||||
// InputAttr adds a attribute to the field's input.
|
||||
func InputAttr(name string, value any) FieldOption {
|
||||
return func(f *baseWidget) {
|
||||
f.inputAttrs[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
// InputAttrs sets the field's input attributes.
|
||||
func InputAttrs(attrs templ.Attributes) FieldOption {
|
||||
return func(f *baseWidget) {
|
||||
maps.Copy(f.inputAttrs, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
type textField struct {
|
||||
fieldWidget
|
||||
}
|
||||
|
||||
// TextField renders a text field (text, email, etc.)
|
||||
func TextField(field forms.Field, options ...FieldOption) templ.Component {
|
||||
return (&textField{widget(
|
||||
field,
|
||||
append([]FieldOption{InputType("text")}, options...)...,
|
||||
)}).component()
|
||||
}
|
||||
|
||||
// DateField renders a [TextField] with a date type.
|
||||
func DateField(field forms.Field, options ...FieldOption) templ.Component {
|
||||
if f, ok := field.(*forms.DatetimeField); ok {
|
||||
options = slices.Insert(options, 0, Value(f.String()))
|
||||
}
|
||||
return TextField(
|
||||
field,
|
||||
append([]FieldOption{InputType("date")}, options...)...,
|
||||
)
|
||||
}
|
||||
|
||||
type textAreaField struct {
|
||||
fieldWidget
|
||||
}
|
||||
|
||||
// TextAreaField renders a textarea field.
|
||||
func TextAreaField(field forms.Field, options ...FieldOption) templ.Component {
|
||||
return (&textAreaField{widget(field, options...)}).component()
|
||||
}
|
||||
|
||||
type checkboxField struct {
|
||||
fieldWidget
|
||||
}
|
||||
|
||||
// CheckboxField renders a checkbox field.
|
||||
func CheckboxField(field forms.Field, options ...FieldOption) templ.Component {
|
||||
return (&checkboxField{widget(field, options...)}).component()
|
||||
}
|
||||
|
||||
type selectField[T comparable] struct {
|
||||
fieldWidget
|
||||
}
|
||||
|
||||
// SelectField renders a select field with options.
|
||||
func SelectField[T comparable](field forms.Field, options ...FieldOption) templ.Component {
|
||||
return (&selectField[T]{widget(field, options...)}).component()
|
||||
}
|
||||
|
||||
type multiSelectField[T comparable] struct {
|
||||
fieldWidget
|
||||
}
|
||||
|
||||
// MultiSelectField renders a list of checkboxes.
|
||||
func MultiSelectField[T comparable](field forms.Field, options ...FieldOption) templ.Component {
|
||||
return (&multiSelectField[T]{widget(field, options...)}).component()
|
||||
}
|
||||
|
||||
type passwordField struct {
|
||||
fieldWidget
|
||||
}
|
||||
|
||||
// PasswordField renders a password field with a reveal controller.
|
||||
func PasswordField(field forms.Field, options ...FieldOption) templ.Component {
|
||||
return (&passwordField{widget(
|
||||
field,
|
||||
append([]FieldOption{InputType("text")}, options...)...,
|
||||
)}).component()
|
||||
}
|
||||
|
||||
type timeTokenField struct {
|
||||
fieldWidget
|
||||
}
|
||||
|
||||
// TimeTokenField renders a field with a helper to select time tokens.
|
||||
func TimeTokenField(field forms.Field, options ...FieldOption) templ.Component {
|
||||
return (&timeTokenField{widget(field, options...)}).component()
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package forms
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"codeberg.org/readeck/readeck/pkg/forms"
|
||||
|
||||
. "codeberg.org/readeck/readeck/components"
|
||||
)
|
||||
|
||||
templ Errors(f forms.Binder) {
|
||||
if !f.IsValid() {
|
||||
<div class="max-w-std" role="alert">
|
||||
@Message(WithMessageType("error")) {
|
||||
<strong class="text-red-800">{ L(ctx).Gettext("Errors") }</strong>
|
||||
if errors := f.Errors(); len(errors) > 0 {
|
||||
for _, err := range errors {
|
||||
<p>{ err.Error() }</p>
|
||||
}
|
||||
} else {
|
||||
<p>{ L(ctx).Gettext("Please check your form for errors.") }</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ (f *fieldWidget) component() {
|
||||
{{
|
||||
f.classes = slices.Insert(f.classes, 0, "field")
|
||||
if len(f.Errors()) > 0 {
|
||||
f.classes = append(f.classes, "field--err with-errors")
|
||||
}
|
||||
}}
|
||||
<div class={ f.classes }>
|
||||
if f.label != "" {
|
||||
<label for={ f.name }>
|
||||
{ f.label }
|
||||
if f.required {
|
||||
<span
|
||||
title={ L(ctx).Gettext("Required") }
|
||||
aria-hidden="true"
|
||||
class="text-red-600"
|
||||
> •</span>
|
||||
<span aria-hidden="true" class="sr-only">{ L(ctx).Gettext("Required") }</span>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
<div { f.controllAttrs... }>
|
||||
{ children... }
|
||||
if errors := f.Errors(); len(errors) > 0 {
|
||||
<ul id={ "errors-" + f.name } class="field--errors">
|
||||
for _, e := range errors {
|
||||
<li>{ e.Error() }</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
if f.help != "" {
|
||||
<span id={ "description-" + f.name } class="field--help">{ f.help }</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ (f *textField) component() {
|
||||
{{
|
||||
f.classes = slices.Insert(f.classes, 0, "field--text")
|
||||
if len(f.inputClasses) == 0 {
|
||||
f.inputClasses = templ.Classes("form-input w-full")
|
||||
}
|
||||
|
||||
if f.required {
|
||||
f.inputAttrs["required"] = true
|
||||
}
|
||||
if f.inputType != "password" {
|
||||
f.inputAttrs["value"] = f.value
|
||||
} else {
|
||||
f.inputAttrs["value"] = ""
|
||||
}
|
||||
|
||||
parent := f.fieldWidget.component
|
||||
}}
|
||||
@parent() {
|
||||
<input
|
||||
type={ f.inputType }
|
||||
id={ f.name }
|
||||
name={ f.name }
|
||||
class={ f.inputClasses }
|
||||
{ f.inputAttrs... }
|
||||
{ f.ariaAttrs()... }
|
||||
/>
|
||||
{ children... }
|
||||
}
|
||||
}
|
||||
|
||||
templ (f *textAreaField) component() {
|
||||
{{
|
||||
f.classes = slices.Insert(f.classes, 0, "field--textarea")
|
||||
|
||||
if len(f.inputClasses) == 0 {
|
||||
f.inputClasses = templ.Classes("form-textarea w-full")
|
||||
}
|
||||
|
||||
if _, ok := f.inputAttrs["rows"]; !ok {
|
||||
f.inputAttrs["rows"] = 3
|
||||
}
|
||||
if f.value == nil {
|
||||
f.value = f.String()
|
||||
}
|
||||
if f.required {
|
||||
f.inputAttrs["required"] = true
|
||||
}
|
||||
|
||||
parent := f.fieldWidget.component
|
||||
}}
|
||||
@parent() {
|
||||
<textarea
|
||||
id={ f.name }
|
||||
name={ f.name }
|
||||
class={ f.inputClasses }
|
||||
{ f.inputAttrs... }
|
||||
{ f.ariaAttrs()... }
|
||||
>{ S("%s", f.value) }</textarea>
|
||||
}
|
||||
}
|
||||
|
||||
templ (f *checkboxField) component() {
|
||||
{{
|
||||
f.classes = slices.Insert(f.classes, 0, "field", "field--checkbox")
|
||||
if len(f.Errors()) > 0 {
|
||||
f.classes = append(f.classes, "field--err with-errors")
|
||||
}
|
||||
if v, ok := f.value.(bool); ok && v {
|
||||
f.inputAttrs["checked"] = "checked"
|
||||
}
|
||||
}}
|
||||
<div class={ f.classes }>
|
||||
<span class="field--spacer"></span>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={ f.name }
|
||||
name={ f.name }
|
||||
class="form-checkbox"
|
||||
value="t"
|
||||
{ f.inputAttrs... }
|
||||
{ f.ariaAttrs()... }
|
||||
/>
|
||||
// end with false so the field is bound
|
||||
<input type="hidden" name={ f.name } value="f"/>
|
||||
<label for={ f.name }>{ f.label }</label>
|
||||
if errors := f.Errors(); len(errors) > 0 {
|
||||
<ul id={ "errors-" + f.name } class="field--errors">
|
||||
for _, e := range errors {
|
||||
<li>{ e.Error() }</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
if f.help != "" {
|
||||
<span id={ "description-" + f.name } class="field--help">{ f.help }</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ (f *selectField[T]) component() {
|
||||
{{
|
||||
choices := forms.ValueChoices[T]{}
|
||||
if fc, ok := f.Field.(interface{ Choices() forms.ValueChoices[T] }); ok {
|
||||
choices = fc.Choices()
|
||||
}
|
||||
if fc, ok := f.inputAttrs["choices"]; ok {
|
||||
delete(f.inputAttrs, "choices")
|
||||
if fc, ok := fc.(interface{ Choices() forms.ValueChoices[T] }); ok {
|
||||
choices = fc.Choices()
|
||||
}
|
||||
}
|
||||
|
||||
f.classes = slices.Insert(f.classes, 0, "field--select")
|
||||
|
||||
if len(f.inputClasses) == 0 {
|
||||
f.inputClasses = templ.Classes("form-select w-full")
|
||||
}
|
||||
if f.required {
|
||||
f.inputAttrs["required"] = true
|
||||
}
|
||||
|
||||
parent := f.fieldWidget.component
|
||||
}}
|
||||
@parent() {
|
||||
<select
|
||||
id={ f.name }
|
||||
name={ f.name }
|
||||
class={ f.inputClasses }
|
||||
{ f.inputAttrs... }
|
||||
{ f.ariaAttrs()... }
|
||||
>
|
||||
for _, c := range choices {
|
||||
<option value={ S("%s", c.Value) } selected?={ f.Field.(forms.TypedField[T]).V() == c.Value }>{ c.Name }</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
}
|
||||
|
||||
templ (f *multiSelectField[T]) component() {
|
||||
{{
|
||||
f.classes = slices.Insert(f.classes, 0, "field", "field--multiselect")
|
||||
if len(f.Errors()) > 0 {
|
||||
f.classes = append(f.classes, "field--err with-errors")
|
||||
}
|
||||
|
||||
choices := forms.ValueChoices[T]{}
|
||||
if fc, ok := f.Field.(interface{ Choices() forms.ValueChoices[T] }); ok {
|
||||
choices = fc.Choices()
|
||||
}
|
||||
if fc, ok := f.inputAttrs["choices"]; ok {
|
||||
delete(f.inputAttrs, "choices")
|
||||
if fc, ok := fc.(interface{ Choices() forms.ValueChoices[T] }); ok {
|
||||
choices = fc.Choices()
|
||||
}
|
||||
}
|
||||
}}
|
||||
<div class={ f.classes }>
|
||||
<label>{ f.label }</label>
|
||||
<div>
|
||||
// null value to bind the field
|
||||
<input type="hidden" name={ f.name } value="＀"/>
|
||||
<ul class="field--choices">
|
||||
{{
|
||||
inputType := "checkbox"
|
||||
inputClasses := append(f.inputClasses, "form-checkbox")
|
||||
if _, ok := f.Field.Value().(T); ok {
|
||||
inputType = "radio"
|
||||
inputClasses = append(f.inputClasses, "form-radio")
|
||||
}
|
||||
}}
|
||||
for _, c := range choices {
|
||||
{{
|
||||
checked := false
|
||||
if v, ok := f.Field.Value().(T); ok {
|
||||
checked = v == c.Value
|
||||
} else if v, ok := f.Field.Value().([]T); ok {
|
||||
checked = slices.Contains(v, c.Value)
|
||||
}
|
||||
}}
|
||||
<li>
|
||||
<input
|
||||
type={ inputType }
|
||||
id={ S("%s_%s", f.name, c.Value) }
|
||||
name={ f.name }
|
||||
value={ S("%s", c.Value) }
|
||||
checked?={ checked }
|
||||
class={ inputClasses }
|
||||
aria-label={ f.label + ": " + c.Name }
|
||||
/>
|
||||
<label for={ S("%s_%s", f.name, c.Value) }>{ c.Name }</label>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
if errors := f.Errors(); len(errors) > 0 {
|
||||
<ul id={ "errors-" + f.name } class="field--errors">
|
||||
for _, e := range errors {
|
||||
<li>{ e.Error() }</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
if f.help != "" {
|
||||
<span id={ "description-" + f.name } class="field--help">{ f.help }</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ (f *passwordField) component() {
|
||||
{{
|
||||
f.classes = slices.Insert(f.classes, 0, "field--text field--composed field--password")
|
||||
|
||||
if len(f.inputClasses) == 0 {
|
||||
f.inputClasses = templ.Classes("form-input w-full")
|
||||
}
|
||||
|
||||
f.controllAttrs["data-controller"] = "pass-reveal"
|
||||
if f.required {
|
||||
f.inputAttrs["required"] = true
|
||||
}
|
||||
f.inputAttrs["data-pass-reveal-target"] = "field"
|
||||
|
||||
parent := f.fieldWidget.component
|
||||
}}
|
||||
@parent() {
|
||||
<div class={ f.inputClasses }>
|
||||
<input
|
||||
type="password"
|
||||
id={ f.name }
|
||||
name={ f.name }
|
||||
{ f.inputAttrs... }
|
||||
{ f.ariaAttrs()... }
|
||||
/>
|
||||
<button type="button" data-pass-reveal-target="btn" data-action="pass-reveal#toggle"></button>
|
||||
<template data-pass-reveal-target="show">
|
||||
@Icon("o-show")
|
||||
</template>
|
||||
<template data-pass-reveal-target="hide">
|
||||
@Icon("o-hide")
|
||||
</template>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ (f *timeTokenField) component() {
|
||||
{{
|
||||
f.classes = slices.Insert(f.classes, 0, "field--text field--composed field--timetoken")
|
||||
f.inputClasses = slices.Insert(f.inputClasses, 0, "form-input w-full")
|
||||
f.inputAttrs["data-timetoken-target"] = "field"
|
||||
if f.required {
|
||||
f.inputAttrs["required"] = true
|
||||
}
|
||||
|
||||
parent := f.fieldWidget.component
|
||||
}}
|
||||
@parent() {
|
||||
<div
|
||||
class={ f.inputClasses }
|
||||
data-controller="timetoken"
|
||||
data-timetoken-hidden-class="hidden"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id={ f.name }
|
||||
name={ f.name }
|
||||
value={ f.String() }
|
||||
{ f.inputAttrs... }
|
||||
/>
|
||||
<button type="button" data-timetoken-target="btn">
|
||||
@Icon("o-calendar")
|
||||
</button>
|
||||
<template data-timetoken-target="template">
|
||||
<div class="timetoken">
|
||||
<label>{ L(ctx).Gettext("The previous") }</label>
|
||||
<div>
|
||||
<input
|
||||
type="number"
|
||||
size="3"
|
||||
min="0"
|
||||
class="form-input"
|
||||
data-timetoken-target="value"
|
||||
data-action="timetoken#update"
|
||||
/>
|
||||
<select
|
||||
class="form-select"
|
||||
data-action="timetoken#update"
|
||||
data-timetoken-target="unit"
|
||||
>
|
||||
<option value="d">{ L(ctx).Gettext("Day(s)") }</option>
|
||||
<option value="w">{ L(ctx).Gettext("Week(s)") }</option>
|
||||
<option value="m">{ L(ctx).Gettext("Month(s)") }</option>
|
||||
<option value="y">{ L(ctx).Gettext("Year(s)") }</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="my-2">- { L(ctx).Pgettext("word", "or") } -</p>
|
||||
<input type="date" class="form-input" data-action="timetoken#update" data-timetoken-target="absolute"/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ FiledDropField(field forms.Field, options ...FieldOption) {
|
||||
{{
|
||||
f := textField{widget(field, options...)}
|
||||
f.inputType = "file"
|
||||
f.classes = slices.Insert(f.classes, 0, "field--filedrop")
|
||||
f.inputClasses = slices.Insert(f.inputClasses, 0, "js:hidden")
|
||||
maps.Copy(f.controllAttrs, templ.Attributes{
|
||||
"data-controller": "dropzone",
|
||||
"data-dropzone-hidden-class": "hidden",
|
||||
"data-dropzone-focus-class": "bg-primary-100",
|
||||
})
|
||||
|
||||
parent := f.component
|
||||
}}
|
||||
@parent() {
|
||||
<p data-dropzone-target="zone">
|
||||
<button
|
||||
type="button"
|
||||
class="mx-auto link"
|
||||
data-dropzone-target="placeholder"
|
||||
data-action="click->dropzone#select"
|
||||
>{ L(ctx).Gettext("Select or drop file") }</button>
|
||||
<span class="max-w-full wrap-anywhere font-semibold" data-dropzone-target="fileinfo"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden text-btn-danger hf:text-btn-danger-hover"
|
||||
data-dropzone-target="clearbtn"
|
||||
data-action="dropzone#clear:stop:prevent"
|
||||
>
|
||||
{ L(ctx).Gettext("remove") }
|
||||
</button>
|
||||
</p>
|
||||
}
|
||||
}
|
||||
Generated
+1728
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
import (
|
||||
"maps"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
type icon struct {
|
||||
src string
|
||||
w int
|
||||
h int
|
||||
class templ.CSSClasses
|
||||
svgClass templ.CSSClasses
|
||||
attrs templ.Attributes
|
||||
}
|
||||
|
||||
// Icon renders an icon from a source sprite.
|
||||
// The default source is "img/icons.svg".
|
||||
func Icon(name string, options ...func(*icon)) templ.Component {
|
||||
i := &icon{
|
||||
src: "img/icons.svg",
|
||||
w: 24,
|
||||
h: 24,
|
||||
class: templ.CSSClasses{"svgicon"},
|
||||
svgClass: templ.CSSClasses{"w-4"},
|
||||
attrs: templ.Attributes{},
|
||||
}
|
||||
|
||||
for _, f := range options {
|
||||
f(i)
|
||||
}
|
||||
|
||||
return i.component(name)
|
||||
}
|
||||
|
||||
// WithIconSrc sets the icon's sprite source.
|
||||
func WithIconSrc(src string) func(*icon) {
|
||||
return func(i *icon) {
|
||||
i.src = src
|
||||
}
|
||||
}
|
||||
|
||||
// WithIconClass sets the icon wrapper's class.
|
||||
func WithIconClass(c ...any) func(*icon) {
|
||||
return func(i *icon) {
|
||||
i.class = templ.Classes(c...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithIconSvgClass sets the icon svg's class.
|
||||
func WithIconSvgClass(c ...any) func(*icon) {
|
||||
return func(i *icon) {
|
||||
i.svgClass = templ.Classes(c...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithIconSize sets the icon's dimension.
|
||||
func WithIconSize(w, h int) func(*icon) {
|
||||
return func(i *icon) {
|
||||
if w == 0 {
|
||||
i.w = 24
|
||||
}
|
||||
if h == 0 {
|
||||
i.h = 24
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithIconAttrs sets the icon wrapper's attributes.
|
||||
func WithIconAttrs(attrs templ.Attributes) func(*icon) {
|
||||
return func(i *icon) {
|
||||
maps.Copy(i.attrs, attrs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
templ (i *icon) component(name string) {
|
||||
<span class={ i.class } { i.attrs... }>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewbox={ S("0 0 %d %d", i.w, i.h) }
|
||||
width={ i.w }
|
||||
height={ i.h }
|
||||
class={ templ.CSSClasses{"inline-block", i.svgClass} }
|
||||
>
|
||||
<use href={ S("%s#%s", Asset(ctx, i.src), name) }></use>
|
||||
</svg>
|
||||
</span>
|
||||
}
|
||||
|
||||
// Spinner renders a spinner icon.
|
||||
templ Spinner() {
|
||||
@Icon("o-spinner", WithIconSvgClass("animate-spin stroke-current"))
|
||||
}
|
||||
Generated
+176
@@ -0,0 +1,176 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1001
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
|
||||
//
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func (i *icon) component(name 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
|
||||
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_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var2 = []any{i.class}
|
||||
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, 1, "<span 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: `components/icon.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, 2, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, i.attrs)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, ">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 = []any{templ.CSSClasses{"inline-block", i.svgClass}}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<svg xmlns=\"http://www.w3.org/2000/svg\" viewbox=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(S("0 0 %d %d", i.w, i.h))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/icon.templ`, Line: 11, Col: 37}
|
||||
}
|
||||
_, 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, "\" width=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(i.w)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/icon.templ`, Line: 12, Col: 14}
|
||||
}
|
||||
_, 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, "\" height=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(i.h)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/icon.templ`, Line: 13, Col: 15}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var4).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/icon.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"><use href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(S("%s#%s", Asset(ctx, i.src), name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/icon.templ`, Line: 16, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"></use></svg></span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Spinner renders a spinner icon.
|
||||
func Spinner() 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_Var10 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var10 == nil {
|
||||
templ_7745c5c3_Var10 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = Icon("o-spinner", WithIconSvgClass("animate-spin stroke-current")).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
|
||||
// BasePage renders an empty page with default and required head elements.
|
||||
func BasePage(title string) templ.Component {
|
||||
return defaultLayout.BasePage(title)
|
||||
}
|
||||
|
||||
// SideMenuLayout renders a page with a side menu.
|
||||
func SideMenuLayout(title string, sidemenu templ.Component) templ.Component {
|
||||
return defaultLayout.SideMenuLayout(title, sidemenu, defaultLayout.SideMenuWrapper())
|
||||
}
|
||||
|
||||
// SideMenuStdLayout renders a page with a side menu.
|
||||
// The content has a standard maximum width.
|
||||
func SideMenuStdLayout(title string, sidemenu templ.Component) templ.Component {
|
||||
return defaultLayout.SideMenuLayout(title, sidemenu, defaultLayout.SideMenuStdWrapper())
|
||||
}
|
||||
|
||||
// Layout is our base layout for every Readeck page.
|
||||
type Layout struct {
|
||||
Head templ.Component
|
||||
}
|
||||
|
||||
// defaultLayout is an instance of [Layout] that we reuse
|
||||
// about everywhere.
|
||||
var defaultLayout = NewLayout()
|
||||
|
||||
// NewLayout returns a new [Layout].
|
||||
func NewLayout() (l *Layout) {
|
||||
return &Layout{
|
||||
Head: templ.NopComponent,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
// BasePage renders the layout's base page with head elements.
|
||||
templ (c *Layout) BasePage(title string) {
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
lang={ L(ctx).Tag.String() }
|
||||
class="overscroll-y-none scroll-pt-20 max-sm:scroll-pt-40"
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>{ title } - Readeck</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta name="csp-nonce" content={ CSPNonce(ctx) }/>
|
||||
<meta name="robots" content="noindex nofollow noarchive"/>
|
||||
<link rel="stylesheet" href={ Asset(ctx, "bundle.css") } nonce={ CSPNonce(ctx) }/>
|
||||
<script type="module" src={ Asset(ctx, "bundle.js") } nonce={ CSPNonce(ctx) }></script>
|
||||
<script nonce={ CSPNonce(ctx) }>
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
</script>
|
||||
<link rel="icon" href={ Asset(ctx, "img/fi/favicon.ico") } sizes="48x48"/>
|
||||
<link rel="icon" href={ Asset(ctx, `img/fi/favicon.svg`) } sizes="any" type="image/svg+xml"/>
|
||||
<link rel="apple-touch-icon" href={ Asset(ctx, `img/fi/apple-touch-icon.png`) }/>
|
||||
<link rel="manifest" href={ URL(ctx, "/manifest.webmanifest") }/>
|
||||
<meta name="theme-color" content="#064c5c"/>
|
||||
@c.Head
|
||||
</head>
|
||||
<body
|
||||
class="
|
||||
no-js bg-app-bg text-app-fg font-sans leading-tight tracking-normal
|
||||
relative
|
||||
print:text-black print:bg-white
|
||||
overscroll-y-none
|
||||
"
|
||||
data-turbo="false"
|
||||
data-turbo-prefetch="false"
|
||||
data-controller="scroll-tracker"
|
||||
data-scroll-tracker-down-class="scrolled-down"
|
||||
>
|
||||
<script nonce={ CSPNonce(ctx) }>
|
||||
if ('noModule' in HTMLScriptElement.prototype) {
|
||||
document.body.classList.remove("no-js")
|
||||
document.body.classList.add("js")
|
||||
}
|
||||
</script>
|
||||
{ children... }
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ (c *Layout) Menu(menuBtn templ.Component) {
|
||||
<div class="layout-topnav" id="menu">
|
||||
@menuBtn {
|
||||
@Icon("o-menu")
|
||||
}
|
||||
<div class="logo">
|
||||
<a href={ URL(ctx, "/") } title={ L(ctx).Gettext("Home page") }>
|
||||
@Icon("o-logo-square", WithIconClass(), WithIconSvgClass("h-11 w-11"))
|
||||
</a>
|
||||
</div>
|
||||
<div class="mainmenu">
|
||||
<menu class="grow">
|
||||
if HasPermission(ctx, "bookmarks", "read") {
|
||||
@c.mainMenuItem(L(ctx).Gettext("Bookmarks"), "/bookmarks/unread", "o-library", PathIs(ctx, "/bookmarks", "/bookmarks/*"))
|
||||
}
|
||||
</menu>
|
||||
if !IsAnonymous(ctx) {
|
||||
<menu class="flex-shrink-0">
|
||||
<li class="no-js:hidden">
|
||||
<button
|
||||
type="button"
|
||||
data-controller="theme"
|
||||
data-action="theme#toggleTheme"
|
||||
title={ L(ctx).Gettext("Change color theme") }
|
||||
>
|
||||
<span data-theme-target="icon">
|
||||
@Icon("o-theme-system")
|
||||
</span>
|
||||
<span class="sr-only">{ L(ctx).Gettext("Change color theme") }</span>
|
||||
<template data-theme-target="iconLight">
|
||||
@Icon("o-theme-light")
|
||||
</template>
|
||||
<template data-theme-target="iconDark">
|
||||
@Icon("o-theme-dark")
|
||||
</template>
|
||||
<template data-theme-target="iconSystem">
|
||||
@Icon("o-theme-system")
|
||||
</template>
|
||||
</button>
|
||||
</li>
|
||||
if HasPermission(ctx, "profile", "read") {
|
||||
@c.mainMenuItem(L(ctx).Gettext("Settings"), "/profile", "o-settings", PathIs(ctx, "/admin", "/admin/*", "/profile", "/profile/*"))
|
||||
}
|
||||
@c.mainMenuItem(L(ctx).Gettext("Documentation"), "/docs/", "o-help", PathIs(ctx, "/docs/*"))
|
||||
</menu>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ (c *Layout) menuButton(name string) {
|
||||
<button
|
||||
type="button"
|
||||
title={ name }
|
||||
class="sidemenu--button"
|
||||
data-action="click->panel#toggle"
|
||||
data-panel-target="button"
|
||||
aria-expanded="false"
|
||||
aria-controls="sidemenu"
|
||||
>
|
||||
{ children... }
|
||||
<span class="sr-only">{ name }</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
templ (c *Layout) mainMenuItem(name, path, icon string, current bool) {
|
||||
<li>
|
||||
<a href={ URL(ctx, path) } title={ name } data-current={ current }>
|
||||
@Icon(icon)
|
||||
<span class="sr-only">{ name }</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
// QuickAccessMenu renders an a11y menu.
|
||||
templ QuickAccessMenu(items [][2]string) {
|
||||
if len(items) > 0 {
|
||||
<ul label={ L(ctx).Gettext("Quick access menu") } class="a11y-nav">
|
||||
for _, item := range items {
|
||||
<li><a href={ "#" + item[0] } class="link">{ item[1] }</a></li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
// SideMenuTitle renders a title used in side menus.
|
||||
templ SideMenuTitle() {
|
||||
<h2
|
||||
id="sidemenu-title"
|
||||
class="h-topnav border-b border-gray-200 uppercase flex items-center justify-center"
|
||||
>
|
||||
{ children... }
|
||||
</h2>
|
||||
}
|
||||
|
||||
// SideMenuLayout renders a layout with a side menu.
|
||||
templ (c *Layout) SideMenuLayout(title string, sidemenu templ.Component, contentWrapper templ.Component) {
|
||||
@c.BasePage(title) {
|
||||
@QuickAccessMenu([][2]string{
|
||||
{"menu", L(ctx).Gettext("Menu")},
|
||||
{"sidemenu", L(ctx).Gettext("Secondary Menu")},
|
||||
{"content", L(ctx).Gettext("Main content")},
|
||||
})
|
||||
<div
|
||||
class="layout"
|
||||
data-controller="panel"
|
||||
data-panel-hidden-class="sidemenu--hidden"
|
||||
data-panel-body-class="max-sm:overflow-hidden"
|
||||
>
|
||||
@c.Menu(c.menuButton(L(ctx).Gettext("Open Menu")))
|
||||
<nav
|
||||
id="sidemenu"
|
||||
tabindex="-1"
|
||||
data-panel-target="panel"
|
||||
aria-labelledby="sidemenu-title"
|
||||
data-action="keydown.esc->panel#toggle"
|
||||
class="sidemenu sidemenu--hidden"
|
||||
>
|
||||
@c.menuButton(L(ctx).Gettext("Close menu")) {
|
||||
@Icon("o-close", WithIconClass("inline-block"), WithIconSvgClass("w-8 h-8"))
|
||||
}
|
||||
@sidemenu
|
||||
</nav>
|
||||
@contentWrapper {
|
||||
{ children... }
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ (c *Layout) SideMenuWrapper() {
|
||||
<div class="layout-content" id="content">
|
||||
@Flashes()
|
||||
{ children... }
|
||||
</div>
|
||||
}
|
||||
|
||||
templ (c *Layout) SideMenuStdWrapper() {
|
||||
<main class="layout-content max-w-std" id="content">
|
||||
@Flashes()
|
||||
@Breadcrumbs()
|
||||
{ children... }
|
||||
</main>
|
||||
}
|
||||
Generated
+890
@@ -0,0 +1,890 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1001
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
|
||||
//
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// BasePage renders the layout's base page with head elements.
|
||||
func (c *Layout) BasePage(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
|
||||
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_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Tag.String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 11, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"overscroll-y-none scroll-pt-20 max-sm:scroll-pt-40\"><head><meta charset=\"UTF-8\"><title>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 16, Col: 17}
|
||||
}
|
||||
_, 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, " - Readeck</title><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"csp-nonce\" content=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(CSPNonce(ctx))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 19, Col: 49}
|
||||
}
|
||||
_, 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, "\"><meta name=\"robots\" content=\"noindex nofollow noarchive\"><link rel=\"stylesheet\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 templ.SafeURL
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(Asset(ctx, "bundle.css"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 21, Col: 57}
|
||||
}
|
||||
_, 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, "\" nonce=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(CSPNonce(ctx))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 21, Col: 81}
|
||||
}
|
||||
_, 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, "\"><script type=\"module\" src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(Asset(ctx, "bundle.js"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 22, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" nonce=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(CSPNonce(ctx))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 22, Col: 78}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"></script><script nonce=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(CSPNonce(ctx))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 23, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">\n\t\t\t\tif (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {\n\t\t\t\t\tdocument.documentElement.classList.add('dark')\n\t\t\t\t} else {\n\t\t\t\t\tdocument.documentElement.classList.remove('dark')\n\t\t\t\t}\n\t\t\t</script><link rel=\"icon\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 templ.SafeURL
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(Asset(ctx, "img/fi/favicon.ico"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 30, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" sizes=\"48x48\"><link rel=\"icon\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 templ.SafeURL
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(Asset(ctx, `img/fi/favicon.svg`))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 31, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" sizes=\"any\" type=\"image/svg+xml\"><link rel=\"apple-touch-icon\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 templ.SafeURL
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(Asset(ctx, `img/fi/apple-touch-icon.png`))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 32, Col: 80}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"><link rel=\"manifest\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 templ.SafeURL
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, "/manifest.webmanifest"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 33, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"><meta name=\"theme-color\" content=\"#064c5c\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = c.Head.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</head><body class=\"no-js bg-app-bg text-app-fg font-sans leading-tight tracking-normal relative print:text-black print:bg-white overscroll-y-none\" data-turbo=\"false\" data-turbo-prefetch=\"false\" data-controller=\"scroll-tracker\" data-scroll-tracker-down-class=\"scrolled-down\"><script nonce=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(CSPNonce(ctx))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 49, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">\n\t\t\t\tif ('noModule' in HTMLScriptElement.prototype) {\n\t\t\t\t\tdocument.body.classList.remove(\"no-js\")\n\t\t\t\t\tdocument.body.classList.add(\"js\")\n\t\t\t\t}\n\t\t\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Layout) Menu(menuBtn templ.Component) 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_Var15 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var15 == nil {
|
||||
templ_7745c5c3_Var15 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"layout-topnav\" id=\"menu\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var16 := 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_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_Err = Icon("o-menu").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = menuBtn.Render(templ.WithChildren(ctx, templ_7745c5c3_Var16), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"logo\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 templ.SafeURL
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, "/"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 66, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" title=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Home page"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 66, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("o-logo-square", WithIconClass(), WithIconSvgClass("h-11 w-11")).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</a></div><div class=\"mainmenu\"><menu class=\"grow\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if HasPermission(ctx, "bookmarks", "read") {
|
||||
templ_7745c5c3_Err = c.mainMenuItem(L(ctx).Gettext("Bookmarks"), "/bookmarks/unread", "o-library", PathIs(ctx, "/bookmarks", "/bookmarks/*")).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</menu> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !IsAnonymous(ctx) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<menu class=\"flex-shrink-0\"><li class=\"no-js:hidden\"><button type=\"button\" data-controller=\"theme\" data-action=\"theme#toggleTheme\" title=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Change color theme"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 83, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"><span data-theme-target=\"icon\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("o-theme-system").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</span> <span class=\"sr-only\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Change color theme"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 88, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</span><template data-theme-target=\"iconLight\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("o-theme-light").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</template><template data-theme-target=\"iconDark\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("o-theme-dark").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</template><template data-theme-target=\"iconSystem\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("o-theme-system").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</template></button></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if HasPermission(ctx, "profile", "read") {
|
||||
templ_7745c5c3_Err = c.mainMenuItem(L(ctx).Gettext("Settings"), "/profile", "o-settings", PathIs(ctx, "/admin", "/admin/*", "/profile", "/profile/*")).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = c.mainMenuItem(L(ctx).Gettext("Documentation"), "/docs/", "o-help", PathIs(ctx, "/docs/*")).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</menu>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Layout) menuButton(name 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
|
||||
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_Var21 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var21 == nil {
|
||||
templ_7745c5c3_Var21 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<button type=\"button\" title=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 113, Col: 14}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" class=\"sidemenu--button\" data-action=\"click->panel#toggle\" data-panel-target=\"button\" aria-expanded=\"false\" aria-controls=\"sidemenu\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var21.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<span class=\"sr-only\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 121, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span></button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Layout) mainMenuItem(name, path, icon string, current bool) 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_Var24 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var24 == nil {
|
||||
templ_7745c5c3_Var24 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<li><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 templ.SafeURL
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, path))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 127, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" title=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 127, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" data-current=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(current)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 127, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon(icon).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<span class=\"sr-only\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 129, Col: 31}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span></a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// QuickAccessMenu renders an a11y menu.
|
||||
func QuickAccessMenu(items [][2]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
|
||||
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_Var29 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var29 == nil {
|
||||
templ_7745c5c3_Var29 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
if len(items) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<ul label=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Quick access menu"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 137, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" class=\"a11y-nav\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range items {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<li><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var31 templ.SafeURL
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs("#" + item[0])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 139, Col: 31}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" class=\"link\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(item[1])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 139, Col: 56}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</ul>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// SideMenuTitle renders a title used in side menus.
|
||||
func SideMenuTitle() 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_Var33 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var33 == nil {
|
||||
templ_7745c5c3_Var33 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<h2 id=\"sidemenu-title\" class=\"h-topnav border-b border-gray-200 uppercase flex items-center justify-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var33.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</h2>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// SideMenuLayout renders a layout with a side menu.
|
||||
func (c *Layout) SideMenuLayout(title string, sidemenu templ.Component, contentWrapper templ.Component) 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_Var34 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var34 == nil {
|
||||
templ_7745c5c3_Var34 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var35 := 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_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_Err = QuickAccessMenu([][2]string{
|
||||
{"menu", L(ctx).Gettext("Menu")},
|
||||
{"sidemenu", L(ctx).Gettext("Secondary Menu")},
|
||||
{"content", L(ctx).Gettext("Main content")},
|
||||
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " <div class=\"layout\" data-controller=\"panel\" data-panel-hidden-class=\"sidemenu--hidden\" data-panel-body-class=\"max-sm:overflow-hidden\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = c.Menu(c.menuButton(L(ctx).Gettext("Open Menu"))).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<nav id=\"sidemenu\" tabindex=\"-1\" data-panel-target=\"panel\" aria-labelledby=\"sidemenu-title\" data-action=\"keydown.esc->panel#toggle\" class=\"sidemenu sidemenu--hidden\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var36 := 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_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_Err = Icon("o-close", WithIconClass("inline-block"), WithIconSvgClass("w-8 h-8")).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = c.menuButton(L(ctx).Gettext("Close menu")).Render(templ.WithChildren(ctx, templ_7745c5c3_Var36), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = sidemenu.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var37 := 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_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_Err = templ_7745c5c3_Var34.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = contentWrapper.Render(templ.WithChildren(ctx, templ_7745c5c3_Var37), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = c.BasePage(title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var35), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Layout) SideMenuWrapper() 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_Var38 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var38 == nil {
|
||||
templ_7745c5c3_Var38 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<div class=\"layout-content\" id=\"content\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Flashes().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var38.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Layout) SideMenuStdWrapper() 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_Var39 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var39 == nil {
|
||||
templ_7745c5c3_Var39 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<main class=\"layout-content max-w-std\" id=\"content\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Flashes().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Breadcrumbs().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var39.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</main>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,22 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
templ List(class ...string) {
|
||||
<div class={ append([]string{"border divide-y shadow rounded"}, class...) }>
|
||||
{ children... }
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ListItem(class ...string) {
|
||||
{{
|
||||
if len(class) == 0 {
|
||||
class = []string{"p-4"}
|
||||
}
|
||||
}}
|
||||
<div class={ class }>
|
||||
{ children... }
|
||||
</div>
|
||||
}
|
||||
Generated
+130
@@ -0,0 +1,130 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1001
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
|
||||
//
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func List(class ...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
|
||||
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_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var2 = []any{append([]string{"border divide-y shadow rounded"}, class...)}
|
||||
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, 1, "<div 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: `components/list.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, 2, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListItem(class ...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
|
||||
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_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
if len(class) == 0 {
|
||||
class = []string{"p-4"}
|
||||
}
|
||||
var templ_7745c5c3_Var5 = []any{class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/list.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, 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, 5, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var4.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,90 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
import (
|
||||
"maps"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
type message struct {
|
||||
typeClass string
|
||||
icon string
|
||||
removable bool
|
||||
delay int
|
||||
class templ.CSSClasses
|
||||
attrs templ.Attributes
|
||||
}
|
||||
|
||||
func newMessage(options ...func(*message)) *message {
|
||||
m := &message{
|
||||
typeClass: "info",
|
||||
icon: "o-info",
|
||||
attrs: templ.Attributes{},
|
||||
}
|
||||
|
||||
for _, f := range options {
|
||||
f(m)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Message renders a message component.
|
||||
func Message(options ...func(*message)) templ.Component {
|
||||
return newMessage(options...).component()
|
||||
}
|
||||
|
||||
// WithMessageType sets the message's type.
|
||||
func WithMessageType(s string) func(*message) {
|
||||
return func(mc *message) {
|
||||
mc.typeClass = s
|
||||
switch mc.typeClass {
|
||||
case "info":
|
||||
mc.icon = "o-info"
|
||||
case "success":
|
||||
mc.icon = "o-check-on"
|
||||
case "error":
|
||||
mc.icon = "o-error"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessageIcon sets the message's icon.
|
||||
func WithMessageIcon(s string) func(*message) {
|
||||
return func(mc *message) {
|
||||
mc.icon = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessageRemovable marks the message as removable.
|
||||
func WithMessageRemovable(mc *message) {
|
||||
mc.removable = true
|
||||
mc.attrs["data-controller"] = "remover"
|
||||
}
|
||||
|
||||
// WithMessageDelay sets a delay to the message for its removal.
|
||||
func WithMessageDelay(d int) func(*message) {
|
||||
return func(mc *message) {
|
||||
mc.delay = d
|
||||
mc.attrs["data-controller"] = "remover"
|
||||
mc.attrs["data-remover-delay-value"] = mc.delay
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessageClass adds a class to the message.
|
||||
func WithMessageClass(c string) func(*message) {
|
||||
return func(mc *message) {
|
||||
mc.class = append(mc.class, c)
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessageAttrs adds the given attributes to the message.
|
||||
func WithMessageAttrs(attrs templ.Attributes) func(*message) {
|
||||
return func(mc *message) {
|
||||
maps.Copy(mc.attrs, attrs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"codeberg.org/readeck/readeck/internal/server"
|
||||
)
|
||||
|
||||
templ (mc *message) component() {
|
||||
<div class={ templ.Classes("message", mc.typeClass, mc.class) } { mc.attrs... }>
|
||||
if mc.icon != "" {
|
||||
<div>
|
||||
@Icon(mc.icon)
|
||||
</div>
|
||||
}
|
||||
<div class="grow">
|
||||
{ children... }
|
||||
</div>
|
||||
if mc.removable {
|
||||
<div class="no-js:hidden">
|
||||
<button data-action="remover#remove" class="remover">
|
||||
@Icon("o-cross")
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
// Flashes shows the flash messages extracted from the user's session.
|
||||
templ Flashes() {
|
||||
if flashes := server.Flashes(server.GetRequest(ctx)); len(flashes) > 0 {
|
||||
<div class="toaster">
|
||||
for _, f := range flashes {
|
||||
if !strings.HasPrefix(f.Type, "_") {
|
||||
{{
|
||||
msg := newMessage(
|
||||
WithMessageType(f.Type),
|
||||
WithMessageDelay(5),
|
||||
WithMessageRemovable,
|
||||
)
|
||||
switch f.Type {
|
||||
case "error":
|
||||
msg.attrs["role"] = "alert"
|
||||
default:
|
||||
msg.attrs["role"] = "status"
|
||||
}
|
||||
}}
|
||||
<div class="drop-shadow-sm mb-1">
|
||||
@msg.component() {
|
||||
<strong>{ f.Message }</strong>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Generated
+216
@@ -0,0 +1,216 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1001
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
|
||||
//
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"codeberg.org/readeck/readeck/internal/server"
|
||||
)
|
||||
|
||||
func (mc *message) component() 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_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var2 = []any{templ.Classes("message", mc.typeClass, mc.class)}
|
||||
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, 1, "<div 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: `components/message.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, 2, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, mc.attrs)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, ">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if mc.icon != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon(mc.icon).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"grow\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if mc.removable {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"no-js:hidden\"><button data-action=\"remover#remove\" class=\"remover\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("o-cross").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</button></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Flashes shows the flash messages extracted from the user's session.
|
||||
func Flashes() 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_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
if flashes := server.Flashes(server.GetRequest(ctx)); len(flashes) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"toaster\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, f := range flashes {
|
||||
if !strings.HasPrefix(f.Type, "_") {
|
||||
msg := newMessage(
|
||||
WithMessageType(f.Type),
|
||||
WithMessageDelay(5),
|
||||
WithMessageRemovable,
|
||||
)
|
||||
switch f.Type {
|
||||
case "error":
|
||||
msg.attrs["role"] = "alert"
|
||||
default:
|
||||
msg.attrs["role"] = "status"
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"drop-shadow-sm mb-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var5 := 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_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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<strong>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(f.Message)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/message.templ`, Line: 54, Col: 26}
|
||||
}
|
||||
_, 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, 14, "</strong>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = msg.component().Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,55 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
import "codeberg.org/readeck/readeck/internal/server"
|
||||
|
||||
templ Pagination(p *server.Pagination) {
|
||||
if p.TotalPages > 1 {
|
||||
<nav class="paginator print:hidden">
|
||||
<div class="paginator--basic">
|
||||
if p.PreviousPage != "" {
|
||||
<a href={ URL(ctx, p.PreviousPage) }>
|
||||
{ L(ctx).Pgettext("page","Previous") }
|
||||
</a>
|
||||
} else {
|
||||
<span></span>
|
||||
}
|
||||
if p.NextPage != "" {
|
||||
<a href={ URL(ctx, p.NextPage) }>
|
||||
{ L(ctx).Pgettext("page","Next") }
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="paginator--extended">
|
||||
if p.PreviousPage != "" {
|
||||
<a href={ URL(ctx, p.PreviousPage) } aria-label={ L(ctx).Gettext("Go to previous page") }>
|
||||
@Icon("o-chevron-l")
|
||||
</a>
|
||||
}
|
||||
for _, page := range p.PageLinks {
|
||||
switch {
|
||||
case page.Index == p.CurrentPage:
|
||||
<span class="paginator--current">{ page.Index }</span>
|
||||
case page.Index != 0:
|
||||
<a
|
||||
href={ URL(ctx, page.URL) }
|
||||
aria-label={ L(ctx).Gettext("Go to page %d", page.Index) }
|
||||
>
|
||||
{ page.Index }
|
||||
</a>
|
||||
default:
|
||||
<span class="paginator--hellip">…</span>
|
||||
}
|
||||
}
|
||||
if p.NextPage != "" {
|
||||
<a href={ URL(ctx, p.NextPage) } aria-label={ L(ctx).Gettext("Go to next page") }>
|
||||
@Icon("o-chevron-r")
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
Generated
+278
@@ -0,0 +1,278 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1001
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
|
||||
//
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import "codeberg.org/readeck/readeck/internal/server"
|
||||
|
||||
func Pagination(p *server.Pagination) 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_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
if p.TotalPages > 1 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<nav class=\"paginator print:hidden\"><div class=\"paginator--basic\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if p.PreviousPage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, p.PreviousPage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/pagination.templ`, Line: 14, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Pgettext("page", "Previous"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/pagination.templ`, Line: 15, Col: 42}
|
||||
}
|
||||
_, 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, 4, "</a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span></span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if p.NextPage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 templ.SafeURL
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, p.NextPage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/pagination.templ`, Line: 21, Col: 35}
|
||||
}
|
||||
_, 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, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Pgettext("page", "Next"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/pagination.templ`, Line: 22, Col: 38}
|
||||
}
|
||||
_, 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, 8, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><div class=\"paginator--extended\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if p.PreviousPage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 templ.SafeURL
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, p.PreviousPage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/pagination.templ`, Line: 28, Col: 39}
|
||||
}
|
||||
_, 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, 11, "\" aria-label=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Go to previous page"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/pagination.templ`, Line: 28, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("o-chevron-l").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
for _, page := range p.PageLinks {
|
||||
switch {
|
||||
case page.Index == p.CurrentPage:
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span class=\"paginator--current\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(page.Index)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/pagination.templ`, Line: 35, Col: 52}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case page.Index != 0:
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 templ.SafeURL
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, page.URL))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/pagination.templ`, Line: 38, Col: 33}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" aria-label=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Go to page %d", page.Index))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/pagination.templ`, Line: 39, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(page.Index)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/pagination.templ`, Line: 41, Col: 20}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
default:
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span class=\"paginator--hellip\">…</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.NextPage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 templ.SafeURL
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, p.NextPage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/pagination.templ`, Line: 48, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" aria-label=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Go to next page"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/pagination.templ`, Line: 48, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("o-chevron-r").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div></nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package components
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"image/color"
|
||||
"strings"
|
||||
|
||||
"github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
// ColorPrimary is blue-800.
|
||||
var ColorPrimary = color.RGBA{6, 76, 92, 255}
|
||||
|
||||
// QRCodeB64 returns a QR code as a base64 data URI.
|
||||
// It panics when src is too big.
|
||||
// Size can be a negative number. See [qrcode.QRCode.Image]
|
||||
// for details. A value of -6 is a good default for URLs.
|
||||
func QRCodeB64(src string, color color.Color, size int) string {
|
||||
qr, err := qrcode.New(src, qrcode.Medium)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
qr.ForegroundColor = color
|
||||
qr.DisableBorder = true
|
||||
|
||||
data, err := qr.PNG(size)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res := new(strings.Builder)
|
||||
res.WriteString("data:image/png;base64,")
|
||||
enc := base64.NewEncoder(base64.StdEncoding, res)
|
||||
enc.Write(data) //nolint:errcheck
|
||||
|
||||
return res.String()
|
||||
}
|
||||
@@ -57,6 +57,7 @@ require (
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20251202014920-1725d2651bd4 // indirect
|
||||
github.com/JohannesKaufmann/dom v0.2.0 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.12.0 // indirect
|
||||
github.com/a-h/templ v0.3.1001 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/antchfx/xpath v1.3.6 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
|
||||
@@ -15,6 +15,8 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
|
||||
github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
|
||||
github.com/a-h/templ v0.3.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY=
|
||||
github.com/a-h/templ v0.3.1001/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/antchfx/htmlquery v1.3.6 h1:RNHHL7YehO5XdO8IM8CynwLKONwRHWkrghbYhQIk9ag=
|
||||
|
||||
@@ -133,6 +133,11 @@ func New(user *users.User, session *sessions.Session) *Preferences {
|
||||
return p
|
||||
}
|
||||
|
||||
// IsLoaded returns whether the user's preferences has been loaded (a user is attached).
|
||||
func (p *Preferences) IsLoaded() bool {
|
||||
return p.user != nil
|
||||
}
|
||||
|
||||
// WidthList returns the list of available widths.
|
||||
func (p *Preferences) WidthList() []string {
|
||||
return readerWidthList
|
||||
|
||||
@@ -25,8 +25,10 @@ type (
|
||||
ctxUnauthorizedKey struct{}
|
||||
)
|
||||
|
||||
// Context setters.
|
||||
// nolint:revive
|
||||
var (
|
||||
withCSPNonce, getCSPNonce = ctxr.WithChecker[string](ctxCSPNonceKey{})
|
||||
withCSPNonce, GetCSPNonce = ctxr.WithChecker[string](ctxCSPNonceKey{})
|
||||
withCSP, getCSP = ctxr.WithChecker[csp.Policy](ctxCSPKey{})
|
||||
withUnauthorized, getUnauthorized = ctxr.WithChecker[int](ctxUnauthorizedKey{})
|
||||
)
|
||||
@@ -137,10 +139,12 @@ func unauthorizedHandler(w http.ResponseWriter, r *http.Request) {
|
||||
redir := urls.AbsoluteURL(r, "/login")
|
||||
|
||||
// Add the current path as a redirect query parameter
|
||||
// to the login route
|
||||
q := redir.Query()
|
||||
q.Add("r", urls.CurrentPath(r))
|
||||
redir.RawQuery = q.Encode()
|
||||
// to the login route.
|
||||
if r.Method == http.MethodGet {
|
||||
q := redir.Query()
|
||||
q.Add("r", urls.CurrentPath(r))
|
||||
redir.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
w.Header().Set("Location", redir.String())
|
||||
w.WriteHeader(http.StatusSeeOther)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"log/slog"
|
||||
@@ -14,16 +15,39 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/CloudyKit/jet/v6"
|
||||
"github.com/a-h/templ"
|
||||
|
||||
"codeberg.org/readeck/readeck/configs"
|
||||
"codeberg.org/readeck/readeck/internal/auth"
|
||||
"codeberg.org/readeck/readeck/internal/auth/users"
|
||||
"codeberg.org/readeck/readeck/internal/profile/preferences"
|
||||
"codeberg.org/readeck/readeck/internal/server/urls"
|
||||
"codeberg.org/readeck/readeck/internal/templates"
|
||||
"codeberg.org/readeck/readeck/pkg/ctxr"
|
||||
"codeberg.org/readeck/readeck/pkg/glob"
|
||||
"codeberg.org/readeck/readeck/pkg/libjet"
|
||||
)
|
||||
|
||||
type (
|
||||
ctxRequestKey struct{}
|
||||
ctxUserKey struct{}
|
||||
ctxPreferencesKey struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
withRequest = ctxr.Setter[*http.Request](ctxRequestKey{})
|
||||
// GetRequest returns the request.
|
||||
GetRequest = ctxr.Getter[*http.Request](ctxRequestKey{})
|
||||
|
||||
withUser = ctxr.Setter[*users.User](ctxUserKey{})
|
||||
// GetUser returns a [users.User].
|
||||
GetUser = ctxr.Getter[*users.User](ctxUserKey{})
|
||||
|
||||
withPreferences = ctxr.Setter[*preferences.Preferences](ctxPreferencesKey{})
|
||||
// GetPreferences returns the user's [preferences.Preferences].
|
||||
GetPreferences = ctxr.Getter[*preferences.Preferences](ctxPreferencesKey{})
|
||||
)
|
||||
|
||||
// TC is a simple type to carry template context.
|
||||
type TC map[string]any
|
||||
|
||||
@@ -143,7 +167,7 @@ func initTemplates() {
|
||||
// TemplateVars returns the default variables set for a template
|
||||
// in the request's context.
|
||||
func TemplateVars(r *http.Request) jet.VarMap {
|
||||
cspNonce, _ := getCSPNonce(r.Context())
|
||||
cspNonce, _ := GetCSPNonce(r.Context())
|
||||
tr := Locale(r)
|
||||
|
||||
user := auth.GetRequestUser(r)
|
||||
@@ -164,3 +188,65 @@ func TemplateVars(r *http.Request) jet.VarMap {
|
||||
Set("pgettext", tr.Pgettext).
|
||||
Set("npgettext", tr.Npgettext)
|
||||
}
|
||||
|
||||
// RenderComponent renders a [templ.Component] in the response's writer.
|
||||
func RenderComponent(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
status int, component templ.Component,
|
||||
) {
|
||||
if w.Header().Get("content-type") == "" {
|
||||
w.Header().Set("content-type", "text/html; charset=utf-8")
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
|
||||
// Set some context information
|
||||
if err := component.Render(componentContext(r), w); err != nil {
|
||||
Err(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RenderTurboStreamComponent yields an HTML response with turbo-stream content-type using the
|
||||
// given component. The template result is enclosed in a turbo-stream tag
|
||||
// with action and target as specified.
|
||||
// You can call this method as many times as needed to output several turbo-stream tags
|
||||
// in the same HTTP response.
|
||||
func RenderTurboStreamComponent(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
component templ.Component,
|
||||
action, target string,
|
||||
attrs map[string]string,
|
||||
) {
|
||||
extraAttrs := new(strings.Builder)
|
||||
for k, v := range attrs {
|
||||
extraAttrs.WriteString(k + `="` + html.EscapeString(v) + `" `)
|
||||
}
|
||||
|
||||
log := Log(r).With(
|
||||
slog.String("action", action),
|
||||
slog.String("target", target),
|
||||
slog.Any("attrs", attrs),
|
||||
)
|
||||
|
||||
w.Header().Set("Content-Type", "text/vnd.turbo-stream.html; charset=utf-8")
|
||||
fmt.Fprintf(w, `<turbo-stream action="%s" %starget="%s"><template>%s`, action, extraAttrs, target, "\n")
|
||||
err := component.Render(componentContext(r), w)
|
||||
fmt.Fprint(w, "</template></turbo-stream>\n\n")
|
||||
if err != nil {
|
||||
log.Error("turbo stream", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("turbo stream")
|
||||
}
|
||||
|
||||
// componentContext returns a [context.Context] with some values
|
||||
// needed for component rendering.
|
||||
// The resulting context always contains the request and its user.
|
||||
func componentContext(r *http.Request) context.Context {
|
||||
ctx := r.Context()
|
||||
ctx = withUser(ctx, auth.GetRequestUser(r))
|
||||
ctx = withRequest(ctx, r)
|
||||
ctx = withPreferences(ctx, &preferences.Preferences{}) // lazy loaded
|
||||
return ctx
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user