Quellcode durchsuchen

SVI Добавление компонентов WUI; 100.0%

SVI vor 1 Jahr
Ursprung
Commit
33470ef94b
53 geänderte Dateien mit 1689 neuen und 187 gelöschten Zeilen
  1. 3 0
      cmd/demo/main.go
  2. 7 0
      kern.go
  3. 5 0
      kern_test.go
  4. 0 2
      mds/mod_kctx/mod_kctx.go
  5. 0 2
      mds/mod_keeper/mod_keeper.go
  6. 5 3
      mds/mod_serv_http/btn_modules/block_modules.html
  7. 0 0
      mds/mod_serv_http/btn_modules/block_row.html
  8. 66 0
      mds/mod_serv_http/btn_modules/btn_modules.go
  9. 27 0
      mds/mod_serv_http/btn_modules/btn_modules_test.go
  10. 16 0
      mds/mod_serv_http/btn_monolit/block_monolit.html
  11. 34 0
      mds/mod_serv_http/btn_monolit/btn_monolit.go
  12. 27 0
      mds/mod_serv_http/btn_monolit/btn_monolit_test.go
  13. 0 2
      mds/mod_serv_http/mod_serv_http.go
  14. 0 70
      mds/mod_serv_http/page_modules/page_modules.go
  15. 0 101
      mds/mod_serv_http/page_modules/page_modules_test.go
  16. 17 3
      mds/mod_serv_http/page_monolit/page_monolit.go
  17. 4 4
      mds/mod_serv_http/page_monolit/page_monolit.html
  18. 1 0
      mds/mod_serv_http/page_monolit/page_monolit_test.go
  19. 84 0
      mds/mod_wui/mod_wui.go
  20. 155 0
      mds/mod_wui/mod_wui_test.go
  21. 42 0
      wui/hx_swap/hx_swap.go
  22. 22 0
      wui/hx_swap/hx_swap_test.go
  23. 42 0
      wui/hx_swap_oob/hx_swap_oob.go
  24. 22 0
      wui/hx_swap_oob/hx_swap_oob_test.go
  25. 42 0
      wui/hx_target/hx_target.go
  26. 22 0
      wui/hx_target/hx_target_test.go
  27. 42 0
      wui/hx_trigger/hx_trigger.go
  28. 22 0
      wui/hx_trigger/hx_trigger_test.go
  29. 39 0
      wui/hx_url/hx_url.go
  30. 21 0
      wui/hx_url/hx_url_test.go
  31. 37 0
      wui/hx_url_method/hx_url_method.go
  32. 19 0
      wui/hx_url_method/hx_url_method_test.go
  33. 39 0
      wui/hx_url_patch/hx_url_patch.go
  34. 19 0
      wui/hx_url_patch/hx_url_patch_test.go
  35. 69 0
      wui/hx_vals/hx_vals.go
  36. 30 0
      wui/hx_vals/hx_vals_test.go
  37. 64 0
      wui/wbutton/wbutton.go
  38. 46 0
      wui/wbutton/wbutton_test.go
  39. 92 0
      wui/whx/whx.go
  40. 65 0
      wui/whx/whx_test.go
  41. 3 0
      wui/wlabel/wlabel.go
  42. 57 0
      wui/wtypes/ihx_swap.go
  43. 29 0
      wui/wtypes/ihx_swap_oob.go
  44. 47 0
      wui/wtypes/ihx_target.go
  45. 157 0
      wui/wtypes/ihx_trigger.go
  46. 11 0
      wui/wtypes/ihx_url.go
  47. 15 0
      wui/wtypes/ihx_url_method.go
  48. 9 0
      wui/wtypes/ihx_url_patch.go
  49. 30 0
      wui/wtypes/ihx_vals.go
  50. 12 0
      wui/wtypes/iwui_button.go
  51. 19 0
      wui/wtypes/iwui_hx.go
  52. 18 0
      wui/wui.go
  53. 35 0
      wui/wui_test.go

+ 3 - 0
cmd/demo/main.go

@@ -20,6 +20,9 @@ func main() {
 	modKernKeep := kern.GetModuleKernelKeeper()
 	app.Add(modKernKeep)
 
+	modWui := kern.GetModuleWui()
+	app.Add(modWui)
+
 	app.Run()
 	app.Wait()
 }

+ 7 - 0
kern.go

@@ -25,6 +25,7 @@ import (
 	"gitp78su.ipnodns.ru/svi/kern/mds/mod_kctx"
 	"gitp78su.ipnodns.ru/svi/kern/mds/mod_keeper"
 	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http"
+	"gitp78su.ipnodns.ru/svi/kern/mds/mod_wui"
 )
 
 // GetKernelCtx -- возвращает контекст ядра
@@ -131,6 +132,12 @@ func GetModuleKernelKeeper() IKernelModule {
 	return modKernelKeeper
 }
 
