Skip to content

Commit

Permalink
✨ Add UI server for rendering YAML UI definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
wesen committed Feb 21, 2025
1 parent bfa10d7 commit 3bf8a94
Show file tree
Hide file tree
Showing 8 changed files with 1,345 additions and 3 deletions.
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,72 @@ go-go-mcp help <topic>
# Show examples for a topic
go-go-mcp help <topic> --example
```

# UI DSL

A simple YAML-based Domain Specific Language for defining user interfaces.

## Components

The DSL supports the following basic components:

### Button
- `text`: Button label
- `type`: primary, secondary, danger, success
- `onclick`: JavaScript event handler

### Title
- `content`: Heading text content

### Text
- `content`: Text content

### Input
- `type`: text, email, password, number, tel
- `placeholder`: Placeholder text
- `value`: Default value
- `required`: Boolean

### Textarea
- `placeholder`: Placeholder text
- `rows`: Number of rows
- `cols`: Number of columns
- `value`: Default value

### Checkbox
- `label`: Checkbox label
- `checked`: Boolean
- `required`: Boolean
- `name`: Form field name

### List
- `type`: ul or ol
- `items`: Array of items or nested components

## Common Attributes

All components support these common attributes:
- `id`: Unique identifier
- `style`: Inline CSS
- `disabled`: Boolean
- `data`: Map of data attributes

## Example

```yaml
form:
id: signup-form
components:
- title:
content: Sign Up
- text:
content: Please fill in your details below.
- input:
type: email
placeholder: Email address
- button:
text: Submit
type: primary
```
See `ui-dsl.yaml` for more comprehensive examples.
26 changes: 25 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -1122,4 +1122,28 @@ Added proper request ID handling to transport package:
Added helper functions for converting between string and JSON-RPC ID types:
- Added StringToID to convert string to json.RawMessage
- Added IDToString to convert json.RawMessage to string
- Improved type safety in ID handling across transports
- Improved type safety in ID handling across transports

# Simplified UI DSL

Simplified the UI DSL by removing class attributes and creating distinct title and text elements:
- Removed class attributes from all components
- Added dedicated title element for headings
- Simplified text element to be just for paragraphs
- Updated documentation to reflect changes

# UI DSL Implementation

Created a YAML-based UI DSL for defining simple user interfaces. The DSL supports common UI components like buttons, text, inputs, textareas, checkboxes, and lists with a clean and intuitive syntax.

- Added `ui-dsl.yaml` with component definitions and examples
- Added documentation in `README.md`
- Included support for common attributes across all components
- Added nested component support for complex layouts

# UI Server Implementation
Added a new UI server that can render YAML UI definitions using HTMX and Bootstrap:
- Created a new command `ui-server` that serves UI definitions from YAML files
- Implemented templ templates for rendering UI components
- Added support for various UI components like buttons, inputs, forms, etc.
- Used HTMX for dynamic interactions and Bootstrap for styling
34 changes: 34 additions & 0 deletions cmd/ui-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package main

import (
"context"
"fmt"
"os"

"github.com/spf13/cobra"
)

func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

var rootCmd = &cobra.Command{
Use: "ui-server [directory]",
Short: "Start a UI server that renders YAML UI definitions",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
dir := args[0]
port, _ := cmd.Flags().GetInt("port")

server := NewServer(dir)
return server.Start(ctx, port)
},
}

func init() {
rootCmd.Flags().IntP("port", "p", 8080, "Port to run the server on")
}
111 changes: 111 additions & 0 deletions cmd/ui-server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package main

import (
"context"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"strings"

"gopkg.in/yaml.v3"
)

type Server struct {
dir string
pages map[string]UIDefinition
}

type UIDefinition struct {
Components map[string]interface{} `yaml:",inline"`
}

func NewServer(dir string) *Server {
return &Server{
dir: dir,
pages: make(map[string]UIDefinition),
}
}

func (s *Server) Start(ctx context.Context, port int) error {
if err := s.loadPages(); err != nil {
return fmt.Errorf("failed to load pages: %w", err)
}

mux := http.NewServeMux()

// Serve static files
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))

// Index page
mux.HandleFunc("/", s.handleIndex())

// Individual pages
for name := range s.pages {
pagePath := "/" + strings.TrimSuffix(name, ".yaml")
mux.HandleFunc(pagePath, s.handlePage(name))
}

addr := fmt.Sprintf(":%d", port)
log.Printf("Starting server on %s", addr)
return http.ListenAndServe(addr, mux)
}

func (s *Server) loadPages() error {
return filepath.WalkDir(s.dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}

if !d.IsDir() && strings.HasSuffix(d.Name(), ".yaml") {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", path, err)
}

var def UIDefinition
if err := yaml.Unmarshal(data, &def); err != nil {
return fmt.Errorf("failed to parse YAML in %s: %w", path, err)
}

relPath, err := filepath.Rel(s.dir, path)
if err != nil {
return fmt.Errorf("failed to get relative path for %s: %w", path, err)
}

s.pages[relPath] = def
}
return nil
})
}

func (s *Server) handleIndex() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}

component := indexTemplate(s.pages)
if err := component.Render(r.Context(), w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}

func (s *Server) handlePage(name string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
def, ok := s.pages[name]
if !ok {
http.NotFound(w, r)
return
}

component := pageTemplate(name, def)
if err := component.Render(r.Context(), w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
Loading

0 comments on commit 3bf8a94

Please sign in to comment.