From 4040333a19db922ccdadb9fc2160f89edc87ab0f Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Sat, 28 Feb 2026 18:12:17 +0300 Subject: [PATCH] Integrate ui-design-code via submodule and runtime assets --- .gitmodules | 3 + README.md | 15 ++++ internal/api/ui.go | 1 + internal/api/ui_design_assets.go | 134 +++++++++++++++++++++++++++++++ internal/api/ui_shared.tmpl | 2 + tools/ui-design-code | 1 + 6 files changed, 156 insertions(+) create mode 100644 .gitmodules create mode 100644 internal/api/ui_design_assets.go create mode 160000 tools/ui-design-code diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c0d6db3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tools/ui-design-code"] + path = tools/ui-design-code + url = ../ui-design-code diff --git a/README.md b/README.md index 32db7fa..731380c 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,18 @@ Architecture documentation was moved to the Project Bible: - `bible/README.md` + +## UI design-code integration + +`core` now serves UI theme assets from `ui-design-code` at runtime: + +- CSS: `/ui-design/static/css/theme.css` +- JS: `/ui-design/static/js/app.js` + +Path resolution order for `ui-design-code`: + +1. `UI_DESIGN_CODE_DIR` env var +2. `./tools/ui-design-code` +3. `../ui-design-code` + +This lets UI updates in `ui-design-code` appear in `core` without copying files. diff --git a/internal/api/ui.go b/internal/api/ui.go index 888424f..a7f537f 100644 --- a/internal/api/ui.go +++ b/internal/api/ui.go @@ -161,6 +161,7 @@ type uiHandlers struct { func RegisterUIRoutes(mux *http.ServeMux, deps UIDependencies) { h := uiHandlers{deps: deps} + registerUIDesignRoutes(mux) mux.HandleFunc("/", h.handleStart) mux.HandleFunc("/ui", h.handleIndex) mux.HandleFunc("/ui/", h.handleIndex) diff --git a/internal/api/ui_design_assets.go b/internal/api/ui_design_assets.go new file mode 100644 index 0000000..3f1f068 --- /dev/null +++ b/internal/api/ui_design_assets.go @@ -0,0 +1,134 @@ +package api + +import ( + "errors" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" +) + +type uiDesignAssets struct { + RootPath string + CSSPath string + JSPath string +} + +func registerUIDesignRoutes(mux *http.ServeMux) { + assets, err := resolveUIDesignAssets() + if err != nil { + log.Printf("ui design assets: %v", err) + return + } + log.Printf("ui design assets: using %s", assets.RootPath) + + mux.HandleFunc("/ui-design/static/css/theme.css", func(w http.ResponseWriter, r *http.Request) { + serveUIDesignFile(w, r, "/ui-design/static/css/theme.css", assets.CSSPath, "text/css; charset=utf-8") + }) + if assets.JSPath != "" { + mux.HandleFunc("/ui-design/static/js/app.js", func(w http.ResponseWriter, r *http.Request) { + serveUIDesignFile(w, r, "/ui-design/static/js/app.js", assets.JSPath, "application/javascript; charset=utf-8") + }) + } +} + +func serveUIDesignFile(w http.ResponseWriter, r *http.Request, routePath, filePath, contentType string) { + if r.URL.Path != routePath { + http.NotFound(w, r) + return + } + if r.Method != http.MethodGet && r.Method != http.MethodHead { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + if _, err := os.Stat(filePath); err != nil { + http.NotFound(w, r) + return + } + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Content-Type", contentType) + http.ServeFile(w, r, filePath) +} + +func resolveUIDesignAssets() (uiDesignAssets, error) { + roots := candidateUIDesignRoots() + if len(roots) == 0 { + return uiDesignAssets{}, errors.New("no ui-design-code path candidates") + } + + cssSuffixes := []string{ + filepath.Join("demo", "web", "static", "css", "app.css"), + filepath.Join("kit", "patterns", "theme-vapor", "static", "vapor.css"), + filepath.Join("kit", "patterns", "theme-aqua-legacy", "demo-aqua-freeze.css"), + } + jsSuffixes := []string{ + filepath.Join("demo", "web", "static", "js", "app.js"), + } + + for _, root := range roots { + cssPath := firstExistingFile(root, cssSuffixes) + if cssPath == "" { + continue + } + jsPath := firstExistingFile(root, jsSuffixes) + return uiDesignAssets{ + RootPath: root, + CSSPath: cssPath, + JSPath: jsPath, + }, nil + } + + return uiDesignAssets{}, fmt.Errorf("ui-design-code not found in candidates: %s", strings.Join(roots, ", ")) +} + +func candidateUIDesignRoots() []string { + seen := make(map[string]struct{}) + var roots []string + + push := func(path string) { + if strings.TrimSpace(path) == "" { + return + } + abs, err := filepath.Abs(path) + if err != nil { + return + } + abs = filepath.Clean(abs) + if _, ok := seen[abs]; ok { + return + } + info, err := os.Stat(abs) + if err != nil || !info.IsDir() { + return + } + seen[abs] = struct{}{} + roots = append(roots, abs) + } + + push(os.Getenv("UI_DESIGN_CODE_DIR")) + push(filepath.Join("tools", "ui-design-code")) + push(filepath.Join("..", "ui-design-code")) + + if _, file, _, ok := runtime.Caller(0); ok { + apiDir := filepath.Dir(file) + coreRoot := filepath.Clean(filepath.Join(apiDir, "..", "..")) + push(filepath.Join(coreRoot, "tools", "ui-design-code")) + push(filepath.Join(coreRoot, "..", "ui-design-code")) + } + + return roots +} + +func firstExistingFile(root string, suffixes []string) string { + for _, suffix := range suffixes { + path := filepath.Join(root, suffix) + info, err := os.Stat(path) + if err == nil && !info.IsDir() { + return path + } + } + return "" +} diff --git a/internal/api/ui_shared.tmpl b/internal/api/ui_shared.tmpl index 540b2af..0fa6a5d 100644 --- a/internal/api/ui_shared.tmpl +++ b/internal/api/ui_shared.tmpl @@ -822,6 +822,8 @@ } } + +