Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
161 lines
3.8 KiB
Go
161 lines
3.8 KiB
Go
// Package gallery shows every component live.
|
|
package gallery
|
|
|
|
import (
|
|
"embed"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"io/fs"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
)
|
|
|
|
// Entry is one component's page.
|
|
type Entry struct {
|
|
ID int
|
|
Name string
|
|
Category string
|
|
Summary string
|
|
Demo func(c *ui.Context, log func(string))
|
|
}
|
|
|
|
// Category is a component package shown in the sidebar.
|
|
type Category struct {
|
|
Key, Name, Icon string
|
|
}
|
|
|
|
var Categories = []Category{
|
|
{"layout", "Layout", "layout-grid"},
|
|
{"typography", "Typography", "type"},
|
|
{"button", "Button", "mouse-pointer-click"},
|
|
{"display", "Display", "id-card"},
|
|
{"input", "Input", "text-cursor-input"},
|
|
{"choice", "Choice", "list-checks"},
|
|
{"feedback", "Feedback", "message-square-warning"},
|
|
{"motion", "Motion", "move"},
|
|
{"fx", "Effects", "sparkles"},
|
|
{"form", "Form", "clipboard-list"},
|
|
{"datetime", "Date & Time", "calendar-clock"},
|
|
{"color", "Color", "palette"},
|
|
{"navigation", "Navigation", "compass"},
|
|
{"overlay", "Overlay", "layers"},
|
|
{"data", "Data", "table"},
|
|
{"chart", "Chart", "chart-column"},
|
|
{"media", "Media", "image"},
|
|
{"chat", "Chat", "message-circle"},
|
|
{"agent", "Agent", "bot"},
|
|
{"code", "Code", "code"},
|
|
{"files", "Files", "folder"},
|
|
{"shop", "Shop", "shopping-cart"},
|
|
{"finance", "Finance", "wallet"},
|
|
{"dashboard", "Dashboard", "layout-dashboard"},
|
|
{"schedule", "Schedule", "calendar-days"},
|
|
{"project", "Project", "kanban"},
|
|
{"mail", "Mail", "mail"},
|
|
{"social", "Social", "users"},
|
|
{"account", "Account", "user-round"},
|
|
{"devtools", "Devtools", "terminal"},
|
|
{"game", "Game", "gamepad-2"},
|
|
{"learn", "Learn", "graduation-cap"},
|
|
{"health", "Health", "heart-pulse"},
|
|
{"home", "Home", "house"},
|
|
{"marketing", "Marketing", "megaphone"},
|
|
{"content", "Content", "book-open"},
|
|
{"desktop", "Desktop", "monitor"},
|
|
{"geo", "Geo", "map-pin"},
|
|
}
|
|
|
|
// Entries lists every registered component in ID order.
|
|
func Entries() []Entry { return entries }
|
|
|
|
func category(key string) (Category, bool) {
|
|
for _, k := range Categories {
|
|
if k.Key == key {
|
|
return k, true
|
|
}
|
|
}
|
|
return Category{}, false
|
|
}
|
|
|
|
func inCategory(key string) []Entry {
|
|
var out []Entry
|
|
for _, e := range entries {
|
|
if e.Category == key {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Slug turns KPIGrid into kpi-grid.
|
|
func (e Entry) Slug() string {
|
|
r := []rune(e.Name)
|
|
var b strings.Builder
|
|
for i, c := range r {
|
|
if i > 0 && unicode.IsUpper(c) {
|
|
prev := r[i-1]
|
|
endsAcronym := unicode.IsUpper(prev) && i+1 < len(r) && unicode.IsLower(r[i+1])
|
|
if unicode.IsLower(prev) || unicode.IsDigit(prev) || endsAcronym {
|
|
b.WriteByte('-')
|
|
}
|
|
}
|
|
b.WriteRune(unicode.ToLower(c))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (e Entry) Path() string { return "/c/" + e.Category + "/" + e.Slug() }
|
|
|
|
//go:embed demos*.go
|
|
var demoFiles embed.FS
|
|
|
|
var codes = sources(demoFiles)
|
|
|
|
// Code is the source of the entry's demo function.
|
|
func (e Entry) Code() string {
|
|
name := funcName(e.Demo)
|
|
src, ok := codes[name]
|
|
if !ok {
|
|
panic("gallery: no source for " + name)
|
|
}
|
|
return src
|
|
}
|
|
|
|
func funcName(f any) string {
|
|
n := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
|
|
return n[strings.LastIndex(n, ".")+1:]
|
|
}
|
|
|
|
// sources indexes every top-level function by name.
|
|
func sources(fsys fs.FS) map[string]string {
|
|
names, err := fs.Glob(fsys, "*.go")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
out := map[string]string{}
|
|
for _, f := range names {
|
|
b, err := fs.ReadFile(fsys, f)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
// A Windows checkout may bring CRLF.
|
|
src := strings.ReplaceAll(string(b), "\r\n", "\n")
|
|
fset := token.NewFileSet()
|
|
file, err := parser.ParseFile(fset, f, src, 0)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
for _, d := range file.Decls {
|
|
if fn, ok := d.(*ast.FuncDecl); ok && fn.Recv == nil {
|
|
out[fn.Name.Name] = src[fset.Position(fn.Pos()).Offset:fset.Position(fn.End()).Offset]
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|