+// GetModuleWui -- возвращает модуль для WUI
+func GetModuleWui() IKernelModule {
+	mod := mod_wui.GetModuleWui()
+	return mod
+}
+
 // NewLogBuf -- возвращает новый буферизованный лог
 func NewLogBuf() ILogBuf {
 	log := log_buf.NewLogBuf()

+ 5 - 0
kern_test.go

@@ -106,6 +106,11 @@ func (sf *tester) new() {
 	if modKernelKeeper == nil {
 		sf.t.Fatalf("new(): modKernelKeeper==nil")
 	}
+	modWui := GetModuleWui()
+	if modWui == nil {
+		sf.t.Fatalf("new(): modWui==nil")
+	}
+
 	logBuf := NewLogBuf()
 	if logBuf == nil {
 		sf.t.Fatalf("new(): ILogBuf==nil")

+ 0 - 2
mds/mod_kctx/mod_kctx.go

@@ -10,7 +10,6 @@ import (
 	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
 	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/http_api"
 	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/page_module"
-	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/page_modules"
 	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/page_monolit"
 )
 
@@ -41,7 +40,6 @@ func GetModuleKernelCtx() *ModuleKernelCtx {
 	}
 	sf.log = sf.Ctx().Log()
 	_ = page_monolit.GetPageMonolit()
-	_ = page_modules.GetPageModules()
 	_ = page_module.GetPageModule()
 
 	_ = http_api.NewHttpApi()

+ 0 - 2
mds/mod_keeper/mod_keeper.go

@@ -10,7 +10,6 @@ import (
 	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
 	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/http_api"
 	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/page_module"
-	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/page_modules"
 	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/page_monolit"
 )
 
@@ -41,7 +40,6 @@ func GetModuleKeeper() *ModuleKeeper {
 	}
 	sf.log = sf.kCtx.Keeper().Log()
 	_ = page_monolit.GetPageMonolit()
-	_ = page_modules.GetPageModules()
 	_ = page_module.GetPageModule()
 
 	_ = http_api.NewHttpApi()

+ 5 - 3
mds/mod_serv_http/page_modules/mod_row_block.html → mds/mod_serv_http/btn_modules/block_modules.html

@@ -1,8 +1,10 @@
 <div id="monolit" hx-swap-oob="true"></div>
 <div id="module" hx-swap-oob="true"></div>
-<div id="modules" class="container border rounded m-3 text-center">
-    <h2>Modules</h2>
-
+<title>Modules</title>
+<div id="modules" hx-swap-oob="true" class="container border rounded m-3 text-center">
+    <div class="container border rounded m-3 text-center">
+        <h2>Modules</h2>
+    </div>
     <p></p>
     <div id="modules_state" class="container" x-post="/modules" hx-trigger="every 2s">
         <div class="row">

+ 0 - 0
mds/mod_serv_http/page_modules/mod_row_val.html → mds/mod_serv_http/btn_modules/block_row.html


+ 66 - 0
mds/mod_serv_http/btn_modules/btn_modules.go

@@ -0,0 +1,66 @@
+package btn_modules
+
+import (
+	_ "embed"
+	"fmt"
+	"strings"
+
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/wui"
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+type BtnModules struct {
+	btn  IWuiButton
+	kCtx IKernelCtx
+}
+
+// NewBtnModules -- возвращает новую кнопку модулей
+func NewBtnModules() *BtnModules {
+	sf := &BtnModules{
+		kCtx: kctx.GetKernelCtx(),
+	}
+	sf.btn = wui.NewWuiButton("Modules", sf.clickMonolit)
+	sf.btn.Hx().Target().Set("#modules")
+	return sf
+}
+
+// Html -- возвращает HTML-представление кнопки
+func (sf *BtnModules) Html() string {
+	return sf.btn.Html()
+}
+
+//go:embed block_modules.html
+var strBlockModules string
+
+//go:embed block_row.html
+var strBlockRow string
+
+// Событие клика по кнопке
+func (sf *BtnModules) clickMonolit() string {
+	mon := sf.kCtx.Get("monolit").Val().(IKernelMonolit)
+	chLst := mon.Ctx().SortedList()
+	strOut := ``
+	for _, val := range chLst {
+		if !strings.Contains(val.Key(), "module_") {
+			continue
+		}
+		lstKey := strings.Split(val.Key(), "_")
+		id := lstKey[1]
+		strRow := strBlockRow
+		strRow = strings.ReplaceAll(strRow, "{.id}", id)
+		strRow = strings.ReplaceAll(strRow, "{.key}", val.Key())
+		moduleName := string(val.Val().(IKernelModule).Name())
+		strRow = strings.ReplaceAll(strRow, "{.name}", moduleName)
+		type_ := fmt.Sprintf("%#T", val.Val())
+		type_ = strings.ReplaceAll(type_, ".", ".<br>")
+		strRow = strings.ReplaceAll(strRow, "{.type}", type_)
+		strRow = strings.ReplaceAll(strRow, "{.createAt}", string(val.CreateAt()))
+		strRow = strings.ReplaceAll(strRow, "{.updateAt}", string(val.UpdateAt()))
+		strRow = strings.ReplaceAll(strRow, "{.comment}", val.Comment())
+		strOut += strRow
+	}
+	strOut = strings.ReplaceAll(strBlockModules, "{.mod_block}", strOut)
+	return strOut
+}

+ 27 - 0
mds/mod_serv_http/btn_modules/btn_modules_test.go

@@ -0,0 +1,27 @@
+package btn_modules
+
+import (
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kmodule"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kmonolit"
+)
+
+func TestBtnModules(t *testing.T) {
+	btn := NewBtnModules()
+	if btn == nil {
+		t.Fatalf("btn==nil")
+	}
+	if html := btn.Html(); html == "" {
+		t.Fatalf("html is empty")
+	}
+	kCtx := kctx.GetKernelCtx()
+	kCtx.Set("isLocal", true, "test")
+	kMon := kmonolit.GetMonolit("test")
+	mod := kmodule.NewKernelModule("test")
+	kMon.Add(mod)
+	if str := btn.clickMonolit(); str == "" {
+		t.Fatalf("strOut is empty")
+	}
+}

+ 16 - 0
mds/mod_serv_http/btn_monolit/block_monolit.html

@@ -0,0 +1,16 @@
+<title>Monolit</title>
+<div id="monolit" hx-swap-oob="true" class="container border rounded m-3 text-center">
+    <div class="container border rounded m-3 text-center">
+        <h2>Monolit</h2>
+    </div>
+    <p></p>
+    <div class="container border rounded m-3 text-center">
+        <span class="btn btn-primary" hx-post="/monolit_state" hx-target="#monolit_state">Monolit</span>
+        <span class="btn btn-primary" hx-post="/monolit_ctx" hx-target="#monolit_state">ctx</span>
+        <span class="btn btn-primary" hx-post="/monolit_log" hx-target="#monolit_state">log</span>
+    </div>
+    <div id="monolit_state" class="container" hx-post="/monolit_state" hx-trigger="load">
+    </div>
+</div>
+<div id="modules" hx-swap-oob="true"></div>
+<div id="module" hx-swap-oob="true"></div>

+ 34 - 0
mds/mod_serv_http/btn_monolit/btn_monolit.go

@@ -0,0 +1,34 @@
+// package btn_monolit -- обработчик для показа блока монолита
+package btn_monolit
+
+import (
+	_ "embed"
+
+	"gitp78su.ipnodns.ru/svi/kern/wui"
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+type BtnMonolit struct {
+	btn IWuiButton
+}
+
+// NewBtnMonolit -- возвращает новую кнопку монолита
+func NewBtnMonolit() *BtnMonolit {
+	sf := &BtnMonolit{}
+	sf.btn = wui.NewWuiButton("Monolit", sf.clickMonolit)
+	sf.btn.Hx().Target().Set("#monolit")
+	return sf
+}
+
+// Html -- возвращает HTML-представление кнопки
+func (sf *BtnMonolit) Html() string {
+	return sf.btn.Html()
+}
+
+//go:embed block_monolit.html
+var strBlockMonolit string
+
+// Событие клика по кнопке
+func (sf *BtnMonolit) clickMonolit() string {
+	return strBlockMonolit
+}

+ 27 - 0
mds/mod_serv_http/btn_monolit/btn_monolit_test.go

@@ -0,0 +1,27 @@
+package btn_monolit
+
+import (
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kmodule"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kmonolit"
+)
+
+func TestBtnModules(t *testing.T) {
+	btn := NewBtnMonolit()
+	if btn == nil {
+		t.Fatalf("btn==nil")
+	}
+	if html := btn.Html(); html == "" {
+		t.Fatalf("html is empty")
+	}
+	kCtx := kctx.GetKernelCtx()
+	kCtx.Set("isLocal", true, "test")
+	kMon := kmonolit.GetMonolit("test")
+	mod := kmodule.NewKernelModule("test")
+	kMon.Add(mod)
+	if str := btn.clickMonolit(); str == "" {
+		t.Fatalf("strOut is empty")
+	}
+}

+ 0 - 2
mds/mod_serv_http/mod_serv_http.go

@@ -9,7 +9,6 @@ import (
 	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
 	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/http_api"
 	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/page_module"
-	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/page_modules"
 	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/page_monolit"
 )
 
@@ -38,7 +37,6 @@ func GetModuleServHttp() *ModuleServHttp {
 	}
 	sf.log = sf.Ctx().Log()
 	_ = page_monolit.GetPageMonolit()
-	_ = page_modules.GetPageModules()
 	_ = page_module.GetPageModule()
 
 	_ = http_api.NewHttpApi()

+ 0 - 70
mds/mod_serv_http/page_modules/page_modules.go

@@ -1,70 +0,0 @@
-// package page_modules -- страница представления модулей
-package page_modules
-
-import (
-	_ "embed"
-	"fmt"
-	"strings"
-
-	"github.com/gofiber/fiber/v2"
-
-	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
-	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
-)
-
-// PageModules -- отображает модули модулей
-type PageModules struct {
-	ctx IKernelCtx
-}
-
-var page *PageModules
-
-// GetPageModules -- возвращает страницу модулей
-func GetPageModules() *PageModules {
-	if page != nil {
-		return page
-	}
-	kCtx := kctx.GetKernelCtx()
-	sf := &PageModules{
-		ctx: kCtx,
-	}
-	fiberApp := kCtx.Get("fiberApp").Val().(*fiber.App)
-	fiberApp.Post("/modules", sf.postModules)
-	page = sf
-	return sf
-}
-
-//go:embed mod_row_block.html
-var strModRowBlock string
-
-//go:embed mod_row_val.html
-var strModRowBlank string
-
-// Индексная страница модулей
-func (sf *PageModules) postModules(ctx *fiber.Ctx) error {
-	ctx.Set("Content-type", "text/html; charset=utf8;\n\n")
-	mon := sf.ctx.Get("monolit").Val().(IKernelMonolit)
-	chLst := mon.Ctx().SortedList()
-	strOut := ``
-	for _, val := range chLst {
-		if !strings.Contains(val.Key(), "module_") {
-			continue
-		}
-		lstKey := strings.Split(val.Key(), "_")
-		id := lstKey[1]
-		strRow := strModRowBlank
-		strRow = strings.ReplaceAll(strRow, "{.id}", id)
-		strRow = strings.ReplaceAll(strRow, "{.key}", val.Key())
-		moduleName := string(val.Val().(IKernelModule).Name())
-		strRow = strings.ReplaceAll(strRow, "{.name}", moduleName)
-		type_ := fmt.Sprintf("%#T", val.Val())
-		type_ = strings.ReplaceAll(type_, ".", ".<br>")
-		strRow = strings.ReplaceAll(strRow, "{.type}", type_)
-		strRow = strings.ReplaceAll(strRow, "{.createAt}", string(val.CreateAt()))
-		strRow = strings.ReplaceAll(strRow, "{.updateAt}", string(val.UpdateAt()))
-		strRow = strings.ReplaceAll(strRow, "{.comment}", val.Comment())
-		strOut += strRow
-	}
-	strOut = strings.ReplaceAll(strModRowBlock, "{.mod_block}", strOut)
-	return ctx.SendString(strOut)
-}

+ 0 - 101
mds/mod_serv_http/page_modules/page_modules_test.go

@@ -1,101 +0,0 @@
-package page_modules
-
-import (
-	"net/http"
-	"os"
-	"testing"
-	"time"
-
-	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
-	"gitp78su.ipnodns.ru/svi/kern/krn/kmodule"
-	"gitp78su.ipnodns.ru/svi/kern/krn/kmonolit"
-	"gitp78su.ipnodns.ru/svi/kern/krn/kserv_http"
-	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
-	"gitp78su.ipnodns.ru/svi/kern/mock/mock_env"
-)
-
-type tester struct {
-	t    *testing.T
-	ctx  IKernelCtx
-	serv IKernelServerHttp
-	page *PageModules
-}
-
-func TestPageMonolit(t *testing.T) {
-	sf := &tester{
-		t:   t,
-		ctx: kctx.GetKernelCtx(),
-	}
-	sf.new()
-	sf.postModulesState()
-	sf.postModulesState1()
-	sf.done()
-}
-
-// Возвращает состояние модуля 1 (теперь добавлен)
-func (sf *tester) postModulesState1() {
-	sf.t.Log("postModulesState1")
-	mon := kmonolit.GetMonolit("test_monolit")
-	ctxMon := mon.Ctx()
-	module := kmodule.NewKernelModule("test_module")
-	module.Log().Debug("test msg")
-	module.Log().Debug("test msg")
-	ctxMod := module.Ctx()
-	ctxMod.Set("demo_key", "demo value", "for demo comment")
-	time.Sleep(time.Millisecond * 20)
-	ctxMon.Set("module_1", module, "test_module")
-	fiberApp := sf.serv.Fiber()
-	req, err := http.NewRequest("POST", "/modules", nil)
-	if err != nil {
-		sf.t.Fatalf("postModulesState1(): in net request, err=%v", err)
-	}
-	resp, err := fiberApp.Test(req)
-	if err != nil {
-		sf.t.Fatalf("postModulesState1(): in make POST, err=%v", err)
-	}
-	if resp.StatusCode != 200 {
-		sf.t.Fatalf("postModulesState1(): status(%v)!=200", resp.StatusCode)
-	}
-}
-
-// Возвращает состояние модуля
-func (sf *tester) postModulesState() {
-	sf.t.Log("postModulesState")
-	fiberApp := sf.serv.Fiber()
-	req, err := http.NewRequest("POST", "/modules", nil)
-	if err != nil {
-		sf.t.Fatalf("postModulesState(): in net request, err=%v", err)
-	}
-	resp, err := fiberApp.Test(req)
-	if err != nil {
-		sf.t.Fatalf("postModulesState(): in make POST, err=%v", err)
-	}
-	if resp.StatusCode != 200 {
-		sf.t.Fatalf("postModulesState(): status(%v)!=200", resp.StatusCode)
-	}
-}
-
-// Освобождает ресурсы
-func (sf *tester) done() {
-	sf.t.Log("done")
-	sf.ctx.Cancel()
-	sf.ctx.Wg().Wait()
-}
-
-// Создаёт новую страницу модуля
-func (sf *tester) new() {
-	sf.t.Log("new")
-	_ = mock_env.MakeEnv()
-	_ = os.Unsetenv("LOCAL_HTTP_URL")
-	os.Setenv("LOCAL_HTTP_URL", "http://localhost:18315/")
-	sf.ctx.Set("isLocal", true, "testing")
-	_ = kmonolit.GetMonolit("test_monolit")
-	sf.serv = kserv_http.GetKernelServHttp()
-
-	sf.page = GetPageModules()
-	if sf.page == nil {
-		sf.t.Fatalf("new(): page==nil")
-	}
-	_ = GetPageModules()
-	go sf.serv.Run()
-}

+ 17 - 3
mds/mod_serv_http/page_monolit/page_monolit.go

@@ -10,11 +10,15 @@ import (
 
 	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
 	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/btn_modules"
+	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/btn_monolit"
 )
 
 // PageMonolit -- страница показа монолита
 type PageMonolit struct {
-	ctx IKernelCtx
+	ctx        IKernelCtx
+	btnMonolit *btn_monolit.BtnMonolit
+	btnModules *btn_modules.BtnModules
 }
 
 var page *PageMonolit
@@ -26,8 +30,11 @@ func GetPageMonolit() *PageMonolit {
 	}
 	kCtx := kctx.GetKernelCtx()
 	sf := &PageMonolit{
-		ctx: kCtx,
+		ctx:        kCtx,
+		btnMonolit: btn_monolit.NewBtnMonolit(),
+		btnModules: btn_modules.NewBtnModules(),
 	}
+
 	fiberApp := kCtx.Get("fiberApp").Val().(*fiber.App)
 	fiberApp.Get("/monolit", sf.getMonolit)
 	fiberApp.Post("/monolit_state", sf.postMonolitState)
@@ -37,6 +44,11 @@ func GetPageMonolit() *PageMonolit {
 	return sf
 }
 
+// Функция обратного вызова при клике на кнопку "Монолит"
+func (sf *PageMonolit) clickMonolit() string {
+	return ""
+}
+
 //go:embed log_block.html
 var strLogBlock string
 
@@ -108,5 +120,7 @@ var strPageMonolit string
 // Индексная страница монолита
 func (sf *PageMonolit) getMonolit(ctx *fiber.Ctx) error {
 	ctx.Set("Content-type", "text/html; charset=utf8;\n\n")
-	return ctx.SendString(strPageMonolit)
+	strOut := strings.ReplaceAll(strPageMonolit, "{.btn_monolit}", sf.btnMonolit.Html())
+	strOut = strings.ReplaceAll(strOut, "{.btn_modules}", sf.btnModules.Html())
+	return ctx.SendString(strOut)
 }

+ 4 - 4
mds/mod_serv_http/page_monolit/page_monolit.html

@@ -23,8 +23,8 @@
                         <h1>kern</h1>
                     </div>
                     <div class="col">
-                        <span class="btn btn-primary" hx-get="/monolit" hx-target="body">Monolit</span>
-                        <span class="btn btn-primary" hx-post="/modules" hx-target="#modules">Modules</span>
+                        {.btn_monolit}
+                        {.btn_modules}
                         <a class="btn btn-primary" href="/monitor" hx-boost="false">Monitor</a>
                     </div>
                 </div>
@@ -42,8 +42,8 @@
         </div>
 
         <!-- main -->
-        <div id="main">
-            <div id="monolit">
+        <div id="main" class="container">
+            <div id="monolit" class="container">
                 <div class="container border rounded m-3 text-center">
                     <h2>Monolit</h2>
                 </div>

+ 1 - 0
mds/mod_serv_http/page_monolit/page_monolit_test.go

@@ -107,6 +107,7 @@ func (sf *tester) getMonolit() {
 // Освобождает ресурсы
 func (sf *tester) done() {
 	sf.t.Log("done")
+	sf.page.clickMonolit()
 	sf.ctx.Cancel()
 	sf.ctx.Wg().Wait()
 }

+ 84 - 0
mds/mod_wui/mod_wui.go

@@ -0,0 +1,84 @@
+// package mod_wui -- модуль WUI
+package mod_wui
+
+import (
+	"fmt"
+	"sync"
+
+	"github.com/gofiber/fiber/v2"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kmodule"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kserv_http"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/http_api"
+	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/page_module"
+	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http/page_monolit"
+	"gitp78su.ipnodns.ru/svi/kern/wui"
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+// ModuleWui -- модуль WUI
+type ModuleWui struct {
+	IKernelModule
+	kCtx      IKernelCtx
+	wCtx      IWuiCtx
+	kServHttp IKernelServerHttp
+	log       ILogBuf
+}
+
+var (
+	mod   *ModuleWui
+	block sync.Mutex
+)
+
+// GetModuleWui -- возвращает новый модуль WUI
+func GetModuleWui() *ModuleWui {
+	block.Lock()
+	defer block.Unlock()
+	if mod != nil {
+		return mod
+	}
+	sf := &ModuleWui{
+		kCtx:          kctx.GetKernelCtx(),
+		wCtx:          wui.GetWuiCtx(),
+		IKernelModule: kmodule.NewKernelModule("wui"),
+		kServHttp:     kserv_http.GetKernelServHttp(),
+	}
+	sf.log = sf.Ctx().Log()
+	_ = page_monolit.GetPageMonolit()
+	_ = page_module.GetPageModule()
+
+	_ = http_api.NewHttpApi()
+	fibApp := sf.kCtx.Get("fiberApp").Val().(*fiber.App)
+	fibApp.Post("/wui/click/:id", sf.wuiClick)
+	mod = sf
+	return sf
+}
+
+// Run -- запускает модуль в работу
+func (sf *ModuleWui) Run() {
+	sf.log.Info("ModuleWui.Run(): module=%v, is run", sf.Name())
+	go sf.kServHttp.Run()
+}
+
+// IsWork -- признак работы модуля
+func (sf *ModuleWui) IsWork() bool {
+	return sf.kCtx.Wg().IsWork()
+}
+
+// Получает событие из сети
+func (sf *ModuleWui) wuiClick(ctx *fiber.Ctx) error {
+	id := ctx.Params("id")
+	widget0 := sf.wCtx.Get(id)
+	if widget0 == nil {
+		strOut := fmt.Sprintf("ModuleWui.wuiClick(): id(%v), widget not exists", id)
+		return ctx.SendString(strOut)
+	}
+	widget1, isOk := widget0.Val().(IWuiButton)
+	if !isOk {
+		strOut := fmt.Sprintf("ModuleWui.wuiClick(): widget(%T) not button", widget0.Val())
+		return ctx.SendString(strOut)
+	}
+	strOut := widget1.Click()
+	return ctx.SendString(strOut)
+}

+ 155 - 0
mds/mod_wui/mod_wui_test.go

@@ -0,0 +1,155 @@
+package mod_wui
+
+import (
+	"net/http"
+	"os"
+	"testing"
+	"time"
+
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/wui/wbutton"
+	"gitp78su.ipnodns.ru/svi/kern/wui/wlabel"
+
+	// "gitp78su.ipnodns.ru/svi/kern/krn/kmodule"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kmonolit"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kserv_http"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_env"
+)
+
+type tester struct {
+	t    *testing.T
+	ctx  IKernelCtx
+	serv IKernelServerHttp
+	mod  IKernelModule
+}
+
+func TestModKernelCtx(t *testing.T) {
+	sf := &tester{
+		t:   t,
+		ctx: kctx.GetKernelCtx(),
+	}
+	sf.ctx.Set("monolitName", "test_monolit", "test")
+	sf.new()
+	sf.click()
+	sf.done()
+}
+
+// Кто-то кликнул в сети на кнопку
+func (sf *tester) click() {
+	sf.t.Log("click")
+	sf.clickBad1()
+	sf.clickBad2()
+	sf.clickGood1()
+}
+
+func (sf *tester) clickGood1() {
+	sf.t.Log("clickGood1")
+	btn := wbutton.NewWuiButton("test_btn", sf.fnClick)
+	fiberApp := sf.serv.Fiber()
+	req, err := http.NewRequest("POST", "/wui/click/"+btn.Id(), nil)
+	if err != nil {
+		sf.t.Fatalf("clickGood1(): in net request, err=%v", err)
+	}
+	resp, err := fiberApp.Test(req)
+	if err != nil {
+		sf.t.Fatalf("clickGood1(): in make POST, err=%v", err)
+	}
+	if resp.StatusCode != 200 {
+		sf.t.Fatalf("clickGood1(): status(%v)!=200", resp.StatusCode)
+	}
+}
+
+// Обратный вызов клика
+func (sf *tester) fnClick() string {
+	sf.t.Log("fnClick")
+	return "test_click"
+}
+
+// Неправильный тип объекта
+func (sf *tester) clickBad2() {
+	sf.t.Log("clickBad2")
+	lbl := wlabel.NewWuiLabel("test_lbl")
+	fiberApp := sf.serv.Fiber()
+	req, err := http.NewRequest("POST", "/wui/click/"+lbl.Id(), nil)
+	if err != nil {
+		sf.t.Fatalf("clickBad2(): in net request, err=%v", err)
+	}
+	resp, err := fiberApp.Test(req)
+	if err != nil {
+		sf.t.Fatalf("clickBad2(): in make POST, err=%v", err)
+	}
+	if resp.StatusCode != 200 {
+		sf.t.Fatalf("clickBad2(): status(%v)!=200", resp.StatusCode)
+	}
+}
+
+// Нет такой кнопки
+func (sf *tester) clickBad1() {
+	sf.t.Log("clickBad1")
+	fiberApp := sf.serv.Fiber()
+	req, err := http.NewRequest("POST", "/wui/click/ert", nil)
+	if err != nil {
+		sf.t.Fatalf("clickBad1(): in net request, err=%v", err)
+	}
+	resp, err := fiberApp.Test(req)
+	if err != nil {
+		sf.t.Fatalf("clickBad1(): in make POST, err=%v", err)
+	}
+	if resp.StatusCode != 200 {
+		sf.t.Fatalf("clickBad1(): status(%v)!=200", resp.StatusCode)
+	}
+}
+
+// Завершение работы
+func (sf *tester) done() {
+	sf.t.Log("done")
+	sf.ctx.Cancel()
+	sf.ctx.Wg().Wait()
+	if isWork := sf.mod.IsWork(); isWork {
+		sf.t.Fatalf("newGood1(): isWork==true")
+	}
+}
+
+// Создание нового модуля HTTP-сервера
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.newBad1()
+	sf.newGood1()
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1")
+	_ = mock_env.MakeEnv()
+	_ = os.Unsetenv("LOCAL_HTTP_URL")
+	os.Setenv("LOCAL_HTTP_URL", "http://localhost:18330/")
+	sf.mod = GetModuleWui()
+	kCtx := kctx.GetKernelCtx()
+	kCtx.Set("isLocal", true, "type msg bus")
+	_ = kmonolit.GetMonolit("test_monolit")
+	sf.serv = kserv_http.GetKernelServHttp()
+	_ = GetModuleWui()
+	if sf.mod == nil {
+		sf.t.Fatalf("newGood1(): mod==nil")
+	}
+
+	go sf.mod.Run()
+	for {
+		time.Sleep(time.Millisecond * 1)
+		if sf.mod.IsWork() {
+			break
+		}
+	}
+	go sf.serv.Run()
+}
+
+// нет ничего для создания модуля
+func (sf *tester) newBad1() {
+	sf.t.Log("newBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad1(): panic==nil")
+		}
+	}()
+	_ = GetModuleWui()
+}

+ 42 - 0
wui/hx_swap/hx_swap.go

@@ -0,0 +1,42 @@
+// package hx_swap -- атрибут HTMX (политика замены)
+package hx_swap
+
+import (
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+// HxSwap -- атрибут HTMX (политика замены)
+type HxSwap struct {
+	sync.RWMutex
+	val string
+}
+
+// NewHxSwap -- возвращает новую политику замены
+func NewHxSwap() *HxSwap {
+	sf := &HxSwap{}
+	_ = IHxSwap(sf)
+	return sf
+}
+
+// String -- возвращает строковое представление тэга
+func (sf *HxSwap) String() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	return `hx-swap="` + sf.val + `"`
+}
+
+// Get -- возвращает хранимое значение политики замена
+func (sf *HxSwap) Get() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.val
+}
+
+// Set -- устанавливает значение политики обмена
+func (sf *HxSwap) Set(val string) {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = val
+}

+ 22 - 0
wui/hx_swap/hx_swap_test.go

@@ -0,0 +1,22 @@
+package hx_swap
+
+import (
+	"testing"
+)
+
+func TestHxSwap(t *testing.T) {
+	swap := NewHxSwap()
+	if swap == nil {
+		t.Fatalf("swap==nil")
+	}
+	if tag := swap.Get(); tag != "" {
+		t.Fatalf("tag not empty")
+	}
+	swap.Set("innerHTML")
+	if tag := swap.Get(); tag != "innerHTML" {
+		t.Fatalf("tag bad")
+	}
+	if str := swap.String(); str != `hx-swap="innerHTML"` {
+		t.Fatalf("str is bad")
+	}
+}

+ 42 - 0
wui/hx_swap_oob/hx_swap_oob.go

@@ -0,0 +1,42 @@
+// package hx_swap_oob -- объект внеполосной подкачки
+package hx_swap_oob
+
+import (
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+// HxSwapOob -- объект внеполосной подкачки
+type HxSwapOob struct {
+	sync.RWMutex
+	val string
+}
+
+// NewHxSwapOob -- возвращает новую внеполосную подкачку
+func NewHxSwapOob() *HxSwapOob {
+	sf := &HxSwapOob{}
+	_ = IHxSwapOob(sf)
+	return sf
+}
+
+// String -- возвращает строковое представление тэга
+func (sf *HxSwapOob) String() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	return `hx-swap-oob="` + sf.val + `"`
+}
+
+// Get -- возвращает хранимое значение внеполосной подкачки
+func (sf *HxSwapOob) Get() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.val
+}
+
+// Set -- устанавливает значение внеполосной подкачки
+func (sf *HxSwapOob) Set(val string) {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = val
+}

+ 22 - 0
wui/hx_swap_oob/hx_swap_oob_test.go

@@ -0,0 +1,22 @@
+package hx_swap_oob
+
+import (
+	"testing"
+)
+
+func TestHxSwapOob(t *testing.T) {
+	swap := NewHxSwapOob()
+	if swap == nil {
+		t.Fatalf("swap==nil")
+	}
+	if tag := swap.Get(); tag != "" {
+		t.Fatalf("tag not empty")
+	}
+	swap.Set("true")
+	if tag := swap.Get(); tag != "true" {
+		t.Fatalf("tag bad")
+	}
+	if str := swap.String(); str != `hx-swap-oob="true"` {
+		t.Fatalf("str is bad")
+	}
+}

+ 42 - 0
wui/hx_target/hx_target.go

@@ -0,0 +1,42 @@
+// package hx_target -- атрибут HTMX (цель замены)
+package hx_target
+
+import (
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+// HxSwap -- атрибут HTMX (цель замены)
+type HxSwap struct {
+	sync.RWMutex
+	val string
+}
+
+// NewHxTarget -- возвращает новую цель замены
+func NewHxTarget() *HxSwap {
+	sf := &HxSwap{}
+	_ = IHxTarget(sf)
+	return sf
+}
+
+// String -- возвращает строковое представление тэга
+func (sf *HxSwap) String() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	return `hx-target="` + sf.val + `"`
+}
+
+// Get -- возвращает хранимое значение цели замена
+func (sf *HxSwap) Get() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.val
+}
+
+// Set -- устанавливает значение цели обмена
+func (sf *HxSwap) Set(val string) {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = val
+}

+ 22 - 0
wui/hx_target/hx_target_test.go

@@ -0,0 +1,22 @@
+package hx_target
+
+import (
+	"testing"
+)
+
+func TestHxTarget(t *testing.T) {
+	swap := NewHxTarget()
+	if swap == nil {
+		t.Fatalf("swap==nil")
+	}
+	if tag := swap.Get(); tag != "" {
+		t.Fatalf("tag not empty")
+	}
+	swap.Set("#main")
+	if tag := swap.Get(); tag != "#main" {
+		t.Fatalf("tag bad")
+	}
+	if str := swap.String(); str != `hx-target="#main"` {
+		t.Fatalf("str is bad")
+	}
+}

+ 42 - 0
wui/hx_trigger/hx_trigger.go

@@ -0,0 +1,42 @@
+// package hx_trigger -- атрибут HTMX (триггер запроса)
+package hx_trigger
+
+import (
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+// HxTrigger -- атрибут HTMX (триггер запроса)
+type HxTrigger struct {
+	sync.RWMutex
+	val string
+}
+
+// NewHxTrigger -- возвращает новый триггер запроса
+func NewHxTrigger() *HxTrigger {
+	sf := &HxTrigger{}
+	_ = IHxSwap(sf)
+	return sf
+}
+
+// String -- возвращает строковое представление тэга
+func (sf *HxTrigger) String() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	return `hx-trigger="` + sf.val + `"`
+}
+
+// Get -- возвращает хранимое значение триггера запроса
+func (sf *HxTrigger) Get() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.val
+}
+
+// Set -- устанавливает значение триггера запроса
+func (sf *HxTrigger) Set(val string) {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = val
+}

