move webserver files to ./web/
This commit is contained in:
41
src/web/app/app.go
Normal file
41
src/web/app/app.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crowsnest/internal/model/database"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
articles *database.ArticleRepository
|
||||
articleVMs *database.ArticleViewModelRepository
|
||||
articlePageVMs *database.ArticlePageViewModelRepository
|
||||
rssItems *database.RSSItemRepository
|
||||
}
|
||||
|
||||
func NewApp(db *sql.DB) *App {
|
||||
return &App{
|
||||
articles: &database.ArticleRepository{DB: db},
|
||||
articleVMs: &database.ArticleViewModelRepository{DB: db},
|
||||
articlePageVMs: &database.ArticlePageViewModelRepository{DB: db},
|
||||
rssItems: &database.RSSItemRepository{DB: db},
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) Routes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// dynamic routes
|
||||
mux.Handle("GET /rss.xml", http.HandlerFunc(app.RSS))
|
||||
|
||||
mux.Handle("GET /", http.HandlerFunc(app.Index))
|
||||
mux.Handle("GET /page/{id}", http.HandlerFunc(app.Index))
|
||||
mux.Handle("POST /up/search", http.HandlerFunc(app.UpSearch))
|
||||
|
||||
mux.Handle("GET /article/{id}", http.HandlerFunc(app.Article))
|
||||
|
||||
// serve files from the "static" directory
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./assets/static"))))
|
||||
|
||||
return mux
|
||||
}
|
||||
33
src/web/app/article.go
Normal file
33
src/web/app/article.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crowsnest/web/html"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Enpoint that returns a list of articles given search terms in the post
|
||||
// request of a search form. Uses the content template.
|
||||
func (app *App) Article(w http.ResponseWriter, req *http.Request) {
|
||||
// get id
|
||||
id, err := strconv.ParseUint(req.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// get articles
|
||||
articlePageVM, err := app.articlePageVMs.ById(int64(id))
|
||||
if err != nil {
|
||||
// treat as no result
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// render page
|
||||
err = html.ArticleLayout(articlePageVM).Render(w)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
42
src/web/app/index.go
Normal file
42
src/web/app/index.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crowsnest/web/html"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// List the latest articles using the base template.
|
||||
func (app *App) Index(w http.ResponseWriter, req *http.Request) {
|
||||
const pageSize = 15
|
||||
var offset, pageId uint64 = 0, 0
|
||||
var err error
|
||||
|
||||
// get page number
|
||||
if pageId, err = strconv.ParseUint(req.PathValue("id"), 10, 32); err == nil {
|
||||
pageId--
|
||||
offset = pageId * pageSize
|
||||
}
|
||||
|
||||
// get articles
|
||||
articleVMs, err := app.articleVMs.All(pageSize, int(offset))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// get count of total articles
|
||||
totalCount, err := app.articles.CountAll()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
totalCount /= pageSize
|
||||
|
||||
// render page
|
||||
err = html.IndexLayout(articleVMs, uint(pageId+1), totalCount+1).Render(w)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
28
src/web/app/rss.go
Normal file
28
src/web/app/rss.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crowsnest/internal/model"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// List the latest articles using the base template.
|
||||
func (app *App) RSS(w http.ResponseWriter, req *http.Request) {
|
||||
// set response headers
|
||||
w.Header().Set("Content-Type", "application/rss+xml")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// get articles
|
||||
feed, err := app.rssItems.All(30)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// write RSS feed to response
|
||||
encoder := xml.NewEncoder(w)
|
||||
encoder.Indent("", " ")
|
||||
if err := encoder.Encode(model.RSSFeedFromItems(feed)); err != nil {
|
||||
http.Error(w, "Error generating RSS feed", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
32
src/web/app/upsearch.go
Normal file
32
src/web/app/upsearch.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crowsnest/web/html"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Enpoint that returns a list of articles given search terms in the post
|
||||
// request of a search form. Uses the content template.
|
||||
func (app *App) UpSearch(w http.ResponseWriter, req *http.Request) {
|
||||
// construct search query
|
||||
searchTerms := req.FormValue("search")
|
||||
if searchTerms == "" {
|
||||
app.Index(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// get articles
|
||||
articleVMs, err := app.articleVMs.Search(searchTerms)
|
||||
if err != nil {
|
||||
// treat as no result
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// render page
|
||||
err = html.IndexLayout(articleVMs, 0, 0).Render(w)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
39
src/web/html/article.go
Normal file
39
src/web/html/article.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"crowsnest/internal/model"
|
||||
"strconv"
|
||||
|
||||
g "maragu.dev/gomponents"
|
||||
h "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func Articles(articles []*model.ArticleViewModel) g.Node {
|
||||
return h.Div(h.Class("content max-w-screen-lg flex flex-col mx-auto"), g.Map(articles, Article))
|
||||
}
|
||||
|
||||
func Article(article *model.ArticleViewModel) g.Node {
|
||||
return h.Div(
|
||||
h.Class("collapse bg-base-200 shadow mb-4"),
|
||||
h.TabIndex("0"),
|
||||
h.Div(h.Class("collapse-title font-medium"), g.Text(article.Title)),
|
||||
h.Div(
|
||||
h.Class("collapse-content"),
|
||||
h.P(
|
||||
h.Class("pb-2"),
|
||||
h.Span(h.Class("badge badge-outline"), g.Text(article.ShortSource)),
|
||||
h.Span(h.Class("badge badge-outline"), g.Text(article.PublishDate)),
|
||||
),
|
||||
h.P(h.Class("card-text"), g.Text(article.Summary)),
|
||||
h.Div(
|
||||
h.Class("flex flex-row-reverse"),
|
||||
h.A(
|
||||
h.Href("/article/"+strconv.Itoa(article.Id)),
|
||||
h.Class("btn btn-active btn-sm btn-primary"),
|
||||
g.Attr("up-follow"),
|
||||
g.Text("Details"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
14
src/web/html/articleLayout.go
Normal file
14
src/web/html/articleLayout.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"crowsnest/internal/model"
|
||||
|
||||
g "maragu.dev/gomponents"
|
||||
)
|
||||
|
||||
func ArticleLayout(articlePageVMs *model.ArticlePageViewModel) g.Node {
|
||||
return Layout(
|
||||
"Crowsnest - "+articlePageVMs.Title,
|
||||
ArticlePage(articlePageVMs),
|
||||
)
|
||||
}
|
||||
55
src/web/html/articlePage.go
Normal file
55
src/web/html/articlePage.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"crowsnest/internal/model"
|
||||
|
||||
g "maragu.dev/gomponents"
|
||||
h "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func ArticlePageColumn(key string, value string) g.Node {
|
||||
return h.P(
|
||||
h.Span(
|
||||
h.Class("badge badge-neutral me-4 w-20"),
|
||||
g.Text(key),
|
||||
),
|
||||
g.Text(value),
|
||||
)
|
||||
}
|
||||
|
||||
func ArticlePage(articlePageVM *model.ArticlePageViewModel) g.Node {
|
||||
return h.Div(
|
||||
h.TabIndex("0"),
|
||||
h.Class("card bg-base-200 shadow mb-4"),
|
||||
h.Div(
|
||||
h.Class("card-body"),
|
||||
h.Div(
|
||||
h.Class("flex flex-row pb-4"),
|
||||
h.Div(
|
||||
h.Class("divider divider-horizontal divider-primary"),
|
||||
),
|
||||
h.Div(
|
||||
h.Class("card-title font-medium"),
|
||||
g.Text(articlePageVM.Title),
|
||||
),
|
||||
),
|
||||
h.Div(
|
||||
h.Class("px-5 pb-4 grid gap-y-4 grid-cols-1"),
|
||||
ArticlePageColumn("Datum", articlePageVM.PublishDate),
|
||||
ArticlePageColumn("Quelle", articlePageVM.ShortSource),
|
||||
ArticlePageColumn("TLDR", articlePageVM.Summary),
|
||||
ArticlePageColumn("Inhalt", articlePageVM.Content),
|
||||
h.Div(
|
||||
h.Class("card-actions justify-end"),
|
||||
h.A(
|
||||
h.Href(articlePageVM.SourceUrl),
|
||||
h.Button(
|
||||
h.Class("btn btn-primary btn-sm"),
|
||||
g.Text("Seite besuchen"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
17
src/web/html/indexLayout.go
Normal file
17
src/web/html/indexLayout.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"crowsnest/internal/model"
|
||||
|
||||
g "maragu.dev/gomponents"
|
||||
)
|
||||
|
||||
func IndexLayout(articleVMs []*model.ArticleViewModel, paginationCurrent uint, paginationTotal uint) g.Node {
|
||||
return Layout(
|
||||
"Crowsnest - Artikel",
|
||||
g.Group{
|
||||
Articles(articleVMs),
|
||||
Pagination(paginationCurrent, paginationTotal),
|
||||
},
|
||||
)
|
||||
}
|
||||
43
src/web/html/layout.go
Normal file
43
src/web/html/layout.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
g "maragu.dev/gomponents"
|
||||
c "maragu.dev/gomponents/components"
|
||||
h "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func Layout(title string, body g.Node) g.Node {
|
||||
completeBody := []g.Node{
|
||||
Navbar(),
|
||||
h.Div(
|
||||
h.Class("content container flex flex-col mx-auto px-4 mt-28"),
|
||||
body,
|
||||
),
|
||||
h.Script(h.Src("https://cdn.tailwindcss.com")),
|
||||
h.Script(
|
||||
g.Text(
|
||||
"up.link.config.followSelectors.push('a[href]')\n" +
|
||||
"up.link.config.instantSelectors.push('a[href]')",
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
return c.HTML5(c.HTML5Props{
|
||||
Title: title,
|
||||
Language: "de",
|
||||
Head: layoutHead(),
|
||||
Body: completeBody,
|
||||
HTMLAttrs: []g.Node{g.Attr("data-theme", "dark")},
|
||||
})
|
||||
}
|
||||
|
||||
func layoutHead() []g.Node {
|
||||
return []g.Node{
|
||||
h.Meta(h.Name("viewport"), h.Content("width=device-width, initial-scale=1")),
|
||||
h.Link(h.Rel("stylesheet"), h.Type("text/css"), h.Href("/static/unpoly.min.css")),
|
||||
h.Script(h.Src("/static/unpoly.min.js")),
|
||||
h.Link(h.Rel("stylesheet"), h.Type("text/css"), h.Href("/static/daisyui.min.css")),
|
||||
h.Script(h.Src("/static/tailwindcss.min.js")),
|
||||
}
|
||||
}
|
||||
|
||||
76
src/web/html/navbar.go
Normal file
76
src/web/html/navbar.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
g "maragu.dev/gomponents"
|
||||
"maragu.dev/gomponents-heroicons/v3/outline"
|
||||
h "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func NavbarLi(href string, text string, active bool) g.Node {
|
||||
return h.Li(h.A(h.Href(href), h.TabIndex("0"), g.If(active, h.Class("active")), g.Text(text)))
|
||||
}
|
||||
|
||||
func SearchForm() g.Node {
|
||||
return h.Form(
|
||||
h.Role("search"),
|
||||
h.Method("post"),
|
||||
h.Action("/up/search"),
|
||||
g.Attr("up-submit"),
|
||||
g.Attr("up-autosubmit"),
|
||||
g.Attr("up-target", ".content"),
|
||||
h.Label(
|
||||
h.Class("input input-bordered input-sm flex items-center gap-2"),
|
||||
h.Input(
|
||||
h.Name("search"),
|
||||
h.Type("search"),
|
||||
h.Class("grow"),
|
||||
h.Placeholder("Suche"),
|
||||
),
|
||||
outline.MagnifyingGlass(
|
||||
h.Class("h-4 w-4 opacity-70"),
|
||||
g.Attr("stroke", "currentColor"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Navbar() g.Node {
|
||||
return h.Nav(
|
||||
h.Class("fixed top-0 z-50 w-full p-4"),
|
||||
h.Div(
|
||||
h.Class("navbar bg-base-300 rounded-box drop-shadow-md"),
|
||||
// links for large screens
|
||||
h.Div(
|
||||
h.Class("flex-1"),
|
||||
h.A(h.Href("/"), h.TabIndex("0"), h.Class("btn btn-ghost text-xl"), g.Text("crowsnest")),
|
||||
h.Ul(
|
||||
h.Class("menu menu-horizontal hidden sm:flex"),
|
||||
NavbarLi("/", "Artikel", true),
|
||||
NavbarLi("/", "Themen", false),
|
||||
),
|
||||
),
|
||||
// search
|
||||
h.Div(h.Class("hidden sm:flex flex-none pe-4"), SearchForm()),
|
||||
// small navbar dropdown
|
||||
h.Div(
|
||||
h.Class("dropdown dropdown-end sm:hidden"),
|
||||
// dropdown button
|
||||
h.Div(
|
||||
h.TabIndex("0"),
|
||||
h.Role("button"),
|
||||
h.Class("btn btn-ghost"),
|
||||
outline.Bars3BottomRight(
|
||||
h.Class("h-6 w-6 opacity-70"),
|
||||
g.Attr("stroke", "currentColor"),
|
||||
),
|
||||
),
|
||||
// dropdown content
|
||||
h.Ul(
|
||||
h.Class("menu dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"),
|
||||
NavbarLi("/", "Artikel", true),
|
||||
NavbarLi("/", "Themen", false),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
45
src/web/html/pagination.go
Normal file
45
src/web/html/pagination.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
g "maragu.dev/gomponents"
|
||||
h "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func PaginationButton(content string, active bool, disabled bool) g.Node {
|
||||
classStr := "join-item btn btn-sm"
|
||||
if disabled { classStr += " btn-disabled" }
|
||||
if active { classStr += " btn-active" }
|
||||
|
||||
return h.A(
|
||||
h.Class(classStr),
|
||||
g.If(!disabled, h.Href("/page/"+content)),
|
||||
g.If(!disabled, h.TabIndex("0")),
|
||||
g.Attr("up-follow"),
|
||||
g.Attr("up-target", ".content"),
|
||||
g.Text(content),
|
||||
)
|
||||
}
|
||||
|
||||
func Pagination(currentPage uint, totalPages uint) g.Node {
|
||||
buttons := make([]g.Node, 0)
|
||||
|
||||
if totalPages > 1 {
|
||||
buttons = append(buttons, PaginationButton("1", currentPage == 1, false))
|
||||
}
|
||||
if currentPage > 3 {
|
||||
buttons = append(buttons, PaginationButton("...", false, true))
|
||||
}
|
||||
for i := max(2, currentPage-1); i <= min(totalPages-1, currentPage+1); i++ {
|
||||
buttons = append(buttons, PaginationButton(strconv.Itoa(int(i)), i == currentPage, false))
|
||||
}
|
||||
if currentPage < totalPages-2 {
|
||||
buttons = append(buttons, PaginationButton("...", false, true))
|
||||
}
|
||||
if totalPages > 1 {
|
||||
buttons = append(buttons, PaginationButton(strconv.Itoa(int(totalPages)), totalPages == currentPage, false))
|
||||
}
|
||||
|
||||
return h.Div(h.Class("join pagination p-5 mx-auto"), h.Span(buttons...))
|
||||
}
|
||||
35
src/web/middleware/logging.go
Normal file
35
src/web/middleware/logging.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type wrappedWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (w *wrappedWriter) WriteHeader(statusCode int) {
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
w.statusCode = statusCode
|
||||
}
|
||||
|
||||
// LoggingMiddleware logs details about each incoming HTTP request.
|
||||
func Logging(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
wrapped := &wrappedWriter{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
// Call the next handler
|
||||
next.ServeHTTP(wrapped, r)
|
||||
|
||||
log.Printf("[request] %d %s %s from %s (%v)",
|
||||
wrapped.statusCode, r.Method, r.URL.Path, r.RemoteAddr, time.Since(start))
|
||||
})
|
||||
}
|
||||
15
src/web/middleware/middleware.go
Normal file
15
src/web/middleware/middleware.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package middleware
|
||||
|
||||
import "net/http"
|
||||
|
||||
type Middleware func(http.Handler) http.Handler
|
||||
|
||||
func CreateStack(xs ...Middleware) Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
for i := len(xs) - 1; i >= 0; i-- {
|
||||
x := xs[i]
|
||||
next = x(next)
|
||||
}
|
||||
return next
|
||||
}
|
||||
}
|
||||
20
src/web/static/daisyui.min.css
vendored
Normal file
20
src/web/static/daisyui.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
83
src/web/static/tailwindcss.min.js
vendored
Normal file
83
src/web/static/tailwindcss.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/web/static/unpoly.min.css
vendored
Normal file
6
src/web/static/unpoly.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
[hidden][hidden]{display:none !important}
|
||||
up-wrapper{display:inline-block}
|
||||
up-bounds{position:absolute}.up-focus-hidden:focus-visible{outline-color:rgba(0,0,0,0) !important;outline-style:none !important}body.up-scrollbar-away{padding-right:calc(var(--up-scrollbar-width) + var(--up-original-padding-right)) !important}body.up-scrollbar-away,html:has(>body.up-scrollbar-away){overflow-y:hidden !important}body.up-scrollbar-away .up-scrollbar-away{right:calc(var(--up-scrollbar-width) + var(--up-original-right)) !important}
|
||||
.up-request-loader{display:none}up-progress-bar{position:fixed;top:0;left:0;z-index:999999999;height:3px;background-color:#007bff}
|
||||
up-focus-trap{position:fixed;top:0;left:0;width:0;height:0}up-cover-viewport,up-drawer-viewport,up-modal-viewport,up-drawer-backdrop,up-modal-backdrop,up-cover,up-drawer,up-modal{top:0;left:0;bottom:0;right:0}up-drawer-box,up-modal-box{box-shadow:0 0 10px 1px rgba(0,0,0,.3)}up-popup{box-shadow:0 0 4px rgba(0,0,0,.3)}up-popup:focus,up-cover-box:focus,up-drawer-box:focus,up-modal-box:focus,up-cover:focus,up-drawer:focus,up-modal:focus,up-popup:focus-visible,up-cover-box:focus-visible,up-drawer-box:focus-visible,up-modal-box:focus-visible,up-cover:focus-visible,up-drawer:focus-visible,up-modal:focus-visible{outline:none}up-cover,up-drawer,up-modal{z-index:2000;position:fixed}up-drawer-backdrop,up-modal-backdrop{position:absolute;background:rgba(0,0,0,.4)}up-cover-viewport,up-drawer-viewport,up-modal-viewport{position:absolute;overflow-y:scroll;overflow-x:hidden;overscroll-behavior:contain;display:flex;align-items:flex-start;justify-content:center}up-popup,up-cover-box,up-drawer-box,up-modal-box{position:relative;box-sizing:border-box;max-width:100%;background-color:#fff;padding:20px;overflow-x:hidden}up-popup-content,up-cover-content,up-drawer-content,up-modal-content{display:block}up-popup{z-index:1000}up-popup-dismiss,up-cover-dismiss,up-drawer-dismiss,up-modal-dismiss{color:#888;position:absolute;top:10px;right:10px;font-size:1.7rem;line-height:.5;cursor:pointer}up-modal[nesting="0"] up-modal-viewport{padding:25px 15px}up-modal[nesting="1"] up-modal-viewport{padding:50px 30px}up-modal[nesting="2"] up-modal-viewport{padding:75px 45px}up-modal[nesting="3"] up-modal-viewport{padding:100px 60px}up-modal[nesting="4"] up-modal-viewport{padding:125px 75px}up-modal[size=small] up-modal-box{width:350px}up-modal[size=medium] up-modal-box{width:650px}up-modal[size=large] up-modal-box{width:1000px}up-modal[size=grow] up-modal-box{width:auto}up-modal[size=full] up-modal-box{width:100%}up-drawer-viewport{justify-content:flex-start}up-drawer[position=right] up-drawer-viewport{justify-content:flex-end}up-drawer-box{min-height:100vh}up-drawer[size=small] up-drawer-box{width:150px}up-drawer[size=medium] up-drawer-box{width:340px}up-drawer[size=large] up-drawer-box{width:600px}up-drawer[size=grow] up-drawer-box{width:auto}up-drawer[size=full] up-drawer-box{width:100%}up-cover-box{width:100%;min-height:100vh;padding:0}up-popup{padding:15px;text-align:left}up-popup[size=small]{width:180px}up-popup[size=medium]{width:300px}up-popup[size=large]{width:550px}up-popup[size=grow] up-popup{width:auto}up-popup[size=full] up-popup{width:100%}
|
||||
[up-clickable][role=link]{cursor:pointer}[up-expand]:not([role]),[up-expand][role=link]{cursor:pointer}
|
||||
1
src/web/static/unpoly.min.js
vendored
Normal file
1
src/web/static/unpoly.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user