~repos /gromer

#golang#htmx#ssr

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 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
- <li id="todo-{todo.ID}" class="{ completed: todo.Completed }">
20
+ <div id="todo-{todo.ID}" class="todo">
11
- <div class="view">
21
+ <div class="row">
12
- <form hx-target="#todo-{todo.ID}" hx-swap="outerHTML">
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
- <input class="checkbox" type="checkbox" checked="{value}" />
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
- <label>{todo.Text}</label>
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="destroy"></button>
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
- </li>
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="relative" x-for="todo in todos">
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.StylesRoute(staticRouter, "/styles.css")
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
- "container": "bg-gray-50 min-h-screen font-sans",
17
+ "bg": "bg-gray-50 min-h-screen font-sans",
18
- "todos-container": "container mx-auto flex flex-col items-center",
18
+ "container": "container mx-auto flex flex-col items-center",
19
- "title": "text-opacity-20 text-red-900 text-8xl text-center",
19
+ "title": "text-opacity-20 text-red-900 text-8xl text-center",
20
+ "main": M{
20
- "main": "mt-8 shadow-xl w-full max-w-prose bg-white",
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
- <div class="container">
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
- <input class="toggle-all" id="toggle-all" type="checkbox" />
64
+ <input type="hidden" name="intent" value="create" />
58
- <label for="toggle-all">Mark all as complete</label>
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 interface{}
36
+ Func interface{}
37
- Args []string
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 GetStyles(k string) string {
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: f,
183
+ Func: f,
173
- Args: 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 += k
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
- "minh": "min-height",
241
+ "min-h": "min-height",
242
- "minw": "min-width",
242
+ "min-w": "min-width",
243
- "maxh": "max-height",
243
+ "max-h": "max-height",
244
- "maxw": "max-width",
244
+ "max-w": "max-width",
245
245
  },
246
246
  Values: MS{
247
- "auto": "auto",
248
- "min": "min-content",
249
- "max": "max-content",
250
- "0": "0px",
251
- "1": "0.25rem",
252
- "2": "0.5rem",
253
- "3": "0.75rem",
254
- "4": "1rem",
255
- "5": "1.25rem",
256
- "6": "1.5rem",
257
- "7": "1.75rem",
258
- "8": "2rem",
259
- "9": "2.25rem",
260
- "10": "2.5rem",
261
- "11": "2.75rem",
262
- "12": "3rem",
263
- "14": "3.5rem",
264
- "16": "4rem",
265
- "20": "5rem",
266
- "24": "6rem",
267
- "28": "7rem",
268
- "32": "8rem",
269
- "36": "9rem",
270
- "40": "10rem",
271
- "44": "11rem",
272
- "48": "12rem",
273
- "52": "13rem",
274
- "56": "14rem",
275
- "60": "15rem",
276
- "64": "16rem",
277
- "72": "18rem",
278
- "80": "20rem",
279
- "96": "24rem",
280
- "px": "1px",
281
- "0.5": "0.125rem",
282
- "1.5": "0.375rem",
283
- "2.5": "0.625rem",
284
- "3.5": "0.875rem",
285
- "1/2": "50%",
286
- "1/3": "33.33%",
287
- "2/3": "66.66%",
288
- "1/4": "25%",
289
- "2/4": "50%",
290
- "3/4": "75%",
291
- "1/5": "20%",
292
- "2/5": "40%",
293
- "3/5": "60%",
294
- "4/5": "80%",
295
- "1/6": "16.66%",
296
- "2/6": "33.33%",
297
- "3/6": "50%",
298
- "4/6": "66.66%",
299
- "5/6": "83.33%",
300
- "1/12": "8.33%",
301
- "2/12": "16.66%",
302
- "3/12": "25%",
303
- "4/12": "33.33%",
304
- "5/12": "41.66%",
305
- "6/12": "50%",
306
- "7/12": "58.33%",
307
- "8/12": "66.66%",
308
- "9/12": "75%",
309
- "10/12": "83.33%",
310
- "11/12": "91.66%",
311
- "full": "100%",
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
- for k, v := range classMap {
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 %d %s %s", r.Method, 599, useragent.Parse(r.UserAgent()).Name, r.URL.Path)
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
- r.URL.Path,
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 StylesRoute(router *mux.Router, path string) {
262
+ func PageStylesRoute(router *mux.Router, route string) {
258
- router.Path(path).Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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.GetStyles(key)))
272
+ w.Write([]byte(gsx.GetPageStyles(key)))
268
273
  })
269
274
  }
270
275
 
271
- func Handle(router *mux.Router, method, route string, h interface{}, meta, styles gsx.M) {
276
+ func ComponentStylesRoute(router *mux.Router, route string) {
277
+ router.Path(route).Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
272
- key := getSum(route, func() [16]byte {
278
+ w.Header().Set("Content-Type", "text/css")
279
+ w.WriteHeader(200)
273
- return md5.Sum([]byte(route))
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", GetStylesUrl(key), "", "")
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, "OPTIONS")
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 GetStylesUrl(k string) string {
331
+ func GetPageStylesUrl(k string) string {
320
332
  sum := getSum("styles.css", func() [16]byte {
321
- return md5.Sum([]byte(gsx.GetStyles(k)))
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
+ }