+ 22 - 0
wui/hx_trigger/hx_trigger_test.go

@@ -0,0 +1,22 @@
+package hx_trigger
+
+import (
+	"testing"
+)
+
+func TestHxTrigger(t *testing.T) {
+	swap := NewHxTrigger()
+	if swap == nil {
+		t.Fatalf("swap==nil")
+	}
+	if tag := swap.Get(); tag != "" {
+		t.Fatalf("tag not empty")
+	}
+	swap.Set("every 1s")
+	if tag := swap.Get(); tag != "every 1s" {
+		t.Fatalf("tag bad")
+	}
+	if str := swap.String(); str != `hx-trigger="every 1s"` {
+		t.Fatalf("str is bad")
+	}
+}

+ 39 - 0
wui/hx_url/hx_url.go

@@ -0,0 +1,39 @@
+// package hx_url -- атрибут HTMX (URL запроса)
+package hx_url
+
+import (
+	"gitp78su.ipnodns.ru/svi/kern/wui/hx_url_method"
+	"gitp78su.ipnodns.ru/svi/kern/wui/hx_url_patch"
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+// HxUrl -- атрибут HTMX (URL запроса)
+type HxUrl struct {
+	method IHxUrlMethod
+	patch  IHxUrlPatch
+}
+
+// NewHxUrl -- возвращает новый URL запроса
+func NewHxUrl(patch string) *HxUrl {
+	sf := &HxUrl{
+		method: hx_url_method.NewHxUrlMethod(),
+		patch:  hx_url_patch.NewHxUrlPatch(patch),
+	}
+	_ = IHxUrl(sf)
+	return sf
+}
+
+// String -- возвращает строковое представление тэга
+func (sf *HxUrl) String() string {
+	return sf.method.Get() + `="` + sf.patch.Get() + `"`
+}
+
+// Method -- возвращает метод запроса
+func (sf *HxUrl) Method() IHxUrlMethod {
+	return sf.method
+}
+
+// Patch -- возвращает путь запроса
+func (sf *HxUrl) Patch() IHxUrlPatch {
+	return sf.patch
+}

+ 21 - 0
wui/hx_url/hx_url_test.go

@@ -0,0 +1,21 @@
+package hx_url
+
+import (
+	"testing"
+)
+
+func TestHxUrl(t *testing.T) {
+	swap := NewHxUrl("/wui/click/abc")
+	if swap == nil {
+		t.Fatalf("swap==nil")
+	}
+	if met := swap.Method(); met == nil {
+		t.Fatalf("met==nil")
+	}
+	if patch := swap.Patch(); patch == nil {
+		t.Fatalf("patch==nil")
+	}
+	if str := swap.String(); str != `hx-post="/wui/click/abc"` {
+		t.Fatalf(`str(%v)!=(hx-post="/wui/click/abc")`, str)
+	}
+}

+ 37 - 0
wui/hx_url_method/hx_url_method.go

@@ -0,0 +1,37 @@
+// package hx_url_method -- атрибут HTMX (метод запроса)
+package hx_url_method
+
+import (
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+// HxSwap -- атрибут HTMX (метод запроса)
+type HxSwap struct {
+	sync.RWMutex
+	val string
+}
+
+// NewHxUrlMethod -- возвращает новый метод запроса
+func NewHxUrlMethod() *HxSwap {
+	sf := &HxSwap{
+		val: "hx-post",
+	}
+	_ = IHxUrlMethod(sf)
+	return sf
+}
+
+// Get -- возвращает хранимое значение метода запроса
+func (sf *HxSwap) Get() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.val
+}
+
+// Set -- устанавливает значение метода запроса
+func (sf *HxSwap) Set(val string) {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = val
+}

+ 19 - 0
wui/hx_url_method/hx_url_method_test.go

@@ -0,0 +1,19 @@
+package hx_url_method
+
+import (
+	"testing"
+)
+
+func TestHxUrlMethod(t *testing.T) {
+	swap := NewHxUrlMethod()
+	if swap == nil {
+		t.Fatalf("swap==nil")
+	}
+	if tag := swap.Get(); tag != "hx-post" {
+		t.Fatalf("tag not empty")
+	}
+	swap.Set("hx-get")
+	if tag := swap.Get(); tag != "hx-get" {
+		t.Fatalf("tag bad")
+	}
+}

+ 39 - 0
wui/hx_url_patch/hx_url_patch.go

@@ -0,0 +1,39 @@
+// package hx_url_patch -- атрибут HTMX (путь запроса)
+package hx_url_patch
+
+import (
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+// HxUrlPatch -- атрибут HTMX (путь запроса)
+type HxUrlPatch struct {
+	sync.RWMutex
+	val string
+}
+
+// NewHxUrlPatch -- возвращает новый путь запроса
+func NewHxUrlPatch(patch string) *HxUrlPatch {
+	Hassert(patch != "", "NewHxUrlPatch(): patch isempty")
+	sf := &HxUrlPatch{
+		val: patch,
+	}
+	_ = IHxUrlMethod(sf)
+	return sf
+}
+
+// Get -- возвращает хранимое значение пути запроса
+func (sf *HxUrlPatch) Get() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.val
+}
+
+// Set -- устанавливает значение пути запроса
+func (sf *HxUrlPatch) Set(val string) {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = val
+}

+ 19 - 0
wui/hx_url_patch/hx_url_patch_test.go

@@ -0,0 +1,19 @@
+package hx_url_patch
+
+import (
+	"testing"
+)
+
+func TestHxUrlPatch(t *testing.T) {
+	swap := NewHxUrlPatch("/wui/form/123")
+	if swap == nil {
+		t.Fatalf("swap==nil")
+	}
+	if patch := swap.Get(); patch != "/wui/form/123" {
+		t.Fatalf("patch bad")
+	}
+	swap.Set("/wui/click/abc")
+	if patch := swap.Get(); patch != "/wui/click/abc" {
+		t.Fatalf("patch bad")
+	}
+}

+ 69 - 0
wui/hx_vals/hx_vals.go

@@ -0,0 +1,69 @@
+// package hx_vals -- атрибут HTMX (словарь значений)
+package hx_vals
+
+import (
+	"encoding/json"
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+// HxVals -- атрибут HTMX (словарь значений)
+type HxVals struct {
+	sync.RWMutex
+	dict map[string]any
+}
+
+// NewHxVals -- возвращает новый словарь значений
+func NewHxVals() *HxVals {
+	sf := &HxVals{
+		dict: map[string]any{},
+	}
+	_ = IHxVals(sf)
+	return sf
+}
+
+// Len -- возвращает размер словаря
+func (sf *HxVals) Len() int {
+	sf.RLock()
+	defer sf.RUnlock()
+	return len(sf.dict)
+}
+
+// Del -- удаляет ключ словаря
+func (sf *HxVals) Del(key string) {
+	sf.Lock()
+	defer sf.Unlock()
+	delete(sf.dict, key)
+}
+
+// Clear -- очищает словарь значений
+func (sf *HxVals) Clear() {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.dict = map[string]any{}
+}
+
+// String -- возвращает строковое представление тэга
+func (sf *HxVals) String() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	binJson, _ := json.Marshal(sf.dict)
+	return `hx-vals='` + string(binJson) + `'`
+}
+
+// Get -- возвращает хранимое значение словарь значений
+func (sf *HxVals) Get(key string) any {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.dict[key]
+}
+
+// Set -- устанавливает значение словарь значений
+func (sf *HxVals) Set(key string, val any) {
+	sf.Lock()
+	defer sf.Unlock()
+	Hassert(key != "", "HxVals.Set(): key is empty")
+	sf.dict[key] = val
+}

+ 30 - 0
wui/hx_vals/hx_vals_test.go

@@ -0,0 +1,30 @@
+package hx_vals
+
+import (
+	"testing"
+)
+
+func TestHxVals(t *testing.T) {
+	vals := NewHxVals()
+	if vals == nil {
+		t.Fatalf("swap==nil")
+	}
+	if tag := vals.Get("test"); tag != nil {
+		t.Fatalf("tag not empty")
+	}
+	vals.Set("innerHTML", 123)
+	if tag := vals.Get("innerHTML"); tag != 123 {
+		t.Fatalf("tag bad")
+	}
+	if len_ := vals.Len(); len_ != 1 {
+		t.Fatalf("bad len")
+	}
+	if str := vals.String(); str != `hx-vals='{"innerHTML":123}'` {
+		t.Fatalf("str(%v) is bad", str)
+	}
+	vals.Del("innerHTML")
+	vals.Clear()
+	if len_ := vals.Len(); len_ != 0 {
+		t.Fatalf("bad len")
+	}
+}

+ 64 - 0
wui/wbutton/wbutton.go

@@ -0,0 +1,64 @@
+// package wbutton -- WUI-кнопка
+package wbutton
+
+import (
+	"strings"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+
+	"gitp78su.ipnodns.ru/svi/kern/wui/wctx"
+	"gitp78su.ipnodns.ru/svi/kern/wui/whx"
+	"gitp78su.ipnodns.ru/svi/kern/wui/wtext"
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+	"gitp78su.ipnodns.ru/svi/kern/wui/wwidget"
+)
+
+// WuiButton -- WUI-кнопка
+type WuiButton struct {
+	IWuiWidget
+	text   IWuiText
+	fnBack func() string
+	hx     IWuiHx
+}
+
+// NewWuiButton -- возвращает новую WUI-кнопку
+func NewWuiButton(text string, fnBack func() string) *WuiButton {
+	Hassert(fnBack != nil, "NewWuiButton(): fnBack==nil")
+	sf := &WuiButton{
+		IWuiWidget: wwidget.NewWuiWidget(),
+		text:       wtext.NewWuiText(text),
+		fnBack:     fnBack,
+	}
+	sf.hx = whx.NewWuiHx("/wui/click/" + sf.Id())
+	wCtx := wctx.GetWuiCtx()
+	wCtx.Set(sf.Id(), sf, "WUI-кнопка")
+	_ = IWuiButton(sf)
+	return sf
+}
+
+// Hx -- возвращает атрибуты HTMX
+func (sf *WuiButton) Hx() IWuiHx {
+	return sf.hx
+}
+
+// Text -- возвращает текст кнопки
+func (sf *WuiButton) Text() IWuiText {
+	return sf.text
+}
+
+// Click -- событие нажатия
+func (sf *WuiButton) Click() string {
+	return sf.fnBack()
+}
+
+const (
+	strBeg = `<span id="{.id}" class="btn btn-primary" {.hx}>{.txt}</span>`
+)
+
+// Html -- возвращает HTML-представление текста
+func (sf *WuiButton) Html() string {
+	strRes := strings.ReplaceAll(strBeg, "{.id}", sf.Id())
+	strRes = strings.ReplaceAll(strRes, "{.txt}", sf.text.Get())
+	strRes = strings.ReplaceAll(strRes, "{.hx}", sf.hx.String())
+	return strRes
+}

+ 46 - 0
wui/wbutton/wbutton_test.go

@@ -0,0 +1,46 @@
+package wbutton
+
+import (
+	"testing"
+)
+
+type tester struct {
+	t      *testing.T
+	chCall chan string
+}
+
+func TestWuiButton(t *testing.T) {
+	sf := &tester{
+		t:      t,
+		chCall: make(chan string, 2),
+	}
+	sf.new()
+}
+
+// Создание кнопки
+func (sf *tester) new() {
+	sf.t.Log("new")
+	btn := NewWuiButton("test_val", sf.fnBack)
+	if btn == nil {
+		sf.t.Fatalf("new(): WuiButton==nil")
+	}
+	if txt := btn.Text(); txt == nil {
+		sf.t.Fatalf("new(): IWuiText==nil")
+	}
+	if html := btn.Html(); html == "" {
+		sf.t.Fatalf("new(): html is empty")
+	}
+	if hx := btn.Hx(); hx == nil {
+		sf.t.Fatalf("new(): IWuiHx==nil")
+	}
+	btn.Click()
+	if str := <-sf.chCall; str != "test" {
+		sf.t.Fatalf("new(): bad called")
+	}
+}
+
+// Функция обратного вызова
+func (sf *tester) fnBack() string {
+	sf.chCall <- "test"
+	return "test_click"
+}

+ 92 - 0
wui/whx/whx.go

@@ -0,0 +1,92 @@
+// package whx -- HTMX-атрибуты WUI-объекта
+package whx
+
+import (
+	"gitp78su.ipnodns.ru/svi/kern/wui/hx_swap"
+	"gitp78su.ipnodns.ru/svi/kern/wui/hx_swap_oob"
+	"gitp78su.ipnodns.ru/svi/kern/wui/hx_target"
+	"gitp78su.ipnodns.ru/svi/kern/wui/hx_trigger"
+	"gitp78su.ipnodns.ru/svi/kern/wui/hx_url"
+	"gitp78su.ipnodns.ru/svi/kern/wui/hx_vals"
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+// WuiHx -- HTMX-атрибуты WUI-объекта
+type WuiHx struct {
+	url     IHxUrl
+	trigger IHxTrigger
+	target  IHxTarget
+	swap    IHxSwap
+	oob     IHxSwapOob
+	vals    IHxVals
+}
+
+// NewWuiHx -- возвращает новые атрибуты HTMX для WUI-объекта
+func NewWuiHx(path string) *WuiHx {
+	sf := &WuiHx{
+		url:     hx_url.NewHxUrl(path),
+		trigger: hx_trigger.NewHxTrigger(),
+		target:  hx_target.NewHxTarget(),
+		swap:    hx_swap.NewHxSwap(),
+		oob:     hx_swap_oob.NewHxSwapOob(),
+		vals:    hx_vals.NewHxVals(),
+	}
+	_ = IWuiHx(sf)
+	return sf
+}
+
+// String -- возвращает строку тэгов
+func (sf *WuiHx) String() string {
+	strOut := sf.url.String() + " " // Не может быть пустым
+	trig := sf.trigger.Get()
+	if trig != "" {
+		strOut += sf.trigger.String() + " "
+	}
+	targ := sf.target.Get()
+	if targ != "" {
+		strOut += sf.target.String() + " "
+	}
+	swap := sf.swap.Get()
+	if swap != "" {
+		strOut += sf.swap.String() + " "
+	}
+	oob := sf.oob.Get()
+	if oob != "" {
+		strOut += sf.oob.String() + " "
+	}
+	valsLen := sf.vals.Len()
+	if valsLen != 0 {
+		strOut += sf.vals.String()
+	}
+	return strOut
+}
+
+// Vals -- возвращает тэг переменных запроса
+func (sf *WuiHx) Vals() IHxVals {
+	return sf.vals
+}
+
+// Url -- возвращает тэг URL
+func (sf *WuiHx) Url() IHxUrl {
+	return sf.url
+}
+
+// Trigger -- возвращает тэг триггера запроса
+func (sf *WuiHx) Trigger() IHxTrigger {
+	return sf.trigger
+}
+
+// Target -- возвращает объект цели замены
+func (sf *WuiHx) Target() IHxTarget {
+	return sf.target
+}
+
+// Oob -- возвращает тэг внеполосной замены
+func (sf *WuiHx) Oob() IHxSwapOob {
+	return sf.oob
+}
+
+// Swap -- возвращает тэг замены
+func (sf *WuiHx) Swap() IHxSwap {
+	return sf.swap
+}

+ 65 - 0
wui/whx/whx_test.go

@@ -0,0 +1,65 @@
+package whx
+
+import (
+	"testing"
+)
+
+type tester struct {
+	t  *testing.T
+	hx *WuiHx
+}
+
+func TestWhx(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+	sf.str()
+}
+
+// Получает строку атрибутов
+func (sf *tester) str() {
+	sf.t.Log("str")
+	sf.hx.Url().Method().Set("hx-put")
+	sf.hx.Trigger().Set("load")
+	sf.hx.Target().Set("#main")
+	sf.hx.Swap().Set("before")
+	sf.hx.Oob().Set("true")
+	sf.hx.Vals().Set("test", 3)
+	str := sf.hx.String()
+	_str := `hx-put="/wui/click/123" hx-trigger="load" hx-target="#main" hx-swap="before" hx-swap-oob="true" hx-vals='{"test":3}'`
+	if str != _str {
+		sf.t.Fatalf("str(): \n\t%v\n\t%v", str, _str)
+	}
+}
+
+// Создаёт новые тэги HTMX
+func (sf *tester) new() {
+	sf.t.Log("new")
+	hx := NewWuiHx("/wui/click/123")
+	if hx == nil {
+		sf.t.Fatalf("new(): IWuiHx==nil")
+	}
+	if vals := hx.Vals(); vals == nil {
+		sf.t.Fatalf("new(): vals==nil")
+	}
+	if url := hx.Url(); url == nil {
+		sf.t.Fatalf("new(): url==nil")
+	}
+	if trig := hx.Trigger(); trig == nil {
+		sf.t.Fatalf("trig(): trig==nil")
+	}
+	if targ := hx.Target(); targ == nil {
+		sf.t.Fatalf("trig(): targ==nil")
+	}
+	if targ := hx.Target(); targ == nil {
+		sf.t.Fatalf("trig(): targ==nil")
+	}
+	if oob := hx.Oob(); oob == nil {
+		sf.t.Fatalf("trig(): oob==nil")
+	}
+	if swap := hx.Swap(); swap == nil {
+		sf.t.Fatalf("trig(): swap==nil")
+	}
+	sf.hx = hx
+}

+ 3 - 0
wui/wlabel/wlabel.go

@@ -4,6 +4,7 @@ package wlabel
 import (
 	"strings"
 
+	"gitp78su.ipnodns.ru/svi/kern/wui/wctx"
 	"gitp78su.ipnodns.ru/svi/kern/wui/wtext"
 	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
 	"gitp78su.ipnodns.ru/svi/kern/wui/wwidget"
@@ -21,6 +22,8 @@ func NewWuiLabel(text string) *WuiLabel {
 		IWuiWidget: wwidget.NewWuiWidget(),
 		text:       wtext.NewWuiText(text),
 	}
+	wCtx := wctx.GetWuiCtx()
+	wCtx.Set(sf.Id(), sf, "WUI-метка")
 	_ = IWuiLabel(sf)
 	return sf
 }

+ 57 - 0
wui/wtypes/ihx_swap.go

@@ -0,0 +1,57 @@
+package wtypes
+
+// IHxSwap -- политика замены элемента (hx-swap)
+//
+//	Возможными значениями этого атрибута являются:
+//
+// innerHTML - Замените внутренний html-код целевого элемента
+//
+// outerHTML - Замените весь целевой элемент ответом
+//
+// textContent - Замените содержимое целевого элемента, не анализируя ответ как HTML
+//
+// beforebegin - Вставьте ответ перед целевым элементом
+//
+// afterbegin - Вставить ответ перед первым дочерним элементом целевого элемента
+//
+// beforeend - Вставить ответ после последнего дочернего элемента целевого элемента
+//
+// afterend - Вставьте ответ после целевого элемента
+//
+// delete - Удаляет целевой элемент независимо от ответа
+//
+// none- Не добавляет контент из ответа (внешние элементы всё равно будут обрабатываться).
+//
+//	Модификаторы
+//
+// Атрибуты hx-swap поддерживают модификаторы для изменения поведения обмена. Они
+// описаны ниже.
+//
+// Переходный период: transition
+// Вы можете изменить время ожидания htmx после получения ответа для замены содержимого,
+// добавив модификатор swap:
+//
+// <div hx-get="/example" hx-swap="innerHTML swap:1s">Get Some HTML & Append It</div>
+//
+// Название: ignoreTitle
+//
+// По умолчанию htmx обновляет заголовок страницы, если находит тег <title> в содержимом
+// ответа. Вы можете отключить это поведение, установив для параметра ignoreTitle
+// значение true.
+//
+//	Прокрутка: scroll & show
+//
+// Вы также можете изменить поведение прокрутки целевого элемента с помощью модификаторов
+// scroll и show, которые принимают значения top и bottom:
+//
+// hx-swap="beforeend scroll:bottom"
+//
+// hx-swap="innerHTML show:top"
+type IHxSwap interface {
+	// Get -- возвращает политику замены элемента
+	Get() string
+	// Set -- устанавливает политику замены элемента
+	Set(string)
+	// String -- возвращает строковое представление тэга
+	String() string
+}

+ 29 - 0
wui/wtypes/ihx_swap_oob.go

@@ -0,0 +1,29 @@
+package wtypes
+
+// IHxSwapOob -- внеполосная подкачка (hx-swap-oob)
+//
+// Атрибут 'hx-swap-oob' позволяет указать, что некоторый контент в ответе должен
+// быть добавлен в DOM не в целевом элементе, а в другом месте, то есть
+// «вне диапазона». Это позволяет добавлять обновления к другим элементам в ответе.
+//
+// В одном ответе может быть несколько целей внеполосной замены
+//
+//	Примеры
+//
+// <div id="alerts" hx-swap-oob="true">
+//
+// Saved!
+//
+// </div>
+//
+// hx-swap-oob="beforeend:#table2"
+//
+// hx-swap-oob="true"
+type IHxSwapOob interface {
+	// Get -- получает атрибут HTMX
+	Get() string
+	// Set -- устанавливает атрибут HTMX
+	Set(string)
+	// String -- возвращает строковое представление тэга
+	String() string
+}

+ 47 - 0
wui/wtypes/ihx_target.go

@@ -0,0 +1,47 @@
+package wtypes
+
+// IHxTarget -- атрибут цели HTMX (hx-target)
+//
+// Атрибут hx-target позволяет выбрать для замены другой элемент, отличный от того,
+// к которому был отправлен AJAX-запрос. Значение этого атрибута может быть:
+//
+//	Селектор CSS-запроса целевого элемента.
+//
+// this что указывает на то, что элемент, на котором находится атрибут hx-target,
+// является целевым.
+//
+// closest <CSS selector> который найдёт ближайший элемент-предок или сам элемент,
+// соответствующий заданному селектору CSS (например, closest tr выберет ближайшую
+// к элементу строку таблицы).
+//
+// find <CSS selector> который найдёт первый дочерний элемент, соответствующий
+// заданному селектору CSS.
+//
+// next который преобразуется в element.nextElementSibling
+//
+// next <CSS selector> который будет сканировать DOM в направлении вперёд в поисках
+// первого элемента, соответствующего заданному селектору CSS. (например,
+// next .error будет нацелен на ближайший следующий элемент с классом error)
+//
+// previous который преобразуется в element.previousElementSibling
+//
+// previous <CSS selector> который будет сканировать DOM в обратном направлении в
+// поисках первого элемента, соответствующего заданному селектору CSS. (например,
+// previous .error будет нацелен на ближайшего предыдущего брата с классом error)
+//
+//	Примеры
+//
+// hx-target="#response-div"
+//
+// В этом примере используется hx-target="this" для создания ссылки,
+// которая обновляется сама по себе при нажатии:
+//
+// <a hx-post="/new-link" hx-target="this" hx-swap="outerHTML">New link</a>
+type IHxTarget interface {
+	// Set -- устанавливает цель атрибута
+	Set(string)
+	// Get -- возвращает цель атрибута
+	Get() string
+	// String -- возвращает строковое значение тэга
+	String() string
+}

+ 157 - 0
wui/wtypes/ihx_trigger.go

@@ -0,0 +1,157 @@
+package wtypes
+
+// IHxTrigger -- атрибут триггера HTMX (hx-trigger)
+//
+// # Список событий разделяется запятыми
+//
+// click -- простой щелчок
+//
+// click[ctrlKey] -- щелчок с удержанием CTRL
+//
+// click[ctrlKey&&shiftKey]
+//
+// click[checkGlobalState()]
+//
+//	Модификаторы:
+//
+// once — событие будет срабатывать только один раз (например, при первом нажатии)
+//
+// changed — событие сработает только в том случае, если значение элемента изменилось.
+// Пожалуйста, обратите внимание, что change — это название события, а changed —
+// название модификатора.
+//
+// delay:<timing declaration> — перед тем как событие вызовет запрос, произойдёт
+// задержка. Если событие произойдёт снова, задержка сбросится.
+//
+// throttle:<timing declaration> — дросселирование произойдёт после того, как событие
+// вызовет запрос. Если событие произойдёт снова до завершения задержки, оно будет
+// проигнорировано, а элемент сработает в конце задержки.
+//
+// from:<Extended CSS selector> — позволяет событию, запускающему запрос, исходить от
+// другого элемента в документе (например, прослушивание события нажатия клавиши в теле
+// документа для поддержки горячих клавиш). Подробнее
+// https://htmx.org/attributes/hx-trigger/
+//
+// target:<CSS selector> — позволяет фильтровать события с помощью CSS-селектора по цели
+// события.
+//
+//	queue:<queue option> — определяет, как события ставятся в очередь, если событие
+//
+// происходит во время выполнения запроса на другое событие. Возможные варианты:
+//
+// first - поставить в очередь первое событие
+//
+// last - поставить в очередь последнее событие (по умолчанию)
+//
+// all - поставить в очередь все события (отправить запрос для каждого события)
+//
+// none - не ставить в очередь новые события
+//
+//	Основные события DOM
+//
+// click - Срабатывает при клике на элемент.
+//
+// dblclick - Срабатывает при двойном клике на элемент.
+//
+// mouseenter - Срабатывает, когда курсор мыши наводится на элемент.
+//
+// mousemove -- Срабатывает, когда мышь двигается по элементу
+//
+// mouseleave - Срабатывает, когда курсор мыши покидает элемент.
+//
+// mouseover - Срабатывает, когда курсор мыши наводится на элемент или его потомков.
+//
+// mouseout - Срабатывает, когда курсор мыши покидает элемент или его потомков.
+//
+// mousedown - Срабатывает при нажатии кнопки мыши на элементе.
+//
+// mouseup - Срабатывает при отпускании кнопки мыши на элементе.
+//
+// keydown - Срабатывает при нажатии клавиши на клавиатуре.
+//
+// keyup - Срабатывает при отпускании клавиши на клавиатуре.
+//
+// keypress - Срабатывает при нажатии и удержании клавиши на клавиатуре.
+//
+// focus - Срабатывает при фокусировке на элементе.
+//
+// blur - Срабатывает при потере фокуса элементом.
+//
+// change - Срабатывает при изменении значения элемента (например, в input или select).
+//
+// input - Срабатывает при вводе данных в элемент (например, в input или textarea).
+//
+// submit - Срабатывает при отправке формы.
+//
+// load - Срабатывает при загрузке элемента (например, изображения).
+//
+// scroll - Срабатывает при прокрутке элемента.
+//
+// resize - Срабатывает при изменении размеров окна.
+//
+//	Нестандартные события
+//
+// load - срабатывает при загрузке (полезно для отложенной загрузки чего-либо)
+//
+// revealed - срабатывает при прокрутке элемента в область просмотра (также полезно
+// для отложенной загрузки). См. документацию.
+//
+// intersect — срабатывает один раз, когда элемент впервые пересекает область просмотра.
+//
+//	Поддерживает два дополнительных параметра:
+//
+// root:<selector> - CSS-селектор корневого элемента для пересечения
+//
+// threshold:<float> — число с плавающей запятой от 0,0 до 1,0, указывающее,
+// при каком проценте пересечения должно срабатывать событие
+//
+// htmx:afterRequest - Срабатывает после завершения AJAX-запроса.
+//
+// htmx:beforeRequest - Срабатывает перед отправкой AJAX-запроса.
+//
+// htmx:configRequest - Срабатывает перед настройкой AJAX-запроса.
+//
+// htmx:afterSettle - Срабатывает после применения изменений в DOM.
+//
+// htmx:afterSwap - Срабатывает после замены содержимого в DOM.
+//
+//	Примеры
+//
+// hx-trigger="every 1s">
+//
+// hx-trigger="load, click delay:1s"></div>
+//
+// В этом примере ресурс будет загружаться сразу после загрузки страницы, а затем
+// снова с задержкой в одну секунду после каждого нажатия.
+//
+// <div hx-trigger="mouseenter delay:500ms" hx-get="/example">Hover me</div>
+//
+//	Указывает элемент, от которого будет считываться событие:
+//
+// <div hx-trigger="click from:#button" hx-get="/example">Click the button</div>
+//
+// <button id="button">Click me</button>
+//
+//	Получение абсолютных координат курсора
+//
+// <div hx-trigger="mousemove"  hx-get="/get-coordinates"
+//
+//	hx-vals='js:{x: event.clientX, y: event.clientY}'
+//
+//	style="width: 300px; height: 200px; border: 1px solid black;">
+//
+//	Наведите курсор на меня
+//
+// </div>
+//
+//	Для относительной позиции:
+//
+// hx-vals='js:{x: event.offsetX, y: event.offsetY}'
+type IHxTrigger interface {
+	// Set -- устанавливает триггер атрибута
+	Set(string)
+	// Get -- возвращает триггер атрибута
+	Get() string
+	// String -- возвращает строковое значение тэга
+	String() string
+}

+ 11 - 0
wui/wtypes/ihx_url.go

@@ -0,0 +1,11 @@
+package wtypes
+
+// IHxUrl -- атрибут метода HTMX
+type IHxUrl interface {
+	// Method -- возвращает метод атрибута
+	Method() IHxUrlMethod
+	// Patch -- возвращает путь атрибута
+	Patch() IHxUrlPatch
+	// String -- возвращает полное значение
+	String() string
+}

+ 15 - 0
wui/wtypes/ihx_url_method.go

@@ -0,0 +1,15 @@
+package wtypes
+
+// IHxUrlMethod -- атрибут метода HTMX
+//
+// hx-get    // READ
+// hx-post   // CREATE (универсальный, по умолчанию)
+// hx-patch  // UPDATE
+// hx-put    // UPDATE PARTIAL
+// hx-delete // DELETE
+type IHxUrlMethod interface {
+	// Get -- возвращает метод атрибута
+	Get() string
+	// Set -- устанавливает метод атрибута
+	Set(string)
+}

+ 9 - 0
wui/wtypes/ihx_url_patch.go

@@ -0,0 +1,9 @@
+package wtypes
+
+// IHxUrlPatch -- атрибут пути HTMX
+type IHxUrlPatch interface {
+	// Get -- возвращает путь атрибута
+	Get() string
+	// Set -- устанавливает путь атрибута
+	Set(string)
+}

+ 30 - 0
wui/wtypes/ihx_vals.go

@@ -0,0 +1,30 @@
+package wtypes
+
+// IHxVals -- словарь значений в элементе HTMX (hx-vals)
+//
+// Атрибут hx-vals позволяет добавлять параметры, которые будут отправляться с запросом AJAX.
+//
+// По умолчанию значением этого атрибута является список значений имени-выражения
+// в формате JSON
+//
+//	Примеры
+//
+// hx-vals='{"myVal": "My Value"}'
+//
+// hx-vals='js:{lastKey: event.key}'
+//
+// hx-vals='js:{x: event.clientX, y: event.clientY}'
+type IHxVals interface {
+	// Get --  возвращает элемент словаря
+	Get(key string) any
+	// Set -- устанавливает элемент словаря
+	Set(key string, val any)
+	// Del -- удаляет элемент из словаря
+	Del(key string)
+	// Clear -- очищает весь словарь
+	Clear()
+	// Len -- возвращает размер словаря
+	Len() int
+	// String -- возвращает строковое представление тэга
+	String() string
+}

+ 12 - 0
wui/wtypes/iwui_button.go

@@ -0,0 +1,12 @@
+package wtypes
+
+// IWuiButton -- WUI-кнопка
+type IWuiButton interface {
+	IWuiWidget
+	// Text -- возвращает текст кнопки
+	Text() IWuiText
+	// Click -- нажатие кнопки
+	Click() string
+	// Hx -- атрибуты HTMX
+	Hx() IWuiHx
+}

+ 19 - 0
wui/wtypes/iwui_hx.go

@@ -0,0 +1,19 @@
+package wtypes
+
+// IWuiHx -- атрибуты HTMX
+type IWuiHx interface {
+	// Url -- возвращает URL HTMX
+	Url() IHxUrl
+	// Trigger -- возвращает триггер HTMX
+	Trigger() IHxTrigger
+	// Target -- возвращает цель HTMX
+	Target() IHxTarget
+	// Swap -- политика замены элемента
+	Swap() IHxSwap
+	// Oob -- политика внеполосной подкачки
+	Oob() IHxSwapOob
+	// Vals -- словарь дополнительных значений
+	Vals() IHxVals
+	// String -- возвращает строку тэгов
+	String() string
+}

+ 18 - 0
wui/wui.go

@@ -1,2 +1,20 @@
 // package wui -- пакет веб-интерфейса
 package wui
+
+import (
+	"gitp78su.ipnodns.ru/svi/kern/wui/wbutton"
+	"gitp78su.ipnodns.ru/svi/kern/wui/wctx"
+	. "gitp78su.ipnodns.ru/svi/kern/wui/wtypes"
+)
+
+// NewWuiButton -- возвращает новую WUI-кнопку
+func NewWuiButton(text string, fnClick func() string) IWuiButton {
+	btn := wbutton.NewWuiButton(text, fnClick)
+	return btn
+}
+
+// GetWuiCtx -- возвращает контекст WUI
+func GetWuiCtx() IWuiCtx {
+	wCtx := wctx.GetWuiCtx()
+	return wCtx
+}

+ 35 - 0
wui/wui_test.go

@@ -0,0 +1,35 @@
+package wui
+
+import (
+	"testing"
+)
+
+type tester struct {
+	t *testing.T
+}
+
+func TestWui(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.get()
+}
+
+// Получает различные WUI-компоненты
+func (sf *tester) get() {
+	sf.t.Log("get")
+	wCtx := GetWuiCtx()
+	if wCtx == nil {
+		sf.t.Fatalf("get(): IWuiCtx==nil")
+	}
+	wBtn := NewWuiButton("test_btn", sf.fnClick)
+	if wBtn == nil {
+		sf.t.Fatalf("get(): IWuiButton==nil")
+	}
+}
+
+// Функция обратного вызова
+func (sf *tester) fnClick() string {
+	sf.t.Log("fnClick")
+	return "test_click"
+}