185 lines
13 KiB
Go
185 lines
13 KiB
Go
package webui
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"kjol/vdom"
|
|
)
|
|
|
|
// Port of web/kit/Icons.tsx. The TSX resolves FontAwesome glyphs from an
|
|
// app-generated registry (@appgen/faIcons). Here the registry is a package var
|
|
// seeded with a small common set; apps add their own with RegisterIcon. Each
|
|
// entry is the inner SVG markup for a 0 0 24 24 viewBox (Heroicons-style), so
|
|
// the outer <svg> just sizes it.
|
|
type iconDef struct {
|
|
viewBox string
|
|
content string
|
|
}
|
|
|
|
var iconRegistry = map[string]iconDef{}
|
|
|
|
// RegisterIcon registers (or overrides) an icon. viewBox is like "0 0 24 24";
|
|
// content is the inner SVG markup (e.g. one or more <path> elements).
|
|
func RegisterIcon(name, viewBox, content string) {
|
|
iconRegistry[name] = iconDef{viewBox: viewBox, content: content}
|
|
}
|
|
|
|
// HasIcon reports whether a name resolves to an icon.
|
|
//
|
|
// It exists so an app can TEST that the names it uses are real. An unknown name renders
|
|
// a correctly-sized but empty box — the layout survives, which is the right runtime
|
|
// behaviour and a terrible debugging experience: the icon is simply not there, and
|
|
// nothing anywhere says why. A one-line test over the names you reference turns that
|
|
// into a build failure.
|
|
func HasIcon(name string) bool {
|
|
_, ok := iconRegistry[name]
|
|
return ok
|
|
}
|
|
|
|
// Icon renders a named icon at the given pixel size (0 => 16). Extra classes are
|
|
// appended. Unknown names render an empty, correctly-sized box (never blanks the
|
|
// layout). Mirrors Icons.tsx's <svg fill=currentColor width height>innerHTML</svg>.
|
|
func Icon(name string, size int, class string) *vdom.VNode {
|
|
if size <= 0 {
|
|
size = 16
|
|
}
|
|
def, ok := iconRegistry[name]
|
|
vb := "0 0 24 24"
|
|
content := ""
|
|
if ok {
|
|
vb = def.viewBox
|
|
content = def.content
|
|
}
|
|
s := strconv.Itoa(size)
|
|
return vdom.Svg(vdom.Attr("xmlns", "http://www.w3.org/2000/svg"),
|
|
vdom.Attr("viewBox", vb),
|
|
vdom.Attr("fill", "currentColor"),
|
|
vdom.Attr("width", s), vdom.Attr("height", s),
|
|
vdom.Attr("aria-hidden", "true"),
|
|
vdom.Attr("class", cx("shrink-0", class)),
|
|
vdom.Raw(content),
|
|
)
|
|
}
|
|
|
|
// IconInline is Icon with inline-block alignment (for use within text runs).
|
|
func IconInline(name string, size int, class string) *vdom.VNode {
|
|
return Icon(name, size, cx("inline-block align-middle", class))
|
|
}
|
|
|
|
// IconSuccess / IconError are green/red inline convenience wrappers.
|
|
func IconSuccess(name string, size int, class string) *vdom.VNode {
|
|
return Icon(name, size, cx("inline-block align-middle text-green-600", class))
|
|
}
|
|
func IconError(name string, size int, class string) *vdom.VNode {
|
|
return Icon(name, size, cx("inline-block align-middle text-red-600 dark:text-red-400", class))
|
|
}
|
|
|
|
// IconContainer is a flex row that vertically centers an icon + text.
|
|
func IconContainer(children ...*vdom.VNode) *vdom.VNode {
|
|
return vdom.Span(kids([]vdom.Mod{vdom.Attr("class", "flex flex-row items-center gap-2")}, children)...)
|
|
}
|
|
|
|
// The default icon set: Heroicons-style outline paths on a 24x24 viewBox, so the
|
|
// kit renders correctly out of the box. Apps override or extend with RegisterIcon.
|
|
//
|
|
// Every name the kit itself asks for MUST be here. An unregistered name renders as
|
|
// an empty (correctly sized) box, which is a silent failure — the layout is right
|
|
// and the glyph is simply missing. That is what "the icons aren't showing" looks
|
|
// like, so if you add an Icon("…") call to a component, register the name too.
|
|
func strokePath(d string) string {
|
|
return `<path stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none" d="` + d + `"/>`
|
|
}
|
|
|
|
// Some glyphs read badly as outlines at 12-16px — a caret, a drag grip, an
|
|
// ellipsis. Those are filled shapes.
|
|
func fillPath(d string) string {
|
|
return `<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="` + d + `"/>`
|
|
}
|
|
|
|
func init() {
|
|
reg := func(name, d string) { RegisterIcon(name, "0 0 24 24", strokePath(d)) }
|
|
solid := func(name, d string) { RegisterIcon(name, "0 0 24 24", fillPath(d)) }
|
|
|
|
// --- navigation / chevrons ---
|
|
reg("chevron-left", "M15.75 19.5 8.25 12l7.5-7.5")
|
|
reg("chevron-right", "m8.25 4.5 7.5 7.5-7.5 7.5")
|
|
reg("chevron-down", "m19.5 8.25-7.5 7.5-7.5-7.5")
|
|
reg("chevron-up", "m4.5 15.75 7.5-7.5 7.5 7.5")
|
|
reg("angles-left", "M18.75 19.5 11.25 12l7.5-7.5M12.75 19.5 5.25 12l7.5-7.5")
|
|
reg("angles-right", "m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5")
|
|
reg("arrow-right", "M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3")
|
|
reg("arrow-right-from-bracket", "M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15M12 9l-3 3m0 0 3 3m-3-3h12.75")
|
|
reg("external-link", "M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25")
|
|
|
|
// The sort carets: filled triangles. As outlines they are a smudge at 16px.
|
|
solid("caret-up", "M12 7.5 5.25 15.75h13.5L12 7.5Z")
|
|
solid("caret-down", "M12 16.5 5.25 8.25h13.5L12 16.5Z")
|
|
|
|
// --- actions ---
|
|
reg("check", "m4.5 12.75 6 6 9-13.5")
|
|
reg("xmark", "M6 18 18 6M6 6l12 12")
|
|
reg("x", "M6 18 18 6M6 6l12 12")
|
|
reg("plus", "M12 4.5v15m7.5-7.5h-15")
|
|
reg("minus", "M4.5 12h15")
|
|
reg("bars", "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5")
|
|
reg("pencil", "m16.86 4.49 1.69-1.69a1.875 1.875 0 1 1 2.65 2.65L10.58 16.07a4.5 4.5 0 0 1-1.9 1.13L6 18l.8-2.69a4.5 4.5 0 0 1 1.13-1.9l8.93-8.92Zm0 0 2.64 2.64")
|
|
reg("trash", "m14.74 9-.35 9m-4.79 0L9.26 9m9.97-3.21c.34.05.68.11 1.02.17M18.16 19.67a2.25 2.25 0 0 1-2.24 2.08H8.08a2.25 2.25 0 0 1-2.24-2.08L4.77 5.79m14.46 0a48.1 48.1 0 0 0-3.48-.4m-12 .57c.34-.06.68-.12 1.02-.17m0 0a48.1 48.1 0 0 1 3.48-.4m7.5 0v-.92c0-1.18-.91-2.16-2.09-2.2a51.96 51.96 0 0 0-3.32 0c-1.18.04-2.09 1.02-2.09 2.2v.92m7.5 0a48.67 48.67 0 0 0-7.5 0")
|
|
solid("ellipsis", "M6.75 12a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm6.75 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm6.75 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z")
|
|
|
|
// The drag grip: six dots. An outline version is unreadable at 12px.
|
|
solid("grip-vertical", "M10 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM10 12a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 6.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM17 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM17 12a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 6.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z")
|
|
|
|
// --- table chrome ---
|
|
reg("filter", "M12 3c2.76 0 5.46.23 8.08.68.53.09.92.56.92 1.1v1.04a2.25 2.25 0 0 1-.66 1.59l-5.43 5.43a2.25 2.25 0 0 0-.66 1.59v2.93a2.25 2.25 0 0 1-1.24 2.01L9.75 21v-6.57a2.25 2.25 0 0 0-.66-1.59L3.66 7.41A2.25 2.25 0 0 1 3 5.82V4.77c0-.54.38-1 .92-1.1A48.3 48.3 0 0 1 12 3Z")
|
|
reg("table-columns", "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5M9 3.75v16.5m6-16.5v16.5")
|
|
reg("list-ol", "M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.01v.01h-.01v-.01Zm0 5.25h.01v.01h-.01V12Zm0 5.25h.01v.01h-.01v-.01Z")
|
|
reg("calculator", "M15.75 15.75V18M8.25 11.25h.01m-.01 2.25h.01m-.01 2.25h.01m-.01 2.25h.01m2.49-6.75h.01m-.01 2.25h.01m-.01 2.25h.01m-.01 2.25h.01m2.5-6.75h.01m-.01 2.25h.01m-.01 2.25h.01m-.01 2.25h.01m2.49-6.75h.01m-.01 2.25h.01M8.25 6h7.5v2.25h-7.5V6ZM12 2.25c-1.89 0-3.76.11-5.59.32-1.1.13-1.91 1.08-1.91 2.19V19.5a2.25 2.25 0 0 0 2.25 2.25h10.5a2.25 2.25 0 0 0 2.25-2.25V4.76c0-1.11-.81-2.06-1.91-2.19A48.5 48.5 0 0 0 12 2.25Z")
|
|
|
|
// --- export ---
|
|
reg("download", "M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3")
|
|
reg("print", "M6.72 13.83c-.24.03-.48.06-.72.1m.72-.1a42.4 42.4 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.17c.24.03.48.06.72.1m-.72-.1L17.66 18m0 0 .23 2.52a1.13 1.13 0 0 1-1.12 1.23H7.23c-.66 0-1.18-.57-1.12-1.23L6.34 18m11.32 0h1.09A2.25 2.25 0 0 0 21 15.75V9.46c0-1.09-.77-2.02-1.84-2.18-.63-.1-1.27-.18-1.91-.25M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.46c0-1.09.77-2.02 1.84-2.18.63-.1 1.27-.18 1.91-.25m10.5 0a48.5 48.5 0 0 0-10.5 0m10.5 0V3.38c0-.63-.5-1.13-1.13-1.13h-8.25c-.62 0-1.12.5-1.12 1.13v3.65")
|
|
reg("file", "M19.5 14.25v-2.63a3.38 3.38 0 0 0-3.38-3.37h-1.5a1.13 1.13 0 0 1-1.12-1.13v-1.5a3.38 3.38 0 0 0-3.38-3.37H8.25m2.25 0H5.63c-.63 0-1.13.5-1.13 1.12v17.25c0 .63.5 1.13 1.13 1.13h12.74c.63 0 1.13-.5 1.13-1.13V11.25a9 9 0 0 0-9-9Z")
|
|
reg("file-csv", "M19.5 14.25v-2.63a3.38 3.38 0 0 0-3.38-3.37h-1.5a1.13 1.13 0 0 1-1.12-1.13v-1.5a3.38 3.38 0 0 0-3.38-3.37H8.25m2.25 0H5.63c-.63 0-1.13.5-1.13 1.12v17.25c0 .63.5 1.13 1.13 1.13h12.74c.63 0 1.13-.5 1.13-1.13V11.25a9 9 0 0 0-9-9Zm-3 12h6m-6 3h6")
|
|
reg("file-pdf", "M19.5 14.25v-2.63a3.38 3.38 0 0 0-3.38-3.37h-1.5a1.13 1.13 0 0 1-1.12-1.13v-1.5a3.38 3.38 0 0 0-3.38-3.37H8.25m2.25 0H5.63c-.63 0-1.13.5-1.13 1.12v17.25c0 .63.5 1.13 1.13 1.13h12.74c.63 0 1.13-.5 1.13-1.13V11.25a9 9 0 0 0-9-9Zm-3 11.25v4.5m0-4.5h1.88a1.13 1.13 0 0 1 0 2.25H7.5")
|
|
|
|
// --- fields ---
|
|
reg("calendar", "M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5")
|
|
reg("envelope", "M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.24a2.25 2.25 0 0 1-1.07 1.92l-7.5 4.62a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.92v-.24")
|
|
reg("globe", "M12 21a9 9 0 0 0 8.72-6.75M12 21a9 9 0 0 1-8.72-6.75M12 21c2.49 0 4.5-4.03 4.5-9S14.49 3 12 3m0 18c-2.49 0-4.5-4.03-4.5-9S9.51 3 12 3m0 0a9 9 0 0 1 7.84 4.58M12 3a9 9 0 0 0-7.84 4.58m15.68 0A11.95 11.95 0 0 1 12 10.5c-3 0-5.74-1.1-7.84-2.92m15.68 0A8.96 8.96 0 0 1 21 12c0 .78-.1 1.53-.28 2.25m0 0A17.92 17.92 0 0 1 12 16.5c-3.16 0-6.13-.82-8.72-2.25m0 0A9.02 9.02 0 0 1 3 12c0-1.6.42-3.11 1.16-4.42")
|
|
reg("user", "M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.5 20.12a7.5 7.5 0 0 1 15 0A17.93 17.93 0 0 1 12 21.75c-2.68 0-5.22-.58-7.5-1.63Z")
|
|
|
|
// --- documentation / section icons ---
|
|
reg("book-open", "M12 6.04A8.97 8.97 0 0 0 6 3.75c-1.05 0-2.06.18-3 .51v14.25A8.99 8.99 0 0 1 6 18c2.3 0 4.4.87 6 2.29m0-14.25a8.97 8.97 0 0 1 6-2.29c1.05 0 2.06.18 3 .51v14.25A8.99 8.99 0 0 0 18 18a8.97 8.97 0 0 0-6 2.29m0-14.25v14.25")
|
|
reg("chart-column", "M3.75 20.25h16.5M6.75 20.25v-6.75m4.5 6.75V8.25m4.5 12V4.5m4.5 15.75v-9")
|
|
reg("server", "M3.75 6.75h16.5v4.5H3.75v-4.5Zm0 6h16.5v4.5H3.75v-4.5ZM6.75 9h.01M6.75 15h.01")
|
|
reg("cloud-arrow-down", "M12 9.75v6.75m0 0-3-3m3 3 3-3m-8.25 6a4.5 4.5 0 0 1-1.41-8.78 5.25 5.25 0 0 1 10.23-2.33 3 3 0 0 1 3.76 3.85A3.75 3.75 0 0 1 18 19.5H6.75Z")
|
|
reg("squares", "M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6Zm0 9.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 8.25 20.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6Zm0 9.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z")
|
|
reg("layers", "m2.25 12 9 5.25L20.25 12M2.25 7.5l9-5.25 9 5.25-9 5.25-9-5.25Zm0 9 9 5.25 9-5.25")
|
|
reg("table", "M3.75 5.25h16.5v13.5H3.75V5.25Zm0 4.5h16.5m-16.5 4.5h16.5M9.75 9.75v9")
|
|
reg("code", "m6.75 7.5-4.5 4.5 4.5 4.5m10.5-9 4.5 4.5-4.5 4.5M14.25 3.75l-4.5 16.5")
|
|
reg("bolt", "M14.25 2.25 4.5 13.5h6l-.75 8.25L19.5 10.5h-6l.75-8.25Z")
|
|
// A keel is the spine of a hull — the thing everything else is built on. Which is
|
|
// the whole idea behind the name, so the boat is not merely decorative.
|
|
reg("sailboat", "M11.25 3.75v12M11.25 15.75H4.5l6.75-12M14.25 15.75h4.5l-4.5-7.5zM2.25 18.75h19.5l-2.4 3H4.65z")
|
|
reg("cube", "m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9")
|
|
reg("shield-check", "M9 12.75 11.25 15 15 9.75m-3-7.04A11.96 11.96 0 0 1 3.6 6 12 12 0 0 0 3 9.75c0 5.59 3.82 10.29 9 11.62 5.18-1.33 9-6.03 9-11.62 0-1.31-.21-2.57-.6-3.75h-.15a11.96 11.96 0 0 1-8.25-3.29Z")
|
|
|
|
// --- theme ---
|
|
reg("sun", "M12 3v1.5m0 15V21m9-9h-1.5m-15 0H3m15.36-6.36-1.06 1.06M6.7 17.3l-1.06 1.06m12.72 0-1.06-1.06M6.7 6.7 5.64 5.64M16.5 12a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z")
|
|
reg("moon", "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z")
|
|
|
|
// --- search / loading ---
|
|
reg("magnifying-glass", "M21 21l-5.2-5.2M17 10.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0Z")
|
|
// An open arc, so that spinning it actually reads as motion — a full ring rotating
|
|
// looks like a ring standing still.
|
|
reg("circle-notch", "M21 12a9 9 0 0 0-9-9")
|
|
|
|
// --- status (the toast set) ---
|
|
reg("info", "M11.25 11.25h1.5v4.5M12 8.25h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z")
|
|
reg("circle-info", "M11.25 11.25h1.5v4.5M12 8.25h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z")
|
|
reg("circle-check", "M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z")
|
|
reg("circle-xmark", "m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z")
|
|
reg("circle-exclamation", "M12 9v3.75m0 3h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z")
|
|
reg("triangle-exclamation", "M12 9v3.75m0 3.75h.01M10.34 3.94 1.7 18a1.5 1.5 0 0 0 1.3 2.25h18a1.5 1.5 0 0 0 1.3-2.25L13.66 3.94a1.5 1.5 0 0 0-2.6 0Z")
|
|
}
|