~repos /gromer
git clone https://pyrossh.dev/repos/gromer.git
gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.
24e0ccde
—
Peter John 3 years ago
implement example
- _example/assets/icons/checked.svg +5 -0
- _example/assets/icons/close.svg +1 -0
- _example/assets/icons/list.svg +1 -0
- _example/assets/icons/unchecked.svg +4 -0
- _example/components/todo.go +23 -7
- _example/containers/TodoList.go +5 -1
- _example/main.go +6 -5
- _example/routes/get.go +21 -12
- gsx/gsx.go +27 -11
- gsx/twx.go +156 -98
- http.go +37 -18
_example/assets/icons/checked.svg
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" fill="rgba(16,185,129,1)">
|
|
2
|
+
<title>Checkmark Circle</title>
|
|
3
|
+
<path
|
|
4
|
+
d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm108.25 138.29l-134.4 160a16 16 0 01-12 5.71h-.27a16 16 0 01-11.89-5.3l-57.6-64a16 16 0 1123.78-21.4l45.29 50.32 122.59-145.91a16 16 0 0124.5 20.58z" />
|
|
5
|
+
</svg>
|
_example/assets/icons/close.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Close</title><path d="M289.94 256l95-95A24 24 0 00351 127l-95 95-95-95a24 24 0 00-34 34l95 95-95 95a24 24 0 1034 34l95-95 95 95a24 24 0 0034-34z"/></svg>
|
_example/assets/icons/list.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>List</title><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M160 144h288M160 256h288M160 368h288"/><circle cx="80" cy="144" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><circle cx="80" cy="256" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><circle cx="80" cy="368" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>
|
_example/assets/icons/unchecked.svg
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" fill="rgba(156,163,175,1)">
|
|
2
|
+
<title>Ellipse</title>
|
|
3
|
+
<path d="M256 464c-114.69 0-208-93.31-208-208S141.31 48 256 48s208 93.31 208 208-93.31 208-208 208z" />
|
|
4
|
+
</svg>
|
_example/components/todo.go
CHANGED
|
@@ -5,22 +5,38 @@ import (
|
|
|
5
5
|
. "github.com/pyros2097/gromer/gsx"
|
|
6
6
|
)
|
|
7
7
|
|
|
8
|
+
var TodoStyles = M{
|
|
9
|
+
"container": "border-t-2 border-gray-100 text-2xl",
|
|
10
|
+
"row": "flex flex-row group",
|
|
11
|
+
"button-1": "ml-4 text-gray-400",
|
|
12
|
+
"label": "flex-1 min-w-0 flex items-center break-all ml-2 p-2 text-gray-800",
|
|
13
|
+
"striked": "text-gray-500 line-through",
|
|
14
|
+
"button-2": "mr-4 text-red-700 c-0 group-hover:opacity-100",
|
|
15
|
+
"unchecked": "text-gray-200",
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
func Todo(c *Context, todo *todos.Todo) *Node {
|
|
9
19
|
return c.Render(`
|
|
10
|
-
<
|
|
20
|
+
<div id="todo-{todo.ID}" class="todo">
|
|
11
|
-
<div class="
|
|
21
|
+
<div class="row">
|
|
12
|
-
<form
|
|
22
|
+
<form hx-post="/" hx-target="#todo-{todo.ID}" hx-swap="outerHTML">
|
|
13
23
|
<input type="hidden" name="intent" value="complete" />
|
|
14
24
|
<input type="hidden" name="id" value="{todo.ID}" />
|
|
25
|
+
<button class="button-1">
|
|
15
|
-
|
|
26
|
+
<img src="{ /assets/icons/unchecked.svg: !todo.Completed, /assets/icons/checked.svg: todo.Completed }" width="24" height="24" />
|
|
27
|
+
</button>
|
|
16
28
|
</form>
|
|
29
|
+
<label class="{ label: true, striked: todo.Completed }">
|
|
30
|
+
{todo.Text}
|
|
17
|
-
<
|
|
31
|
+
</label>
|
|
18
32
|
<form hx-post="/" hx-target="#todo-{todo.ID}" hx-swap="delete">
|
|
19
33
|
<input type="hidden" name="intent" value="delete" />
|
|
20
34
|
<input type="hidden" name="id" value="{todo.ID}" />
|
|
21
|
-
<button class="
|
|
35
|
+
<button class="button-2">
|
|
36
|
+
<img src="/assets/icons/close.svg" width="24" height="24" />
|
|
37
|
+
</button>
|
|
22
38
|
</form>
|
|
23
39
|
</div>
|
|
24
|
-
</
|
|
40
|
+
</div>
|
|
25
41
|
`)
|
|
26
42
|
}
|
_example/containers/TodoList.go
CHANGED
|
@@ -6,6 +6,10 @@ import (
|
|
|
6
6
|
. "github.com/pyros2097/gromer/gsx"
|
|
7
7
|
)
|
|
8
8
|
|
|
9
|
+
var TodoListStyles = M{
|
|
10
|
+
"container": "list-none",
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
func TodoList(c *Context, page int, filter string) *Node {
|
|
10
14
|
index := Default(page, 1)
|
|
11
15
|
todos, err := todos.GetAllTodo(c, todos.GetAllTodoParams{
|
|
@@ -17,7 +21,7 @@ func TodoList(c *Context, page int, filter string) *Node {
|
|
|
17
21
|
}
|
|
18
22
|
c.Set("todos", todos)
|
|
19
23
|
return c.Render(`
|
|
20
|
-
<ul id="todo-list" class="
|
|
24
|
+
<ul id="todo-list" class="todolist" x-for="todo in todos">
|
|
21
25
|
<Todo />
|
|
22
26
|
</ul>
|
|
23
27
|
`)
|
_example/main.go
CHANGED
|
@@ -19,10 +19,10 @@ import (
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
func init() {
|
|
22
|
-
gsx.RegisterComponent(components.Todo, "todo")
|
|
22
|
+
gsx.RegisterComponent(components.Todo, components.TodoStyles, "todo")
|
|
23
|
-
gsx.RegisterComponent(components.Checkbox, "value")
|
|
23
|
+
gsx.RegisterComponent(components.Checkbox, nil, "value")
|
|
24
|
-
gsx.RegisterComponent(containers.TodoCount, "filter")
|
|
24
|
+
gsx.RegisterComponent(containers.TodoCount, nil, "filter")
|
|
25
|
-
gsx.RegisterComponent(containers.TodoList, "page", "filter")
|
|
25
|
+
gsx.RegisterComponent(containers.TodoList, containers.TodoListStyles, "page", "filter")
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
func main() {
|
|
@@ -35,7 +35,8 @@ func main() {
|
|
|
35
35
|
staticRouter.Use(gromer.CompressMiddleware)
|
|
36
36
|
gromer.StaticRoute(staticRouter, "/gromer/", gromer_assets.FS)
|
|
37
37
|
gromer.StaticRoute(staticRouter, "/assets/", assets.FS)
|
|
38
|
-
gromer.
|
|
38
|
+
gromer.PageStylesRoute(staticRouter, "/styles.css")
|
|
39
|
+
gromer.ComponentStylesRoute(staticRouter, "/components.css")
|
|
39
40
|
|
|
40
41
|
pageRouter := baseRouter.NewRoute().Subrouter()
|
|
41
42
|
gromer.Handle(pageRouter, "GET", "/", routes.GET, routes.Meta, routes.Styles)
|
_example/routes/get.go
CHANGED
|
@@ -14,10 +14,16 @@ var (
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
Styles = M{
|
|
17
|
-
"
|
|
17
|
+
"bg": "bg-gray-50 min-h-screen font-sans",
|
|
18
|
-
"
|
|
18
|
+
"container": "container mx-auto flex flex-col items-center",
|
|
19
|
-
"title":
|
|
19
|
+
"title": "text-opacity-20 text-red-900 text-8xl text-center",
|
|
20
|
+
"main": M{
|
|
20
|
-
|
|
21
|
+
"container": "mt-8 shadow-xl w-full max-w-prose bg-white",
|
|
22
|
+
"input-box": "flex flex-row text-2xl h-16",
|
|
23
|
+
"button": "ml-4 w-8 disabled",
|
|
24
|
+
"input-form": "flex flex-1",
|
|
25
|
+
"input": "flex-1 min-w-0 p-2 placeholder:text-gray-300",
|
|
26
|
+
},
|
|
21
27
|
"bottom": M{
|
|
22
28
|
"container": "flex flex-row items-center flex-wrap sm:flex-nowrap p-2 font-light border-t-2 border-gray-100",
|
|
23
29
|
"row": "flex-1 flex flex-row",
|
|
@@ -44,18 +50,21 @@ type GetParams struct {
|
|
|
44
50
|
|
|
45
51
|
func GET(c *Context, params GetParams) (*Node, int, error) {
|
|
46
52
|
return c.Render(`
|
|
53
|
+
<div class="bg">
|
|
47
|
-
|
|
54
|
+
<div class="container">
|
|
48
|
-
<div class="todos-container">
|
|
49
55
|
<header>
|
|
50
56
|
<h1 class="title">todos</h1>
|
|
51
|
-
<form hx-post="/" hx-target="#todo-list" hx-swap="afterbegin" _="on htmx:afterOnLoad set #text.value to ''">
|
|
52
|
-
<input type="hidden" name="intent" value="create" />
|
|
53
|
-
<input class="new-todo" id="text" name="text" placeholder="What needs to be done?" autofocus="false" autocomplete="off" />
|
|
54
|
-
</form>
|
|
55
57
|
</header>
|
|
56
58
|
<main class="main">
|
|
59
|
+
<div class="input-box">
|
|
60
|
+
<button class="button">
|
|
61
|
+
<img src="/assets/icons/list.svg" width="24" height="24" />
|
|
62
|
+
</button>
|
|
63
|
+
<form class="input-form" hx-post="/" hx-target="#todo-list" hx-swap="afterbegin" _="on htmx:afterOnLoad set #text.value to ''">
|
|
57
|
-
|
|
64
|
+
<input type="hidden" name="intent" value="create" />
|
|
58
|
-
|
|
65
|
+
<input id="text" name="text" class="input" placeholder="What needs to be done?" autofocus="false" autocomplete="off">
|
|
66
|
+
</form>
|
|
67
|
+
</div>
|
|
59
68
|
<TodoList id="todo-list" page="{params.Page}" filter="{params.Filter}"></TodoList>
|
|
60
69
|
<div class="bottom">
|
|
61
70
|
<div class="section-1">
|
gsx/gsx.go
CHANGED
|
@@ -33,8 +33,9 @@ type (
|
|
|
33
33
|
MS map[string]string
|
|
34
34
|
Arr []interface{}
|
|
35
35
|
ComponentFunc struct {
|
|
36
|
-
Func
|
|
36
|
+
Func interface{}
|
|
37
|
-
Args
|
|
37
|
+
Args []string
|
|
38
|
+
Classes M
|
|
38
39
|
}
|
|
39
40
|
link struct {
|
|
40
41
|
Rel string
|
|
@@ -161,16 +162,27 @@ func SetClasses(k string, m M) {
|
|
|
161
162
|
classesMap[k] = m
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
func
|
|
165
|
+
func GetPageStyles(k string) string {
|
|
165
166
|
return normalizeCss + "\n" + computeCss(classesMap[k], k)
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
func GetComponentStyles() string {
|
|
170
|
+
css := ""
|
|
171
|
+
for k, v := range compMap {
|
|
172
|
+
if v.Classes != nil {
|
|
173
|
+
css += computeCss(v.Classes, k)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return css
|
|
177
|
+
}
|
|
178
|
+
|
|
168
|
-
func RegisterComponent(f interface{}, args ...string) {
|
|
179
|
+
func RegisterComponent(f interface{}, classes M, args ...string) {
|
|
169
180
|
name := strings.ToLower(getFunctionName(f))
|
|
170
181
|
assertName("component", name)
|
|
171
182
|
compMap[name] = ComponentFunc{
|
|
172
|
-
Func:
|
|
183
|
+
Func: f,
|
|
173
|
-
Args:
|
|
184
|
+
Args: args,
|
|
185
|
+
Classes: classes,
|
|
174
186
|
}
|
|
175
187
|
}
|
|
176
188
|
|
|
@@ -212,7 +224,11 @@ func convert(ref string, i interface{}) interface{} {
|
|
|
212
224
|
}
|
|
213
225
|
|
|
214
226
|
func getRefValue(c *Context, ref string) interface{} {
|
|
227
|
+
if ref == "true" {
|
|
228
|
+
return true
|
|
229
|
+
} else if ref == "false" {
|
|
230
|
+
return false
|
|
215
|
-
if f, ok := funcMap[ref]; ok {
|
|
231
|
+
} else if f, ok := funcMap[ref]; ok {
|
|
216
232
|
return f.(func() string)()
|
|
217
233
|
} else {
|
|
218
234
|
parts := strings.Split(strings.ReplaceAll(ref, "!", ""), ".")
|
|
@@ -343,8 +359,8 @@ func populate(c *Context, n *html.Node) {
|
|
|
343
359
|
}
|
|
344
360
|
}
|
|
345
361
|
} else if at.Val != "" && strings.Contains(at.Val, "{") {
|
|
346
|
-
if at.Key == "class" {
|
|
362
|
+
if at.Key == "class" || at.Key == "src" {
|
|
347
|
-
classes :=
|
|
363
|
+
classes := []string{}
|
|
348
364
|
kvstrings := strings.Split(strings.TrimSpace(removeBrackets(at.Val)), ",")
|
|
349
365
|
for _, kv := range kvstrings {
|
|
350
366
|
kvarray := strings.Split(kv, ":")
|
|
@@ -352,13 +368,13 @@ func populate(c *Context, n *html.Node) {
|
|
|
352
368
|
v := strings.TrimSpace(kvarray[1])
|
|
353
369
|
varValue := getRefValue(c, v)
|
|
354
370
|
if varValue.(bool) {
|
|
355
|
-
classes
|
|
371
|
+
classes = append(classes, k)
|
|
356
372
|
}
|
|
357
373
|
}
|
|
358
374
|
n.Attr[i] = html.Attribute{
|
|
359
375
|
Namespace: at.Namespace,
|
|
360
376
|
Key: at.Key,
|
|
361
|
-
Val: classes,
|
|
377
|
+
Val: strings.Join(classes, " "),
|
|
362
378
|
}
|
|
363
379
|
} else {
|
|
364
380
|
n.Attr[i] = html.Attribute{
|
gsx/twx.go
CHANGED
|
@@ -238,77 +238,96 @@ var sizes = KeyValues{
|
|
|
238
238
|
"left": "left",
|
|
239
239
|
"bottom": "bottom",
|
|
240
240
|
"right": "right",
|
|
241
|
-
"
|
|
241
|
+
"min-h": "min-height",
|
|
242
|
-
"
|
|
242
|
+
"min-w": "min-width",
|
|
243
|
-
"
|
|
243
|
+
"max-h": "max-height",
|
|
244
|
-
"
|
|
244
|
+
"max-w": "max-width",
|
|
245
245
|
},
|
|
246
246
|
Values: MS{
|
|
247
|
-
"auto":
|
|
248
|
-
"min":
|
|
249
|
-
"max":
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
"
|
|
263
|
-
"
|
|
264
|
-
"
|
|
265
|
-
"
|
|
266
|
-
"
|
|
267
|
-
"
|
|
268
|
-
"
|
|
269
|
-
"
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
"
|
|
286
|
-
"1/
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
"
|
|
291
|
-
"
|
|
292
|
-
"
|
|
293
|
-
"
|
|
294
|
-
"
|
|
295
|
-
"
|
|
296
|
-
"
|
|
297
|
-
"
|
|
298
|
-
"
|
|
299
|
-
"
|
|
300
|
-
"
|
|
301
|
-
"
|
|
302
|
-
"
|
|
303
|
-
"
|
|
304
|
-
"
|
|
305
|
-
"
|
|
306
|
-
"
|
|
307
|
-
"
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
"
|
|
311
|
-
"
|
|
247
|
+
"auto": "auto",
|
|
248
|
+
"min": "min-content",
|
|
249
|
+
"max": "max-content",
|
|
250
|
+
"fit": "fit-content",
|
|
251
|
+
"0": "0px",
|
|
252
|
+
"0.5": "0.125rem",
|
|
253
|
+
"1": "0.25rem",
|
|
254
|
+
"1.5": "0.375rem",
|
|
255
|
+
"2": "0.5rem",
|
|
256
|
+
"2.5": "0.625rem",
|
|
257
|
+
"3": "0.75rem",
|
|
258
|
+
"3.5": "0.875rem",
|
|
259
|
+
"4": "1rem",
|
|
260
|
+
"5": "1.25rem",
|
|
261
|
+
"6": "1.5rem",
|
|
262
|
+
"7": "1.75rem",
|
|
263
|
+
"8": "2rem",
|
|
264
|
+
"9": "2.25rem",
|
|
265
|
+
"10": "2.5rem",
|
|
266
|
+
"11": "2.75rem",
|
|
267
|
+
"12": "3rem",
|
|
268
|
+
"14": "3.5rem",
|
|
269
|
+
"16": "4rem",
|
|
270
|
+
"20": "5rem",
|
|
271
|
+
"24": "6rem",
|
|
272
|
+
"28": "7rem",
|
|
273
|
+
"32": "8rem",
|
|
274
|
+
"36": "9rem",
|
|
275
|
+
"40": "10rem",
|
|
276
|
+
"44": "11rem",
|
|
277
|
+
"48": "12rem",
|
|
278
|
+
"52": "13rem",
|
|
279
|
+
"56": "14rem",
|
|
280
|
+
"60": "15rem",
|
|
281
|
+
"64": "16rem",
|
|
282
|
+
"72": "18rem",
|
|
283
|
+
"80": "20rem",
|
|
284
|
+
"96": "24rem",
|
|
285
|
+
"px": "1px",
|
|
286
|
+
"1/2": "50%",
|
|
287
|
+
"1/3": "33.33%",
|
|
288
|
+
"2/3": "66.66%",
|
|
289
|
+
"1/4": "25%",
|
|
290
|
+
"2/4": "50%",
|
|
291
|
+
"3/4": "75%",
|
|
292
|
+
"1/5": "20%",
|
|
293
|
+
"2/5": "40%",
|
|
294
|
+
"3/5": "60%",
|
|
295
|
+
"4/5": "80%",
|
|
296
|
+
"1/6": "16.66%",
|
|
297
|
+
"2/6": "33.33%",
|
|
298
|
+
"3/6": "50%",
|
|
299
|
+
"4/6": "66.66%",
|
|
300
|
+
"5/6": "83.33%",
|
|
301
|
+
"1/12": "8.33%",
|
|
302
|
+
"2/12": "16.66%",
|
|
303
|
+
"3/12": "25%",
|
|
304
|
+
"4/12": "33.33%",
|
|
305
|
+
"5/12": "41.66%",
|
|
306
|
+
"6/12": "50%",
|
|
307
|
+
"7/12": "58.33%",
|
|
308
|
+
"8/12": "66.66%",
|
|
309
|
+
"9/12": "75%",
|
|
310
|
+
"10/12": "83.33%",
|
|
311
|
+
"11/12": "91.66%",
|
|
312
|
+
"full": "100%",
|
|
313
|
+
"none": "none",
|
|
314
|
+
"xs": "20rem",
|
|
315
|
+
"sm": "24rem",
|
|
316
|
+
"md": "28rem",
|
|
317
|
+
"lg": "32rem",
|
|
318
|
+
"xl": "36rem",
|
|
319
|
+
"2xl": "42rem",
|
|
320
|
+
"3xl": "48rem",
|
|
321
|
+
"4xl": "56rem",
|
|
322
|
+
"5xl": "64rem",
|
|
323
|
+
"6xl": "72rem",
|
|
324
|
+
"7xl": "80rem",
|
|
325
|
+
"prose": "65ch",
|
|
326
|
+
"screen-sm": "640px",
|
|
327
|
+
"screen-md": "768px",
|
|
328
|
+
"screen-lg": "1024px",
|
|
329
|
+
"screen-xl": "1280px",
|
|
330
|
+
"screen-2xl": "1536px",
|
|
312
331
|
},
|
|
313
332
|
}
|
|
314
333
|
|
|
@@ -443,6 +462,27 @@ var twClassLookup = MS{
|
|
|
443
462
|
"ring-4": "box-shadow: 0 0 0 calc(4px + 0px) rgba(59, 130, 246, 0.5);",
|
|
444
463
|
"ring-8": "box-shadow: 0 0 0 calc(8px + 0px) rgba(59, 130, 246, 0.5);",
|
|
445
464
|
"ring": "box-shadow: 0 0 0 calc(3px + 0px) rgba(59, 130, 246, 0.5);",
|
|
465
|
+
"invisible": "visibility: hidden;",
|
|
466
|
+
"opacity-0": "opacity: 0;",
|
|
467
|
+
"opacity-5": "opacity: 0.05;",
|
|
468
|
+
"opacity-10": "opacity: 0.1;",
|
|
469
|
+
"opacity-20": "opacity: 0.2;",
|
|
470
|
+
"opacity-25": "opacity: 0.25;",
|
|
471
|
+
"opacity-30": "opacity: 0.3;",
|
|
472
|
+
"opacity-40": "opacity: 0.4;",
|
|
473
|
+
"opacity-50": "opacity: 0.5;",
|
|
474
|
+
"opacity-60": "opacity: 0.6;",
|
|
475
|
+
"opacity-70": "opacity: 0.7;",
|
|
476
|
+
"opacity-75": "opacity: 0.75;",
|
|
477
|
+
"opacity-80": "opacity: 0.8;",
|
|
478
|
+
"opacity-90": "opacity: 0.9;",
|
|
479
|
+
"opacity-95": "opacity: 0.95;",
|
|
480
|
+
"opacity-100": "opacity: 1;",
|
|
481
|
+
"list-none": "list-style-type: none;",
|
|
482
|
+
"list-disc": "list-style-type: disc;",
|
|
483
|
+
"list-decimal": "list-style-type: decimal;",
|
|
484
|
+
"min-h-screen": "height: 100vh;",
|
|
485
|
+
"min-w-screen": "width: 100vw;",
|
|
446
486
|
}
|
|
447
487
|
|
|
448
488
|
func init() {
|
|
@@ -474,6 +514,50 @@ func mapApply(obj KeyValues) {
|
|
|
474
514
|
}
|
|
475
515
|
}
|
|
476
516
|
|
|
517
|
+
func getClassName(parent, k string) string {
|
|
518
|
+
if parent != "" {
|
|
519
|
+
if k == "container" {
|
|
520
|
+
return "." + parent
|
|
521
|
+
} else {
|
|
522
|
+
return "." + parent + " ." + k
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return "." + k
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
func computeCss(classMap M, parent string) string {
|
|
529
|
+
p := "\n"
|
|
530
|
+
for k, v := range classMap {
|
|
531
|
+
switch it := v.(type) {
|
|
532
|
+
case string:
|
|
533
|
+
p += getClassName(parent, k)
|
|
534
|
+
p += " {\n"
|
|
535
|
+
classes := strings.Split(it, " ")
|
|
536
|
+
for _, c := range classes {
|
|
537
|
+
if s, ok := twClassLookup[c]; ok {
|
|
538
|
+
p += " " + s + "\n"
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
p += "}\n"
|
|
542
|
+
for _, c := range classes {
|
|
543
|
+
if strings.Contains(c, ":") {
|
|
544
|
+
arr := strings.Split(c, ":")
|
|
545
|
+
if arr[0] == "placeholder" {
|
|
546
|
+
p += "\n" + getClassName(parent, k) + "::placeholder" + " {\n"
|
|
547
|
+
if s, ok := twClassLookup[arr[1]]; ok {
|
|
548
|
+
p += " " + s + "\n"
|
|
549
|
+
}
|
|
550
|
+
p += "}\n"
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
case M:
|
|
555
|
+
p += computeCss(it, k)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return p
|
|
559
|
+
}
|
|
560
|
+
|
|
477
561
|
var normalizeCss = `*, ::before, ::after { box-sizing: border-box; }
|
|
478
562
|
html { -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; line-height: 1.15; -webkit-text-size-adjust: 100%; }
|
|
479
563
|
body { margin: 0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; }
|
|
@@ -521,32 +605,6 @@ pre, code, kbd, samp { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco,
|
|
|
521
605
|
img, svg, video, canvas, audio, iframe, embed, object { display: block; vertical-align: middle; }
|
|
522
606
|
img, video { max-width: 100%; height: auto; }
|
|
523
607
|
[hidden] { display: none; }
|
|
524
|
-
*, ::before, ::after { --tw-border-opacity: 1; border-color: rgba(229, 231, 235, var(--tw-border-opacity)); }
|
|
608
|
+
*, ::before, ::after { --tw-border-opacity: 1; border-color: rgba(229, 231, 235, var(--tw-border-opacity)); }
|
|
525
|
-
|
|
526
|
-
func computeCss(classMap M, parent string) string {
|
|
527
|
-
p := "\n"
|
|
528
|
-
|
|
609
|
+
form { display: flex; }
|
|
529
|
-
switch it := v.(type) {
|
|
530
|
-
case string:
|
|
531
|
-
if parent != "" {
|
|
532
|
-
if k == "container" {
|
|
533
|
-
p += "." + parent
|
|
534
|
-
} else {
|
|
535
|
-
p += "." + parent + " ." + k
|
|
536
|
-
|
|
610
|
+
`
|
|
537
|
-
} else {
|
|
538
|
-
p += "." + k
|
|
539
|
-
}
|
|
540
|
-
p += " {\n"
|
|
541
|
-
for _, c := range strings.Split(it, " ") {
|
|
542
|
-
if s, ok := twClassLookup[c]; ok {
|
|
543
|
-
p += " " + s + "\n"
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
p += "}\n"
|
|
547
|
-
case M:
|
|
548
|
-
p += computeCss(it, k)
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
return p
|
|
552
|
-
}
|
http.go
CHANGED
|
@@ -6,7 +6,6 @@ import (
|
|
|
6
6
|
"embed"
|
|
7
7
|
"encoding/json"
|
|
8
8
|
"fmt"
|
|
9
|
-
"net"
|
|
10
9
|
"net/http"
|
|
11
10
|
"net/url"
|
|
12
11
|
"os"
|
|
@@ -29,6 +28,7 @@ import (
|
|
|
29
28
|
"github.com/rs/zerolog"
|
|
30
29
|
"github.com/rs/zerolog/log"
|
|
31
30
|
"github.com/rs/zerolog/pkgerrors"
|
|
31
|
+
"github.com/segmentio/go-camelcase"
|
|
32
32
|
"xojoc.pw/useragent"
|
|
33
33
|
)
|
|
34
34
|
|
|
@@ -201,20 +201,25 @@ func PerformRequest(route string, h interface{}, c *gsx.Context, w http.Response
|
|
|
201
201
|
|
|
202
202
|
func LogMiddleware(next http.Handler) http.Handler {
|
|
203
203
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
204
|
+
url := r.URL.Path
|
|
205
|
+
if r.URL.RawQuery != "" {
|
|
206
|
+
url += "?" + r.URL.RawQuery
|
|
207
|
+
}
|
|
208
|
+
// ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
|
209
|
+
// if len(ip) > 0 && ip[0] == '[' {
|
|
210
|
+
// ip = ip[1 : len(ip)-1]
|
|
211
|
+
// }
|
|
212
|
+
ua := useragent.Parse(r.UserAgent()).Name
|
|
204
213
|
defer func() {
|
|
205
214
|
if err := recover(); err != nil {
|
|
206
|
-
log.Error().Msgf("%s
|
|
215
|
+
log.Error().Msgf("%s 599 %s %s", r.Method, ua, url)
|
|
207
216
|
RespondError(w, 599, errors.Errorf(fmt.Sprintf("%+v", err)))
|
|
208
217
|
}
|
|
209
218
|
}()
|
|
210
219
|
m := httpsnoop.CaptureMetrics(next, w, r)
|
|
211
|
-
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
|
212
|
-
if len(ip) > 0 && ip[0] == '[' {
|
|
213
|
-
ip = ip[1 : len(ip)-1]
|
|
214
|
-
}
|
|
215
220
|
log.Info().Msgf("%s %d %.2fkb %s %s %s", r.Method, m.Code, float64(m.Written)/1024.0, m.Duration.Round(time.Millisecond).String(),
|
|
216
|
-
useragent.Parse(r.UserAgent()).Name,
|
|
217
|
-
|
|
221
|
+
ua,
|
|
222
|
+
url,
|
|
218
223
|
)
|
|
219
224
|
})
|
|
220
225
|
}
|
|
@@ -254,8 +259,8 @@ func StaticRoute(router *mux.Router, path string, fs embed.FS) {
|
|
|
254
259
|
router.PathPrefix(path).Methods("GET").Handler(http.StripPrefix(path, http.FileServer(http.FS(fs))))
|
|
255
260
|
}
|
|
256
261
|
|
|
257
|
-
func
|
|
262
|
+
func PageStylesRoute(router *mux.Router, route string) {
|
|
258
|
-
router.Path(
|
|
263
|
+
router.Path(route).Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
259
264
|
err := r.ParseForm()
|
|
260
265
|
if err != nil {
|
|
261
266
|
RespondError(w, 400, err)
|
|
@@ -264,26 +269,33 @@ func StylesRoute(router *mux.Router, path string) {
|
|
|
264
269
|
key := r.Form.Get("key")
|
|
265
270
|
w.Header().Set("Content-Type", "text/css")
|
|
266
271
|
w.WriteHeader(200)
|
|
267
|
-
w.Write([]byte(gsx.
|
|
272
|
+
w.Write([]byte(gsx.GetPageStyles(key)))
|
|
268
273
|
})
|
|
269
274
|
}
|
|
270
275
|
|
|
271
|
-
func
|
|
276
|
+
func ComponentStylesRoute(router *mux.Router, route string) {
|
|
277
|
+
router.Path(route).Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
272
|
-
|
|
278
|
+
w.Header().Set("Content-Type", "text/css")
|
|
279
|
+
w.WriteHeader(200)
|
|
273
|
-
|
|
280
|
+
w.Write([]byte(gsx.GetComponentStyles()))
|
|
274
281
|
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
func Handle(router *mux.Router, method, route string, h interface{}, meta, styles gsx.M) {
|
|
285
|
+
key := camelcase.Camelcase(route)
|
|
275
286
|
gsx.SetClasses(key, styles)
|
|
276
287
|
router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
|
|
277
288
|
newCtx := context.WithValue(context.WithValue(r.Context(), "url", r.URL), "header", r.Header)
|
|
278
289
|
c := gsx.NewContext(newCtx, r.Header.Get("HX-Request") == "true")
|
|
279
290
|
c.Set("requestId", uuid.NewString())
|
|
280
|
-
c.Link("stylesheet",
|
|
291
|
+
c.Link("stylesheet", GetPageStylesUrl(key), "", "")
|
|
292
|
+
c.Link("stylesheet", GetComponentsStylesUrl(), "", "")
|
|
281
293
|
c.Link("icon", "/assets/favicon.ico", "image/x-icon", "image")
|
|
282
294
|
c.Script("/gromer/js/htmx@1.7.0.js", false)
|
|
283
295
|
c.Script("/gromer/js/alpinejs@3.9.6.js", true)
|
|
284
296
|
c.Meta(meta)
|
|
285
297
|
PerformRequest(route, h, c, w, r)
|
|
286
|
-
}).Methods(method
|
|
298
|
+
}).Methods(method)
|
|
287
299
|
}
|
|
288
300
|
|
|
289
301
|
func GetUrl(ctx context.Context) *url.URL {
|
|
@@ -316,9 +328,16 @@ func GetAssetUrl(fs embed.FS, path string) string {
|
|
|
316
328
|
return fmt.Sprintf("/assets/%s?hash=%s", path, sum)
|
|
317
329
|
}
|
|
318
330
|
|
|
319
|
-
func
|
|
331
|
+
func GetPageStylesUrl(k string) string {
|
|
320
332
|
sum := getSum("styles.css", func() [16]byte {
|
|
321
|
-
return md5.Sum([]byte(gsx.
|
|
333
|
+
return md5.Sum([]byte(gsx.GetPageStyles(k)))
|
|
322
334
|
})
|
|
323
335
|
return fmt.Sprintf("/styles.css?key=%s&hash=%s", k, sum)
|
|
324
336
|
}
|
|
337
|
+
|
|
338
|
+
func GetComponentsStylesUrl() string {
|
|
339
|
+
sum := getSum("components.css", func() [16]byte {
|
|
340
|
+
return md5.Sum([]byte(gsx.GetComponentStyles()))
|
|
341
|
+
})
|
|
342
|
+
return fmt.Sprintf("/components.css?hash=%s", sum)
|
|
343
|
+
}
|