init - add project files
This commit is contained in:
21
ui/alpine.go
Normal file
21
ui/alpine.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
type AlpineStore map[string]string
|
||||
|
||||
func (as AlpineStore) Init() Node {
|
||||
script := "document.addEventListener('alpine:init', () => {"
|
||||
|
||||
for k, v := range as {
|
||||
script += fmt.Sprintf("Alpine.store('%s', %s);", k, v)
|
||||
}
|
||||
|
||||
script += "})"
|
||||
|
||||
return Script(Raw(script))
|
||||
}
|
||||
175
ui/app_layout.go
Normal file
175
ui/app_layout.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"maxwarden/auth"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
const (
|
||||
LAYOUT_SECTION_VAULT = iota
|
||||
LAYOUT_SECTION_TOOLS = iota
|
||||
LAYOUT_SECTION_ACCOUNT = iota
|
||||
LAYOUT_SECTION_API = iota
|
||||
)
|
||||
|
||||
type NavGroup struct {
|
||||
SectionId int
|
||||
Title string
|
||||
URL string
|
||||
SubGroup []NavGroup
|
||||
NewTab bool
|
||||
}
|
||||
|
||||
var NavGroups = []NavGroup{
|
||||
{SectionId: LAYOUT_SECTION_VAULT, Title: "Vault", URL: "/app", SubGroup: nil},
|
||||
{
|
||||
Title: "Tools",
|
||||
SubGroup: []NavGroup{
|
||||
{SectionId: LAYOUT_SECTION_TOOLS, Title: "Password Generator", URL: "/app/examples/forms"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func AppLayout(title string, identity auth.Identity, session map[string]interface{}, children ...Node) Node {
|
||||
navbarDropdown := func(dropdownHeader Node, dropdownItems Node) Node {
|
||||
return Div(
|
||||
InlineStyle("$me{cursor: pointer; position: relative; margin-left: $3;}"),
|
||||
Div(
|
||||
Class("button"),
|
||||
InlineStyle(`
|
||||
$me {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding-top: $2;
|
||||
padding-bottom: $2;
|
||||
padding-left: $3;
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--text-sm--line-height);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: $color(white);
|
||||
}
|
||||
|
||||
$me:hover{color: $color(white);}
|
||||
`),
|
||||
Button(
|
||||
Div(InlineStyle("$me{cursor: pointer; display: flex; align-items: center;}"),
|
||||
dropdownHeader, Span(Text(" ")),
|
||||
Icon(ICON_CHEVRON_DOWN, 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
Div(
|
||||
Class("dropdown"),
|
||||
InlineStyle(`$me{display: none; position: absolute; right: 0; z-index: 10; padding-top: $1; padding-bottom: $1; margin-top: $2; width: $48; background-color: $color(white); transform-origin: top right; box-shadow: var(--shadow-lg);}`),
|
||||
TabIndex("-1"),
|
||||
dropdownItems,
|
||||
),
|
||||
InlineScript(`
|
||||
let button = me(".button", me());
|
||||
let dropdown = me(".dropdown", me());
|
||||
|
||||
button.on("click", ev => { toggleShowHide(dropdown) });
|
||||
onClickOutsideOrEscape(me(), () => { hide(dropdown) });
|
||||
`),
|
||||
)
|
||||
}
|
||||
|
||||
navbarDropdownItem := func(name string, url string, newPage bool) Node {
|
||||
return A(InlineStyle(`$me{display: block; padding-top: $2; padding-bottom: $2; padding-left: $4; padding-right: $4; font-size: var(--text-sm); line-height: $5; color: $color(neutral-700); } $me:hover{background: $color(neutral-100);}`), Href(url), TabIndex("-1"), Text(name), If(newPage, Target("_blank")))
|
||||
}
|
||||
|
||||
navbarLink := func(name string, url string, newPage bool) Node {
|
||||
return A(
|
||||
InlineStyle(`$me{ padding-left: $3; padding-right: $3; padding-top: $2; padding-bottom: $2; font-size: var(--text-sm); font-weight: var(--font-weight-medium); color: $color(white);}`),
|
||||
InlineStyle("$me:hover{color: $color(white);}"),
|
||||
Href(url),
|
||||
Text(name),
|
||||
If(newPage, Target("_blank")),
|
||||
)
|
||||
}
|
||||
|
||||
return RootLayout(title+" | MaxWarden",
|
||||
Body(InlineStyle("$me{background-color: $color(light-grey); height: 100%;}"),
|
||||
Div(InlineStyle("$me{min-height: 100%}"),
|
||||
Nav(InlineStyle("$me{background-color: $color(deep-blue);}"),
|
||||
Div(InlineStyle("$me{margin-left: auto; margin-right: auto; max-width: var(--container-7xl);}"),
|
||||
Div(InlineStyle("$me{display: flex; height: $16; align-items: center; justify-content: space-between;}"),
|
||||
Div(InlineStyle("$me{align-items: center; display: flex;}"),
|
||||
Div(InlineStyle("@media $lg-{ $me{display: block;}}"),
|
||||
Div(InlineStyle(`$me{margin-left: $1; display: flex; align-items: baseline;} $me:not(:last-child){ margin-left: $4; }`),
|
||||
Map(NavGroups, func(nav NavGroup) Node {
|
||||
if len(nav.SubGroup) > 0 {
|
||||
return navbarDropdown(
|
||||
Text(nav.Title),
|
||||
Map(nav.SubGroup, func(sub NavGroup) Node {
|
||||
return navbarDropdownItem(sub.Title, sub.URL, sub.NewTab)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
return navbarLink(nav.Title, nav.URL, nav.NewTab)
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
Div(InlineStyle("$me{display: none;} @media $md { $me{ display: block; }}"),
|
||||
Div(InlineStyle("$me{ margin-left: $4; display: flex; align-items: center;} @media $md { $me{ margin-left: $6;}}"),
|
||||
Div(InlineStyle("$me{position: relative; margin-left: $3;}"),
|
||||
navbarDropdown(
|
||||
Icon(ICON_USERS, 24),
|
||||
Group{
|
||||
navbarDropdownItem("My Profile", "/app/account", false),
|
||||
navbarDropdownItem("Logout", "/auth/logout", false),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Div(InlineStyle("$me{margin-right: $2; display: flex;} @media $md{ $me{display: none;}}"),
|
||||
Button(
|
||||
InlineStyle("$me{position: relative; display: inline-flex; justify-items: center; padding: $2; color: $color(neutral-400)}"),
|
||||
InlineStyle("$me:hover{color: $color(white); background-color: $color(neutral-900);}"),
|
||||
Type("button"),
|
||||
Span(InlineStyle("$me{position: absolute;}")),
|
||||
Icon(ICON_MENU, 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Div(InlineStyle("@media $md { $me {display: none; }}"),
|
||||
Div(Class("space-y-1 px-2 pb-3 pt-2 sm:px-3"),
|
||||
A(Href("/app/dashboard"), Class("block hover:bg-neutral-900 px-3 py-2 text-base font-medium text-white"), Text("Dashboard")),
|
||||
),
|
||||
Div(Class("border-t border-neutral-700 pb-3 pt-4"),
|
||||
Div(Class("flex items-center px-5"),
|
||||
Div(Class("flex-shrink-0"),
|
||||
Img(Class("h-10 w-10 rounded-full"), Src(""), Alt("profile picture")),
|
||||
),
|
||||
Div(Class("ml-3"),
|
||||
// Div(Class("text-base/5 font-medium text-white"), Text(identity.User.Firstname+" "+identity.User.Lastname)),
|
||||
// Div(Class("text-sm font-medium text-neutral-400"), Text(identity.User.Email)),
|
||||
),
|
||||
),
|
||||
Div(Class("mt-3 space-y-1 px-2"),
|
||||
A(Href("/auth/logout"), Class("block px-3 py-2 text-base font-medium text-neutral-200 hover:bg-neutral-900 hover:text-white"), Text("Log out")),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Header(InlineStyle("$me{background-color: $color(white); box-shadow: var(--shadow-sm);}"),
|
||||
Div(InlineStyle("$me{margin-left: auto; margin-right: auto; max-width: var(--container-7xl); padding: $4;} @media $lg { $me{ padding-left: $8; padding-right: $8;}}"),
|
||||
H1(InlineStyle("$me{font-size: var(--text-3xl); font-weight: var(--font-weight-bold); color: $color(neutral-950); letter-spacing: var(--tracking-tight);}"), Text(title)),
|
||||
),
|
||||
),
|
||||
Main(
|
||||
Div(InlineStyle("$me{margin-left: auto; margin-right: auto; max-width: var(--container-7xl); padding: $6 $4;} @media $sm { $me{padding-left: $6; padding-right: $6; }} @media $lg { $me{padding-left: $8; padding-right: $8;}}"),
|
||||
Group(children),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
375
ui/autotable.go
Normal file
375
ui/autotable.go
Normal file
@@ -0,0 +1,375 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
. "maxwarden/basic"
|
||||
|
||||
"maxwarden/database"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
hx "maragu.dev/gomponents-htmx"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
const TABLE_ABOVE_PREFIX = "_tableAbove"
|
||||
const FORM_BIND_SUFFIX = "_form"
|
||||
const FORM_PAGINATION_SUFFIX = "_paginationForm"
|
||||
|
||||
// FILTER COMPONENTS
|
||||
|
||||
// Autotable bind search to col
|
||||
func BindSearch(elId string, identifier string) Node {
|
||||
return Group{
|
||||
FormAttr(elId + FORM_BIND_SUFFIX),
|
||||
Name(database.SEARCH_URL_KEY_PREFIX + identifier),
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
AUTOTABLE_HEADER_COLOR_DEFAULT = iota
|
||||
)
|
||||
|
||||
type AutoTableOptions struct {
|
||||
Compact bool
|
||||
Shadow bool
|
||||
Hover bool // highlight rows when hovering over them
|
||||
Alternate bool // highlight alternating rows
|
||||
HeaderBorderY bool
|
||||
BorderX bool
|
||||
BorderY bool
|
||||
Color int // "enum"
|
||||
}
|
||||
|
||||
// THE TABLE
|
||||
// Note that "aboveTable" node is not swapped with HTMX, but "belowTable" is.
|
||||
func AutoTable[E any](tableId string, url string, cols []database.ColInfo, f database.Filter, entities []E, aboveTable Node, rowComponent func(E) Node, belowTable Node, opts AutoTableOptions) Node {
|
||||
paginationButton := func(icon string, page int) Node {
|
||||
return Button(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
padding-left: $3;
|
||||
padding-right: $3;
|
||||
padding-top: $1;
|
||||
padding-bottom: $1;
|
||||
min-height: $9;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
color: $color(neutral-800);
|
||||
cursor: pointer;
|
||||
}
|
||||
`),
|
||||
Icon(icon, 16),
|
||||
hx.Get(url+database.QueryParamsFromPagenum(page, f)),
|
||||
hx.Swap(CSSID(tableId)),
|
||||
hx.Target(CSSID(tableId)),
|
||||
hx.Select(CSSID(tableId)),
|
||||
hx.Trigger("click"),
|
||||
)
|
||||
}
|
||||
|
||||
return Group{
|
||||
If((aboveTable != nil) && (tableId != ""),
|
||||
Div(ID(tableId+TABLE_ABOVE_PREFIX),
|
||||
aboveTable,
|
||||
),
|
||||
),
|
||||
Div(
|
||||
If(tableId != "",
|
||||
Group{
|
||||
ID(tableId),
|
||||
Form(ID(tableId+FORM_BIND_SUFFIX),
|
||||
AutoComplete("off"),
|
||||
hx.Get(url),
|
||||
hx.Trigger("keyup delay:100ms from:(#"+tableId+TABLE_ABOVE_PREFIX+" input), change from:(#"+tableId+TABLE_ABOVE_PREFIX+" input[type=date]), change from:(#"+tableId+TABLE_ABOVE_PREFIX+" input[type=datetime-local]), change from:(#"+tableId+TABLE_ABOVE_PREFIX+" select)"),
|
||||
hx.Swap("outerHTML"),
|
||||
hx.Target(CSSID(tableId)),
|
||||
hx.Select(CSSID(tableId)),
|
||||
Input(Type("hidden"), Name(database.ORDER_BY_URL_KEY), Value(f.OrderBy)),
|
||||
Input(Type("hidden"), Name(database.ORDER_DESC_URL_KEY), Value(ToString(f.OrderDescending))),
|
||||
Input(Type("hidden"), Name(database.ITEMS_PER_PAGE_URL_KEY), Value(ToString(f.Pagination.MaxItemsPerPage))),
|
||||
),
|
||||
},
|
||||
),
|
||||
Div(
|
||||
If(opts.Shadow,
|
||||
InlineStyle(`
|
||||
$me {
|
||||
background-color: $color(white);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
`),
|
||||
),
|
||||
InlineStyle(`
|
||||
$me {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $color(gray-700);
|
||||
background-color: $color(white);
|
||||
}
|
||||
`),
|
||||
Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`),
|
||||
Table(
|
||||
InlineStyle("$me { table-layout: fixed; width: 100%; }"),
|
||||
// ---- TABLE HEADER ----
|
||||
THead(
|
||||
Tr(
|
||||
Map(cols, func(col database.ColInfo) Node {
|
||||
return Th(
|
||||
If(col.DisplayPosition == database.COL_POS_RIGHT,
|
||||
InlineStyle("$me { text-align: right; }"),
|
||||
),
|
||||
InlineStyle(`
|
||||
$me {
|
||||
border-bottom: 1px solid $color(neutral-200);
|
||||
}
|
||||
|
||||
`),
|
||||
If(opts.HeaderBorderY,
|
||||
InlineStyle(`
|
||||
$me:not(:first-child) {
|
||||
border-left: 1px solid $color(neutral-200);
|
||||
}
|
||||
`),
|
||||
),
|
||||
IfElse(opts.Compact,
|
||||
InlineStyle("$me { padding: $2 $3; }"),
|
||||
InlineStyle("$me { padding: $4 $4; }"),
|
||||
),
|
||||
If(col.Sortable,
|
||||
Group{
|
||||
hx.Get(url + database.QueryParamsFromOrderBy(col.DbName, !f.OrderDescending && (col.DbName == f.OrderBy), f)),
|
||||
hx.Swap(CSSID(tableId)),
|
||||
hx.Target(CSSID(tableId)),
|
||||
hx.Select(CSSID(tableId)),
|
||||
hx.Trigger("click"),
|
||||
InlineStyle(`
|
||||
$me {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
$me:hover {
|
||||
background-color: $color(neutral-200);
|
||||
}
|
||||
`),
|
||||
},
|
||||
),
|
||||
P(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: $2;
|
||||
font-size: var(--text-sm);
|
||||
color: $color(neutral-800);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
`),
|
||||
If(col.DisplayPosition == database.COL_POS_LEFT,
|
||||
InlineStyle("$me { flex-direction: row; }"),
|
||||
),
|
||||
If(col.DisplayPosition == database.COL_POS_RIGHT,
|
||||
InlineStyle("$me { flex-direction: row-reverse; }"),
|
||||
),
|
||||
Text(col.DisplayName),
|
||||
If((f.OrderBy == col.DbName) && (col.DbName != ""),
|
||||
Group{
|
||||
InlineStyle("$me { color: $color(black); text-decoration: underline; }"),
|
||||
IfElse(f.OrderDescending,
|
||||
Icon(ICON_ARROW_DOWN, 16),
|
||||
Icon(ICON_ARROW_UP, 16),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
// ---- TABLE BODY ----
|
||||
TBody(
|
||||
InlineStyle(`
|
||||
$me > tr > td {
|
||||
overflow-x: auto;
|
||||
font-size: var(--text-sm);
|
||||
color: $color(neutral-700);
|
||||
}
|
||||
`),
|
||||
If(opts.Alternate,
|
||||
InlineStyle("$me > tr:nth-child(even) { background: $color(neutral-100) }"),
|
||||
),
|
||||
If(opts.Hover && len(entities) > 0,
|
||||
InlineStyle("$me > tr:hover {background-color: $color(neutral-100);}"),
|
||||
),
|
||||
If(opts.BorderX && f.Pagination.Enabled,
|
||||
InlineStyle("$me > tr {border-bottom: 1px solid $color(neutral-200); }"),
|
||||
),
|
||||
If(opts.BorderX && !f.Pagination.Enabled,
|
||||
InlineStyle("$me > tr:not(:last-child) {border-bottom: 1px solid $color(neutral-200); }"),
|
||||
),
|
||||
If(opts.BorderY,
|
||||
InlineStyle(`
|
||||
$me > tr > td:not(:first-child) {
|
||||
border-left: 1px solid $color(neutral-200);
|
||||
}
|
||||
`),
|
||||
),
|
||||
IfElse(opts.Compact,
|
||||
InlineStyle("$me > tr > td { padding: $1 $3; }"),
|
||||
InlineStyle("$me > tr > td { padding: $4; }"),
|
||||
),
|
||||
IfElse(len(entities) > 0,
|
||||
Map(entities, func(e E) Node {
|
||||
return rowComponent(e)
|
||||
}),
|
||||
Tr(
|
||||
Td(
|
||||
Text("Dataset contains no entries."),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// ---- PAGINATION ----
|
||||
If(f.Pagination.Enabled,
|
||||
Div(InlineStyle("$me { display: flex; justify-content: space-between; align-items: center; }"),
|
||||
IfElse(opts.Compact,
|
||||
InlineStyle("$me { padding: $1 $4; }"),
|
||||
InlineStyle("$me { padding: $3 $4; }"),
|
||||
),
|
||||
Div(
|
||||
InlineStyle("$me { display: flex; align-items: center; font-size: var(--text-sm); color: $color(neutral-500); }"),
|
||||
B(Icon(ICON_LIST_ORDERED, 16)),
|
||||
Span(InlineStyle("$me {margin-right: $3;}")), ToText(f.Pagination.ViewRangeLower), Text("-"), ToText(f.Pagination.ViewRangeUpper), Text(" of "), ToText(f.Pagination.TotalItems),
|
||||
),
|
||||
|
||||
Div(InlineStyle("$me { display: flex; align-items: center; }"),
|
||||
Div(InlineStyle("$me { margin-right: $3; font-size: var(--text-sm); color: $color(neutral-500);}"),
|
||||
Span(Text("Items per page:")),
|
||||
),
|
||||
Form(
|
||||
ID(tableId+FORM_PAGINATION_SUFFIX),
|
||||
hx.Get(url),
|
||||
hx.Trigger("change from:(#"+tableId+FORM_PAGINATION_SUFFIX+" select)"),
|
||||
hx.Target(CSSID(tableId)),
|
||||
hx.Select(CSSID(tableId)),
|
||||
hx.Swap("outerHTML"),
|
||||
|
||||
MapMapWithKey(f.Search, func(s string, v string) Node {
|
||||
return Input(Type("hidden"), Name(database.SEARCH_URL_KEY_PREFIX+s), Value(v))
|
||||
}),
|
||||
|
||||
Input(Type("hidden"), Name(database.ORDER_BY_URL_KEY), Value(f.OrderBy)),
|
||||
Input(Type("hidden"), Name(database.ORDER_DESC_URL_KEY), Value(ToString(f.OrderDescending))),
|
||||
Select(
|
||||
IfElse(opts.Compact,
|
||||
InlineStyle("$me { padding: $2; }"),
|
||||
InlineStyle("$me { padding: $3; }"),
|
||||
),
|
||||
InlineStyle(`
|
||||
$me {
|
||||
background-color: $color(gray-50);
|
||||
border: 1px solid $color(gray-50);
|
||||
font-size: var(--text-sm);
|
||||
display: block;
|
||||
margin-right: $3;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
`),
|
||||
Name(database.ITEMS_PER_PAGE_URL_KEY),
|
||||
Option(If(f.Pagination.MaxItemsPerPage == 5, Selected()), Text("5"), Value("5")),
|
||||
Option(If(f.Pagination.MaxItemsPerPage == 10, Selected()), Text("10"), Value("10")),
|
||||
Option(If(f.Pagination.MaxItemsPerPage == 25, Selected()), Text("25"), Value("25")),
|
||||
Option(If(f.Pagination.MaxItemsPerPage == 50, Selected()), Text("50"), Value("50")),
|
||||
Option(If(f.Pagination.MaxItemsPerPage == 100, Selected()), Text("100"), Value("100")),
|
||||
),
|
||||
),
|
||||
|
||||
paginationButton(ICON_CHEVRON_FIRST, 1),
|
||||
paginationButton(ICON_CHEVRON_LEFT, f.Pagination.PreviousPage),
|
||||
|
||||
Div(
|
||||
InlineStyle("$me { font-size: var(--text-sm); display: flex; justify-content: center; color: $color(neutral-500); padding: $3; min-height: $9; }"),
|
||||
Text("Page "),
|
||||
ToText(f.Pagination.CurrentPage),
|
||||
Text(" of "),
|
||||
ToText(f.Pagination.TotalPages),
|
||||
),
|
||||
|
||||
paginationButton(ICON_CHEVRON_RIGHT, f.Pagination.NextPage),
|
||||
paginationButton(ICON_CHEVRON_LAST, f.Pagination.TotalPages),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
belowTable,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func AutotableSearchGroup(children ...Node) Node {
|
||||
return Div(InlineStyle("$me { width: 100%; display: flex; justify-content: space-between; margin-bottom: $3; margin-top: $1; }"),
|
||||
Div(InlineStyle("$me { width: 100%; position: relative; }"),
|
||||
Div(InlineStyle("$me { position: relative; display: flex; flex-direction: row; align-items: center; gap: $1;}"),
|
||||
Group(children),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func AutotableSearchDropdown(c ...Node) Node {
|
||||
return Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
width: 100%;
|
||||
}
|
||||
`),
|
||||
FormSelect(Group(c)),
|
||||
)
|
||||
}
|
||||
|
||||
func AutotableSearch(c ...Node) Node {
|
||||
return Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
width: 100%;
|
||||
}
|
||||
`),
|
||||
FormInput(Group(c)),
|
||||
)
|
||||
}
|
||||
|
||||
// "premade" autotable for simple tables that don't feature dynamic filtering.
|
||||
// We can still use the styling and general form of the fancy autotable defined above, but
|
||||
// for simple datasets because why not. This also gives you the option to "upgrade"
|
||||
// to the "full" table later on, since you are using the same api
|
||||
func AutoTableLite[E any](columnNames []string, entities []E, rowComponent func(E) Node, opts AutoTableOptions) Node {
|
||||
cols := []database.ColInfo{}
|
||||
|
||||
for _, v := range columnNames {
|
||||
cols = append(cols, database.ColInfo{DisplayName: v})
|
||||
}
|
||||
|
||||
return AutoTable(
|
||||
"",
|
||||
"",
|
||||
cols,
|
||||
database.Filter{},
|
||||
entities,
|
||||
nil,
|
||||
rowComponent,
|
||||
nil,
|
||||
opts,
|
||||
)
|
||||
}
|
||||
79
ui/buttons.go
Normal file
79
ui/buttons.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
// BUTTONS
|
||||
func ButtonUI(children ...Node) Node {
|
||||
return Button(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
cursor: pointer;
|
||||
background-color: $color(deep-blue);
|
||||
color: $color(neutral-50);
|
||||
padding: $1.5 $8 $1.5 $8;
|
||||
font-size: var(--text-sm);
|
||||
border-radius: var(--radius-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
$me:hover {
|
||||
background: $color(indigo-blue);
|
||||
}
|
||||
`),
|
||||
Group(children),
|
||||
)
|
||||
}
|
||||
|
||||
func ButtonUIOutline(children ...Node) Node {
|
||||
return ButtonUI(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
background: none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 0 0 0, inset 0 0 0 1px $color(gray-600);
|
||||
color: $color(gray-600);
|
||||
}
|
||||
|
||||
$me:hover {
|
||||
background: none;
|
||||
box-shadow: 0 0 0 0, inset 0 0 0 2px $color(gray-600);
|
||||
}
|
||||
`),
|
||||
Group(children),
|
||||
)
|
||||
}
|
||||
|
||||
func ButtonUIDanger(children ...Node) Node {
|
||||
return ButtonUI(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
background: $color(red-700);
|
||||
color: $color(white);
|
||||
}
|
||||
|
||||
$me:hover {
|
||||
background: $color(red-800);
|
||||
}
|
||||
`),
|
||||
Group(children),
|
||||
)
|
||||
}
|
||||
|
||||
func ButtonUISuccess(children ...Node) Node {
|
||||
return ButtonUI(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
background: $color(green-700);
|
||||
color: $color(white);
|
||||
}
|
||||
|
||||
$me:hover {
|
||||
background: $color(green-800);
|
||||
}
|
||||
`),
|
||||
Group(children),
|
||||
)
|
||||
}
|
||||
102
ui/error.go
Normal file
102
ui/error.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func ErrorPage(status int) Node {
|
||||
return RootLayout("An error has occurred.",
|
||||
Section(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
background: $color(white);
|
||||
}
|
||||
`),
|
||||
Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
padding-top: $8;
|
||||
padding-bottom: $8;
|
||||
padding-left: $4;
|
||||
padding-right: $4;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
max-width: var(--container-xl);
|
||||
}
|
||||
|
||||
@media $lg {
|
||||
$me {
|
||||
padding-top: $16;
|
||||
padding-bottom: $16;
|
||||
padding-right: $6;
|
||||
padding-left: $6;
|
||||
}
|
||||
}
|
||||
`),
|
||||
Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
max-width: var(--container-sm);
|
||||
text-align: center
|
||||
}
|
||||
`),
|
||||
H1(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
margin-bottom: $4;
|
||||
font-size: var(--text-7xl);
|
||||
color: $color(indigo-blue);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
}
|
||||
|
||||
@media $lg {
|
||||
$me {
|
||||
font-size: var(--text-9xl);
|
||||
}
|
||||
}
|
||||
`),
|
||||
ToText(status),
|
||||
),
|
||||
P(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
margin-bottom: $4;
|
||||
font-size: var(--text-lg);
|
||||
color: $color(neutral-500);
|
||||
}
|
||||
`),
|
||||
Text("We're sorry, an error has occured."),
|
||||
),
|
||||
A(
|
||||
Href("/"),
|
||||
InlineStyle(`
|
||||
$me {
|
||||
display: inline-flex;
|
||||
color: $color(white);
|
||||
background: $color(indigo-blue);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border-radius: var(--radius-xs);
|
||||
font-size: var(--text-sm);
|
||||
padding-left: $5;
|
||||
padding-right: $5;
|
||||
padding-top: $3;
|
||||
padding-bottom: $3;
|
||||
text-align: center;
|
||||
margin-top: $4;
|
||||
margin-bottom: $4;
|
||||
}
|
||||
|
||||
$me:hover {
|
||||
background: $color(neutral-950);
|
||||
}
|
||||
`),
|
||||
Text("Back to Homepage"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
76
ui/extensions.go
Normal file
76
ui/extensions.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// This file contains a few extensions to the "gomponents" library.
|
||||
package ui
|
||||
|
||||
import (
|
||||
"maxwarden/security"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
)
|
||||
|
||||
// Map a `map[T]U` to a [Group]
|
||||
func MapMap[T comparable, U comparable](m map[T]U, cb func(U) Node) Group {
|
||||
var nodes []Node
|
||||
|
||||
for k, _ := range m {
|
||||
nodes = append(nodes, cb(m[k]))
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Map a `map[T]U` to a [Group]
|
||||
// The callback provided must take the map key as an argument
|
||||
func MapMapWithKey[T comparable, U comparable](m map[T]U, cb func(T, U) Node) Group {
|
||||
var nodes []Node
|
||||
|
||||
for k, _ := range m {
|
||||
nodes = append(nodes, cb(k, m[k]))
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Map a slice of anything to a [Group] (which is just a slice of [Node]-s).
|
||||
// The callback must accept the index as an argument.
|
||||
func MapWithIndex[T any](collection []T, callback func(index int, item T) Node) Group {
|
||||
nodes := make([]Node, 0, len(collection))
|
||||
for index, item := range collection {
|
||||
nodes = append(nodes, callback(index, item))
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func IfElse(condition bool, t Node, f Node) Node {
|
||||
if condition {
|
||||
return t
|
||||
} else {
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
func IffElse(condition bool, t func() Node, f func() Node) Node {
|
||||
if condition {
|
||||
return t()
|
||||
} else {
|
||||
return f()
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitizes user input HTML
|
||||
func SafeRaw(html string) Node {
|
||||
sanitized := security.SanitizationPolicy.Sanitize(html)
|
||||
return Raw(sanitized)
|
||||
}
|
||||
|
||||
func CSSID(input string) string {
|
||||
return "#" + input
|
||||
}
|
||||
|
||||
// For some reason this isn't included in the base distribution
|
||||
func Template(children ...Node) Node {
|
||||
return El("template", children...)
|
||||
}
|
||||
|
||||
func Open() Node {
|
||||
return Attr("open")
|
||||
}
|
||||
76
ui/forms.go
Normal file
76
ui/forms.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
// FORMS
|
||||
func FormInput(children ...Node) Node {
|
||||
return Input(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
background-color: $color(white);
|
||||
padding: $2;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
color: $color(neutral-900);
|
||||
border-bottom: 1px solid $color(neutral-300);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
|
||||
@media $sm {
|
||||
$me {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
}
|
||||
`),
|
||||
Group(children),
|
||||
)
|
||||
}
|
||||
|
||||
func FormSelect(children ...Node) Node {
|
||||
return Select(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
padding: $3;
|
||||
background-color: $color(white);
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
color: $color(neutral-900);
|
||||
border-bottom: 1px solid $color(neutral-300);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
@media $sm {
|
||||
$me {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
}
|
||||
`),
|
||||
Group(children),
|
||||
)
|
||||
}
|
||||
|
||||
func FormTextarea(children ...Node) Node {
|
||||
return Textarea(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
display: block;
|
||||
padding: $3;
|
||||
width: 100%;
|
||||
font-size: var(--text-sm);
|
||||
color: $color(neutral-900);
|
||||
background-color: $color(white);
|
||||
border-bottom: 1px solid $color(neutral-300);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
`),
|
||||
Group(children),
|
||||
)
|
||||
}
|
||||
|
||||
func FormLabel(children ...Node) Node {
|
||||
return Label(InlineStyle("$me { color: $color(neutral-900); font-size: var(--text-sm); } "), Group(children))
|
||||
}
|
||||
168
ui/general.go
Normal file
168
ui/general.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
. "maxwarden/basic"
|
||||
"time"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
hx "maragu.dev/gomponents-htmx"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
// DUMMY TEXT
|
||||
const LOREM_IPSUM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam ultrices iaculis dui sed porttitor. Integer sed est fringilla, condimentum magna ac, sodales dui. Sed tempor pretium scelerisque. Vivamus pulvinar iaculis libero nec blandit. Mauris tempus velit in neque elementum, ac elementum diam feugiat. Aenean malesuada, nunc a interdum volutpat, diam est lacinia magna, nec fermentum massa lectus non urna. Cras vitae turpis finibus, porta est tincidunt, efficitur neque. Suspendisse suscipit a nulla mollis sodales. Nam vitae nulla vulputate, dictum purus eget, malesuada justo. Vestibulum ultricies eget neque ac volutpat. Mauris et molestie elit. Donec et suscipit urna. Duis in mi in ipsum faucibus finibus.`
|
||||
|
||||
// Splitters / dividers / section splits
|
||||
func Divider() Node {
|
||||
return Hr(
|
||||
InlineStyle("$me { color: $color(neutral-200); margin-bottom: $3; margin-top: $1; }"),
|
||||
)
|
||||
}
|
||||
|
||||
// By default, HTML element styles are reset by tailwind's preflight css
|
||||
// (we use their styles even though we aren't using tailwind).
|
||||
// This element reverts all child elements back to the _browser_ defaults,
|
||||
// and applies additional styles to make user-input HTML look nicer.
|
||||
//
|
||||
// Useful for markdown content, blogs, etc.
|
||||
func Prose(children ...Node) Node {
|
||||
return Div(
|
||||
InlineStyle(`
|
||||
$me * {
|
||||
all: revert;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
`),
|
||||
Group(children),
|
||||
)
|
||||
}
|
||||
|
||||
// BADGES
|
||||
func BadgeSuccess(children ...Node) Node {
|
||||
return Span(Class("w-fit inline-flex overflow-hidden rounded-sm border border-green-500 bg-white text-xs font-medium text-green-500 dark:border-green-500 dark:bg-neutral-950 dark:text-green-500"),
|
||||
Span(Class("px-2 py-1 bg-green-500/10 dark:bg-green-500/10"), Group(children)),
|
||||
)
|
||||
}
|
||||
|
||||
func BadgeWarning(children ...Node) Node {
|
||||
return Span(Class("w-fit inline-flex overflow-hidden rounded-sm border border-amber-500 bg-white text-xs font-medium text-amber-500 dark:border-amber-500 dark:bg-neutral-950 dark:text-amber-500"),
|
||||
Span(Class("px-2 py-1 bg-amber-500/10 dark:bg-amber-500/10"), Group(children)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// CONTAINERS
|
||||
func Flex(n ...Node) Node {
|
||||
return Div(
|
||||
InlineStyle("$me { display: flex; align-items: center; flex-direction: row; gap: $3; }"),
|
||||
Group(n),
|
||||
)
|
||||
}
|
||||
|
||||
func CardNoPadding(body ...Node) Node {
|
||||
return Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
background-color: $color(white);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
`),
|
||||
Group(body),
|
||||
)
|
||||
}
|
||||
func Card(body ...Node) Node {
|
||||
return Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
background-color: $color(white);
|
||||
padding: $5;
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
`),
|
||||
Group(body),
|
||||
)
|
||||
}
|
||||
|
||||
// EMAIL
|
||||
func ExampleEmailComponent(body string) Node {
|
||||
return EmailRoot(
|
||||
H1(Text("This email is automatically generated.")),
|
||||
P(Text(body)),
|
||||
)
|
||||
}
|
||||
|
||||
// FORMATTERS
|
||||
func FormatTime(utcTime time.Time) Node {
|
||||
return Text(TimeToTimeString(utcTime))
|
||||
}
|
||||
|
||||
func FormatDateTime(utcTime time.Time) Node {
|
||||
return Text(TimeToString(utcTime))
|
||||
}
|
||||
|
||||
func FormatDate(utcTime time.Time) Node {
|
||||
return Text(DateToString(utcTime))
|
||||
}
|
||||
|
||||
func ToText(i interface{}) Node {
|
||||
return Text(ToString(i))
|
||||
}
|
||||
|
||||
// TEXT
|
||||
func PageLink(location string, display Node, newPage bool) Node {
|
||||
return A(
|
||||
Href(location),
|
||||
InlineStyle("$me{text-decoration: underline; color: $color(blue-600);} $me:hover{color: $color(blue-800);}"),
|
||||
display,
|
||||
If(newPage, Target("_blank")),
|
||||
)
|
||||
}
|
||||
|
||||
// TABLES
|
||||
|
||||
// Row Item Helpers
|
||||
func TdLeft(c ...Node) Node {
|
||||
return Td(InlineStyle("$me { text-align: left; }"), Group(c))
|
||||
}
|
||||
|
||||
func TdRight(c ...Node) Node {
|
||||
return Td(InlineStyle("$me { text-align: right; }"), Group(c))
|
||||
}
|
||||
|
||||
func TdCenter(c ...Node) Node {
|
||||
return Td(InlineStyle("$me { text-align: center; }"), Group(c))
|
||||
}
|
||||
|
||||
// HTMX Helpers
|
||||
func HxLoad(url string) Node {
|
||||
return Div(hx.Get(url), hx.Trigger("load"),
|
||||
Loader(),
|
||||
)
|
||||
}
|
||||
|
||||
func Loader() Node {
|
||||
return Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 5px solid #FFF;
|
||||
border-bottom-color: $color(neutral-800);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`),
|
||||
)
|
||||
}
|
||||
28
ui/grid.go
Normal file
28
ui/grid.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func Grid2x2(children ...Node) Node {
|
||||
return Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $5;
|
||||
padding: $5
|
||||
max-width: var(--container-lg);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media $md- {
|
||||
$me {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`),
|
||||
Group(children),
|
||||
)
|
||||
}
|
||||
57
ui/icons.go
Normal file
57
ui/icons.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// make sure each icon does not have a height width attribute set! this gets appended at the component level!
|
||||
|
||||
const (
|
||||
// primary icons
|
||||
// https://lucide.dev
|
||||
ICON_ARROW_UP_DOWN = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-down"><path d="m21 16-4 4-4-4"/><path d="M17 20V4"/><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/></svg>`
|
||||
ICON_ARROW_DOWN_WIDE_NARROW = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down-wide-narrow"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="M11 4h10"/><path d="M11 8h7"/><path d="M11 12h4"/></svg>`
|
||||
ICON_ARROW_UP_WIDE_NARROW = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-wide-narrow"><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/><path d="M11 12h10"/><path d="M11 16h7"/><path d="M11 20h4"/></svg>`
|
||||
ICON_ARROW_UP = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>`
|
||||
ICON_ARROW_DOWN = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down"><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg>`
|
||||
ICON_ARROW_RIGHT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>`
|
||||
ICON_ARROW_LEFT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>`
|
||||
ICON_CHEVRON_FIRST = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-first"><path d="m17 18-6-6 6-6"/><path d="M7 6v12"/></svg>`
|
||||
ICON_CHEVRON_LAST = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-last"><path d="m7 18 6-6-6-6"/><path d="M17 6v12"/></svg>`
|
||||
ICON_CHEVRON_LEFT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-left"><path d="m15 18-6-6 6-6"/></svg>`
|
||||
ICON_CHEVRON_RIGHT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right"><path d="m9 18 6-6-6-6"/></svg>`
|
||||
ICON_CHEVRON_DOWN = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>`
|
||||
ICON_CHEVRON_UP = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up"><path d="m18 15-6-6-6 6"/></svg>`
|
||||
ICON_CHEVRONS_UP_DOWN = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-up-down"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>`
|
||||
ICON_MENU = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>`
|
||||
ICON_USERS = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>`
|
||||
ICON_GLOBE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>`
|
||||
ICON_CODE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>`
|
||||
ICON_MAIL = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>`
|
||||
ICON_HOUSE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-house"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>`
|
||||
ICON_NEWSPAPER = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-newspaper"><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/><path d="M18 14h-8"/><path d="M15 18h-5"/><path d="M10 6h8v4h-8V6Z"/></svg>`
|
||||
ICON_RSS = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rss"><path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/></svg>`
|
||||
ICON_EYE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>`
|
||||
ICON_LIST_ORDERED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-ordered"><path d="M10 12h11"/><path d="M10 18h11"/><path d="M10 6h11"/><path d="M4 10h2"/><path d="M4 6h1v4"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>`
|
||||
ICON_X_DIALOG_CLOSE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="1.4" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>`
|
||||
ICON_PENCIL = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pencil"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg>`
|
||||
|
||||
//material
|
||||
ICON_SEARCH = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" class="w-8 h-8 text-slate-600"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /></svg>`
|
||||
|
||||
// brand icons
|
||||
// https://simpleicons.org
|
||||
ICON_GO = `<svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><title>Go</title><path d="M1.811 10.231c-.047 0-.058-.023-.035-.059l.246-.315c.023-.035.081-.058.128-.058h4.172c.046 0 .058.035.035.07l-.199.303c-.023.036-.082.07-.117.07zM.047 11.306c-.047 0-.059-.023-.035-.058l.245-.316c.023-.035.082-.058.129-.058h5.328c.047 0 .07.035.058.07l-.093.28c-.012.047-.058.07-.105.07zm2.828 1.075c-.047 0-.059-.035-.035-.07l.163-.292c.023-.035.07-.07.117-.07h2.337c.047 0 .07.035.07.082l-.023.28c0 .047-.047.082-.082.082zm12.129-2.36c-.736.187-1.239.327-1.963.514-.176.046-.187.058-.34-.117-.174-.199-.303-.327-.548-.444-.737-.362-1.45-.257-2.115.175-.795.514-1.204 1.274-1.192 2.22.011.935.654 1.706 1.577 1.835.795.105 1.46-.175 1.987-.77.105-.13.198-.27.315-.434H10.47c-.245 0-.304-.152-.222-.35.152-.362.432-.97.596-1.274a.315.315 0 01.292-.187h4.253c-.023.316-.023.631-.07.947a4.983 4.983 0 01-.958 2.29c-.841 1.11-1.94 1.8-3.33 1.986-1.145.152-2.209-.07-3.143-.77-.865-.655-1.356-1.52-1.484-2.595-.152-1.274.222-2.419.993-3.424.83-1.086 1.928-1.776 3.272-2.02 1.098-.2 2.15-.07 3.096.571.62.41 1.063.97 1.356 1.648.07.105.023.164-.117.2m3.868 6.461c-1.064-.024-2.034-.328-2.852-1.029a3.665 3.665 0 01-1.262-2.255c-.21-1.32.152-2.489.947-3.529.853-1.122 1.881-1.706 3.272-1.95 1.192-.21 2.314-.095 3.33.595.923.63 1.496 1.484 1.648 2.605.198 1.578-.257 2.863-1.344 3.962-.771.783-1.718 1.273-2.805 1.495-.315.06-.63.07-.934.106zm2.78-4.72c-.011-.153-.011-.27-.034-.387-.21-1.157-1.274-1.81-2.384-1.554-1.087.245-1.788.935-2.045 2.033-.21.912.234 1.835 1.075 2.21.643.28 1.285.244 1.905-.07.923-.48 1.425-1.228 1.484-2.233z"/></svg>`
|
||||
ICON_HTMX = `<svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><title>htmx</title><path d="M0 13.01v-2l7.09-2.98.58 1.94-5.1 2.05 5.16 2.05-.63 1.9Zm16.37 1.03 5.18-2-5.16-2.09.65-1.88L24 10.95v2.12L17 16zm-2.85-9.98H16l-5.47 15.88H8.05Z"/></svg>`
|
||||
ICON_X_DOT_COM = `<svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><title>X</title><path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/></svg>`
|
||||
ICON_XAI_GROK = `<svg role="img" viewBox="0 0 33 32" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><g><path d="M12.745 20.54l10.97-8.19c.539-.4 1.307-.244 1.564.38 1.349 3.288.746 7.241-1.938 9.955-2.683 2.714-6.417 3.31-9.83 1.954l-3.728 1.745c5.347 3.697 11.84 2.782 15.898-1.324 3.219-3.255 4.216-7.692 3.284-11.693l.008.009c-1.351-5.878.332-8.227 3.782-13.031L33 0l-4.54 4.59v-.014L12.743 20.544M10.48 22.531c-3.837-3.707-3.175-9.446.1-12.755 2.42-2.449 6.388-3.448 9.852-1.979l3.72-1.737c-.67-.49-1.53-1.017-2.515-1.387-4.455-1.854-9.789-.931-13.41 2.728-3.483 3.523-4.579 8.94-2.697 13.561 1.405 3.454-.899 5.898-3.22 8.364C1.49 30.2.666 31.074 0 32l10.478-9.466"></path></g></svg>`
|
||||
ICON_GITHUB = `<svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>`
|
||||
ICON_4CH = `<svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><title>4chan</title><path d="M11.07 8.82S9.803 1.079 5.145 1.097C2.006 1.109.78 4.124 3.055 4.802c0 0-2.698.973-2.698 2.697 0 1.725 4.274 3.54 10.713 1.32zm1.931 5.924s.904 7.791 5.558 7.991c3.136.135 4.503-2.82 2.262-3.604 0 0 2.74-.845 2.82-2.567.08-1.723-4.105-3.737-10.64-1.82zm-3.672-1.55s-7.532 2.19-6.952 6.813c.39 3.114 3.53 3.969 3.93 1.63 0 0 1.29 2.559 3.002 2.351 1.712-.208 3-4.67.02-10.794zm5.623-2.467s7.727-1.35 7.66-6.008c-.046-3.138-3.074-4.333-3.728-2.051 0 0-1-2.686-2.726-2.668-1.724.018-3.494 4.312-1.206 10.727z"/></svg>`
|
||||
)
|
||||
|
||||
func Icon(icon string, size int) Node {
|
||||
size_string := strconv.Itoa(size)
|
||||
return SVG(InlineStyle("$me{display: inline;}"), Height(size_string), Width(size_string), Raw(icon))
|
||||
}
|
||||
11
ui/markdown.go
Normal file
11
ui/markdown.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/gomarkdown/markdown"
|
||||
. "maragu.dev/gomponents"
|
||||
)
|
||||
|
||||
func Markdown(input string) Node {
|
||||
html := markdown.ToHTML([]byte(input), nil, nil)
|
||||
return SafeRaw(string(html))
|
||||
}
|
||||
105
ui/modals.go
Normal file
105
ui/modals.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
// DIALOG / MODAL
|
||||
func ModalActuator(id string, contents Node) Node {
|
||||
return Span(
|
||||
InlineScriptf(`
|
||||
let act = me();
|
||||
let dialog = me("#%s_dialog");
|
||||
act.on("click", () => { dialog.showModal(); });
|
||||
`, id),
|
||||
contents,
|
||||
)
|
||||
}
|
||||
|
||||
func ModalCloser() Node {
|
||||
return Class("modal-close-el")
|
||||
}
|
||||
|
||||
func Modal(id string, header Node, body Node, closeElements []Node) Node {
|
||||
return Dialog(
|
||||
ID(id+"_dialog"),
|
||||
InlineStyle(`
|
||||
$me {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
translate: -50% -50%;
|
||||
z-index: 10;
|
||||
background: $color(white);
|
||||
box-shadow: var(--shadow-md);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
$me::backdrop {
|
||||
background: $color(black/30);
|
||||
}
|
||||
`),
|
||||
|
||||
// Header
|
||||
Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: $6 $8 $1 $8;
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
`),
|
||||
header,
|
||||
|
||||
Div(
|
||||
Class("modal-close-el"),
|
||||
InlineStyle("$me { cursor: pointer; }"),
|
||||
Icon(ICON_X_DIALOG_CLOSE, 24),
|
||||
),
|
||||
),
|
||||
|
||||
//Modal contents
|
||||
Div(
|
||||
ID(id), // we use the passed id here so that swapping the content is easier
|
||||
InlineStyle(`
|
||||
$me {
|
||||
padding: $3 $8 $8 $8;
|
||||
color: $color(neutral-700);
|
||||
}
|
||||
`),
|
||||
body,
|
||||
),
|
||||
|
||||
// footer
|
||||
If(len(closeElements) > 0,
|
||||
Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
padding: $3 $8 $3 $8;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: $2;
|
||||
}
|
||||
`),
|
||||
|
||||
Group(closeElements),
|
||||
),
|
||||
),
|
||||
|
||||
InlineScript(`
|
||||
let dialog = me();
|
||||
let close_buttons = any(".modal-close-el", me())
|
||||
|
||||
dialog.on("click", (ev) => {
|
||||
if (ev.target === dialog) {
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
close_buttons.on("click", () => { dialog.close(); });
|
||||
`),
|
||||
)
|
||||
}
|
||||
28
ui/quill.go
Normal file
28
ui/quill.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
// QuillJS editor components
|
||||
|
||||
func Quill() Node {
|
||||
return Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
width: 100%;
|
||||
background: $color(white);
|
||||
min-height: $64;
|
||||
}
|
||||
`),
|
||||
|
||||
InlineScript(`
|
||||
let editor = me();
|
||||
|
||||
let quill = new Quill(editor, {
|
||||
theme: 'snow',
|
||||
});
|
||||
`),
|
||||
)
|
||||
}
|
||||
88
ui/root.go
Normal file
88
ui/root.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"maxwarden/config"
|
||||
"maxwarden/security"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func RootLayout(title string, children ...Node) Node {
|
||||
// automatically invalidates cached css when file hash changes
|
||||
css_hash, err := security.QuickFileHash("./wwwroot/css/style.css")
|
||||
if err != nil {
|
||||
return Text("Error hashing style.css")
|
||||
}
|
||||
|
||||
metagen_css_hash, err := security.QuickFileHash("./wwwroot/css/style.metagen.css")
|
||||
if err != nil {
|
||||
return Text("Error hashing style.css")
|
||||
}
|
||||
|
||||
// automatically invalidates cached js when file hash changes
|
||||
js_hash, err := security.QuickFileHash("./wwwroot/js/index.js")
|
||||
if err != nil {
|
||||
return Text("Error hashing index.js")
|
||||
}
|
||||
|
||||
return Doctype(
|
||||
HTML(InlineStyle("$me { height: 100%; }"),
|
||||
Lang("en"),
|
||||
Head(
|
||||
Meta(Charset("utf-8")),
|
||||
Meta(Name("viewport"), Content("width=device-width, initial-scale=1")),
|
||||
TitleEl(Text(title)),
|
||||
Meta(Name("description"), Content("Previous")),
|
||||
|
||||
/////////////////////////////////
|
||||
// STYLES
|
||||
/////////////////////////////////
|
||||
Link(Rel("apple-touch-icon"), Attr("sizes", "180x180"), Href("/apple-touch-icon.png")),
|
||||
Link(Rel("icon"), Type("image/png"), Attr("sizes", "32x32"), Href("/favicon-32x32.png")),
|
||||
Link(Rel("icon"), Type("image/png"), Attr("sizes", "16x16"), Href("/favicon-16x16.png")),
|
||||
Link(Rel("manifest"), Href("/site.webmanifest")),
|
||||
|
||||
Link(Rel("stylesheet"), Href("/css/style.css?v="+css_hash)),
|
||||
Link(Rel("stylesheet"), Href("/css/style.metagen.css?v="+metagen_css_hash)),
|
||||
Link(Rel("stylesheet"), Href("/lib/highlight/default.min.css")),
|
||||
Link(Rel("stylesheet"), Href("/lib/quill/quill.snow.css")),
|
||||
|
||||
/////////////////////////////////
|
||||
// SCRIPTS
|
||||
/////////////////////////////////
|
||||
Script(Src("/js/index.js?v="+js_hash)),
|
||||
|
||||
// https://github.com/gnat/surreal
|
||||
Script(Src("/lib/surreal/surreal.js")),
|
||||
|
||||
// use minified htmx only in prod
|
||||
IfElse(config.DEBUG,
|
||||
Script(Src("/lib/htmx/htmx.js")),
|
||||
Script(Src("/lib/htmx/htmx.min.js")),
|
||||
),
|
||||
|
||||
Script(Src("/lib/chartjs/chart.js")),
|
||||
Script(Src("/lib/highlight/highlight.min.js")),
|
||||
Script(Src("/lib/quill/quill.js")),
|
||||
),
|
||||
Group(children), // expected to provide body
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func EmailRoot(children ...Node) Node {
|
||||
return Doctype(
|
||||
HTML(
|
||||
Lang("en"),
|
||||
Head(
|
||||
Meta(Charset("utf-8")),
|
||||
Meta(Name("viewport"), Content("width=device-width, initial-scale=1")),
|
||||
Meta(Name("description"), Content("Previous")),
|
||||
),
|
||||
Body(
|
||||
Group(children),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
55
ui/scripts.go
Normal file
55
ui/scripts.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "maxwarden/basic"
|
||||
"maxwarden/config"
|
||||
"strings"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
// This component generates a <script> tag that automatically includes a blank scope { }.
|
||||
// Avoid using the javascript `var` keyword inside the scope as it will be placed in the global
|
||||
// scope.
|
||||
//
|
||||
// WARNING: DO NOT PASS USER INPUT TO THIS COMPONENT!!
|
||||
// THIS COULD BE A ONE WAY TICKET TO XSS IF USED IMPROPERLY!
|
||||
func InlineScript(script string) Node {
|
||||
// minify in production only
|
||||
if !config.DEBUG {
|
||||
script = strings.ReplaceAll(script, "\n", " ")
|
||||
script = strings.ReplaceAll(script, "\t", "")
|
||||
}
|
||||
|
||||
return Script(Raw(`{` + script + `}`))
|
||||
}
|
||||
|
||||
// Wrapper function around `InlineScript` that allows for printf style format strings + data
|
||||
//
|
||||
// WARNING: DO NOT PASS USER INPUT TO THIS COMPONENT!!
|
||||
// THIS COULD BE A ONE WAY TICKET TO XSS IF USED IMPROPERLY!
|
||||
func InlineScriptf(scriptFormat string, items ...interface{}) Node {
|
||||
return InlineScript(fmt.Sprintf(scriptFormat, items...))
|
||||
}
|
||||
|
||||
// Convert Go arrays to a string containing a javascript array.
|
||||
// Useful for injecting data into a dynamically generated script.
|
||||
func MakeJsArray[T any](list []T) string {
|
||||
var out_string string
|
||||
|
||||
out_string += "["
|
||||
|
||||
for i, v := range list {
|
||||
if i == len(list)-1 {
|
||||
out_string += fmt.Sprintf("'%s'", ToString(v))
|
||||
} else {
|
||||
out_string += fmt.Sprintf("'%s',", ToString(v))
|
||||
}
|
||||
}
|
||||
|
||||
out_string += "]"
|
||||
|
||||
return out_string
|
||||
}
|
||||
47
ui/styles.go
Normal file
47
ui/styles.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"maxwarden/basic"
|
||||
"maxwarden/security"
|
||||
"strings"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
)
|
||||
|
||||
// @Macro - At runtime this inserts an attribute component with the hash of the input
|
||||
// At compile time, the input is collected, and used to generate a global css file.
|
||||
// WARNING: THE PREPROCESSOR EXPANSION ONLY WORKS WITH *STRING LITERAL* INPUTS
|
||||
// ATTEMPTING TO USE THIS WITH DYNAMIC INPUTS WILL FAIL COMPILATION
|
||||
//
|
||||
// For generating "dynamic" styles, use the If() / IfElse() component, and multiple calls
|
||||
// to `InlineStyle`.
|
||||
//
|
||||
// Ex:
|
||||
//
|
||||
// func MyComponent(cond bool) Node {
|
||||
// return IfElse(cond, InlineStyle("color: green;"), InlineStyle("color: red;"))
|
||||
// }
|
||||
//
|
||||
// THE FOLLOWING DOES NOT WORK:
|
||||
//
|
||||
// func MyInvalidComponent(cond bool) Node {
|
||||
// var css string
|
||||
//
|
||||
// if cond {
|
||||
// css = "color: green;"
|
||||
// } else {
|
||||
// css = "color: red;"
|
||||
// }
|
||||
//
|
||||
// return InlineStyle(css) <--- THIS IS AN ERROR! THE PREPROCESSOR HAS NO IDEA WHAT THE VALUE OF `css` IS,
|
||||
// SINCE IT IS NOT KNOWN AT COMPILE TIME!
|
||||
// }
|
||||
func InlineStyle(input string) Node {
|
||||
input = strings.ReplaceAll(input, "\n", " ")
|
||||
input = strings.ReplaceAll(input, "\t", "")
|
||||
|
||||
s, _ := security.HighwayHash58(input)
|
||||
s = basic.GetFirstNChars(s, 8)
|
||||
|
||||
return Attr("__inlinecss_" + s)
|
||||
}
|
||||
Reference in New Issue
Block a user