From 5143c211d341a4bbd63c93559fdd86670dea4b43 Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 14 Mar 2026 09:49:41 +0000 Subject: [PATCH] feat: modernise template with core/go-api provider pattern Replace Cobra CLI + raw net/http with core/go-api Engine. DemoProvider implements RouteGroup for plug-and-play registration. Lit element updated to fetch from Go API. Add .core/build.yaml and CLAUDE.md. Co-Authored-By: Virgil --- .core/build.yaml | 7 ++++ CLAUDE.md | 43 +++++++++++++++++++++ README.md | 73 +++++++++++++++++++---------------- cmd/demo-cli/cmd/root.go | 24 ------------ cmd/demo-cli/cmd/serve.go | 33 ---------------- cmd/demo-cli/main.go | 9 ----- go.mod | 10 ++--- go_dev_server.log | 3 -- main.go | 72 ++++++++++++++++++++++++++++++++++ provider.go | 42 ++++++++++++++++++++ static.go | 17 ++++++++ ui/index.html | 4 +- ui/src/index.ts | 81 +++++++++++++++++++++++++++++++-------- 13 files changed, 295 insertions(+), 123 deletions(-) create mode 100644 .core/build.yaml create mode 100644 CLAUDE.md delete mode 100644 cmd/demo-cli/cmd/root.go delete mode 100644 cmd/demo-cli/cmd/serve.go delete mode 100644 cmd/demo-cli/main.go delete mode 100644 go_dev_server.log create mode 100644 main.go create mode 100644 provider.go create mode 100644 static.go diff --git a/.core/build.yaml b/.core/build.yaml new file mode 100644 index 0000000..dd06162 --- /dev/null +++ b/.core/build.yaml @@ -0,0 +1,7 @@ +project: element-template +binary: core-element-template +targets: + - os: darwin + arch: arm64 + - os: linux + arch: amd64 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8da6842 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,43 @@ +# CLAUDE.md + +This file provides guidance to Claude Code when working with this repository. + +## Project Overview + +Starter template for building custom HTML elements backed by a Go API using the Core ecosystem. Clone this to create a new service provider that can plug into core/ide. + +## Build & Development + +```bash +# Build UI (Lit custom element) +cd ui && npm install && npm run build + +# Run development server +go run . --port 8080 + +# Build binary +core build + +# Quality assurance +core go qa +``` + +## Architecture + +- **`main.go`** — Creates `api.Engine`, registers providers, serves embedded UI +- **`provider.go`** — Example `DemoProvider` implementing `api.RouteGroup` +- **`static.go`** — Static file serving helper +- **`ui/`** — Lit custom element that talks to the Go API via fetch + +## Creating Your Own Provider + +1. Rename `DemoProvider` in `provider.go` +2. Update `Name()`, `BasePath()`, and routes in `RegisterRoutes()` +3. Update the Lit element in `ui/src/index.ts` to match your API +4. Update the custom element tag in `ui/index.html` + +## Conventions + +- UK English in all user-facing strings +- EUPL-1.2 licence +- Conventional commits diff --git a/README.md b/README.md index bed095c..204784d 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,52 @@ # Core Element Template -This repository is a template for developers to create custom HTML elements for the core web3 framework. It includes a Go backend, a Lit-based custom element, and a full release cycle configuration. +Starter template for building custom HTML elements backed by a Go API. Part of the [Core ecosystem](https://core.help). -## Getting Started - -1. **Clone the repository:** - ```bash - git clone https://github.com/your-username/core-element-template.git - ``` - -2. **Install the dependencies:** - ```bash - cd core-element-template - go mod tidy - cd ui - npm install - ``` - -3. **Run the development server:** - ```bash - go run ./cmd/demo-cli serve - ``` - This will start the Go backend and serve the Lit custom element. - -## Building the Custom Element - -To build the Lit custom element, run the following command: +## Quick Start ```bash -cd ui -npm run build +# Clone and rename +git clone https://forge.lthn.ai/core/element-template.git my-element +cd my-element + +# Install UI dependencies and build +cd ui && npm install && npm run build && cd .. + +# Run +go run . --port 8080 ``` -This will create a JavaScript file in the `dist` directory that you can use in any HTML page. +Open `http://localhost:8080` — you'll see the `` fetching data from the Go API. -## Contributing +## What's Included -Contributions are welcome! Please feel free to submit a Pull Request. +| Component | Technology | Purpose | +|-----------|-----------|---------| +| Go backend | core/go-api (Gin) | REST API with CORS, middleware | +| Custom element | Lit 3 | Self-contained web component | +| Build config | `.core/build.yaml` | Cross-platform binary builds | -## License +## Making It Yours -This project is licensed under the EUPL-1.2 License - see the [LICENSE](LICENSE) file for details. +1. Update `go.mod` module path +2. Rename `DemoProvider` in `provider.go` — implement your API +3. Rename `CoreDemoElement` in `ui/src/index.ts` — implement your UI +4. Update the element tag in `ui/index.html` + +## Service Provider Pattern + +The `DemoProvider` implements `api.RouteGroup`: + +```go +func (p *DemoProvider) Name() string { return "demo" } +func (p *DemoProvider) BasePath() string { return "/api/v1/demo" } +func (p *DemoProvider) RegisterRoutes(rg *gin.RouterGroup) { + rg.GET("/hello", p.hello) +} +``` + +Register it with `engine.Register(&DemoProvider{})` and it gets middleware, CORS, and OpenAPI for free. The same provider can plug into core/ide's registry. + +## Licence + +EUPL-1.2 diff --git a/cmd/demo-cli/cmd/root.go b/cmd/demo-cli/cmd/root.go deleted file mode 100644 index d7ef5ff..0000000 --- a/cmd/demo-cli/cmd/root.go +++ /dev/null @@ -1,24 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" -) - -var rootCmd = &cobra.Command{ - Use: "demo-cli", - Short: "A demo CLI for the core-element-template", - Long: `A longer description that spans multiple lines and likely contains examples and usage of using your application.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Hello from the demo CLI!") - }, -} - -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} diff --git a/cmd/demo-cli/cmd/serve.go b/cmd/demo-cli/cmd/serve.go deleted file mode 100644 index 9cde5d2..0000000 --- a/cmd/demo-cli/cmd/serve.go +++ /dev/null @@ -1,33 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - "net/http" - - "github.com/spf13/cobra" -) - -var serveCmd = &cobra.Command{ - Use: "serve", - Short: "Starts the HTTP server", - Long: `Starts the HTTP server to serve the frontend and the API.`, - Run: func(cmd *cobra.Command, args []string) { - http.HandleFunc("/api/v1/demo", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello, world!") - }) - - fs := http.FileServer(http.Dir("./ui/dist")) - http.Handle("/", fs) - - log.Println("Listening on :8080...") - err := http.ListenAndServe(":8080", nil) - if err != nil { - log.Fatal(err) - } - }, -} - -func init() { - rootCmd.AddCommand(serveCmd) -} diff --git a/cmd/demo-cli/main.go b/cmd/demo-cli/main.go deleted file mode 100644 index 0ac7f55..0000000 --- a/cmd/demo-cli/main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/your-username/core-element-template/cmd/demo-cli/cmd" -) - -func main() { - cmd.Execute() -} diff --git a/go.mod b/go.mod index a44a71a..e84eb74 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ -module github.com/your-username/core-element-template +module forge.lthn.ai/core/element-template -go 1.24.3 +go 1.26.0 require ( - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/cobra v1.10.1 // indirect - github.com/spf13/pflag v1.0.10 // indirect + forge.lthn.ai/core/go-api v0.1.0 + forge.lthn.ai/core/go-log v0.0.1 + forge.lthn.ai/core/go-ws v0.1.3 ) diff --git a/go_dev_server.log b/go_dev_server.log deleted file mode 100644 index 3074ebd..0000000 --- a/go_dev_server.log +++ /dev/null @@ -1,3 +0,0 @@ -2025/11/14 15:44:19 Listening on :8080... -2025/11/14 15:44:19 listen tcp :8080: bind: address already in use -exit status 1 diff --git a/main.go b/main.go new file mode 100644 index 0000000..a5dca2a --- /dev/null +++ b/main.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: EUPL-1.2 + +// core-element-template demonstrates the service provider pattern. +// It serves a Lit custom element backed by a Go API using core/go-api. +// +// Usage: +// +// go run . [--port 8080] +package main + +import ( + "context" + "embed" + "flag" + "io/fs" + "os" + "os/signal" + "syscall" + + api "forge.lthn.ai/core/go-api" + "forge.lthn.ai/core/go-log" + "github.com/gin-gonic/gin" +) + +//go:embed all:ui/dist +var uiAssets embed.FS + +func main() { + port := flag.String("port", "8080", "HTTP server port") + flag.Parse() + + logger := log.New("element-template") + + // Create API engine with middleware + engine, err := api.New( + api.WithCORS(), + ) + if err != nil { + logger.Error("failed to create API engine", "error", err) + os.Exit(1) + } + + // Register the demo provider + engine.Register(&DemoProvider{}) + + // Serve the Lit custom element UI as static files + staticFS, err := fs.Sub(uiAssets, "ui/dist") + if err != nil { + logger.Error("failed to load UI assets", "error", err) + os.Exit(1) + } + engine.Router().NoRoute(gin.WrapH( + noCache(staticFS), + )) + + // Start server + ctx, cancel := signal.NotifyContext(context.Background(), + syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + addr := ":" + *port + logger.Info("starting server", "addr", addr) + + go func() { + if err := engine.Serve(ctx, addr); err != nil { + logger.Error("server error", "error", err) + } + }() + + <-ctx.Done() + logger.Info("shutting down") +} diff --git a/provider.go b/provider.go new file mode 100644 index 0000000..bc4e864 --- /dev/null +++ b/provider.go @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package main + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +// DemoProvider is an example service provider that exposes a REST API. +// Replace this with your own provider implementation. +type DemoProvider struct{} + +// Name implements api.RouteGroup. +func (p *DemoProvider) Name() string { return "demo" } + +// BasePath implements api.RouteGroup. +func (p *DemoProvider) BasePath() string { return "/api/v1/demo" } + +// RegisterRoutes implements api.RouteGroup. +func (p *DemoProvider) RegisterRoutes(rg *gin.RouterGroup) { + rg.GET("/hello", p.hello) + rg.GET("/status", p.status) +} + +func (p *DemoProvider) hello(c *gin.Context) { + name := c.DefaultQuery("name", "World") + c.JSON(http.StatusOK, gin.H{ + "message": "Hello, " + name + "!", + }) +} + +func (p *DemoProvider) status(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "running", + "uptime": time.Since(startTime).String(), + }) +} + +var startTime = time.Now() diff --git a/static.go b/static.go new file mode 100644 index 0000000..ffbba49 --- /dev/null +++ b/static.go @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package main + +import ( + "io/fs" + "net/http" +) + +// noCache wraps a filesystem with cache-busting headers for development. +func noCache(fsys fs.FS) http.Handler { + fileServer := http.FileServer(http.FS(fsys)) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + fileServer.ServeHTTP(w, r) + }) +} diff --git a/ui/index.html b/ui/index.html index d3cdf80..3260366 100644 --- a/ui/index.html +++ b/ui/index.html @@ -3,10 +3,10 @@ - Hello World + Core Element Demo - + diff --git a/ui/src/index.ts b/ui/src/index.ts index 5181f35..f6e4756 100644 --- a/ui/src/index.ts +++ b/ui/src/index.ts @@ -1,30 +1,81 @@ import { LitElement, html, css } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { customElement, property, state } from 'lit/decorators.js'; /** - * A simple "Hello World" custom element. + * A demo custom element backed by a Go API. + * Replace this with your own element implementation. * - * @element hello-world-element + * @element core-demo-element */ -@customElement('hello-world-element') -export class HelloWorldElement extends LitElement { - /** - * The name to say hello to. - * @attr - */ - @property({ type: String }) - name = 'World'; +@customElement('core-demo-element') +export class CoreDemoElement extends LitElement { + /** The API base URL. Defaults to current origin. */ + @property({ type: String, attribute: 'api-url' }) + apiUrl = ''; + + @state() + private message = 'Loading...'; + + @state() + private uptime = ''; static styles = css` :host { display: block; - border: solid 1px gray; - padding: 16px; - max-width: 800px; + font-family: system-ui, -apple-system, sans-serif; + padding: 1.5rem; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + max-width: 480px; + } + h2 { + margin: 0 0 1rem; + font-size: 1.25rem; + color: #1a1b26; + } + .status { + font-size: 0.875rem; + color: #64748b; + } + .badge { + display: inline-block; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + background: #dcfce7; + color: #166534; } `; + connectedCallback() { + super.connectedCallback(); + this.fetchData(); + } + + private async fetchData() { + const base = this.apiUrl || window.location.origin; + try { + const [helloRes, statusRes] = await Promise.all([ + fetch(`${base}/api/v1/demo/hello`), + fetch(`${base}/api/v1/demo/status`), + ]); + const hello = await helloRes.json(); + const status = await statusRes.json(); + this.message = hello.message; + this.uptime = status.uptime; + } catch { + this.message = 'Failed to connect to API'; + } + } + render() { - return html`

Hello, ${this.name}!

`; + return html` +

${this.message}

+
+ running + Uptime: ${this.uptime} +
+ `; } }