diff --git a/contribs/gnodev/pkg/proxy/path_interceptor.go b/contribs/gnodev/pkg/proxy/path_interceptor.go index 8109753c0d9..bd0daf4690e 100644 --- a/contribs/gnodev/pkg/proxy/path_interceptor.go +++ b/contribs/gnodev/pkg/proxy/path_interceptor.go @@ -1,10 +1,8 @@ package proxy import ( - "bufio" "bytes" "encoding/json" - "errors" "fmt" "go/parser" "go/token" @@ -12,6 +10,7 @@ import ( "log/slog" "net" "net/http" + "net/http/httputil" gopath "path" "strconv" "strings" @@ -28,10 +27,11 @@ type PathHandler func(path ...string) type PathInterceptor struct { proxyAddr, targetAddr net.Addr - logger *slog.Logger - listener net.Listener - handlers []PathHandler - muHandlers sync.RWMutex + logger *slog.Logger + server *http.Server + reverseProxy *httputil.ReverseProxy + handlers []PathHandler + muHandlers sync.RWMutex } // NewPathInterceptor creates a new path proxy interceptor. @@ -51,14 +51,31 @@ func NewPathInterceptor(logger *slog.Logger, target net.Addr) (*PathInterceptor, // Immediately close this listener after proxy initialization defer targetListener.Close() + targetHost := proxyAddr.String() + proxy := &PathInterceptor{ - listener: proxyListener, logger: logger, targetAddr: target, proxyAddr: proxyAddr, + reverseProxy: &httputil.ReverseProxy{ + Director: func(req *http.Request) { + req.URL.Scheme = "http" + req.URL.Host = targetHost + req.Host = targetHost + }, + // Disable keep-alive so each request gets a fresh connection. + // The target node may restart at any time (during lazy-load reload), + // which would leave pooled connections dead. + // Cost is negligible since the target is always localhost. + Transport: &http.Transport{DisableKeepAlives: true}, + }, + } + + proxy.server = &http.Server{ + Handler: proxy, } - go proxy.handleConnections() + go proxy.server.Serve(proxyListener) return proxy, nil } @@ -80,133 +97,84 @@ func (proxy *PathInterceptor) TargetAddress() string { return fmt.Sprintf("%s://%s", proxy.targetAddr.Network(), proxy.targetAddr.String()) } -// handleConnections manages incoming connections to the proxy. -func (proxy *PathInterceptor) handleConnections() { - defer proxy.listener.Close() - - for { - conn, err := proxy.listener.Accept() - if err != nil { - if !errors.Is(err, net.ErrClosed) { - proxy.logger.Debug("failed to accept connection", "error", err) - } +// ServeHTTP intercepts HTTP requests, extracts package paths, and forwards to the target. +func (proxy *PathInterceptor) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Handle WebSocket upgrades via raw TCP pipe + if isWebSocket(r) { + proxy.handleWebSocket(w, r) + return + } - return - } + // Read body for path interception + body, err := io.ReadAll(r.Body) + r.Body.Close() + if err != nil { + proxy.logger.Debug("body read failed", "error", err) + http.Error(w, "failed to read body", http.StatusBadGateway) + return + } - proxy.logger.Debug("new connection", "remote", conn.RemoteAddr()) - go proxy.handleConnection(conn) + // Intercept paths — this may trigger a synchronous node reload + if err := proxy.handleRequest(body); err != nil { + proxy.logger.Debug("request handler warning", "error", err) } + + // Restore body for forwarding + r.Body = io.NopCloser(bytes.NewReader(body)) + r.ContentLength = int64(len(body)) + + // Forward to the target node (fresh connection per request) + proxy.reverseProxy.ServeHTTP(w, r) } -// handleConnection processes a single connection between client and target. -func (proxy *PathInterceptor) handleConnection(inConn net.Conn) { - logger := proxy.logger.With(slog.String("in", inConn.RemoteAddr().String())) +// handleWebSocket hijacks the client connection and pipes data to the target. +func (proxy *PathInterceptor) handleWebSocket(w http.ResponseWriter, r *http.Request) { + // Dial the target + targetConn, err := net.Dial(proxy.proxyAddr.Network(), proxy.proxyAddr.String()) + if err != nil { + proxy.logger.Debug("websocket upstream dial failed", "error", err) + http.Error(w, "upstream dial failed", http.StatusBadGateway) + return + } + + // Hijack the client connection + hijacker, ok := w.(http.Hijacker) + if !ok { + proxy.logger.Debug("hijacking not supported") + http.Error(w, "hijacking not supported", http.StatusInternalServerError) + targetConn.Close() + return + } - // Establish a connection to the target - outConn, err := net.Dial(proxy.proxyAddr.Network(), proxy.proxyAddr.String()) + clientConn, _, err := hijacker.Hijack() if err != nil { - logger.Error("target connection failed", "target", proxy.proxyAddr.String(), "error", err) - inConn.Close() + proxy.logger.Debug("hijack failed", "error", err) + http.Error(w, "hijack failed", http.StatusInternalServerError) + targetConn.Close() return } - logger = logger.With(slog.String("out", outConn.RemoteAddr().String())) - // Coordinate connection closure - var closeOnce sync.Once - closeConnections := func() { - inConn.Close() - outConn.Close() + // Forward the original upgrade request to the target + if err := r.Write(targetConn); err != nil { + clientConn.Close() + targetConn.Close() + return } - // Setup bidirectional copying + // Bidirectional copy var wg sync.WaitGroup wg.Add(2) - - // Response path (target -> client) go func() { defer wg.Done() - defer closeOnce.Do(closeConnections) - - _, err := io.Copy(inConn, outConn) - if err == nil || errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { - return // Connection has been closed - } - - logger.Debug("response copy error", "error", err) + io.Copy(targetConn, clientConn) + targetConn.Close() }() - - // Request path (client -> target) go func() { defer wg.Done() - defer closeOnce.Do(closeConnections) - - var buffer bytes.Buffer - tee := io.TeeReader(inConn, &buffer) - reader := bufio.NewReader(tee) - - // Process HTTP requests - if err := proxy.processHTTPRequests(reader, &buffer, outConn); err != nil { - if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { - return // Connection has been closed - } - - if _, isNetError := err.(net.Error); isNetError { - logger.Debug("request processing error", "error", err) - return - } - - // Continue processing the connection if not a network error - } - - // Forward remaining data after HTTP processing - if buffer.Len() > 0 { - if _, err := outConn.Write(buffer.Bytes()); err != nil { - logger.Debug("buffer flush failed", "error", err) - } - } - - // Directly pipe remaining traffic - if _, err := io.Copy(outConn, inConn); err != nil && !errors.Is(err, net.ErrClosed) { - logger.Debug("raw copy failed", "error", err) - } + io.Copy(clientConn, targetConn) + clientConn.Close() }() - wg.Wait() - logger.Debug("connection closed") -} - -// processHTTPRequests handles the HTTP request/response cycle. -func (proxy *PathInterceptor) processHTTPRequests(reader *bufio.Reader, buffer *bytes.Buffer, outConn net.Conn) error { - for { - request, err := http.ReadRequest(reader) - if err != nil { - return fmt.Errorf("read request failed: %w", err) - } - - // Check for websocket upgrade - if isWebSocket(request) { - return errors.New("websocket upgrade requested") - } - - // Read and process the request body - body, err := io.ReadAll(request.Body) - request.Body.Close() - if err != nil { - return fmt.Errorf("body read failed: %w", err) - } - - if err := proxy.handleRequest(body); err != nil { - proxy.logger.Debug("request handler warning", "error", err) - } - - // Forward the original request bytes - if _, err := outConn.Write(buffer.Bytes()); err != nil { - return fmt.Errorf("request forward failed: %w", err) - } - - buffer.Reset() // Prepare for the next request - } } func isWebSocket(req *http.Request) bool { @@ -223,7 +191,6 @@ func (upaths uniqPaths) list() []string { return paths } -// Add a path to func (upaths uniqPaths) addPath(path string) { path = cleanupPath(path) upaths[path] = struct{}{} @@ -272,9 +239,9 @@ func (proxy *PathInterceptor) handleRequest(body []byte) error { return nil } -// Close closes the proxy listener. +// Close closes the proxy server and listener. func (proxy *PathInterceptor) Close() error { - return proxy.listener.Close() + return proxy.server.Close() } // parseRPCRequest unmarshals and processes RPC requests, returning paths. @@ -342,17 +309,16 @@ func handleQuery(path string, data []byte, upaths uniqPaths) error { switch path { case ".app/simulate": return handleTx(data, upaths) - case "vm/qrender", "vm/qfile", "vm/qfuncs", "vm/qeval": path, _, _ := strings.Cut(string(data), ":") // Cut arguments out upaths.addPath(path) - return nil - + case "vm/qpkg_json": + upaths.addPath(string(data)) + case "vm/qobject", "vm/qobject_json", "vm/qtype_json": // operate on already-loaded state default: return fmt.Errorf("unhandled: %q", path) } - - // XXX: handle more cases + return nil } func cleanupPath(path string) string { diff --git a/contribs/gnodev/pkg/proxy/path_interceptor_test.go b/contribs/gnodev/pkg/proxy/path_interceptor_test.go index 7f10a62f5e3..468d7d01d77 100644 --- a/contribs/gnodev/pkg/proxy/path_interceptor_test.go +++ b/contribs/gnodev/pkg/proxy/path_interceptor_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/gnolang/gno/contribs/gnodev/pkg/proxy" + "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/integration" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" @@ -250,3 +251,89 @@ func Render(_ string) string { return foo.Render("bar") }`, } }) } + +// TestProxyQuerySurvivesNodeRestart reproduces the lazy loading bug: +// when a PathHandler triggers a node reload (stop old node + start new node), +// the query that triggered it must still get a valid response. +// With the old TCP proxy, the persistent outConn died during reload +// and the response was lost. +func TestProxyRestart(t *testing.T) { + const targetPath = "gno.land/r/target/foo" + + pkg := std.MemPackage{ + Name: "foo", + Path: targetPath, + Files: []*std.MemFile{ + { + Name: "foo.gno", + Body: `package foo + +func Render(_ string) string { return "foo" } +`, + }, + }, + } + pkg.SetFile("gnomod.toml", gnolang.GenGnoModLatest(pkg.Path)) + pkg.Sort() + + rootdir := gnoenv.RootDir() + cfg := integration.TestingMinimalNodeConfig(rootdir) + logger := log.NewTestingLogger(t) + + tmp := t.TempDir() + sock := filepath.Join(tmp, "node.sock") + addr, err := net.ResolveUnixAddr("unix", sock) + require.NoError(t, err) + + // Create proxy + interceptor, err := proxy.NewPathInterceptor(logger, addr) + require.NoError(t, err) + defer interceptor.Close() + cfg.TMConfig.RPC.ListenAddress = interceptor.ProxyAddress() + cfg.SkipGenesisSigVerification = true + + // Setup genesis + privKey := secp256k1.GenPrivKey() + cfg.Genesis.AppState = integration.GenerateTestingGenesisState(privKey, pkg) + + // Start the initial node + node, _ := integration.TestingInMemoryNode(t, logger, cfg) + + // Register a handler that restarts the node (simulating devNode.Reload) + restarted := make(chan struct{}, 1) + interceptor.HandlePath(func(paths ...string) { + // Stop the current node — this kills the RPC server + require.NoError(t, node.Stop()) + + // Start a fresh node on the same address (same cfg) + newNode, err := gnoland.NewInMemoryNode(logger, cfg) + require.NoError(t, err) + require.NoError(t, newNode.Start()) + select { + case <-newNode.Ready(): + case <-time.After(10 * time.Second): + t.Fatal("node didn't become ready after restart") + } + node = newNode + t.Cleanup(func() { newNode.Stop() }) + + restarted <- struct{}{} + }) + + cli, err := client.NewHTTPClient(interceptor.TargetAddress()) + require.NoError(t, err) + + // This query triggers the handler which restarts the node mid-request. + // With the HTTP reverse proxy, the forward happens AFTER the restart, + // so it connects to the new node and succeeds. + res, err := cli.ABCIQuery(context.Background(), "vm/qrender", []byte(targetPath+":\n")) + require.NoError(t, err, "query must succeed even after node restart") + assert.Nil(t, res.Response.Error) + + select { + case <-restarted: + // Good — handler restarted the node before the query was forwarded + default: + t.Fatal("handler was not called") + } +} diff --git a/docs/builders/connect-clients-and-apps.md b/docs/builders/connect-clients-and-apps.md index b59e91c7d19..e2f715aadf1 100644 --- a/docs/builders/connect-clients-and-apps.md +++ b/docs/builders/connect-clients-and-apps.md @@ -22,5 +22,9 @@ Gno.land networks expose several RPC endpoints that allow you to: All RPC endpoints for each network can be found in the [Networks documentation](../resources/gnoland-networks.md). - - +### Query Endpoints + +There are two families of query endpoints: + +- **Text-oriented** (`vm/qrender`, `vm/qeval`, `vm/qfile`, etc.) — return human-readable strings, suitable for CLI tools like `gnokey`. See [Interacting with gnokey](../users/interact-with-gnokey.md#querying-a-gnoland-network). +- **JSON/structured** (`vm/qeval_json`, `vm/qpkg_json`, `vm/qobject_json`, `vm/qtype_json`) — return Amino JSON, suitable for programmatic access by frontends and tools. See [Querying On-Chain State (JSON APIs)](query-state-api.md). diff --git a/docs/builders/query-state-api.md b/docs/builders/query-state-api.md new file mode 100644 index 00000000000..847576fc709 --- /dev/null +++ b/docs/builders/query-state-api.md @@ -0,0 +1,211 @@ +# Querying On-Chain State (JSON APIs) + +Gno.land exposes a set of ABCI query endpoints that return structured JSON +representations of on-chain state. These are designed for programmatic access +by frontends, explorers, and developer tools — as opposed to the text-oriented +endpoints documented in [Interacting with gnokey](../users/interact-with-gnokey.md#querying-a-gnoland-network). + +All endpoints are accessed via `ABCIQuery` with path `vm/` and a +`-data` payload. They return Amino JSON, the standard encoding used by Gno's +type system. + +## Endpoints + +| Endpoint | Data | Returns | +|---|---|---| +| `vm/qeval_json` | `.` | Amino JSON of the evaluated expression | +| `vm/qpkg_json` | `` | Named top-level variables of a package | +| `vm/qobject_json` | `` | Children of a persisted object | +| `vm/qtype_json` | `` | Type definition (struct fields, etc.) | + +### `vm/qeval_json` + +Evaluates an expression in read-only mode and returns the result as Amino JSON +instead of a printed string. This is the JSON counterpart to `vm/qeval`. + +```bash +gnokey query vm/qeval_json --data 'gno.land/r/demo/counter.GetCounter()' +``` + +```json +[{"T":{"@type":"/gno.PrimitiveType","value":"32"},"V":{"@type":"/gno.StringValue","value":"10"}}] +``` + +The response is an array of `TypedValue` objects — one per return value. See +[Amino JSON format](#amino-json-format) below. + +### `vm/qpkg_json` + +Returns the named top-level variables of a package as an object with `names` +and `values` arrays. This is the entry point for exploring a package's state. + +```bash +gnokey query vm/qpkg_json --data 'gno.land/r/demo/counter' +``` + +```json +{ + "names": ["counter", "Increment", "Render"], + "values": [ + {"T":{"@type":"/gno.PrimitiveType","value":"32"},"V":{"@type":"/gno.StringValue","value":"10"}}, + {"T":{"@type":"/gno.FuncType", ...},"V":{"@type":"/gno.FuncValue", ...}}, + {"T":{"@type":"/gno.FuncType", ...},"V":{"@type":"/gno.FuncValue", ...}} + ] +} +``` + +Each entry in `values` corresponds to the same index in `names`. Variables that +hold persisted objects (structs, slices, maps, etc.) will contain a `RefValue` +with an `ObjectID` that can be drilled into with `vm/qobject_json`. + +### `vm/qobject_json` + +Retrieves the fields or elements of a persisted object by its ObjectID. Use +this to drill into values returned by `vm/qpkg_json` or other objects. + +```bash +gnokey query vm/qobject_json --data '0186fce2acb457084a538e1c8b26f0f2b30e1d44:2' +``` + +```json +[ + {"N":"AQAAAAAAAAA=","T":{"@type":"/gno.PrimitiveType","value":"4"}}, + {"T":{"@type":"/gno.PointerType", ...},"V":{"@type":"/gno.PointerValue", ...}}, + {"T":{"@type":"/gno.PrimitiveType","value":"32"},"V":{"@type":"/gno.StringValue","value":"5"}} +] +``` + +The response is an array of `TypedValue` objects — one per field (for structs) +or element (for arrays/slices). Struct fields are returned by index; use +`vm/qtype_json` to resolve field names. + +### `vm/qtype_json` + +Retrieves a type definition by its TypeID. Primarily used to resolve struct +field names for objects returned by `vm/qobject_json`. + +```bash +gnokey query vm/qtype_json --data 'gno.land/p/demo/avl.Node' +``` + +```json +{ + "typeid": "gno.land/p/demo/avl.Node", + "type": { + "@type": "/gno.DeclaredType", + "PkgPath": "gno.land/p/demo/avl", + "Name": "Node", + "Base": { + "@type": "/gno.StructType", + "Fields": [ + {"Name": "key", "Type": {"@type": "/gno.PrimitiveType", "value": "16"}}, + {"Name": "value", "Type": {"@type": "/gno.InterfaceType"}}, + {"Name": "height", "Type": {"@type": "/gno.PrimitiveType", "value": "256"}}, + {"Name": "size", "Type": {"@type": "/gno.PrimitiveType", "value": "32"}}, + {"Name": "leftNode", "Type": {"@type": "/gno.PointerType", ...}}, + {"Name": "rightNode", "Type": {"@type": "/gno.PointerType", ...}} + ], + ... + }, + ... + } +} +``` + +## Amino JSON Format + +All JSON endpoints use Amino encoding — Gno's native type serialization. Each +value is represented as a `TypedValue` with up to three fields: + +| Field | Description | +|---|---| +| `T` | Type descriptor with an `@type` discriminator | +| `V` | Value payload (strings, structs, refs, etc.) | +| `N` | Base64-encoded 8-byte little-endian numeric value (for primitives) | + +### Type discriminators (`T.@type`) + +| `@type` | Kind | +|---|---| +| `/gno.PrimitiveType` | bool, int, uint, string, etc. (value is the numeric kind ID) | +| `/gno.PointerType` | Pointer to another type | +| `/gno.ArrayType` | Fixed-length array | +| `/gno.SliceType` | Slice | +| `/gno.StructType` | Struct with named fields | +| `/gno.MapType` | Map | +| `/gno.FuncType` | Function signature | +| `/gno.InterfaceType` | Interface | +| `/gno.DeclaredType` | Named type wrapping a base type | +| `/gno.RefType` | Lazy reference to a type (resolved via `qtype_json`) | + +### Value discriminators (`V.@type`) + +| `@type` | Description | +|---|---| +| `/gno.StringValue` | String value | +| `/gno.StructValue` | Inline struct fields | +| `/gno.ArrayValue` | Inline array elements | +| `/gno.SliceValue` | Slice with base/offset/length/maxcap | +| `/gno.PointerValue` | Pointer to a value | +| `/gno.MapValue` | Map with key-value list | +| `/gno.FuncValue` | Function closure | +| `/gno.RefValue` | Reference to a persisted object (has `ObjectID`) | +| `/gno.TypeValue` | Reified type | + +### Primitive type IDs + +The `PrimitiveType` value field is a numeric ID (powers of 2): + +| ID | Type | ID | Type | +|---|---|---|---| +| 4 | bool | 2048 | uint | +| 16 | string | 4096 | uint8 | +| 32 | int | 8192 | uint16 | +| 64 | int8 | 32768 | uint32 | +| 128 | int16 | 65536 | uint64 | +| 512 | int32 | 1048576 | float32 | +| 1024 | int64 | 2097152 | float64 | + +Primitive values are encoded in the `N` field as base64 of an 8-byte +little-endian integer (for numerics and bool), or in the `V` field as a +`StringValue` (for strings, and for int/uint which may exceed 64 bits). + +### Lazy references + +Large or nested values are not inlined — they are replaced with `RefValue`: + +```json +{"V": {"@type": "/gno.RefValue", "ObjectID": "0186fce2...:4"}} +``` + +Use `vm/qobject_json` with the `ObjectID` to fetch the object's contents. + +Similarly, declared types may appear as `RefType`: + +```json +{"T": {"@type": "/gno.RefType", "ID": "gno.land/p/demo/avl.Node"}} +``` + +Use `vm/qtype_json` with the `ID` to resolve the type definition. + +## Traversal Pattern + +A typical client traverses the state tree as follows: + +1. **`vm/qpkg_json`** — get named package variables +2. For each variable with a `RefValue`, call **`vm/qobject_json`** with its `ObjectID` +3. If the object's type is a `RefType`, call **`vm/qtype_json`** with its `ID` to get struct field names +4. Repeat step 2 recursively for nested `RefValue` references + +This lazy-loading pattern avoids transferring the entire object graph upfront. + +## Client Libraries + +- **[@gnojs/amino](../../misc/gnojs/)** — TypeScript library that decodes Amino JSON into a navigable tree of `StateNode` objects. Handles all value types, primitive decoding, and struct field name resolution. +- **[gnoclient](https://gnolang.github.io/gno/github.com/gnolang/gno/gno.land/pkg/gnoclient.html)** — Go client (use `ABCIQuery` with the paths above) +- **[gno-js-client](https://github.com/gnolang/gno-js-client)** / **[tm2-js-client](https://github.com/gnolang/tm2-js-client)** — JavaScript/TypeScript clients for RPC access + +## See Also + +- [Interacting with gnokey](../users/interact-with-gnokey.md#querying-a-gnoland-network) — text-oriented query endpoints (`vm/qrender`, `vm/qfile`, `vm/qeval`, etc.) +- [Connecting Clients and Applications](connect-clients-and-apps.md) — client library overview diff --git a/docs/users/interact-with-gnokey.md b/docs/users/interact-with-gnokey.md index a1efa7dfc30..c7848650998 100644 --- a/docs/users/interact-with-gnokey.md +++ b/docs/users/interact-with-gnokey.md @@ -1032,6 +1032,10 @@ Below is a list of queries a user can make with `gnokey`: - `vm/qpaths` - lists all existing package paths - `vm/qstorage` - returns storage usage and deposit locked in a realm +For JSON-structured endpoints designed for programmatic access (`vm/qeval_json`, +`vm/qpkg_json`, `vm/qobject_json`, `vm/qtype_json`), see +[Querying On-Chain State (JSON APIs)](../builders/query-state-api.md). + Let's see how we can use them. ### `auth/accounts` diff --git a/examples/gno.land/r/demo/closuretest/closuretest.gno b/examples/gno.land/r/demo/closuretest/closuretest.gno new file mode 100644 index 00000000000..ef222dbbe43 --- /dev/null +++ b/examples/gno.land/r/demo/closuretest/closuretest.gno @@ -0,0 +1,43 @@ +package closuretest + +import "strconv" + +var ( + count int + stepper func() int +) + +var ( + accumulator func(int) + history []int +) + +func init() { + step := 3 + stepper = func() int { + count += step + return count + } + + maxLen := 10 + history = make([]int, 0, maxLen) + accumulator = func(val int) { + if len(history) < maxLen { + history = append(history, val) + } + } +} + +func Step() string { + result := stepper() + return "count=" + strconv.Itoa(result) +} + +func Accumulate(val int) string { + accumulator(val) + return "history length=" + strconv.Itoa(len(history)) +} + +func Render(_ string) string { + return "closuretest: count=" + strconv.Itoa(count) + " history=" + strconv.Itoa(len(history)) +} diff --git a/examples/gno.land/r/demo/closuretest/gnomod.toml b/examples/gno.land/r/demo/closuretest/gnomod.toml new file mode 100644 index 00000000000..eed4fe39988 --- /dev/null +++ b/examples/gno.land/r/demo/closuretest/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/demo/closuretest" +gno = "0.9" diff --git a/gno.land/adr/adr-003-state-explorer.md b/gno.land/adr/adr-003-state-explorer.md new file mode 100644 index 00000000000..3b5780331d2 --- /dev/null +++ b/gno.land/adr/adr-003-state-explorer.md @@ -0,0 +1,113 @@ +# ADR-003: State Explorer for gnoweb + +## Status + +Accepted + +## Context + +Gno realms persist their entire state on-chain as an object graph (structs, +maps, slices, pointers, closures, etc.) encoded in Amino JSON. While +developers can inspect state via CLI queries (`vm/qpkg_json`, `vm/qobject_json`, +`vm/qtype_json`), there was no visual tool for browsing this state tree. + +Understanding realm state is critical for debugging, auditing, and building +confidence in on-chain logic. The raw Amino JSON is verbose and requires +knowledge of encoding details (base64 primitives, `RefValue` references, +`HeapItemValue` wrappers, positional struct fields without names). + +## Decision + +Add a **State Explorer** tab to gnoweb that renders the persisted state of any +realm or package as an interactive, expandable tree. + +### Architecture + +The system has three layers: + +1. **VM query endpoints** (`vm/qpkg_json`, `vm/qobject_json`, `vm/qtype_json`) + return raw Amino JSON for package variables, individual objects by ObjectID, + and type definitions by TypeID respectively. + +2. **[`@gnojs/amino`](../../misc/gnojs/README.md)** (TypeScript library in + `misc/gnojs/`) decodes Amino JSON into a UI-friendly `StateNode` tree. Each node has a name, type, kind, + optional value, and optional children. The decoder handles: + - Primitive values (base64 `N` field, `StringValue`) + - Structs with positional fields (names resolved via `qtype_json`) + - Collections (arrays, slices, maps) with inline or lazy-loaded children + - Pointers and `RefValue` references for lazy object graph traversal + - `HeapItemValue` transparent unwrapping + - `ExportRefValue` cycle detection + - Functions with source location extraction + - Closures with captured variable decoding (via `Captures` field) + - Type values and package references + +3. **gnoweb controller** (`controller-state-explorer.ts`) renders `StateNode` + trees as interactive HTML with: + - Expandable/collapsible rows with toggle arrows + - Color-coded types by kind (struct, map, func, closure, primitive, etc.) + - Lazy-loading of persisted objects via `fetch()` to the state JSON API + - Struct field name resolution via `qtype_json` round-trips + - Inline syntax-highlighted source code for function declarations + - Closure capture display with "Captured variables:" label + - ObjectID display on hover with click-to-copy + - Source file links for navigating to function definitions + +### gnoweb Integration + +- **State tab**: Added as a top-level navigation tab (Content, State, Source, + Actions) for realm and package views. +- **State JSON API**: `$state&json` serves raw JSON; `$state&oid=...&json` + serves individual objects; `$state&tid=...&json` serves type definitions; + `$state&file=...&start=N&end=N&json` serves syntax-highlighted source + snippets for function bodies. +- **State HTML view**: `$state` renders the full page with the state explorer + component, which bootstraps from server-rendered initial data. + +### Closure Support + +Closures are detected by the presence of a non-empty `Captures` array in +`FuncValue` (the `IsClosure` boolean field is unreliable in persisted state). +Captures are `TypedValue` entries with `heapItemType` types pointing to +`RefValue` heap items. The decoder assigns kind `"closure"` (rendered in blue) +to distinguish from regular functions (purple). When expanded, closures show +both the syntax-highlighted source code and the captured variables as child +nodes. + +### OID Navigation + +The searchbar detects ObjectID patterns (hex/colon format) and redirects to +`$state&oid=...` on the current realm, enabling direct navigation to any +persisted object. + +## Consequences + +### Positive + +- Visual debugging of on-chain state without CLI tools +- Lazy-loading enables browsing arbitrarily large object graphs +- Struct field names resolved from type definitions improve readability +- Closure captures made visible for understanding captured variable state +- Consistent with gnoweb's existing tab navigation pattern + +### Negative + +- Each object expansion requires a network round-trip to the node +- Struct field name resolution adds an additional round-trip per unique type +- PurgeCSS requires safelist entries for dynamically-constructed CSS classes + (e.g., `b-state-kind--${kind}`) + +### Files + +- `gno.land/pkg/gnoweb/handler_http.go` — `GetStateView`, `ServeStateJSON` +- `gno.land/pkg/gnoweb/components/views/state.html` — state view template +- `gno.land/pkg/gnoweb/components/layout_header.go` — State tab in navigation +- `gno.land/pkg/gnoweb/frontend/js/controller-state-explorer.ts` — tree controller +- `gno.land/pkg/gnoweb/frontend/css/06-blocks.css` — state explorer styles +- `gno.land/pkg/gnoweb/frontend/js/controller-searchbar.ts` — OID detection +- `gno.land/pkg/gnoweb/frontend/postcss.config.cjs` — PurgeCSS safelist +- `misc/gnojs/src/decode.ts` — Amino JSON decoder +- `misc/gnojs/src/types.ts` — Amino type definitions +- `misc/gnojs/src/type-utils.ts` — type name/kind/signature utilities +- `gno.land/pkg/sdk/vm/keeper.go` — `QueryPkgJSON`, `QueryObjectJSON`, `QueryTypeJSON` +- `gno.land/pkg/sdk/vm/handler.go` — `qpkg_json`, `qobject_json`, `qtype_json` routes diff --git a/gno.land/pkg/gnoweb/Makefile b/gno.land/pkg/gnoweb/Makefile index 316418f8b88..781c2563046 100644 --- a/gno.land/pkg/gnoweb/Makefile +++ b/gno.land/pkg/gnoweb/Makefile @@ -36,6 +36,7 @@ output_static := $(patsubst $(src_dir_static)/%, $(out_dir_static)/%, $(input_st src_dir_js := frontend/js out_dir_js := $(PUBLIC_DIR)/js input_js := $(shell find $(src_dir_js) -name '*.ts') +gnojs_src := $(shell find ../../../misc/gnojs/src -name '*.ts' 2>/dev/null) # Separate shared and controller files shared_js := $(src_dir_js)/controller.ts controller_js := $(filter-out $(shared_js),$(input_js)) @@ -88,7 +89,7 @@ $(out_dir_js)/controller.js: $(shared_js) $(esbuild) NODE_ENV=production $(esbuild) $< --log-level=error --bundle --outfile=$@ --format=esm --minify # Build controller files with shared chunk reference -$(out_dir_js)/%.js: $(src_dir_js)/%.ts $(out_dir_js)/controller.js +$(out_dir_js)/%.js: $(src_dir_js)/%.ts $(out_dir_js)/controller.js $(gnojs_src) NODE_ENV=production $(esbuild) $< --log-level=error --bundle --outdir=$(out_dir_js) --format=esm --define:process.env.NODE_ENV="\"production\"" --minify --external:./controller.js # Rule to copy static files while preserving directory structure diff --git a/gno.land/pkg/gnoweb/client.go b/gno.land/pkg/gnoweb/client.go index 21dbf79b344..c655c134049 100644 --- a/gno.land/pkg/gnoweb/client.go +++ b/gno.land/pkg/gnoweb/client.go @@ -48,6 +48,18 @@ type ClientAdapter interface { // Doc retrieves the JSON doc suitable for printing from a // specified package path. Doc(ctx context.Context, path string) (*doc.JSONDocumentation, error) + + // StatePkg retrieves the root state tree for a package. + // Returns raw JSON bytes of the package block variables. + StatePkg(ctx context.Context, path string) ([]byte, error) + + // StateObject retrieves the children of an object by ObjectID. + // Returns raw JSON bytes of the object's fields/elements. + StateObject(ctx context.Context, oid string) ([]byte, error) + + // StateType retrieves a type definition by TypeID. + // Returns raw JSON bytes of the type (for resolving struct field names). + StateType(ctx context.Context, typeId string) ([]byte, error) } type rpcClient struct { @@ -171,6 +183,29 @@ func (c *rpcClient) Doc(ctx context.Context, pkgPath string) (*doc.JSONDocumenta return jdoc, nil } +// StatePkg retrieves root state tree for a package via vm/qpkg_json. +func (c *rpcClient) StatePkg(ctx context.Context, path string) ([]byte, error) { + const qpath = "vm/qpkg_json" + + path = strings.Trim(path, "/") + data := fmt.Sprintf("%s/%s", c.domain, path) + return c.query(ctx, qpath, []byte(data)) +} + +// StateObject retrieves an object by ObjectID via vm/qobject_json. +func (c *rpcClient) StateObject(ctx context.Context, oid string) ([]byte, error) { + const qpath = "vm/qobject_json" + + return c.query(ctx, qpath, []byte(oid)) +} + +// StateType retrieves a type definition by TypeID via vm/qtype_json. +func (c *rpcClient) StateType(ctx context.Context, typeId string) ([]byte, error) { + const qpath = "vm/qtype_json" + + return c.query(ctx, qpath, []byte(typeId)) +} + // query sends a query to the RPC client and returns the response // data. func (c *rpcClient) query(ctx context.Context, qpath string, data []byte) ([]byte, error) { diff --git a/gno.land/pkg/gnoweb/client_mock.go b/gno.land/pkg/gnoweb/client_mock.go index 9d0d20f2402..c6cfa629919 100644 --- a/gno.land/pkg/gnoweb/client_mock.go +++ b/gno.land/pkg/gnoweb/client_mock.go @@ -138,6 +138,25 @@ func (m *MockClient) Doc(ctx context.Context, path string) (*doc.JSONDocumentati return &doc.JSONDocumentation{Funcs: pkg.Functions}, nil } +// StatePkg returns mock package state data for testing. +func (m *MockClient) StatePkg(_ context.Context, path string) ([]byte, error) { + _, exists := m.Packages[path] + if !exists { + return nil, ErrClientPackageNotFound + } + return []byte(`[]`), nil +} + +// StateObject returns mock object state data for testing. +func (m *MockClient) StateObject(_ context.Context, _ string) ([]byte, error) { + return []byte(`[]`), nil +} + +// StateType returns mock type data for testing. +func (m *MockClient) StateType(_ context.Context, _ string) ([]byte, error) { + return []byte(`{}`), nil +} + // Helper: check if package has a Render(string) string function. func pkgHasRender(pkg *MockPackage) bool { if len(pkg.Functions) == 0 { diff --git a/gno.land/pkg/gnoweb/components/layout_header.go b/gno.land/pkg/gnoweb/components/layout_header.go index 1ec4b7a3357..8b254291441 100644 --- a/gno.land/pkg/gnoweb/components/layout_header.go +++ b/gno.land/pkg/gnoweb/components/layout_header.go @@ -37,10 +37,11 @@ func StaticHeaderGeneralLinks() []HeaderLink { } func StaticHeaderDevLinks(u weburl.GnoURL, mode ViewMode) []HeaderLink { - contentURL, sourceURL, helpURL := u, u, u + contentURL, sourceURL, helpURL, stateURL := u, u, u, u contentURL.WebQuery = url.Values{} sourceURL.WebQuery = url.Values{"source": {""}} helpURL.WebQuery = url.Values{"help": {""}} + stateURL.WebQuery = url.Values{"state": {""}} contentLink := HeaderLink{ Label: "Content", @@ -63,6 +64,13 @@ func StaticHeaderDevLinks(u weburl.GnoURL, mode ViewMode) []HeaderLink { IsActive: isActive(u.WebQuery, "Actions"), } + stateLink := HeaderLink{ + Label: "State", + URL: stateURL.EncodeWebURL(), + Icon: "ico-grid", + IsActive: isActive(u.WebQuery, "State"), + } + switch mode { case ViewModeExplorer: return []HeaderLink{} @@ -71,7 +79,7 @@ func StaticHeaderDevLinks(u weburl.GnoURL, mode ViewMode) []HeaderLink { case ViewModePackage: return []HeaderLink{contentLink, sourceLink} default: - return []HeaderLink{contentLink, sourceLink, actionsLink} + return []HeaderLink{contentLink, stateLink, sourceLink, actionsLink} } } @@ -90,7 +98,9 @@ func EnrichHeaderData(data HeaderData, mode ViewMode) HeaderData { func isActive(webQuery url.Values, label string) bool { switch label { case "Content": - return !webQuery.Has("source") && !webQuery.Has("help") + return !webQuery.Has("source") && !webQuery.Has("help") && !webQuery.Has("state") + case "State": + return webQuery.Has("state") case "Source": return webQuery.Has("source") case "Actions": diff --git a/gno.land/pkg/gnoweb/components/layout_index.go b/gno.land/pkg/gnoweb/components/layout_index.go index f68d69a3991..7ac692ddc72 100644 --- a/gno.land/pkg/gnoweb/components/layout_index.go +++ b/gno.land/pkg/gnoweb/components/layout_index.go @@ -74,7 +74,7 @@ func IndexLayout(data IndexData) Component { // Set dev mode based on view type and mode switch data.BodyView.Type { - case HelpViewType, SourceViewType, DirectoryViewType, StatusViewType: + case HelpViewType, SourceViewType, DirectoryViewType, StatusViewType, StateViewType: dataLayout.IsDevmodView = true } diff --git a/gno.land/pkg/gnoweb/components/layout_test.go b/gno.land/pkg/gnoweb/components/layout_test.go index 720ca9b0e96..23267c0800f 100644 --- a/gno.land/pkg/gnoweb/components/layout_test.go +++ b/gno.land/pkg/gnoweb/components/layout_test.go @@ -112,7 +112,7 @@ func TestEnrichHeaderData(t *testing.T) { enrichedData := EnrichHeaderData(data, ViewModeHome) assert.NotEmpty(t, enrichedData.Links.General, "expected general links to be populated") - assert.Len(t, enrichedData.Links.Dev, 3, "expected dev links with Actions for home mode") + assert.Len(t, enrichedData.Links.Dev, 4, "expected dev links with State and Actions for home mode") } func TestIsActive(t *testing.T) { @@ -160,6 +160,22 @@ func TestIsActive(t *testing.T) { label: "Actions", expected: true, }, + { + name: "State active when state present", + query: url.Values{ + "state": []string{""}, + }, + label: "State", + expected: true, + }, + { + name: "Content inactive when state present", + query: url.Values{ + "state": []string{""}, + }, + label: "Content", + expected: false, + }, { name: "Unknown label returns false", query: url.Values{}, @@ -186,10 +202,11 @@ func TestStaticHeaderDevLinks_WithRealmMode(t *testing.T) { // Test realm mode (default case) links := StaticHeaderDevLinks(u, ViewModeRealm) - assert.Len(t, links, 3, "expected Content, Source, and Actions links") + assert.Len(t, links, 4, "expected Content, State, Source, and Actions links") assert.Equal(t, "Content", links[0].Label) - assert.Equal(t, "Source", links[1].Label) - assert.Equal(t, "Actions", links[2].Label) + assert.Equal(t, "State", links[1].Label) + assert.Equal(t, "Source", links[2].Label) + assert.Equal(t, "Actions", links[3].Label) } func TestStaticHeaderDevLinks_WithPackageMode(t *testing.T) { @@ -231,7 +248,7 @@ func TestEnrichHeaderData_WithRealmMode(t *testing.T) { enriched := EnrichHeaderData(data, ViewModeRealm) assert.Equal(t, "/r/test/pkg", enriched.RealmPath) assert.Empty(t, enriched.Links.General) - assert.Len(t, enriched.Links.Dev, 3, "expected Content, Source, and Actions links") + assert.Len(t, enriched.Links.Dev, 4, "expected Content, State, Source, and Actions links") } func TestEnrichHeaderData_WithExplorerMode(t *testing.T) { diff --git a/gno.land/pkg/gnoweb/components/view_state.go b/gno.land/pkg/gnoweb/components/view_state.go new file mode 100644 index 00000000000..34721635460 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/view_state.go @@ -0,0 +1,24 @@ +package components + +import "html/template" + +const StateViewType ViewType = "state-view" + +// StateData holds data for rendering the state explorer view. +type StateData struct { + PkgPath string + NodesJSON string // JSON string embedded in the template for initial render. +} + +type stateViewParams struct { + PkgPath string + NodesJSON template.JS +} + +// StateView creates a new View for the state explorer. +func StateView(data StateData) *View { + return NewTemplateView(StateViewType, "renderState", stateViewParams{ + PkgPath: data.PkgPath, + NodesJSON: template.JS(data.NodesJSON), + }) +} diff --git a/gno.land/pkg/gnoweb/components/views/state.html b/gno.land/pkg/gnoweb/components/views/state.html new file mode 100644 index 00000000000..f60028cdca4 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/views/state.html @@ -0,0 +1,12 @@ +{{ define "renderState" }} +
+
+
+ +
+
+ +
+{{ end }} diff --git a/gno.land/pkg/gnoweb/frontend/css/06-blocks.css b/gno.land/pkg/gnoweb/frontend/css/06-blocks.css index 0d266626aed..3e7e6672255 100644 --- a/gno.land/pkg/gnoweb/frontend/css/06-blocks.css +++ b/gno.land/pkg/gnoweb/frontend/css/06-blocks.css @@ -2482,3 +2482,226 @@ main.dev-mode .b-toc a { } } } + +/* ===== STATE EXPLORER ===== */ +.b-state-explorer { + margin-block-start: var(--g-space-4); + padding-block-start: var(--g-space-4); + padding-bottom: var(--g-space-24); + grid-column: 1 / -1; + + @media (--lg) { + margin-block-start: 0; + padding-block-start: var(--g-space-6); + } +} + +.b-state-explorer__header { + margin-block-end: var(--g-space-4); +} + +.b-state-explorer__path { + font-size: var(--g-font-size-90); + color: var(--s-color-text-tertiary); + margin-block-end: var(--g-spacing-050); + + &:empty { + display: none; + } +} + +.b-state-explorer__path-link { + color: var(--s-color-text-link); + + &:hover { + color: var(--s-color-text-link-hover); + } +} + +.b-state-explorer__count { + font-size: var(--g-font-size-90); + color: var(--s-color-text-secondary); + white-space: nowrap; +} + +.b-state-tree { + font-family: var(--g-font-family-mono); + font-size: var(--g-font-size-90); + line-height: 1; +} + +/* Row = line + children */ +.b-state-row__line { + display: flex; + align-items: baseline; + flex-wrap: nowrap; + gap: 0 0.4em; + padding: 0.2rem 0; + border-radius: 3px; +} + +.b-state-row__line:hover { + background-color: var(--s-color-bg-alt); +} + +/* Toggle arrow */ +.b-state-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1em; + flex-shrink: 0; + font-size: 0.65em; + color: var(--s-color-text-secondary); + user-select: none; + cursor: pointer; +} + +.b-state-toggle:hover { + color: var(--s-color-text-primary); +} + +.b-state-toggle:empty { + cursor: default; +} + +.b-state-toggle--loading { + animation: state-pulse 0.8s ease-in-out infinite; +} + +@keyframes state-pulse { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 1; } +} + +/* Name */ +.b-state-name { + font-weight: 600; + color: var(--s-color-text-primary); +} + +/* Separator */ +.b-state-sep { + color: var(--s-color-text-secondary); + opacity: 0.5; +} + +/* Type — colored by kind */ +.b-state-type { + font-style: italic; +} + +.b-state-kind--struct { color: var(--s-color-text-tip); } +.b-state-kind--map { color: var(--s-color-text-caution); } +.b-state-kind--array { color: var(--s-color-text-success); } +.b-state-kind--slice { color: var(--s-color-text-success); } +.b-state-kind--pointer { color: var(--s-color-text-warning); } +.b-state-kind--func { color: var(--s-color-text-tip); } +.b-state-kind--closure { color: var(--s-color-text-info); } +.b-state-kind--nil { color: var(--s-color-text-secondary); } +.b-state-kind--primitive { color: var(--s-color-text-secondary); } + +/* Metadata (length, etc) */ +.b-state-meta { + color: var(--s-color-text-secondary); + font-size: 0.85em; +} + +/* Values — colored by kind */ +.b-state-val { + word-break: break-all; +} + +.b-state-val--primitive { color: var(--s-color-text-info); } +.b-state-val--nil { color: var(--s-color-text-secondary); font-style: italic; } +.b-state-val--func { color: var(--s-color-text-tip); } +.b-state-val--closure { color: var(--s-color-text-info); } + +/* ObjectID — subtle, appears on hover */ +.b-state-oid { + font-size: 0.75em; + color: var(--s-color-text-secondary); + opacity: 0; + cursor: pointer; + transition: opacity 0.15s; +} + +.b-state-row__line:hover .b-state-oid { + opacity: 0.5; +} + +.b-state-oid:hover { + opacity: 1 !important; + text-decoration: underline; +} + +/* Source code block (contains Chroma-highlighted HTML) */ +.b-state-source { + position: relative; + margin: 0.25rem 0; + border-radius: var(--s-rounded); + font-size: var(--g-font-size-90); + line-height: 1.5; + overflow-x: auto; + tab-size: 4; +} + +.b-state-source__link { + position: absolute; + top: var(--g-space-2); + right: var(--g-space-2); + font-size: var(--g-font-size-80); + color: var(--s-color-text-tertiary); + text-decoration: none; + padding: var(--g-space-1) var(--g-space-2); + border-radius: var(--s-rounded); + background-color: var(--s-color-bg-surface-secondary); + z-index: 1; + + &:hover { + color: var(--s-color-text-link); + background-color: var(--s-color-bg-surface-tertiary); + } +} + +.b-state-source pre { + margin: 0; + padding: var(--g-space-4) var(--g-space-1); + overflow-x: auto; + background-color: var(--s-color-bg-base); + border-radius: var(--s-rounded); +} + +.b-state-source code { + font-family: var(--g-font-family-mono); +} + +/* Source link */ +.b-state-srclink { + color: var(--s-color-text-secondary); + font-size: var(--g-font-size-80); + text-decoration: none; + display: inline-block; + margin-top: 0.125rem; +} + +.b-state-srclink:hover { + color: var(--s-color-text-link); + text-decoration: underline; +} + +/* Closure captures label */ +.b-state-captures-label { + color: var(--s-color-text-info); + font-size: 0.85em; + font-style: italic; + margin-top: var(--g-space-2); + margin-bottom: var(--g-space-1); +} + +/* Error */ +.b-state-err { + color: var(--s-color-text-caution); + font-style: italic; + padding-left: 1.5rem; +} diff --git a/gno.land/pkg/gnoweb/frontend/js/controller-searchbar.ts b/gno.land/pkg/gnoweb/frontend/js/controller-searchbar.ts index 0258fca7708..3c0731b5f4e 100644 --- a/gno.land/pkg/gnoweb/frontend/js/controller-searchbar.ts +++ b/gno.land/pkg/gnoweb/frontend/js/controller-searchbar.ts @@ -1,5 +1,8 @@ import { BaseController } from "./controller.js"; +// Matches Amino object IDs like "a]0000000001" or "ff61a23bc5:12" +const OID_PATTERN = /^[a-f0-9\]:.]+$/i; + export class SearchbarController extends BaseController { protected connect(): void { this.initializeDOM({ @@ -14,22 +17,39 @@ export class SearchbarController extends BaseController { const inputElement = this.getDOMElement("input") as HTMLInputElement; let url = inputElement?.value.trim(); - if (url) { - // Check if the URL has a proper scheme - if (!/^https?:\/\//i.test(url)) { - const baseUrl = window.location.origin; - url = `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`; - } + if (!url) { + console.error("SearchBarController: Please enter a URL to search."); + return; + } - try { - window.location.href = new URL(url).href; - } catch (_error) { - console.error( - "SearchBarController: Invalid URL. Please enter a valid URL starting with http:// or https://.", - ); + // Detect object IDs and redirect to state view + if (OID_PATTERN.test(url) && !url.startsWith("/")) { + const realmPath = this._currentRealmPath(); + if (realmPath) { + window.location.href = `${realmPath}$state&oid=${encodeURIComponent(url)}`; + return; } - } else { - console.error("SearchBarController: Please enter a URL to search."); } + + // Check if the URL has a proper scheme + if (!/^https?:\/\//i.test(url)) { + const baseUrl = window.location.origin; + url = `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`; + } + + try { + window.location.href = new URL(url).href; + } catch (_error) { + console.error( + "SearchBarController: Invalid URL. Please enter a valid URL starting with http:// or https://.", + ); + } + } + + private _currentRealmPath(): string | null { + const path = window.location.pathname; + // Match realm paths like /r/demo/tamagotchi + const match = path.match(/^(\/r\/[^$]+)/); + return match ? match[1] : null; } } diff --git a/gno.land/pkg/gnoweb/frontend/js/controller-state-explorer.ts b/gno.land/pkg/gnoweb/frontend/js/controller-state-explorer.ts new file mode 100644 index 00000000000..2b706cd5f2e --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/controller-state-explorer.ts @@ -0,0 +1,406 @@ +import type { + AminoFuncValue, + QobjectResponse, + QpkgResponse, + QtypeResponse, + StateNode, +} from "@gnojs/amino"; +import { + decodeFuncObject, + decodeObject, + decodePkg, + structFieldNames, +} from "@gnojs/amino"; +import { BaseController } from "./controller.js"; + +const ARROW_RIGHT = "\u25B6"; +const ARROW_DOWN = "\u25BC"; + +export class StateExplorerController extends BaseController { + private declare pkgPath: string; + private declare typeCache: Map; + private declare sourceCache: Map; + + protected connect(): void { + this.pkgPath = this.getValue("pkg-path"); + this.typeCache = new Map(); + this.sourceCache = new Map(); + + // Show realm path when navigated via OID (gnoweb uses $state&oid=... in path) + if (window.location.pathname.includes("oid=")) { + this._showPathInfo(); + } + + const dataEl = this.getTarget("initial-data"); + if (dataEl?.textContent) { + try { + const raw: QpkgResponse = JSON.parse(dataEl.textContent); + const nodes = decodePkg(raw); + const tree = this.getTarget("tree"); + if (tree) { + this._renderNodes(nodes, tree, 0); + this._updateCount(nodes.length); + } + } catch (err) { + console.error("Failed to parse initial state data:", err); + } + } + } + + private _updateCount(n: number): void { + const countEl = this.getTarget("count"); + if (countEl) { + const kind = this.pkgPath.startsWith("/r/") ? "Realm" : "Package"; + countEl.textContent = `${kind} top-level declarations (${n})`; + } + } + + private _showPathInfo(): void { + const el = this.getTarget("path-info"); + if (!el) return; + const link = document.createElement("a"); + link.href = this.pkgPath; + link.textContent = this.pkgPath; + link.className = "b-state-explorer__path-link"; + el.textContent = "Realm: "; + el.appendChild(link); + } + + private _renderNodes( + nodes: StateNode[], + container: HTMLElement, + depth: number, + ): void { + const fragment = document.createDocumentFragment(); + for (const node of nodes) { + fragment.appendChild(this._createRow(node, depth)); + } + container.appendChild(fragment); + } + + private _createRow(node: StateNode, depth: number): HTMLElement { + const row = document.createElement("div"); + row.className = "b-state-row"; + + const line = document.createElement("div"); + line.className = "b-state-row__line"; + line.style.paddingLeft = `${depth * 1.25 + 0.25}rem`; + + // Toggle arrow + const toggle = document.createElement("span"); + toggle.className = "b-state-toggle"; + if (node.expandable || (node.children && node.children.length > 0)) { + toggle.textContent = ARROW_RIGHT; + toggle.addEventListener("click", () => + this._toggle(toggle, row, node, depth), + ); + } + line.appendChild(toggle); + + // Name + const nameEl = document.createElement("span"); + nameEl.className = "b-state-name"; + nameEl.textContent = node.name; + line.appendChild(nameEl); + + // Separator + line.appendChild(this._sep(":")); + + // Type + const typeEl = document.createElement("span"); + typeEl.className = `b-state-type b-state-kind--${node.kind}`; + typeEl.textContent = node.type; + line.appendChild(typeEl); + + // Length + if (node.length !== undefined && node.length > 0) { + const lenEl = document.createElement("span"); + lenEl.className = "b-state-meta"; + lenEl.textContent = `(len=${node.length})`; + line.appendChild(lenEl); + } + + // Value + if (node.value !== undefined && node.value !== "") { + line.appendChild(this._sep("=")); + const valEl = document.createElement("span"); + valEl.className = `b-state-val b-state-val--${node.kind}`; + valEl.textContent = node.value; + line.appendChild(valEl); + } + + // Source location (for functions) — clickable link to source view + if (node.source) { + const srcLink = document.createElement("a"); + srcLink.className = "b-state-meta b-state-srclink"; + srcLink.textContent = `${node.source.file}:${node.source.startLine}`; + srcLink.href = `${this.pkgPath}$source&file=${encodeURIComponent(node.source.file)}#L${node.source.startLine}`; + srcLink.title = "View source"; + line.appendChild(srcLink); + } + + // ObjectID (subtle, on hover) + if (node.objectId) { + const oid = node.objectId; + const oidEl = document.createElement("span"); + oidEl.className = "b-state-oid"; + oidEl.textContent = oid; + oidEl.title = "Object ID \u2014 click to copy"; + oidEl.addEventListener("click", (e) => { + e.stopPropagation(); + navigator.clipboard.writeText(oid); + oidEl.textContent = "copied!"; + setTimeout(() => { + oidEl.textContent = oid; + }, 1000); + }); + line.appendChild(oidEl); + } + + row.appendChild(line); + + // Children container + const kids = document.createElement("div"); + kids.className = "b-state-kids"; + if (node.children && node.children.length > 0) { + this._renderNodes(node.children, kids, depth + 1); + } else { + kids.hidden = true; + } + row.appendChild(kids); + + return row; + } + + private _sep(char: string): HTMLElement { + const s = document.createElement("span"); + s.className = "b-state-sep"; + s.textContent = char; + return s; + } + + private async _toggle( + toggle: HTMLElement, + row: HTMLElement, + node: StateNode, + depth: number, + ): Promise { + const kids = row.querySelector(".b-state-kids") as HTMLElement; + if (!kids) return; + + const isHidden = kids.hidden; + + if (isHidden) { + // Expand — lazy-fetch if needed + if (kids.children.length === 0) { + // Closures with inline children: render source + captures + if ( + node.kind === "closure" && + node.children && + node.children.length > 0 + ) { + if (node.source) { + await this._renderSourceBlock( + node.source.file, + node.source.startLine, + node.source.endLine, + kids, + depth, + ); + } + // Render capture label + children + const label = document.createElement("div"); + label.className = "b-state-captures-label"; + label.style.paddingLeft = `${(depth + 1) * 1.25 + 0.25}rem`; + label.textContent = "Captured variables:"; + kids.appendChild(label); + this._renderNodes(node.children, kids, depth + 1); + } else if (node.objectId) { + toggle.classList.add("b-state-toggle--loading"); + try { + const url = `${this.pkgPath}$state&oid=${encodeURIComponent(node.objectId)}&json`; + const resp = await fetch(url); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const raw: QobjectResponse = await resp.json(); + + if (node.kind === "func" || node.kind === "closure") { + await this._expandFunc( + raw.value as AminoFuncValue, + kids, + depth, + ); + } else { + let childNodes = decodeObject(raw); + + // Resolve struct field names via qtype_json if parent has a typeId + if (node.typeId && childNodes.length > 0) { + childNodes = await this._resolveFieldNames( + node.typeId, + childNodes, + ); + } + + this._renderNodes(childNodes, kids, depth + 1); + } + } catch (err) { + console.error("State fetch error:", err); + const errEl = document.createElement("span"); + errEl.className = "b-state-err"; + errEl.textContent = "Failed to load"; + kids.appendChild(errEl); + } + toggle.classList.remove("b-state-toggle--loading"); + } + } + kids.hidden = false; + toggle.textContent = ARROW_DOWN; + } else { + kids.hidden = true; + toggle.textContent = ARROW_RIGHT; + } + } + + // Render a syntax-highlighted source block inline. + private async _renderSourceBlock( + file: string, + startLine: number, + endLine: number, + container: HTMLElement, + depth: number, + ): Promise { + try { + const html = await this._fetchSourceHTML(file, startLine, endLine); + + const wrapper = document.createElement("div"); + wrapper.className = "b-state-source"; + wrapper.style.paddingLeft = `${(depth + 1) * 1.25 + 0.25}rem`; + + // Source link in top-right corner + const link = document.createElement("a"); + link.className = "b-state-source__link"; + link.textContent = `${file}:${startLine}`; + link.href = `${this.pkgPath}$source&file=${encodeURIComponent(file)}#L${startLine}`; + link.title = "View source"; + wrapper.appendChild(link); + + const code = document.createElement("div"); + code.innerHTML = html; + wrapper.appendChild(code); + + container.appendChild(wrapper); + } catch (err) { + console.error("Source fetch error:", err); + const errEl = document.createElement("span"); + errEl.className = "b-state-err"; + errEl.textContent = `Failed to load source: ${err instanceof Error ? err.message : String(err)}`; + container.appendChild(errEl); + } + } + + // Fetch syntax-highlighted source and display it inline, plus captures for closures. + private async _expandFunc( + fv: AminoFuncValue, + container: HTMLElement, + depth: number, + ): Promise { + const loc = fv.Source?.Location; + if (!loc?.File || !loc?.Span) { + const info = decodeFuncObject(fv); + if (info.source) { + const el = document.createElement("a"); + el.className = "b-state-meta b-state-srclink"; + el.textContent = `${info.source.file}:${info.source.startLine}`; + el.href = `${this.pkgPath}$source&file=${encodeURIComponent(info.source.file)}#L${info.source.startLine}`; + container.appendChild(el); + } + // Still show captures even without source location + if (fv.Captures && fv.Captures.length > 0) { + this._renderCaptures(fv, container, depth); + } + return; + } + + const file = loc.File; + const startLine = parseInt(loc.Span.Pos.Line) || 1; + const endLine = parseInt(loc.Span.End.Line) || startLine; + + await this._renderSourceBlock(file, startLine, endLine, container, depth); + + // Show captures for closures + if (fv.Captures && fv.Captures.length > 0) { + this._renderCaptures(fv, container, depth); + } + } + + // Render closure captures as child nodes. + private _renderCaptures( + fv: AminoFuncValue, + container: HTMLElement, + depth: number, + ): void { + const label = document.createElement("div"); + label.className = "b-state-captures-label"; + label.style.paddingLeft = `${(depth + 1) * 1.25 + 0.25}rem`; + label.textContent = "Captured variables:"; + container.appendChild(label); + + const decoded = decodeFuncObject(fv); + if (decoded.children && decoded.children.length > 0) { + this._renderNodes(decoded.children, container, depth + 1); + } + } + + // Fetch syntax-highlighted HTML for a line range of a source file. + private async _fetchSourceHTML( + fileName: string, + start: number, + end: number, + ): Promise { + const cacheKey = `${fileName}:${start}-${end}`; + const cached = this.sourceCache.get(cacheKey); + if (cached !== undefined) return cached; + + const url = `${this.pkgPath}$state&file=${encodeURIComponent(fileName)}&start=${start}&end=${end}&json`; + const resp = await fetch(url); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const html = await resp.text(); + this.sourceCache.set(cacheKey, html); + return html; + } + + // Resolve struct field names from type info. + private async _resolveFieldNames( + typeId: string, + children: StateNode[], + ): Promise { + // Skip stdlib types — field names for stdlib structs (e.g. time.Time) + // are rarely useful and the extra round-trip isn't worth it. + if (!typeId.includes("/")) { + return children; + } + + let names = this.typeCache.get(typeId); + if (!names) { + try { + const url = `${this.pkgPath}$state&tid=${encodeURIComponent(typeId)}&json`; + const resp = await fetch(url); + if (resp.ok) { + const raw: QtypeResponse = await resp.json(); + const resolved = structFieldNames(raw.type); + if (resolved) { + names = resolved; + this.typeCache.set(typeId, names); + } + } + } catch { + // Type resolution failed — keep index-based names + } + } + if (names) { + for (let i = 0; i < children.length && i < names.length; i++) { + children[i].name = names[i]; + } + } + return children; + } +} diff --git a/gno.land/pkg/gnoweb/frontend/js/index.ts b/gno.land/pkg/gnoweb/frontend/js/index.ts index f58505ff099..895e976f520 100644 --- a/gno.land/pkg/gnoweb/frontend/js/index.ts +++ b/gno.land/pkg/gnoweb/frontend/js/index.ts @@ -5,6 +5,9 @@ declare const process: { env: { NODE_ENV: string } }; (() => { // TODO: Make CONTROLLER_PATH build-safe (BASE_URL, CDN, hashing, etc.) const CONTROLLER_PATH = "/public/js/controller-"; + // Extract version from our own script src (?v=...) for cache busting dynamic imports. + const selfScript = document.querySelector('script[src*="index.js"]') as HTMLScriptElement | null; + const versionSuffix = selfScript?.src.match(/(\?v=[^&]*)/)?.[1] || ""; const modulePromises = new Map>>(); // load one controller for a provided set of elements (no re-query) @@ -26,7 +29,7 @@ declare const process: { env: { NODE_ENV: string } }; const pascal = camel.charAt(0).toUpperCase() + camel.slice(1); // Only kebab-case file naming, prefixed with "controller-" - const path = `${CONTROLLER_PATH}${kebab}.js`; + const path = `${CONTROLLER_PATH}${kebab}.js${versionSuffix}`; // import the controller module with promise cache (dedupe concurrent imports) let modulePromise = modulePromises.get(path); diff --git a/gno.land/pkg/gnoweb/frontend/package-lock.json b/gno.land/pkg/gnoweb/frontend/package-lock.json index 6d162854514..ebd70e126a3 100644 --- a/gno.land/pkg/gnoweb/frontend/package-lock.json +++ b/gno.land/pkg/gnoweb/frontend/package-lock.json @@ -5,6 +5,9 @@ "packages": { "": { "name": "gnoweb", + "dependencies": { + "@gnojs/amino": "file:../../../../misc/gnojs" + }, "devDependencies": { "@biomejs/biome": "2.0.5", "@fullhuman/postcss-purgecss": "^7.0.2", @@ -18,6 +21,11 @@ "postcss-preset-env": "^10.2.4" } }, + "../../../../misc/gnojs": { + "name": "@gnojs/amino", + "version": "0.1.0", + "license": "MIT" + }, "node_modules/@biomejs/biome": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.0.5.tgz", @@ -1748,6 +1756,10 @@ "postcss": "^8.0.0" } }, + "node_modules/@gnojs/amino": { + "resolved": "../../../../misc/gnojs", + "link": true + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", diff --git a/gno.land/pkg/gnoweb/frontend/package.json b/gno.land/pkg/gnoweb/frontend/package.json index 5cfd5664080..6428894ed52 100644 --- a/gno.land/pkg/gnoweb/frontend/package.json +++ b/gno.land/pkg/gnoweb/frontend/package.json @@ -1,5 +1,8 @@ { "name": "gnoweb", + "dependencies": { + "@gnojs/amino": "file:../../../../misc/gnojs" + }, "devDependencies": { "@biomejs/biome": "2.0.5", "@fullhuman/postcss-purgecss": "^7.0.2", diff --git a/gno.land/pkg/gnoweb/frontend/postcss.config.cjs b/gno.land/pkg/gnoweb/frontend/postcss.config.cjs index aa8f69ddd67..95353ba3edf 100644 --- a/gno.land/pkg/gnoweb/frontend/postcss.config.cjs +++ b/gno.land/pkg/gnoweb/frontend/postcss.config.cjs @@ -45,7 +45,7 @@ module.exports = (ctx) => { "u-sr-only", "data-theme", ], - deep: [/c-realm-view\b/, /c-readme-view\b/], + deep: [/c-realm-view\b/, /c-readme-view\b/, /b-state-/], }, variables: true, defaultExtractor: (content) => content.match(/[\w-:/%.]+(? 0 && endLine >= startLine { + lines := strings.Split(string(source), "\n") + if startLine <= len(lines) { + end := endLine + if end > len(lines) { + end = len(lines) + } + source = []byte(strings.Join(lines[startLine-1:end], "\n")) + } + } + + // Render with syntax highlighting. + var buf bytes.Buffer + if err := h.Renderer.RenderSource(&buf, fileName, source); err != nil { + h.Logger.Warn("unable to render source file", "file", fileName, "error", err) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Write(source) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(buf.Bytes()) + return + } + + var raw []byte + var err error + + if oid := gnourl.WebQuery.Get("oid"); oid != "" { + raw, err = h.Client.StateObject(ctx, oid) + } else if tid := gnourl.WebQuery.Get("tid"); tid != "" { + raw, err = h.Client.StateType(ctx, tid) + } else { + raw, err = h.Client.StatePkg(ctx, gnourl.Path) + } + + if err != nil { + h.Logger.Warn("unable to fetch state data", "error", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.Write(raw) +} + // ServeSourceDownload handles downloading a source file as plain text. func (h *HTTPHandler) ServeSourceDownload(ctx context.Context, gnourl *weburl.GnoURL, w http.ResponseWriter, r *http.Request) { pkgPath := gnourl.Path diff --git a/gno.land/pkg/gnoweb/handler_http_test.go b/gno.land/pkg/gnoweb/handler_http_test.go index d48f0b7b217..9888a60baeb 100644 --- a/gno.land/pkg/gnoweb/handler_http_test.go +++ b/gno.land/pkg/gnoweb/handler_http_test.go @@ -76,6 +76,18 @@ func (s *stubClient) ListPaths(ctx context.Context, prefix string, limit int) ([ return nil, errors.New("stubClient: ListPaths not implemented") } +func (s *stubClient) StatePkg(_ context.Context, _ string) ([]byte, error) { + return []byte(`[]`), nil +} + +func (s *stubClient) StateObject(_ context.Context, _ string) ([]byte, error) { + return []byte(`[]`), nil +} + +func (s *stubClient) StateType(_ context.Context, _ string) ([]byte, error) { + return []byte(`{}`), nil +} + type rawRenderer struct{} func (rawRenderer) RenderRealm(w io.Writer, u *weburl.GnoURL, src []byte, ctx gnoweb.RealmRenderContext) (md.Toc, error) { @@ -1046,6 +1058,179 @@ func TestHTTPHandler_DownloadWithContext(t *testing.T) { assert.Contains(t, rr.Body.String(), content) } +// TestHTTPHandler_GetStateView tests the state explorer page. +func TestHTTPHandler_GetStateView(t *testing.T) { + t.Parallel() + + mockPackage := &gnoweb.MockPackage{ + Domain: "example.com", + Path: "/r/mock/path", + Files: map[string]string{ + "render.gno": `package main; func Render(path string) string { return "hi" }`, + "gno.mod": `module example.com/r/mock/path`, + }, + Functions: []*doc.JSONFunc{ + {Name: "Render", Params: []*doc.JSONField{{Name: "path", Type: "string"}}, Results: []*doc.JSONField{{Name: "", Type: "string"}}}, + }, + } + + config := newTestHandlerConfig(t, gnoweb.NewMockClient(mockPackage)) + logger := slog.New(slog.NewTextHandler(&testingLogger{t}, &slog.HandlerOptions{})) + handler, err := gnoweb.NewHTTPHandler(logger, config) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodGet, "/r/mock/path$state", nil) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + body := rr.Body.String() + assert.Contains(t, body, "state-explorer", "should contain the state explorer controller") + assert.Contains(t, body, "/r/mock/path", "should contain the package path") +} + +// TestHTTPHandler_GetStateView_NotFound tests state view for a missing package. +func TestHTTPHandler_GetStateView_NotFound(t *testing.T) { + t.Parallel() + + config := newTestHandlerConfig(t, gnoweb.NewMockClient()) + logger := slog.New(slog.NewTextHandler(&testingLogger{t}, &slog.HandlerOptions{})) + handler, err := gnoweb.NewHTTPHandler(logger, config) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodGet, "/r/nonexistent$state", nil) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusNotFound, rr.Code) +} + +// TestHTTPHandler_ServeStateJSON tests the state JSON API endpoints. +func TestHTTPHandler_ServeStateJSON(t *testing.T) { + t.Parallel() + + stateJSON := `{"names":["x"],"values":[{"T":{"@type":"/gno.PrimitiveType","value":"32"}}]}` + objectJSON := `{"objectid":"abc:1","value":{"@type":"/gno.StructValue","Fields":[]}}` + typeJSON := `{"typeid":"gno.land/r/test.Foo","type":{"@type":"/gno.StructType","PkgPath":"gno.land/r/test","Fields":[]}}` + + client := &stubClient{ + realmFunc: func(ctx context.Context, path, args string) ([]byte, error) { + return []byte("realm"), nil + }, + } + + // Override state methods with real data + statePkgFunc := func(_ context.Context, path string) ([]byte, error) { + if path == "/r/test/pkg" { + return []byte(stateJSON), nil + } + return nil, gnoweb.ErrClientPackageNotFound + } + stateObjectFunc := func(_ context.Context, oid string) ([]byte, error) { + if oid == "abc:1" { + return []byte(objectJSON), nil + } + return nil, fmt.Errorf("not found") + } + stateTypeFunc := func(_ context.Context, tid string) ([]byte, error) { + if tid == "gno.land/r/test.Foo" { + return []byte(typeJSON), nil + } + return nil, fmt.Errorf("not found") + } + + // We can't set these on stubClient since the methods are defined on the type, + // so use a wrapper that implements the interface. + wrapper := &stateStubClient{ + stubClient: client, + statePkgFunc: statePkgFunc, + stateObjectFunc: stateObjectFunc, + stateTypeFunc: stateTypeFunc, + } + + config := newTestHandlerConfig(t, wrapper) + + cases := []struct { + Path string + Status int + ContentType string + Contain string + }{ + // Package state JSON + { + Path: "/r/test/pkg$state&json", + Status: http.StatusOK, + ContentType: "application/json", + Contain: `"names"`, + }, + // Object state JSON (colon in OID must be URL-encoded) + { + Path: "/r/test/pkg$state&oid=abc%3A1&json", + Status: http.StatusOK, + ContentType: "application/json", + Contain: `"objectid"`, + }, + // Type JSON + { + Path: "/r/test/pkg$state&tid=gno.land/r/test.Foo&json", + Status: http.StatusOK, + ContentType: "application/json", + Contain: `"typeid"`, + }, + } + + for _, tc := range cases { + t.Run(strings.TrimPrefix(tc.Path, "/"), func(t *testing.T) { + t.Parallel() + + logger := slog.New(slog.NewTextHandler(&testingLogger{t}, &slog.HandlerOptions{})) + handler, err := gnoweb.NewHTTPHandler(logger, config) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, tc.Path, nil) + require.NoError(t, err) + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.Equal(t, tc.Status, rr.Code) + if tc.ContentType != "" { + assert.Equal(t, tc.ContentType, rr.Header().Get("Content-Type")) + } + assert.Contains(t, rr.Body.String(), tc.Contain) + }) + } +} + +// stateStubClient wraps stubClient but overrides state methods with custom functions. +type stateStubClient struct { + *stubClient + statePkgFunc func(context.Context, string) ([]byte, error) + stateObjectFunc func(context.Context, string) ([]byte, error) + stateTypeFunc func(context.Context, string) ([]byte, error) +} + +func (s *stateStubClient) StatePkg(ctx context.Context, path string) ([]byte, error) { + if s.statePkgFunc != nil { + return s.statePkgFunc(ctx, path) + } + return s.stubClient.StatePkg(ctx, path) +} + +func (s *stateStubClient) StateObject(ctx context.Context, oid string) ([]byte, error) { + if s.stateObjectFunc != nil { + return s.stateObjectFunc(ctx, oid) + } + return s.stubClient.StateObject(ctx, oid) +} + +func (s *stateStubClient) StateType(ctx context.Context, tid string) ([]byte, error) { + if s.stateTypeFunc != nil { + return s.stateTypeFunc(ctx, tid) + } + return s.stubClient.StateType(ctx, tid) +} + // TestHTTPHandler_Post_OpenRedirectBlocked tests that protocol-relative URLs // are blocked as a defense-in-depth measure. func TestHTTPHandler_Post_OpenRedirectBlocked(t *testing.T) { @@ -1258,79 +1443,3 @@ func TestHTTPHandler_Post_HiddenPathField(t *testing.T) { }) } } - -func TestHTTPHandler_ThemeCookie(t *testing.T) { - t.Parallel() - - mockPackage := &gnoweb.MockPackage{ - Domain: "example.com", - Path: "/r/mock/path", - Files: map[string]string{ - "render.gno": `package main; func Render(path string) string { return "hello" }`, - "gno.mod": `module example.com/r/mock/path`, - }, - } - - config := newTestHandlerConfig(t, gnoweb.NewMockClient(mockPackage)) - - cases := []struct { - name string - cookieValue string - wantAttr string - }{ - { - name: "success: dark cookie renders data-theme dark", - cookieValue: "dark", - wantAttr: `data-theme="dark"`, - }, - { - name: "success: light cookie renders data-theme light", - cookieValue: "light", - wantAttr: `data-theme="light"`, - }, - { - name: "edge: no cookie renders no data-theme", - cookieValue: "", - wantAttr: "", - }, - { - name: "edge: invalid cookie value ignored", - cookieValue: "purple", - wantAttr: "", - }, - { - name: "edge: system cookie value ignored", - cookieValue: "system", - wantAttr: "", - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - logger := slog.New(slog.NewTextHandler(&testingLogger{t}, &slog.HandlerOptions{})) - handler, err := gnoweb.NewHTTPHandler(logger, config) - require.NoError(t, err) - - req := httptest.NewRequest(http.MethodGet, "/r/mock/path", nil) - if tc.cookieValue != "" { - req.AddCookie(&http.Cookie{Name: "theme", Value: tc.cookieValue}) - } - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - body := rr.Body.String() - if tc.wantAttr != "" { - assert.Contains(t, body, tc.wantAttr, - "expected HTML to contain %q", tc.wantAttr) - } else { - assert.NotContains(t, body, `data-theme=`, - "expected HTML to not contain data-theme attribute") - } - }) - } -} diff --git a/gno.land/pkg/gnoweb/public/js/controller-searchbar.js b/gno.land/pkg/gnoweb/public/js/controller-searchbar.js index 72580ff09c6..1500dac0c9a 100644 --- a/gno.land/pkg/gnoweb/public/js/controller-searchbar.js +++ b/gno.land/pkg/gnoweb/public/js/controller-searchbar.js @@ -1 +1 @@ -import{BaseController as n}from"./controller.js";var e=class extends n{connect(){this.initializeDOM({input:this.getTarget("input"),breadcrumb:this.getTarget("breadcrumb")})}searchUrl(r){r.preventDefault();let t=this.getDOMElement("input")?.value.trim();if(t){/^https?:\/\//i.test(t)||(t=`${window.location.origin}${t.startsWith("/")?"":"/"}${t}`);try{window.location.href=new URL(t).href}catch{console.error("SearchBarController: Invalid URL. Please enter a valid URL starting with http:// or https://.")}}else console.error("SearchBarController: Please enter a URL to search.")}};export{e as SearchbarController}; +import{BaseController as i}from"./controller.js";var o=/^[a-f0-9\]:.]+$/i,a=class extends i{connect(){this.initializeDOM({input:this.getTarget("input"),breadcrumb:this.getTarget("breadcrumb")})}searchUrl(n){n.preventDefault();let t=this.getDOMElement("input")?.value.trim();if(!t){console.error("SearchBarController: Please enter a URL to search.");return}if(o.test(t)&&!t.startsWith("/")){let e=this._currentRealmPath();if(e){window.location.href=`${e}$state&oid=${encodeURIComponent(t)}`;return}}/^https?:\/\//i.test(t)||(t=`${window.location.origin}${t.startsWith("/")?"":"/"}${t}`);try{window.location.href=new URL(t).href}catch{console.error("SearchBarController: Invalid URL. Please enter a valid URL starting with http:// or https://.")}}_currentRealmPath(){let r=window.location.pathname.match(/^(\/r\/[^$]+)/);return r?r[1]:null}};export{a as SearchbarController}; diff --git a/gno.land/pkg/gnoweb/public/js/controller-state-explorer.js b/gno.land/pkg/gnoweb/public/js/controller-state-explorer.js new file mode 100644 index 00000000000..eb108830d9a --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/controller-state-explorer.js @@ -0,0 +1 @@ +var s={Invalid:1,UntypedBool:2,Bool:4,UntypedString:8,String:16,Int:32,Int8:64,Int16:128,UntypedRune:256,Int32:512,Int64:1024,Uint:2048,Uint8:4096,DataByte:8192,Uint16:16384,Uint32:32768,Uint64:65536,Float32:131072,Float64:262144,UntypedBigint:524288,UntypedBigdec:1048576},w={[s.Bool]:"bool",[s.UntypedBool]:"untyped bool",[s.String]:"string",[s.UntypedString]:"untyped string",[s.Int]:"int",[s.Int8]:"int8",[s.Int16]:"int16",[s.UntypedRune]:"rune",[s.Int32]:"int32",[s.Int64]:"int64",[s.Uint]:"uint",[s.Uint8]:"uint8",[s.DataByte]:"databyte",[s.Uint16]:"uint16",[s.Uint32]:"uint32",[s.Uint64]:"uint64",[s.Float32]:"float32",[s.Float64]:"float64",[s.UntypedBigint]:"untyped bigint",[s.UntypedBigdec]:"untyped bigdec"};function x(n){return w[n]??`prim(${n})`}function V(n,t){let a=atob(n),e=new Uint8Array(8);for(let r=0;r`}}function f(n){if(!n)return"";switch(n["@type"]){case"/gno.PrimitiveType":return x(parseInt(n.value));case"/gno.PointerType":return"*"+f(n.Elt);case"/gno.ArrayType":return`[${n.Len}]${f(n.Elt)}`;case"/gno.SliceType":return"[]"+f(n.Elt);case"/gno.MapType":return`map[${f(n.Key)}]${f(n.Value)}`;case"/gno.StructType":return"struct{...}";case"/gno.FuncType":return"func(...)";case"/gno.InterfaceType":return"interface{...}";case"/gno.RefType":{let t=n.ID,a=t.lastIndexOf(".");if(a>=0){let o=t.substring(0,a).split("/");return o[o.length-1]+t.substring(a)}return t}case"/gno.DeclaredType":{let t=(n.PkgPath||"").split("/");return t[t.length-1]+"."+n.Name}case"/gno.TypeType":return"type";case"/gno.PackageType":return"package";case"/gno.ChanType":return`chan ${f(n.Elt)}`;default:return n["@type"].replace("/gno.","")}}function S(n){if(!n)return"nil";switch(n["@type"]){case"/gno.PrimitiveType":return"primitive";case"/gno.PointerType":return"pointer";case"/gno.ArrayType":return"array";case"/gno.SliceType":return"slice";case"/gno.StructType":return"struct";case"/gno.MapType":return"map";case"/gno.FuncType":return"func";case"/gno.InterfaceType":return"interface";case"/gno.RefType":return"ref";case"/gno.DeclaredType":return S(n.Base);case"/gno.TypeType":return"type";case"/gno.PackageType":return"package";case"/gno.ChanType":return"chan";default:return"unknown"}}function k(n){if(n)return n["@type"]==="/gno.DeclaredType"?n.Base:n}function b(n){if(n){if(n["@type"]==="/gno.StructType")return n.Fields.map(t=>t.Name);if(n["@type"]==="/gno.DeclaredType")return b(n.Base)}}function I(n){if(n){if(n["@type"]==="/gno.RefType")return n.ID;if(n["@type"]==="/gno.DeclaredType")return n.PkgPath+"."+n.Name}}function A(n){if(!n||n["@type"]!=="/gno.FuncType")return"func()";let t=n,a=(t.Params||[]).filter(r=>!(r.Name.startsWith("cur")&&r.Type?.["@type"]==="/gno.RefType")).map(r=>{let p=f(r.Type);return r.Name&&!r.Name.startsWith(".")?`${r.Name} ${p}`:p}).join(", "),e=(t.Results||[]).map(r=>{let p=f(r.Type);return r.Name&&!r.Name.startsWith(".")?`${r.Name} ${p}`:p}),o=e.length===0?"":e.length===1?` ${e[0]}`:` (${e.join(", ")})`;return`func(${a})${o}`}function C(n){let t=[];for(let a=0;a",kind:"nil",value:"nil",expandable:!1};let r=f(a),p=S(a),l=k(a),u=I(a);if(p==="func"&&e&&e["@type"]==="/gno.RefValue"){let i=e,c=A(a);return{name:n,type:c,kind:"func",expandable:!0,objectId:i.ObjectID}}if(e&&e["@type"]==="/gno.RefValue"){let i=e;return i.PkgPath?{name:n,type:r,kind:"package",value:i.PkgPath,expandable:!1}:{name:n,type:r,kind:p,expandable:!0,objectId:i.ObjectID,typeId:u}}if(e&&e["@type"]==="/gno.ExportRefValue")return{name:n,type:r,kind:p,value:``,expandable:!1};if(e&&e["@type"]==="/gno.HeapItemValue")return m(n,e.Value);if(e&&e["@type"]==="/gno.TypeValue")return{name:n,type:"type",kind:"type",value:f(e.Type),expandable:!1};if(l&&l["@type"]==="/gno.PrimitiveType"){let i=parseInt(l.value);if((i===s.String||i===s.UntypedString)&&e&&e["@type"]==="/gno.StringValue"){let c=e.value,d=c.length>256?JSON.stringify(c.substring(0,256))+"...":JSON.stringify(c);return{name:n,type:r,kind:"primitive",value:d,expandable:!1}}return o?{name:n,type:r,kind:"primitive",value:V(o,i),expandable:!1}:i===s.Bool||i===s.UntypedBool?{name:n,type:r,kind:"primitive",value:"false",expandable:!1}:i===s.String||i===s.UntypedString?{name:n,type:r,kind:"primitive",value:'""',expandable:!1}:{name:n,type:r,kind:"primitive",value:"0",expandable:!1}}if(e&&e["@type"]==="/gno.StructValue"){let i=e,c=i.ObjectInfo?.ID,d=b(l),y=i.Fields.map((g,h)=>{let T=d&&h0,children:y,objectId:c,typeId:u,length:i.Fields.length}}if(e&&e["@type"]==="/gno.ArrayValue"){let i=e,c=i.ObjectInfo?.ID;if(i.Data){let g=atob(i.Data).length;return{name:n,type:r,kind:"array",value:`[${g}]byte{...}`,expandable:!1,objectId:c,length:g}}let d=i.List||[],y=d.map((g,h)=>m(String(h),g));return{name:n,type:r,kind:"array",expandable:d.length>0,children:y,objectId:c,length:d.length}}if(e&&e["@type"]==="/gno.SliceValue"){let i=e,c=parseInt(i.Length)||0;if(i.Base&&i.Base["@type"]==="/gno.RefValue"){let d=i.Base;return{name:n,type:r,kind:"slice",expandable:c>0,objectId:d.ObjectID,length:c}}if(i.Base&&i.Base["@type"]==="/gno.ArrayValue"){let d=i.Base,y=parseInt(i.Offset)||0;if(d.Data)return{name:n,type:r,kind:"slice",value:`[]byte (len=${c})`,expandable:!1,length:c};let h=(d.List||[]).slice(y,y+c).map((T,$)=>m(String($),T));return{name:n,type:r,kind:"slice",expandable:h.length>0,children:h,length:c}}return{name:n,type:r,kind:"slice",expandable:c>0,length:c}}if(e&&e["@type"]==="/gno.MapValue"){let i=e,c=i.ObjectInfo?.ID,d=i.List?.List||[],y=d.map(g=>{let h=R(g.Key);return m(h,g.Value)});return{name:n,type:r,kind:"map",expandable:y.length>0,children:y,objectId:c,length:d.length}}if(e&&e["@type"]==="/gno.PointerValue"){let i=e;if(i.Base&&i.Base["@type"]==="/gno.RefValue"){let c=i.Base;return{name:n,type:r,kind:"pointer",expandable:!0,objectId:c.ObjectID}}if(i.TV){let c=m("*",i.TV);return{name:n,type:r,kind:"pointer",expandable:!0,children:[c]}}return{name:n,type:r,kind:"pointer",value:"nil",expandable:!1}}if(e&&e["@type"]==="/gno.FuncValue"){let i=e,c=i.Type?A(i.Type):`func ${i.Name}()`,d=P(i),y=i.Captures&&i.Captures.length>0,g=y?"closure":"func";if(y){let h=i.Captures.map((T,$)=>m("value",T));return{name:n,type:c,kind:g,expandable:!0,source:d,children:h}}return{name:n,type:c,kind:g,expandable:!!d,source:d}}return!e&&!o?{name:n,type:r,kind:p,value:"",expandable:!1}:{name:n,type:r,kind:p,value:e?`<${e["@type"]}>`:"",expandable:!1}}function P(n){let t=n.Source?.Location;if(!(!t?.File||!t?.Span))return{file:t.File,startLine:parseInt(t.Span.Pos.Line)||0,endLine:parseInt(t.Span.End.Line)||0}}function v(n){let t=n.Type?A(n.Type):`func ${n.Name}()`,a=P(n),e=n.Captures&&n.Captures.length>0,o=e?"closure":"func";if(e){let r=n.Captures.map((p,l)=>m("value",p));return{name:n.Name,type:t,kind:o,expandable:!0,source:a,children:r}}return{name:n.Name,type:t,kind:o,expandable:!1,source:a}}function L(n){if(!n)return[];switch(n["@type"]){case"/gno.StructValue":return n.Fields.map((a,e)=>m(String(e),a));case"/gno.ArrayValue":{let t=n;return t.Data?[{name:"data",type:"[]byte",kind:"primitive",value:`[${atob(t.Data).length}]byte{...}`,expandable:!1}]:(t.List||[]).map((a,e)=>m(String(e),a))}case"/gno.MapValue":return(n.List?.List||[]).map(a=>{let e=R(a.Key);return m(e,a.Value)});case"/gno.HeapItemValue":return[m("value",n.Value)];case"/gno.Block":return(n.Values||[]).map((a,e)=>m(String(e),a));default:return[]}}function R(n){let t=n.T,a=n.V,e=n.N;if(!t)return"nil";let o=k(t);if(o&&o["@type"]==="/gno.PrimitiveType"){let r=parseInt(o.value);if((r===s.String||r===s.UntypedString)&&a&&a["@type"]==="/gno.StringValue"){let p=a.value;return p.length>64?JSON.stringify(p.substring(0,64))+"...":JSON.stringify(p)}if(e)return V(e,r)}return f(t)}import{BaseController as U}from"./controller.js";var E="\u25B6",j="\u25BC",F=class extends U{connect(){this.pkgPath=this.getValue("pkg-path"),this.typeCache=new Map,this.sourceCache=new Map,window.location.pathname.includes("oid=")&&this._showPathInfo();let t=this.getTarget("initial-data");if(t?.textContent)try{let a=JSON.parse(t.textContent),e=C(a),o=this.getTarget("tree");o&&(this._renderNodes(e,o,0),this._updateCount(e.length))}catch(a){console.error("Failed to parse initial state data:",a)}}_updateCount(t){let a=this.getTarget("count");if(a){let e=this.pkgPath.startsWith("/r/")?"Realm":"Package";a.textContent=`${e} top-level declarations (${t})`}}_showPathInfo(){let t=this.getTarget("path-info");if(!t)return;let a=document.createElement("a");a.href=this.pkgPath,a.textContent=this.pkgPath,a.className="b-state-explorer__path-link",t.textContent="Realm: ",t.appendChild(a)}_renderNodes(t,a,e){let o=document.createDocumentFragment();for(let r of t)o.appendChild(this._createRow(r,e));a.appendChild(o)}_createRow(t,a){let e=document.createElement("div");e.className="b-state-row";let o=document.createElement("div");o.className="b-state-row__line",o.style.paddingLeft=`${a*1.25+.25}rem`;let r=document.createElement("span");r.className="b-state-toggle",(t.expandable||t.children&&t.children.length>0)&&(r.textContent=E,r.addEventListener("click",()=>this._toggle(r,e,t,a))),o.appendChild(r);let p=document.createElement("span");p.className="b-state-name",p.textContent=t.name,o.appendChild(p),o.appendChild(this._sep(":"));let l=document.createElement("span");if(l.className=`b-state-type b-state-kind--${t.kind}`,l.textContent=t.type,o.appendChild(l),t.length!==void 0&&t.length>0){let i=document.createElement("span");i.className="b-state-meta",i.textContent=`(len=${t.length})`,o.appendChild(i)}if(t.value!==void 0&&t.value!==""){o.appendChild(this._sep("="));let i=document.createElement("span");i.className=`b-state-val b-state-val--${t.kind}`,i.textContent=t.value,o.appendChild(i)}if(t.source){let i=document.createElement("a");i.className="b-state-meta b-state-srclink",i.textContent=`${t.source.file}:${t.source.startLine}`,i.href=`${this.pkgPath}$source&file=${encodeURIComponent(t.source.file)}#L${t.source.startLine}`,i.title="View source",o.appendChild(i)}if(t.objectId){let i=t.objectId,c=document.createElement("span");c.className="b-state-oid",c.textContent=i,c.title="Object ID \u2014 click to copy",c.addEventListener("click",d=>{d.stopPropagation(),navigator.clipboard.writeText(i),c.textContent="copied!",setTimeout(()=>{c.textContent=i},1e3)}),o.appendChild(c)}e.appendChild(o);let u=document.createElement("div");return u.className="b-state-kids",t.children&&t.children.length>0?this._renderNodes(t.children,u,a+1):u.hidden=!0,e.appendChild(u),e}_sep(t){let a=document.createElement("span");return a.className="b-state-sep",a.textContent=t,a}async _toggle(t,a,e,o){let r=a.querySelector(".b-state-kids");if(!r)return;if(r.hidden){if(r.children.length===0){if(e.kind==="closure"&&e.children&&e.children.length>0){e.source&&await this._renderSourceBlock(e.source.file,e.source.startLine,e.source.endLine,r,o);let l=document.createElement("div");l.className="b-state-captures-label",l.style.paddingLeft=`${(o+1)*1.25+.25}rem`,l.textContent="Captured variables:",r.appendChild(l),this._renderNodes(e.children,r,o+1)}else if(e.objectId){t.classList.add("b-state-toggle--loading");try{let l=`${this.pkgPath}$state&oid=${encodeURIComponent(e.objectId)}&json`,u=await fetch(l);if(!u.ok)throw new Error(`HTTP ${u.status}`);let i=await u.json();if(e.kind==="func"||e.kind==="closure")await this._expandFunc(i.value,r,o);else{let c=N(i);e.typeId&&c.length>0&&(c=await this._resolveFieldNames(e.typeId,c)),this._renderNodes(c,r,o+1)}}catch(l){console.error("State fetch error:",l);let u=document.createElement("span");u.className="b-state-err",u.textContent="Failed to load",r.appendChild(u)}t.classList.remove("b-state-toggle--loading")}}r.hidden=!1,t.textContent=j}else r.hidden=!0,t.textContent=E}async _renderSourceBlock(t,a,e,o,r){try{let p=await this._fetchSourceHTML(t,a,e),l=document.createElement("div");l.className="b-state-source",l.style.paddingLeft=`${(r+1)*1.25+.25}rem`;let u=document.createElement("a");u.className="b-state-source__link",u.textContent=`${t}:${a}`,u.href=`${this.pkgPath}$source&file=${encodeURIComponent(t)}#L${a}`,u.title="View source",l.appendChild(u);let i=document.createElement("div");i.innerHTML=p,l.appendChild(i),o.appendChild(l)}catch(p){console.error("Source fetch error:",p);let l=document.createElement("span");l.className="b-state-err",l.textContent=`Failed to load source: ${p instanceof Error?p.message:String(p)}`,o.appendChild(l)}}async _expandFunc(t,a,e){let o=t.Source?.Location;if(!o?.File||!o?.Span){let u=v(t);if(u.source){let i=document.createElement("a");i.className="b-state-meta b-state-srclink",i.textContent=`${u.source.file}:${u.source.startLine}`,i.href=`${this.pkgPath}$source&file=${encodeURIComponent(u.source.file)}#L${u.source.startLine}`,a.appendChild(i)}t.Captures&&t.Captures.length>0&&this._renderCaptures(t,a,e);return}let r=o.File,p=parseInt(o.Span.Pos.Line)||1,l=parseInt(o.Span.End.Line)||p;await this._renderSourceBlock(r,p,l,a,e),t.Captures&&t.Captures.length>0&&this._renderCaptures(t,a,e)}_renderCaptures(t,a,e){let o=document.createElement("div");o.className="b-state-captures-label",o.style.paddingLeft=`${(e+1)*1.25+.25}rem`,o.textContent="Captured variables:",a.appendChild(o);let r=v(t);r.children&&r.children.length>0&&this._renderNodes(r.children,a,e+1)}async _fetchSourceHTML(t,a,e){let o=`${t}:${a}-${e}`,r=this.sourceCache.get(o);if(r!==void 0)return r;let p=`${this.pkgPath}$state&file=${encodeURIComponent(t)}&start=${a}&end=${e}&json`,l=await fetch(p);if(!l.ok)throw new Error(`HTTP ${l.status}`);let u=await l.text();return this.sourceCache.set(o,u),u}async _resolveFieldNames(t,a){if(!t.includes("/"))return a;let e=this.typeCache.get(t);if(!e)try{let o=`${this.pkgPath}$state&tid=${encodeURIComponent(t)}&json`,r=await fetch(o);if(r.ok){let p=await r.json(),l=b(p.type);l&&(e=l,this.typeCache.set(t,e))}}catch{}if(e)for(let o=0;op.toUpperCase()),i.charAt(0).toLowerCase()+i.slice(1)}(()=>{let i="/public/js/controller-",m=new Map,p=async(e,o)=>{if(o.length===0)return;let l=b(e);if(!/^[a-z0-9-]+$/.test(l)){console.error(`\u274C Invalid controller name: ${e}`);return}let d=M(e),c=d.charAt(0).toUpperCase()+d.slice(1),a=`${i}${l}.js`,s=m.get(a);s||(s=import(a),m.set(a,s));let t;try{t=await s}catch(n){m.delete(a),console.error(`\u274C Failed to load ${a}:`,n);return}let r,f=t.default;if(typeof f=="function")r=/^\s*class\b/.test(Function.prototype.toString.call(f))?u=>new f(u):f;else{let n=t[`${c}Controller`];n&&(r=u=>new n(u))}if(typeof r!="function"){console.error(`\u274C Invalid controller export for ${e}. Expected default or named class "${c}Controller"`);return}let h=`data-controller-initialized-${l}`,g=o.filter(n=>!n.hasAttribute(h));g.length!==0&&g.forEach(n=>{try{r(n),n.setAttribute(h,"1")}catch(u){console.error(`\u274C Controller runtime error for ${e}:`,u,n)}})},L=e=>{let o=new Map;return e.querySelectorAll("[data-controller]").forEach(l=>{let d=l.getAttribute("data-controller");d&&d.split(/\s+/).filter(Boolean).forEach(c=>{let a=o.get(c)||[];a.push(l),o.set(c,a)})}),o},T=async()=>{let e=L(document);e.size!==0&&(await Promise.all(Array.from(e.entries()).map(([o,l])=>p(o,l))),document.dispatchEvent(new CustomEvent("controllers:ready",{detail:{names:Array.from(e.keys())}})))},v=()=>{let e=new Map,o=!1,l=()=>{if(o=!1,e.size===0)return;let s=[];for(let[t,r]of e)s.push(p(t,[...r]));e.clear(),Promise.all(s).catch(t=>console.error("Observer batch error:",t))},d=()=>{o||(o=!0,queueMicrotask(l))},c=s=>{if(!s.querySelector?.("[data-controller]"))return;let t=L(s);for(let[r,f]of t){let g=`data-controller-initialized-${b(r)}`,n=f.filter(E=>!E.hasAttribute(g));if(n.length===0)continue;let u=e.get(r)??new Set;n.forEach(E=>u.add(E)),e.set(r,u)}t.size&&d()};new MutationObserver(s=>{for(let t of s)t.type==="childList"?t.addedNodes.forEach(r=>{r.nodeType===1&&c(r)}):t.type==="attributes"&&t.target instanceof HTMLElement&&c(t.target)}).observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["data-controller"]})};document.addEventListener("DOMContentLoaded",async()=>{await T(),v()})})(); +function L(i){return i.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function v(i){return i=i.replace(/-([a-z])/g,(T,p)=>p.toUpperCase()),i.charAt(0).toLowerCase()+i.slice(1)}(()=>{let i="/public/js/controller-",p=document.querySelector('script[src*="index.js"]')?.src.match(/(\?v=[^&]*)/)?.[1]||"",g=new Map,b=async(e,o)=>{if(o.length===0)return;let l=L(e);if(!/^[a-z0-9-]+$/.test(l)){console.error(`\u274C Invalid controller name: ${e}`);return}let d=v(e),c=d.charAt(0).toUpperCase()+d.slice(1),a=`${i}${l}.js${p}`,s=g.get(a);s||(s=import(a),g.set(a,s));let t;try{t=await s}catch(n){g.delete(a),console.error(`\u274C Failed to load ${a}:`,n);return}let r,f=t.default;if(typeof f=="function")r=/^\s*class\b/.test(Function.prototype.toString.call(f))?u=>new f(u):f;else{let n=t[`${c}Controller`];n&&(r=u=>new n(u))}if(typeof r!="function"){console.error(`\u274C Invalid controller export for ${e}. Expected default or named class "${c}Controller"`);return}let h=`data-controller-initialized-${l}`,m=o.filter(n=>!n.hasAttribute(h));m.length!==0&&m.forEach(n=>{try{r(n),n.setAttribute(h,"1")}catch(u){console.error(`\u274C Controller runtime error for ${e}:`,u,n)}})},M=e=>{let o=new Map;return e.querySelectorAll("[data-controller]").forEach(l=>{let d=l.getAttribute("data-controller");d&&d.split(/\s+/).filter(Boolean).forEach(c=>{let a=o.get(c)||[];a.push(l),o.set(c,a)})}),o},w=async()=>{let e=M(document);e.size!==0&&(await Promise.all(Array.from(e.entries()).map(([o,l])=>b(o,l))),document.dispatchEvent(new CustomEvent("controllers:ready",{detail:{names:Array.from(e.keys())}})))},y=()=>{let e=new Map,o=!1,l=()=>{if(o=!1,e.size===0)return;let s=[];for(let[t,r]of e)s.push(b(t,[...r]));e.clear(),Promise.all(s).catch(t=>console.error("Observer batch error:",t))},d=()=>{o||(o=!0,queueMicrotask(l))},c=s=>{if(!s.querySelector?.("[data-controller]"))return;let t=M(s);for(let[r,f]of t){let m=`data-controller-initialized-${L(r)}`,n=f.filter(E=>!E.hasAttribute(m));if(n.length===0)continue;let u=e.get(r)??new Set;n.forEach(E=>u.add(E)),e.set(r,u)}t.size&&d()};new MutationObserver(s=>{for(let t of s)t.type==="childList"?t.addedNodes.forEach(r=>{r.nodeType===1&&c(r)}):t.type==="attributes"&&t.target instanceof HTMLElement&&c(t.target)}).observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["data-controller"]})};document.addEventListener("DOMContentLoaded",async()=>{await w(),y()})})(); diff --git a/gno.land/pkg/gnoweb/public/main.css b/gno.land/pkg/gnoweb/public/main.css index 6063b3c47b8..af23f360613 100644 --- a/gno.land/pkg/gnoweb/public/main.css +++ b/gno.land/pkg/gnoweb/public/main.css @@ -1,6 +1,6 @@ -:root{--g-px-base:16;--g-space-mult:4;--g-space-base:calc(1rem/var(--g-space-mult));--g-breakpoint-max:calc(1580/var(--g-px-base)*1rem);--g-z-min:-1;--g-z-1:1;--g-z-max:9999;--g-duration-75:75ms;--g-duration-150:150ms;--g-opacity-50:0.5;--g-grid-1:repeat(1,minmax(0,1fr));--g-grid-10:repeat(10,minmax(0,1fr));--g-space-px:1px;--g-space-0-5:calc(var(--g-space-base)*0.5);--g-space-1:var(--g-space-base);--g-space-1-5:calc(var(--g-space-base)*1.5);--g-space-2:calc(var(--g-space-base)*2);--g-space-2-5:calc(var(--g-space-base)*2.5);--g-space-3:calc(var(--g-space-base)*3);--g-space-4:calc(var(--g-space-base)*4);--g-space-4-5:calc(var(--g-space-base)*4.5);--g-space-5:calc(var(--g-space-base)*5);--g-space-6:calc(var(--g-space-base)*6);--g-space-7:calc(var(--g-space-base)*7);--g-space-8:calc(var(--g-space-base)*8);--g-space-10:calc(var(--g-space-base)*10);--g-space-12:calc(var(--g-space-base)*12);--g-space-14:calc(var(--g-space-base)*14);--g-space-20:calc(var(--g-space-base)*20);--g-space-24:calc(var(--g-space-base)*24);--g-space-28:calc(var(--g-space-base)*28);--g-space-32:calc(var(--g-space-base)*32);--g-space-36:calc(var(--g-space-base)*36);--g-space-44:calc(var(--g-space-base)*44);--g-space-48:calc(var(--g-space-base)*48);--g-space-52:calc(var(--g-space-base)*52);--g-space-72:calc(var(--g-space-base)*72);--g-space-96:calc(var(--g-space-base)*96);--g-font-size-50:calc(12/var(--g-px-base)*1rem);--g-font-size-100:calc(14/var(--g-px-base)*1rem);--g-font-size-200:calc(16/var(--g-px-base)*1rem);--g-font-size-300:calc(18/var(--g-px-base)*1rem);--g-font-size-400:calc(20/var(--g-px-base)*1rem);--g-font-size-500:calc(22/var(--g-px-base)*1rem);--g-font-size-600:calc(24/var(--g-px-base)*1rem);--g-font-size-700:calc(32/var(--g-px-base)*1rem);--g-font-size-800:calc(38/var(--g-px-base)*1rem);--g-font-family-mono:"Roboto",'Menlo, Consolas, "Ubuntu Mono", "Roboto Mono", "DejaVu Sans Mono", monospace';--g-font-family-inter-var:"Inter",'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif';--g-font-normal:400;--g-font-medium:500;--g-font-semibold:600;--g-font-bold:700;--g-italic:oblique 14deg;--g-line-height-tight:1.25;--g-line-height-snug:1.375;--g-line-height-normal:1.5;--g-border-radius-sm:calc(4/var(--g-px-base)*1rem);--g-border-radius:calc(6/var(--g-px-base)*1rem);--g-border-radius-full:9999px;--g-color-light:#fff;--g-color-transparent:transparent;--g-color-gray-50:#f0f0f0;--g-color-gray-100:#e2e2e2;--g-color-gray-200:#bdbdbd;--g-color-gray-300:#999;--g-color-gray-400:#7c7c7c;--g-color-gray-500:#696969;--g-color-gray-600:#585858;--g-color-gray-700:#292929;--g-color-gray-750:#1f1f1f;--g-color-gray-800:#141414;--g-color-gray-850:#0e0e0e;--g-color-gray-900:#090909;--g-color-green-50:#e7efed;--g-color-green-400:#60ab96;--g-color-green-500:#277b63;--g-color-green-600:#226c57;--g-color-green-900:#144134;--g-color-green-950:#002c20;--g-color-blue-400:#49afeb;--g-color-blue-600:#3e96c9;--g-color-blue-900:#21506b;--g-color-yellow-50:#fff7eb;--g-color-yellow-400:#facc32;--g-color-yellow-600:#fbbf24;--g-color-yellow-900:#7b4807;--g-color-yellow-950:#362600;--g-color-red-400:#eb6c49;--g-color-red-600:#c95c3e;--g-color-red-900:#6b2521;--g-color-purple-400:#7f49eb;--g-color-purple-600:#6c3ec9;--g-color-purple-900:#39216b}@supports (color:color(display-p3 0 0 0%)){:root{--g-color-green-950:#002c20;--g-color-yellow-50:#fff7eb;--g-color-yellow-950:#362600}@media (color-gamut:p3){:root{--g-color-green-950:color(display-p3 0.04602 0.17026 0.1277);--g-color-yellow-50:color(display-p3 0.99709 0.97106 0.92232);--g-color-yellow-950:color(display-p3 0.2031 0.15112 0.01811)}}}:root{--s-color-bg-base:var(--g-color-light,#fff);--s-color-bg-base-dev:var(--g-color-gray-50,#f0f0f0);--s-color-bg-surface-primary:var(--g-color-gray-50,#f0f0f0);--s-color-bg-surface-primary-hover:var(--g-color-gray-100,#f0f0f0);--s-color-bg-surface-secondary:var(--g-color-gray-100,#e2e2e2);--s-color-bg-surface-quaternary:var(--g-color-gray-400,#7c7c7c);--s-color-bg-brand-default:var(--g-color-green-600,#226c57);--s-color-bg-brand-weak:var(--g-color-green-50,#f0f9ff);--s-color-bg-success-default:var(--g-color-green-600,#144134);--s-color-bg-info-default:var(--g-color-blue-600,#21506b);--s-color-bg-warning-default:var(--g-color-yellow-600,#665100);--s-color-bg-warning-weak:var(--g-color-yellow-50,#f9d985);--s-color-bg-warning-action:var(--g-color-yellow-400,#f9d985);--s-color-bg-caution-default:var(--g-color-red-600,#610);--s-color-bg-tip-default:var(--g-color-purple-600,#49216b);--s-color-bg-note-default:var(--g-color-gray-600,#21506b);--s-color-bg-input:var(--g-color-light,#fff);--s-color-text-base:var(--g-color-light,#fff);--s-color-text-primary:var(--g-color-gray-900,#080809);--s-color-text-secondary:var(--g-color-gray-600,#454a4e);--s-color-text-tertiary:var(--g-color-gray-400,#f0f0f0);--s-color-text-tertiary-hover:var(--g-color-gray-600,#e2e2e2);--s-color-text-quaternary:var(--g-color-gray-100,#f0f0f0);--s-color-text-brand-default:var(--g-color-light,#fff);--s-color-text-link:var(--g-color-green-600,#226c57);--s-color-text-link-hover:var(--g-color-green-600,#226c57);--s-color-text-success:var(--g-color-green-900,#144134);--s-color-text-info:var(--g-color-blue-900,#21506b);--s-color-text-warning:var(--g-color-yellow-900,#665100);--s-color-text-caution:var(--g-color-red-900,#610);--s-color-text-tip:var(--g-color-purple-900,#49216b);--s-color-border-primary:var(--g-color-gray-200,#bdbdbd);--s-color-border-secondary:var(--g-color-gray-100,#e2e2e2);--s-color-border-tertiary:var(--g-color-gray-300,#999);--s-color-border-quaternary:var(--g-color-gray-400,#7c7c7c);--s-color-border-transparent:var(--g-color-transparent,transparent);--s-color-border-input:var(--g-color-gray-300,#999);--s-color-border-brand-default:var(--g-color-green-600,#226c57);--s-color-border-success:var(--g-color-green-600,#144134);--s-color-border-info:var(--g-color-blue-600,#21506b);--s-color-border-warning:var(--g-color-yellow-600,#665100);--s-color-border-error:var(--g-color-red-600,#610);--s-color-border-tip:var(--g-color-purple-600,#49216b);--s-color-border-note:var(--g-color-gray-600,#21506b);--s-rounded-sm:var(--g-border-radius-sm,4px);--s-rounded:var(--g-border-radius,6px);--s-rounded-full:var(--g-border-radius-full,9999px);--s-border:var(--g-space-px,1px) solid var(--s-color-border-primary);--s-border-secondary:var(--g-space-px,1px) solid var(--s-color-border-secondary)}[data-theme=dark]{--s-color-bg-base:var(--g-color-gray-850);--s-color-bg-base-dev:var(--g-color-gray-800);--s-color-bg-surface-primary:var(--g-color-gray-800);--s-color-bg-surface-primary-hover:var(--g-color-gray-750);--s-color-bg-surface-secondary:var(--g-color-gray-750);--s-color-bg-surface-quaternary:var(--g-color-gray-600);--s-color-bg-brand-weak:var(--g-color-green-950);--s-color-bg-warning-weak:var(--g-color-yellow-950);--s-color-bg-input:var(--g-color-gray-800);--s-color-text-primary:var(--g-color-gray-100);--s-color-text-secondary:var(--g-color-gray-200);--s-color-text-tertiary:var(--g-color-gray-400);--s-color-text-tertiary-hover:var(--g-color-gray-300);--s-color-text-quaternary:var(--g-color-gray-500);--s-color-text-brand-default:var(--g-color-light);--s-color-text-link:var(--g-color-green-500);--s-color-text-link-hover:var(--g-color-green-400);--s-color-text-success:var(--g-color-green-400);--s-color-text-info:var(--g-color-blue-400);--s-color-text-warning:var(--g-color-yellow-400);--s-color-text-caution:var(--g-color-red-400);--s-color-text-tip:var(--g-color-purple-400);--s-color-border-primary:var(--g-color-gray-700);--s-color-border-secondary:var(--g-color-gray-750);--s-color-border-tertiary:var(--g-color-gray-600);--s-color-border-quaternary:var(--g-color-gray-500);--s-color-border-input:var(--g-color-gray-700);--s-color-border-brand-default:var(--g-color-green-600);--s-color-border-success:var(--g-color-green-400);--s-color-border-info:var(--g-color-blue-400);--s-color-border-warning:var(--g-color-yellow-400);--s-color-border-error:var(--g-color-red-400);--s-color-border-tip:var(--g-color-purple-400);--s-color-border-note:var(--g-color-gray-600)}*,::backdrop,::file-selector-button,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}html{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}h1,h2,h3{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub{bottom:-.25em;font-size:75%;line-height:0;position:relative;vertical-align:baseline}table{border-collapse:collapse;border-color:inherit;text-indent:0}summary{display:list-item}menu,ol,ul{list-style:none}embed,img,object,svg{display:block;vertical-align:middle}img{height:auto;max-width:100%}::file-selector-button,button,input,select,textarea{background-color:transparent;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}::file-selector-button{margin-right:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}::-webkit-calendar-picker-indicator{line-height:1}::file-selector-button,button,input:where([type=button],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}@font-face{font-display:swap;font-family:Roboto;font-style:normal;font-weight:900;src:url(fonts/roboto/roboto-mono-normal.woff2) format("woff2"),url(fonts/roboto/roboto-mono-normal.woff) format("woff")}@font-face{font-display:block;font-family:Inter;font-style:oblique 0deg 10deg;font-variant:normal;font-weight:100 900;src:url(fonts/intervar/Intervar.woff2) format("woff2")}html{background-color:var(--s-color-bg-base);color:var(--s-color-text-secondary);font-family:var(--g-font-family-inter-var);font-feature-settings:"kern" on,"liga" on,"calt" off,"zero" on,contextual common-ligatures,"kern";-webkit-font-feature-settings:"kern" on,"liga" on,"calt" off,"zero" on;font-size:calc(var(--g-px-base)*1px);line-height:var(--g-line-height-normal);-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-kerning:normal;font-variant-ligatures:contextual common-ligatures;text-rendering:optimizeLegibility}body{display:flex;flex-direction:column;min-height:100vh}main{background-color:var(--s-color-bg-base);flex-grow:2;width:100%}main.dev-mode{background-color:var(--s-color-bg-base-dev)}main>section{display:grid;grid-auto-flow:dense;grid-template-columns:var(--g-grid-1);grid-column-gap:var(--g-space-20);-moz-column-gap:var(--g-space-20);column-gap:var(--g-space-20);min-height:100%;padding-left:var(--g-space-4);padding-right:var(--g-space-4)}@media (min-width:calc(640 / 16 * 1rem)){main>section{padding-left:var(--g-space-10);padding-right:var(--g-space-10)}}@media (min-width:calc(820 / 16 * 1rem)){main>section{grid-template-columns:var(--g-grid-10)}}@media (min-width:calc(1366 / 16 * 1rem)){main>section{-moz-column-gap:var(--g-space-32);column-gap:var(--g-space-32)}}svg{max-height:100%;max-width:100%}form{margin-bottom:0;margin-top:0}code{font-family:var(--g-font-mono)}summary{cursor:pointer}md-renderer{margin-top:var(--g-space-4);padding-bottom:var(--g-space-24)}@media (min-width:calc(820 / 16 * 1rem)){md-renderer{grid-column:span 7;margin-top:0}}::-moz-selection{background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base)}::selection{background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base)}summary::-webkit-details-marker{display:none}summary::marker{display:none}.c-stack{display:flex;flex-direction:column;justify-content:flex-start}.c-stack>*+*{margin-top:var(--g-space-4)}.c-inline{align-items:center;display:inline-flex;gap:var(--g-space-3)}.c-between{align-items:center;display:flex;justify-content:space-between}.c-center{box-sizing:border-box;margin-left:auto;margin-right:auto;max-width:var(--g-breakpoint-max);padding-left:var(--g-space-4);padding-right:var(--g-space-4)}@media (min-width:calc(640 / 16 * 1rem)){.c-center{padding-left:var(--g-space-10);padding-right:var(--g-space-10)}}.c-full-screen{align-items:center;display:flex;flex-direction:column;grid-column:1/-1;height:100%;justify-content:center;margin-top:var(--g-space-10);padding-bottom:var(--g-space-24);width:100%}.c-reel{display:flex;overflow:scroll}.c-icon{flex-shrink:0;height:1.15em;width:1.15em}.c-with-icon{align-items:flex-start;display:inline-flex}.c-with-icon .c-icon,.c-with-icon--inline .c-icon{margin-left:.3em;margin-right:.3em;margin-top:.15em}.c-with-icon--inline{display:inline-block}.c-with-icon--inline>*{vertical-align:middle}.c-with-icon--inline .c-icon{margin-top:0}.c-view-grid{display:flex;flex-direction:column}@media (min-width:calc(640 / 16 * 1rem)){.c-view-grid{-moz-column-gap:var(--g-space-8);column-gap:var(--g-space-8);flex-direction:row}}@media (min-width:calc(820 / 16 * 1rem)){.c-view-grid{display:grid;grid-template-columns:var(--g-grid-10);grid-column-gap:var(--g-space-20);-moz-column-gap:var(--g-space-20);column-gap:var(--g-space-20)}}@media (min-width:calc(1366 / 16 * 1rem)){.c-view-grid{-moz-column-gap:var(--g-space-32);column-gap:var(--g-space-32)}}.c-toggle-btn>input{display:none}.c-toggle-btn label{visibility:hidden}.c-toggle-btn input:checked+label{visibility:visible}.c-readme-view,.c-realm-view{--cr-px-base:var(--g-px-base);--cr-space-mult:1;--cr-space-base:calc(1em/var(--g-space-mult)*var(--cr-space-mult));--cr-space-0:0;--cr-space-0-5:calc(var(--cr-space-base)*0.5);--cr-space-1:var(--cr-space-base);--cr-space-2:calc(var(--cr-space-base)*2);--cr-space-3:calc(var(--cr-space-base)*3);--cr-space-4:calc(var(--cr-space-base)*4);--cr-space-5:calc(var(--cr-space-base)*5);--cr-space-7:calc(var(--cr-space-base)*7);--cr-space-8:calc(var(--cr-space-base)*8);--cr-space-24:calc(var(--cr-space-base)*24);--cr-color-brand-default:var(--s-color-text-link);display:block;font-size:calc(var(--cr-px-base)*1px);padding-top:var(--g-space-4);word-break:break-word}.c-readme-view:empty,.c-realm-view:empty{display:none}.c-realm-view:has(.b-btn:only-child){display:none}.c-readme-view:has(.b-btn:only-child){display:none}@media (min-width:calc(820 / 16 * 1rem)){.c-readme-view,.c-realm-view{grid-row-start:1;padding-top:var(--g-space-6)}}.c-readme-view a,.c-realm-view a{color:var(--cr-color-brand-default);display:inline-block;font-weight:inherit;position:relative;text-wrap:balance;vertical-align:top}.c-readme-view a:hover,.c-realm-view a:hover{-webkit-text-decoration:underline;text-decoration:underline}.c-realm-view a:has(>img){vertical-align:middle}.c-readme-view a:has(>img){vertical-align:middle}.c-readme-view a>span,.c-realm-view a>span{margin-bottom:.1em}.c-readme-view a>.tooltip+.tooltip,.c-realm-view a>.tooltip+.tooltip{margin-left:.2em}.c-readme-view a>.tooltip:last-of-type,.c-realm-view a>.tooltip:last-of-type{margin-right:.2em}.c-realm-view a:has(>img:first-child):has(.tooltip:last-child):not(:has(>:nth-child(3)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-readme-view a:has(>img:first-child):has(.tooltip:last-child):not(:has(>:nth-child(3)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-realm-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-readme-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-realm-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip:first-of-type{bottom:var(--g-space-2);left:var(--g-space-7);position:absolute}.c-readme-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip:first-of-type{bottom:var(--g-space-2);left:var(--g-space-7);position:absolute}.c-readme-view h1+h2,.c-readme-view h2+h3,.c-readme-view h3+h4,.c-realm-view h1+h2,.c-realm-view h2+h3,.c-realm-view h3+h4{margin-top:var(--cr-space-4)}.c-readme-view h1,.c-readme-view h2,.c-readme-view h3,.c-readme-view h4,.c-realm-view h1,.c-realm-view h2,.c-realm-view h3,.c-realm-view h4{color:var(--s-color-text-primary);line-height:var(--g-line-height-tight);margin-top:var(--cr-space-4)}.c-readme-view h1,.c-realm-view h1{font-size:var(--g-font-size-700);font-weight:var(--g-font-bold);margin-bottom:var(--cr-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view h1,.c-realm-view h1{font-size:var(--g-font-size-800)}}.c-readme-view h2,.c-realm-view h2{font-size:var(--g-font-size-500);font-weight:var(--g-font-bold)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view h2,.c-realm-view h2{font-size:var(--g-font-size-600)}}.c-readme-view h2 *,.c-realm-view h2 *{font-weight:var(--g-font-bold)}.c-readme-view h3,.c-readme-view h4,.c-realm-view h3,.c-realm-view h4{color:var(--s-color-text-secondary);font-weight:var(--g-font-semibold)}.c-readme-view h3,.c-realm-view h3{font-size:var(--g-font-size-400);margin-top:var(--cr-space-4)}.c-readme-view h4,.c-realm-view h4{font-size:var(--g-font-size-300);margin-top:var(--cr-space-3)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view h4,.c-realm-view h4{font-size:var(--g-font-size-300)}}.c-readme-view h3 *,.c-readme-view h4 *,.c-realm-view h3 *,.c-realm-view h4 *{font-weight:var(--g-font-semibold)}.c-readme-view h5,.c-readme-view h6,.c-realm-view h5,.c-realm-view h6{font-size:var(--g-font-size-300);font-weight:var(--g-font-bold);margin-bottom:var(--cr-space-0);margin-top:var(--cr-space-0)}.c-readme-view h5+p,.c-readme-view h6+p,.c-realm-view h5+p,.c-realm-view h6+p{margin-top:var(--cr-space-0)}.c-readme-view img,.c-realm-view img{border:1px solid var(--s-color-bg-surface-primary);border-radius:var(--g-border-radius-sm);margin-bottom:var(--cr-space-2);margin-top:var(--cr-space-2);max-width:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none}.c-readme-view figure,.c-realm-view figure{margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3);text-align:center}.c-readme-view figcaption,.c-realm-view figcaption{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100)}.c-readme-view video,.c-realm-view video{margin-bottom:var(--g-space-4);margin-top:var(--g-space-4);max-width:100%}.c-readme-view p,.c-realm-view p{margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3)}.c-realm-view p:has(>a:only-child>img){margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4)}.c-readme-view p:has(>a:only-child>img){margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4)}.c-realm-view p:has(>a:only-child>img) img{margin-bottom:0;margin-top:0}.c-readme-view p:has(>a:only-child>img) img{margin-bottom:0;margin-top:0}.c-readme-view strong,.c-readme-view strong *,.c-realm-view strong,.c-realm-view strong *{font-weight:var(--g-font-bold)}.c-readme-view em,.c-realm-view em{font-style:var(--g-italic)}.c-readme-view blockquote,.c-realm-view blockquote{border-left:solid var(--g-space-0-5) var(--s-color-border-tertiary);color:var(--s-color-text-secondary);margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4);padding-left:var(--g-space-3)}.c-readme-view blockquote>blockquote,.c-realm-view blockquote>blockquote{margin-bottom:var(--cr-space-7);margin-top:var(--cr-space-7)}.c-readme-view caption,.c-realm-view caption{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100);margin-top:var(--cr-space-2);text-align:left}.c-readme-view q,.c-realm-view q{quotes:"“" "”"}.c-readme-view q:before,.c-realm-view q:before{content:open-quote}.c-readme-view q:after,.c-realm-view q:after{content:close-quote}.c-readme-view details,.c-realm-view details{margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3)}.c-readme-view summary,.c-realm-view summary{cursor:pointer;font-weight:var(--g-font-bold)}.c-readme-view math,.c-realm-view math{font-family:var(--g-font-family-mono)}.c-readme-view small,.c-realm-view small{font-size:var(--g-font-size-100)}.c-readme-view del,.c-realm-view del{-webkit-text-decoration:line-through;text-decoration:line-through}.c-readme-view sub,.c-realm-view sub{font-size:var(--g-font-size-50);vertical-align:sub}.c-readme-view sup,.c-realm-view sup{font-size:var(--g-font-size-50);padding-left:var(--space-px);vertical-align:middle}.c-readme-view sup>a,.c-realm-view sup>a{vertical-align:middle}.c-readme-view ol,.c-readme-view ul,.c-realm-view ol,.c-realm-view ul{margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4);padding-left:var(--g-space-4)}.c-readme-view ul,.c-realm-view ul{list-style:disc}.c-readme-view ol,.c-realm-view ol{list-style:decimal}.c-readme-view ol ol,.c-readme-view ol ul,.c-readme-view ul ol,.c-readme-view ul ul,.c-realm-view ol ol,.c-realm-view ol ul,.c-realm-view ul ol,.c-realm-view ul ul{margin-bottom:var(--cr-space-2);margin-top:var(--cr-space-2);padding-left:var(--g-space-4)}.c-readme-view li,.c-realm-view li{margin-bottom:var(--cr-space-1);margin-top:var(--cr-space-1)}.c-readme-view code,.c-readme-view pre,.c-realm-view code,.c-realm-view pre{font-family:var(--g-font-family-mono)}.c-readme-view pre,.c-readme-view pre.chroma-chroma,.c-realm-view pre,.c-realm-view pre.chroma-chroma{background-color:var(--s-color-bg-surface-primary);border-radius:var(--g-border-radius-sm);margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3);overflow-x:auto;padding:var(--cr-space-4)}.c-readme-view :not(pre)>code,.c-realm-view :not(pre)>code{background-color:var(--s-color-bg-surface-secondary);border-radius:var(--g-border-radius-sm);font-size:.96em;padding:var(--cr-space-0-5) var(--cr-space-1)}.c-readme-view a code,.c-realm-view a code{color:inherit}.c-readme-view hr,.c-realm-view hr{border-top:var(--s-border-secondary);margin-bottom:var(--cr-space-8);margin-top:var(--cr-space-8)}.c-readme-view table,.c-realm-view table{border-collapse:collapse;display:block;margin-bottom:var(--cr-space-5);margin-top:var(--cr-space-5);max-width:100%;width:100%}.c-readme-view td,.c-readme-view th,.c-realm-view td,.c-realm-view th{border:var(--s-border);padding:var(--cr-space-2) var(--cr-space-4);white-space:normal;word-break:break-word}.c-readme-view th,.c-realm-view th{background-color:var(--s-color-bg-surface-secondary);font-weight:var(--g-font-bold)}.c-readme-view button,.c-readme-view input,.c-readme-view select,.c-readme-view textarea,.c-realm-view button,.c-realm-view input,.c-realm-view select,.c-realm-view textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--s-color-bg-input);border:var(--s-border);padding:var(--cr-space-2) var(--cr-space-4)}.c-readme-view>.realm-view__btns:first-child+*,.c-readme-view>:first-child:not(.realm-view__btns),.c-realm-view>.realm-view__btns:first-child+*,.c-realm-view>:first-child:not(.realm-view__btns){margin-top:0!important}.c-readme-view .footnote-backref,.c-readme-view h1:not(.does-not-exist),.c-readme-view h2:not(.does-not-exist),.c-readme-view h3:not(.does-not-exist),.c-readme-view h4:not(.does-not-exist),.c-readme-view sup:not(.does-not-exist),.c-realm-view .footnote-backref,.c-realm-view h1:not(.does-not-exist),.c-realm-view h2:not(.does-not-exist),.c-realm-view h3:not(.does-not-exist),.c-realm-view h4:not(.does-not-exist),.c-realm-view sup:not(.does-not-exist){scroll-margin-top:var(--cr-space-24)}.c-readme-view .b-btn,.c-realm-view .b-btn{color:var(--s-color-text-secondary);display:inline-flex}.c-readme-view .b-btn:hover,.c-realm-view .b-btn:hover{-webkit-text-decoration:none;text-decoration:none}.c-readme-view .b-btn:first-child,.c-realm-view .b-btn:first-child{float:right;margin-top:var(--g-space-4)}.c-readme-view>.b-btn:first-child+*,.c-readme-view>:first-child:not(.b-btn),.c-realm-view>.b-btn:first-child+*,.c-realm-view>:first-child:not(.b-btn){margin-top:0}.c-readme-view{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius);margin-bottom:var(--g-space-6);padding:var(--g-space-6) var(--g-space-4) var(--g-space-4);width:100%}@media (min-width:calc(820 / 16 * 1rem)){.c-readme-view{grid-row-start:auto}}.b-header{background-color:var(--s-color-bg-base);border-bottom:var(--s-border);font-size:var(--g-font-size-100);position:sticky;top:0;z-index:var(--g-z-max)}.b-header nav{align-items:stretch;height:auto}.b-header .main-nav{align-items:stretch;display:flex;flex:1 1 auto;gap:var(--g-space-1);height:100%;min-width:0;padding-bottom:var(--g-space-2);padding-top:var(--g-space-2);width:100%}@media (min-width:calc(820 / 16 * 1rem)){.b-header .main-nav{grid-column:span 7}}.b-header .main-nav--explorer{grid-column:span 10}.b-header .user-picture{border:var(--s-border-secondary);border-radius:var(--s-rounded);cursor:pointer;flex-shrink:0;height:var(--g-space-10);width:var(--g-space-10)}.b-header .user-picture>svg{height:100%;width:100%}.b-main-navigation{color:var(--s-color-text-quaternary);height:auto;position:relative;width:100%}.b-main-navigation>.inner{align-items:center;background-color:var(--s-color-bg-surface-secondary);border:var(--s-border-secondary);border-radius:var(--s-rounded);height:100%;padding-left:var(--g-space-1-5);padding-right:var(--g-space-1-5);position:relative}@media (min-width:calc(640 / 16 * 1rem)){.b-main-navigation>.inner{padding-right:var(--g-space-8)}}.b-main-navigation>.inner:has([data-role=header-input-search]:focus-within){border-color:var(--s-color-border-tertiary)}.b-main-navigation .searchbar{bottom:0;color:var(--s-color-text-secondary);font-size:var(--g-font-size-200);font-weight:var(--g-font-medium);left:0;padding:var(--g-space-1-5);padding-right:var(--g-space-8);position:absolute;right:0;top:0}.b-main-navigation .searchbar>input{background-color:transparent;height:100%;outline:none;width:100%}.b-main-navigation .searchbar:focus-within+.b-breadcrumb{display:none}.b-main-navigation .network-toggle{align-items:center;background-color:var(--g-color-transparent);border-radius:var(--g-border-radius);cursor:pointer;display:none;height:calc(100% - 2px);justify-content:center;padding:var(--g-space-1-5);position:absolute;right:1px;top:1px;z-index:var(--g-z-max)}@media (min-width:calc(640 / 16 * 1rem)){.b-main-navigation .network-toggle{display:flex}}.b-main-navigation .network-toggle>svg{color:var(--s-color-text-tertiary);height:var(--g-space-5);width:var(--g-space-5)}.b-main-navigation .network-toggle:hover>svg{color:var(--s-color-text-tertiary-hover)}.b-main-navigation .b-popup-dialog>.inner{color:var(--s-color-text-tertiary);width:var(--g-space-72)}.b-main-navigation .b-popup-dialog header>span{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100);font-weight:var(--g-font-semibold)}.b-main-navigation .b-popup-dialog .item{display:flex;gap:var(--g-space-1)}.b-main-navigation .b-popup-dialog .item>svg{height:var(--g-space-4);width:var(--g-space-4)}.b-main-navigation .b-popup-dialog .item-content{display:flex;flex-direction:column}.b-main-navigation .b-popup-dialog .item-label{font-size:var(--g-font-size-50)}.b-main-navigation .b-popup-dialog .item-value{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100);font-weight:var(--g-font-semibold)}.b-main-menu{display:flex;flex:0 0 auto;grid-column:span 3;height:var(--g-space-12)}@media (min-width:calc(640 / 16 * 1rem)){.b-main-menu{height:auto}}.b-main-menu .menu-toggle{align-items:center;cursor:pointer;display:flex;margin-left:auto;order:3}.b-main-menu .menu-toggle>svg{height:var(--g-space-5);margin-left:var(--g-space-4);width:var(--g-space-5)}@media (min-width:calc(820 / 16 * 1rem)){.b-main-menu .menu-toggle>svg{margin-left:var(--g-space-2)}}.b-main-menu .menu-toggle-input~.menu-dev{display:none}.b-main-menu .menu-toggle-input:checked~.menu-dev{display:flex}.b-main-menu .menu-toggle-input:checked~.menu-general{display:none}.b-main-menu .menu-dev,.b-main-menu .menu-general{display:flex;height:100%;justify-content:flex-end}.b-menu-link:last-child,.b-menu-link:last-child .link{margin-right:0}.b-menu-link .link{align-items:center;color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-100);font-weight:var(--g-font-semibold);gap:var(--g-space-1);height:100%;margin-right:var(--g-space-3);position:relative}.b-menu-link .link:hover{color:var(--s-color-text-tertiary-hover)}.b-menu-link .link:after{background-color:var(--s-color-bg-brand-default);border-radius:var(--s-rounded) var(--s-rounded) 0 0;bottom:0;content:"";height:var(--g-space-1);left:0;position:absolute;transition:width var(--g-transition-fast);width:0}.b-menu-link .link>svg{flex-shrink:0;height:var(--g-space-5);min-width:var(--g-space-2);width:var(--g-space-5)}@media (min-width:calc(1020 / 16 * 1rem)){.b-menu-link .link>svg{display:none}}@media (min-width:calc(1366 / 16 * 1rem)){.b-menu-link .link>svg{display:inline-block;height:var(--g-space-4-5);width:var(--g-space-4-5)}}@media (min-width:calc(640 / 16 * 1rem)){.b-menu-link .link{font-weight:var(--g-font-bold)}}@media (min-width:calc(1366 / 16 * 1rem)){.b-menu-link .link{margin-right:var(--g-space-6);padding-right:var(--g-space-1)}}@media (min-width:calc(640 / 16 * 1rem)){.b-menu-link .link-label{display:none}}@media (min-width:calc(1020 / 16 * 1rem)){.b-menu-link .link-label{display:inline}}.b-menu-link .link--icon{font-weight:var(--g-font-regular);margin-right:var(--g-space-4)}@media (min-width:calc(480 / 16 * 1rem)){.b-menu-link .link--icon{margin-right:var(--g-space-6)}}.b-menu-link .link--is-active{color:var(--s-color-text-secondary)}.b-menu-link .link--is-active:after{width:100%}.b-menu-link .link--is-active>svg{color:var(--s-color-bg-brand-default)}.menu-general .link{color:var(--s-color-text-secondary)}.menu-general .link:hover{color:var(--s-color-text-link-hover)}.b-breadcrumb{display:flex}.b-breadcrumb,.b-breadcrumb:after{background-color:var(--s-color-bg-surface-secondary)}.b-breadcrumb:after{bottom:0;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:0}.b-breadcrumb>ol{color:var(--s-color-text-primary);display:flex;font-weight:var(--g-font-semibold);line-height:var(--g-line-height-snug)}.b-breadcrumb .argument,.b-breadcrumb .element,.b-breadcrumb .query{align-items:center;display:flex;white-space:nowrap;z-index:var(--g-z-1)}.b-breadcrumb .argument:not(:first-child):before,.b-breadcrumb .element:not(:first-child):before,.b-breadcrumb .query:not(:first-child):before{color:var(--s-color-text-tertiary);content:"/";line-height:var(--g-line-height-normal);padding-left:.18rem;padding-right:.18rem;padding-top:var(--g-space-px)}.b-breadcrumb .argument a,.b-breadcrumb .element a,.b-breadcrumb .query a{background-color:var(--s-color-bg-base);border:1px solid var(--s-color-border-transparent);border-radius:var(--s-rounded-sm);display:inline-block;min-width:var(--g-space-4);padding:var(--g-space-0-5);text-align:center}.b-breadcrumb .argument a:hover,.b-breadcrumb .element a:hover,.b-breadcrumb .query a:hover{background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base)}.b-breadcrumb .argument:not(:first-child):before{content:":"}.b-breadcrumb .argument a{background-color:var(--s-color-bg-surface-quaternary);color:var(--s-color-text-base)}.b-breadcrumb .query:not(:first-child):before{content:"&"}.b-breadcrumb .query:nth-child(1 of .query):before{content:"?"}.b-breadcrumb .query label{background-color:var(--s-color-bg-surface-primary);border:var(--s-border);border-radius:var(--s-rounded-sm);color:var(--s-color-text-secondary);cursor:text;display:inline-flex;height:100%;min-width:var(--g-space-4);padding:var(--g-space-0-5) var(--g-space-1);position:relative;text-align:center;width:100%}.b-breadcrumb .query label:focus-within{border-color:var(--s-color-border-quaternary)}.b-breadcrumb .query label:hover{border-color:var(--s-color-border-quaternary)}.b-breadcrumb .query input{background-color:var(--s-color-bg-surface-primary);max-width:10ch;order:3;outline:none;field-sizing:content}@supports not (field-sizing:content){.b-breadcrumb .query input{width:5rem!important}}.b-breadcrumb .query input::-moz-placeholder{opacity:0}.b-breadcrumb .query input::placeholder{opacity:0}.b-breadcrumb .query input:-moz-placeholder{width:var(--g-space-px)}.b-breadcrumb .query input:placeholder-shown{width:var(--g-space-px)}.b-breadcrumb .query input:placeholder-shown::-moz-placeholder{color:var(--g-color-transparent)}.b-breadcrumb .query input:-moz-placeholder::placeholder{color:var(--g-color-transparent)}.b-breadcrumb .query input:placeholder-shown::placeholder{color:var(--g-color-transparent)}.b-footer{border-top:var(--s-border);font-size:var(--g-font-size-100);padding-bottom:var(--g-space-4);padding-top:var(--g-space-4);width:100%}.b-footer>nav{flex-direction:column;row-gap:var(--g-space-8)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer>nav{flex-wrap:wrap}}.b-footer .logo{color:var(--s-color-text-primary);grid-column:1/-1;width:var(--g-space-44)}.b-footer .logo:hover{color:var(--s-color-text-primary);-webkit-text-decoration:none;text-decoration:none}@media (min-width:calc(1020 / 16 * 1rem)){.b-footer .logo{align-self:center;grid-column:1/3;grid-row:1/1;width:60%}}.b-footer .nav-primary{display:flex;gap:var(--g-space-10);grid-column:1/-1;grid-row:2/3}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .nav-primary{align-items:center;flex:1 0 0%;flex-direction:row;gap:var(--g-space-6);justify-content:space-between}}@media (min-width:calc(1020 / 16 * 1rem)){.b-footer .nav-primary{grid-column:2/8;grid-row:1/1}}.b-footer .nav-primary>ul{display:flex;flex:1;flex-direction:column;flex-wrap:wrap;gap:var(--g-space-1) var(--g-space-3)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .nav-primary>ul{flex:initial;flex-direction:row}.b-footer .nav-social{margin-left:auto}}@media (min-width:calc(820 / 16 * 1rem)){.b-footer .nav-social{grid-column:span 3;justify-self:end;margin-left:0}}.b-footer .nav-theme{align-items:center;display:flex;gap:var(--g-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .nav-theme{flex-basis:100%}}@media (min-width:calc(820 / 16 * 1rem)){.b-footer .nav-theme{grid-column:span 3}}.b-footer .nav-theme .nav-theme-label{color:var(--s-color-text-secondary)}.b-footer .nav-theme:has([data-theme-target=sun]:not(.u-hidden)) .nav-theme-label:before{content:"Light"}.b-footer .nav-theme:has([data-theme-target=moon]:not(.u-hidden)) .nav-theme-label:before{content:"Dark"}.b-footer .nav-theme:has([data-theme-target=system]:not(.u-hidden)) +:root{--g-px-base:16;--g-space-mult:4;--g-space-base:calc(1rem/var(--g-space-mult));--g-breakpoint-max:calc(1580/var(--g-px-base)*1rem);--g-z-min:-1;--g-z-1:1;--g-z-max:9999;--g-duration-75:75ms;--g-duration-150:150ms;--g-opacity-50:0.5;--g-grid-1:repeat(1,minmax(0,1fr));--g-grid-10:repeat(10,minmax(0,1fr));--g-space-px:1px;--g-space-0-5:calc(var(--g-space-base)*0.5);--g-space-1:var(--g-space-base);--g-space-1-5:calc(var(--g-space-base)*1.5);--g-space-2:calc(var(--g-space-base)*2);--g-space-2-5:calc(var(--g-space-base)*2.5);--g-space-3:calc(var(--g-space-base)*3);--g-space-4:calc(var(--g-space-base)*4);--g-space-4-5:calc(var(--g-space-base)*4.5);--g-space-5:calc(var(--g-space-base)*5);--g-space-6:calc(var(--g-space-base)*6);--g-space-7:calc(var(--g-space-base)*7);--g-space-8:calc(var(--g-space-base)*8);--g-space-10:calc(var(--g-space-base)*10);--g-space-12:calc(var(--g-space-base)*12);--g-space-14:calc(var(--g-space-base)*14);--g-space-20:calc(var(--g-space-base)*20);--g-space-24:calc(var(--g-space-base)*24);--g-space-28:calc(var(--g-space-base)*28);--g-space-32:calc(var(--g-space-base)*32);--g-space-36:calc(var(--g-space-base)*36);--g-space-44:calc(var(--g-space-base)*44);--g-space-48:calc(var(--g-space-base)*48);--g-space-52:calc(var(--g-space-base)*52);--g-space-72:calc(var(--g-space-base)*72);--g-space-96:calc(var(--g-space-base)*96);--g-font-size-50:calc(12/var(--g-px-base)*1rem);--g-font-size-100:calc(14/var(--g-px-base)*1rem);--g-font-size-200:calc(16/var(--g-px-base)*1rem);--g-font-size-300:calc(18/var(--g-px-base)*1rem);--g-font-size-400:calc(20/var(--g-px-base)*1rem);--g-font-size-500:calc(22/var(--g-px-base)*1rem);--g-font-size-600:calc(24/var(--g-px-base)*1rem);--g-font-size-700:calc(32/var(--g-px-base)*1rem);--g-font-size-800:calc(38/var(--g-px-base)*1rem);--g-font-family-mono:"Roboto",'Menlo, Consolas, "Ubuntu Mono", "Roboto Mono", "DejaVu Sans Mono", monospace';--g-font-family-inter-var:"Inter",'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif';--g-font-normal:400;--g-font-medium:500;--g-font-semibold:600;--g-font-bold:700;--g-italic:oblique 14deg;--g-line-height-tight:1.25;--g-line-height-snug:1.375;--g-line-height-normal:1.5;--g-border-radius-sm:calc(4/var(--g-px-base)*1rem);--g-border-radius:calc(6/var(--g-px-base)*1rem);--g-border-radius-full:9999px;--g-color-light:#fff;--g-color-transparent:transparent;--g-color-gray-50:#f0f0f0;--g-color-gray-100:#e2e2e2;--g-color-gray-200:#bdbdbd;--g-color-gray-300:#999;--g-color-gray-400:#7c7c7c;--g-color-gray-500:#696969;--g-color-gray-600:#585858;--g-color-gray-700:#292929;--g-color-gray-750:#1f1f1f;--g-color-gray-800:#141414;--g-color-gray-850:#0e0e0e;--g-color-gray-900:#090909;--g-color-green-50:#e7efed;--g-color-green-400:#60ab96;--g-color-green-500:#277b63;--g-color-green-600:#226c57;--g-color-green-900:#144134;--g-color-green-950:#002c20;--g-color-blue-400:#49afeb;--g-color-blue-600:#3e96c9;--g-color-blue-900:#21506b;--g-color-yellow-50:#fff7eb;--g-color-yellow-400:#facc32;--g-color-yellow-600:#fbbf24;--g-color-yellow-900:#7b4807;--g-color-yellow-950:#362600;--g-color-red-400:#eb6c49;--g-color-red-600:#c95c3e;--g-color-red-900:#6b2521;--g-color-purple-400:#7f49eb;--g-color-purple-600:#6c3ec9;--g-color-purple-900:#39216b}@supports (color:color(display-p3 0 0 0%)){:root{--g-color-green-950:#002c20;--g-color-yellow-50:#fff7eb;--g-color-yellow-950:#362600}@media (color-gamut:p3){:root{--g-color-green-950:color(display-p3 0.04602 0.17026 0.1277);--g-color-yellow-50:color(display-p3 0.99709 0.97106 0.92232);--g-color-yellow-950:color(display-p3 0.2031 0.15112 0.01811)}}}:root{--s-color-bg-base:var(--g-color-light,#fff);--s-color-bg-base-dev:var(--g-color-gray-50,#f0f0f0);--s-color-bg-surface-primary:var(--g-color-gray-50,#f0f0f0);--s-color-bg-surface-primary-hover:var(--g-color-gray-100,#f0f0f0);--s-color-bg-surface-secondary:var(--g-color-gray-100,#e2e2e2);--s-color-bg-surface-tertiary:var(--g-color-gray-200,#bdbdbd);--s-color-bg-surface-quaternary:var(--g-color-gray-400,#7c7c7c);--s-color-bg-brand-default:var(--g-color-green-600,#226c57);--s-color-bg-brand-weak:var(--g-color-green-50,#f0f9ff);--s-color-bg-success-default:var(--g-color-green-600,#144134);--s-color-bg-info-default:var(--g-color-blue-600,#21506b);--s-color-bg-warning-default:var(--g-color-yellow-600,#665100);--s-color-bg-warning-weak:var(--g-color-yellow-50,#f9d985);--s-color-bg-warning-action:var(--g-color-yellow-400,#f9d985);--s-color-bg-caution-default:var(--g-color-red-600,#610);--s-color-bg-tip-default:var(--g-color-purple-600,#49216b);--s-color-bg-note-default:var(--g-color-gray-600,#21506b);--s-color-bg-input:var(--g-color-light,#fff);--s-color-text-base:var(--g-color-light,#fff);--s-color-text-primary:var(--g-color-gray-900,#080809);--s-color-text-secondary:var(--g-color-gray-600,#454a4e);--s-color-text-tertiary:var(--g-color-gray-400,#f0f0f0);--s-color-text-tertiary-hover:var(--g-color-gray-600,#e2e2e2);--s-color-text-quaternary:var(--g-color-gray-100,#f0f0f0);--s-color-text-brand-default:var(--g-color-light,#fff);--s-color-text-link:var(--g-color-green-600,#226c57);--s-color-text-link-hover:var(--g-color-green-600,#226c57);--s-color-text-success:var(--g-color-green-900,#144134);--s-color-text-info:var(--g-color-blue-900,#21506b);--s-color-text-warning:var(--g-color-yellow-900,#665100);--s-color-text-caution:var(--g-color-red-900,#610);--s-color-text-tip:var(--g-color-purple-900,#49216b);--s-color-border-primary:var(--g-color-gray-200,#bdbdbd);--s-color-border-secondary:var(--g-color-gray-100,#e2e2e2);--s-color-border-tertiary:var(--g-color-gray-300,#999);--s-color-border-quaternary:var(--g-color-gray-400,#7c7c7c);--s-color-border-transparent:var(--g-color-transparent,transparent);--s-color-border-input:var(--g-color-gray-300,#999);--s-color-border-brand-default:var(--g-color-green-600,#226c57);--s-color-border-success:var(--g-color-green-600,#144134);--s-color-border-info:var(--g-color-blue-600,#21506b);--s-color-border-warning:var(--g-color-yellow-600,#665100);--s-color-border-error:var(--g-color-red-600,#610);--s-color-border-tip:var(--g-color-purple-600,#49216b);--s-color-border-note:var(--g-color-gray-600,#21506b);--s-rounded-sm:var(--g-border-radius-sm,4px);--s-rounded:var(--g-border-radius,6px);--s-rounded-full:var(--g-border-radius-full,9999px);--s-border:var(--g-space-px,1px) solid var(--s-color-border-primary);--s-border-secondary:var(--g-space-px,1px) solid var(--s-color-border-secondary)}[data-theme=dark]{--s-color-bg-base:var(--g-color-gray-850);--s-color-bg-base-dev:var(--g-color-gray-800);--s-color-bg-surface-primary:var(--g-color-gray-800);--s-color-bg-surface-primary-hover:var(--g-color-gray-750);--s-color-bg-surface-secondary:var(--g-color-gray-750);--s-color-bg-surface-tertiary:var(--g-color-gray-700);--s-color-bg-surface-quaternary:var(--g-color-gray-600);--s-color-bg-brand-weak:var(--g-color-green-950);--s-color-bg-warning-weak:var(--g-color-yellow-950);--s-color-bg-input:var(--g-color-gray-800);--s-color-text-primary:var(--g-color-gray-100);--s-color-text-secondary:var(--g-color-gray-200);--s-color-text-tertiary:var(--g-color-gray-400);--s-color-text-tertiary-hover:var(--g-color-gray-300);--s-color-text-quaternary:var(--g-color-gray-500);--s-color-text-brand-default:var(--g-color-light);--s-color-text-link:var(--g-color-green-500);--s-color-text-link-hover:var(--g-color-green-400);--s-color-text-success:var(--g-color-green-400);--s-color-text-info:var(--g-color-blue-400);--s-color-text-warning:var(--g-color-yellow-400);--s-color-text-caution:var(--g-color-red-400);--s-color-text-tip:var(--g-color-purple-400);--s-color-border-primary:var(--g-color-gray-700);--s-color-border-secondary:var(--g-color-gray-750);--s-color-border-tertiary:var(--g-color-gray-600);--s-color-border-quaternary:var(--g-color-gray-500);--s-color-border-input:var(--g-color-gray-700);--s-color-border-brand-default:var(--g-color-green-600);--s-color-border-success:var(--g-color-green-400);--s-color-border-info:var(--g-color-blue-400);--s-color-border-warning:var(--g-color-yellow-400);--s-color-border-error:var(--g-color-red-400);--s-color-border-tip:var(--g-color-purple-400);--s-color-border-note:var(--g-color-gray-600)}*,::backdrop,::file-selector-button,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}html{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}h1,h2,h3{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub{bottom:-.25em;font-size:75%;line-height:0;position:relative;vertical-align:baseline}table{border-collapse:collapse;border-color:inherit;text-indent:0}summary{display:list-item}menu,ol,ul{list-style:none}embed,img,object,svg{display:block;vertical-align:middle}img{height:auto;max-width:100%}::file-selector-button,button,input,select,textarea{background-color:transparent;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}::file-selector-button{margin-right:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}::-webkit-calendar-picker-indicator{line-height:1}::file-selector-button,button,input:where([type=button],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}@font-face{font-display:swap;font-family:Roboto;font-style:normal;font-weight:900;src:url(fonts/roboto/roboto-mono-normal.woff2) format("woff2"),url(fonts/roboto/roboto-mono-normal.woff) format("woff")}@font-face{font-display:block;font-family:Inter;font-style:oblique 0deg 10deg;font-variant:normal;font-weight:100 900;src:url(fonts/intervar/Intervar.woff2) format("woff2")}html{background-color:var(--s-color-bg-base);color:var(--s-color-text-secondary);font-family:var(--g-font-family-inter-var);font-feature-settings:"kern" on,"liga" on,"calt" off,"zero" on,contextual common-ligatures,"kern";-webkit-font-feature-settings:"kern" on,"liga" on,"calt" off,"zero" on;font-size:calc(var(--g-px-base)*1px);line-height:var(--g-line-height-normal);-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-kerning:normal;font-variant-ligatures:contextual common-ligatures;text-rendering:optimizeLegibility}body{display:flex;flex-direction:column;min-height:100vh}main{background-color:var(--s-color-bg-base);flex-grow:2;width:100%}main.dev-mode{background-color:var(--s-color-bg-base-dev)}main>section{display:grid;grid-auto-flow:dense;grid-template-columns:var(--g-grid-1);grid-column-gap:var(--g-space-20);-moz-column-gap:var(--g-space-20);column-gap:var(--g-space-20);min-height:100%;padding-left:var(--g-space-4);padding-right:var(--g-space-4)}@media (min-width:calc(640 / 16 * 1rem)){main>section{padding-left:var(--g-space-10);padding-right:var(--g-space-10)}}@media (min-width:calc(820 / 16 * 1rem)){main>section{grid-template-columns:var(--g-grid-10)}}@media (min-width:calc(1366 / 16 * 1rem)){main>section{-moz-column-gap:var(--g-space-32);column-gap:var(--g-space-32)}}svg{max-height:100%;max-width:100%}form{margin-bottom:0;margin-top:0}code{font-family:var(--g-font-mono)}summary{cursor:pointer}md-renderer{margin-top:var(--g-space-4);padding-bottom:var(--g-space-24)}@media (min-width:calc(820 / 16 * 1rem)){md-renderer{grid-column:span 7;margin-top:0}}::-moz-selection{background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base)}::selection{background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base)}summary::-webkit-details-marker{display:none}summary::marker{display:none}.c-stack{display:flex;flex-direction:column;justify-content:flex-start}.c-stack>*+*{margin-top:var(--g-space-4)}.c-inline{align-items:center;display:inline-flex;gap:var(--g-space-3)}.c-between{align-items:center;display:flex;justify-content:space-between}.c-center{box-sizing:border-box;margin-left:auto;margin-right:auto;max-width:var(--g-breakpoint-max);padding-left:var(--g-space-4);padding-right:var(--g-space-4)}@media (min-width:calc(640 / 16 * 1rem)){.c-center{padding-left:var(--g-space-10);padding-right:var(--g-space-10)}}.c-full-screen{align-items:center;display:flex;flex-direction:column;grid-column:1/-1;height:100%;justify-content:center;margin-top:var(--g-space-10);padding-bottom:var(--g-space-24);width:100%}.c-reel{display:flex;overflow:scroll}.c-icon{flex-shrink:0;height:1.15em;width:1.15em}.c-with-icon{align-items:flex-start;display:inline-flex}.c-with-icon .c-icon,.c-with-icon--inline .c-icon{margin-left:.3em;margin-right:.3em;margin-top:.15em}.c-with-icon--inline{display:inline-block}.c-with-icon--inline>*{vertical-align:middle}.c-with-icon--inline .c-icon{margin-top:0}.c-view-grid{display:flex;flex-direction:column}@media (min-width:calc(640 / 16 * 1rem)){.c-view-grid{-moz-column-gap:var(--g-space-8);column-gap:var(--g-space-8);flex-direction:row}}@media (min-width:calc(820 / 16 * 1rem)){.c-view-grid{display:grid;grid-template-columns:var(--g-grid-10);grid-column-gap:var(--g-space-20);-moz-column-gap:var(--g-space-20);column-gap:var(--g-space-20)}}@media (min-width:calc(1366 / 16 * 1rem)){.c-view-grid{-moz-column-gap:var(--g-space-32);column-gap:var(--g-space-32)}}.c-toggle-btn>input{display:none}.c-toggle-btn label{visibility:hidden}.c-toggle-btn input:checked+label{visibility:visible}.c-readme-view,.c-realm-view{--cr-px-base:var(--g-px-base);--cr-space-mult:1;--cr-space-base:calc(1em/var(--g-space-mult)*var(--cr-space-mult));--cr-space-0:0;--cr-space-0-5:calc(var(--cr-space-base)*0.5);--cr-space-1:var(--cr-space-base);--cr-space-2:calc(var(--cr-space-base)*2);--cr-space-3:calc(var(--cr-space-base)*3);--cr-space-4:calc(var(--cr-space-base)*4);--cr-space-5:calc(var(--cr-space-base)*5);--cr-space-7:calc(var(--cr-space-base)*7);--cr-space-8:calc(var(--cr-space-base)*8);--cr-space-24:calc(var(--cr-space-base)*24);--cr-color-brand-default:var(--s-color-text-link);display:block;font-size:calc(var(--cr-px-base)*1px);padding-top:var(--g-space-4);word-break:break-word}.c-readme-view:empty,.c-realm-view:empty{display:none}.c-realm-view:has(.b-btn:only-child){display:none}.c-readme-view:has(.b-btn:only-child){display:none}@media (min-width:calc(820 / 16 * 1rem)){.c-readme-view,.c-realm-view{grid-row-start:1;padding-top:var(--g-space-6)}}.c-readme-view a,.c-realm-view a{color:var(--cr-color-brand-default);display:inline-block;font-weight:inherit;position:relative;text-wrap:balance;vertical-align:top}.c-readme-view a:hover,.c-realm-view a:hover{-webkit-text-decoration:underline;text-decoration:underline}.c-realm-view a:has(>img){vertical-align:middle}.c-readme-view a:has(>img){vertical-align:middle}.c-readme-view a>span,.c-realm-view a>span{margin-bottom:.1em}.c-readme-view a>.tooltip+.tooltip,.c-realm-view a>.tooltip+.tooltip{margin-left:.2em}.c-readme-view a>.tooltip:last-of-type,.c-realm-view a>.tooltip:last-of-type{margin-right:.2em}.c-realm-view a:has(>img:first-child):has(.tooltip:last-child):not(:has(>:nth-child(3)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-readme-view a:has(>img:first-child):has(.tooltip:last-child):not(:has(>:nth-child(3)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-realm-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-readme-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-realm-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip:first-of-type{bottom:var(--g-space-2);left:var(--g-space-7);position:absolute}.c-readme-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip:first-of-type{bottom:var(--g-space-2);left:var(--g-space-7);position:absolute}.c-readme-view h1+h2,.c-readme-view h2+h3,.c-readme-view h3+h4,.c-realm-view h1+h2,.c-realm-view h2+h3,.c-realm-view h3+h4{margin-top:var(--cr-space-4)}.c-readme-view h1,.c-readme-view h2,.c-readme-view h3,.c-readme-view h4,.c-realm-view h1,.c-realm-view h2,.c-realm-view h3,.c-realm-view h4{color:var(--s-color-text-primary);line-height:var(--g-line-height-tight);margin-top:var(--cr-space-4)}.c-readme-view h1,.c-realm-view h1{font-size:var(--g-font-size-700);font-weight:var(--g-font-bold);margin-bottom:var(--cr-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view h1,.c-realm-view h1{font-size:var(--g-font-size-800)}}.c-readme-view h2,.c-realm-view h2{font-size:var(--g-font-size-500);font-weight:var(--g-font-bold)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view h2,.c-realm-view h2{font-size:var(--g-font-size-600)}}.c-readme-view h2 *,.c-realm-view h2 *{font-weight:var(--g-font-bold)}.c-readme-view h3,.c-readme-view h4,.c-realm-view h3,.c-realm-view h4{color:var(--s-color-text-secondary);font-weight:var(--g-font-semibold)}.c-readme-view h3,.c-realm-view h3{font-size:var(--g-font-size-400);margin-top:var(--cr-space-4)}.c-readme-view h4,.c-realm-view h4{font-size:var(--g-font-size-300);margin-top:var(--cr-space-3)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view h4,.c-realm-view h4{font-size:var(--g-font-size-300)}}.c-readme-view h3 *,.c-readme-view h4 *,.c-realm-view h3 *,.c-realm-view h4 *{font-weight:var(--g-font-semibold)}.c-readme-view h5,.c-readme-view h6,.c-realm-view h5,.c-realm-view h6{font-size:var(--g-font-size-300);font-weight:var(--g-font-bold);margin-bottom:var(--cr-space-0);margin-top:var(--cr-space-0)}.c-readme-view h5+p,.c-readme-view h6+p,.c-realm-view h5+p,.c-realm-view h6+p{margin-top:var(--cr-space-0)}.c-readme-view img,.c-realm-view img{border:1px solid var(--s-color-bg-surface-primary);border-radius:var(--g-border-radius-sm);margin-bottom:var(--cr-space-2);margin-top:var(--cr-space-2);max-width:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none}.c-readme-view figure,.c-realm-view figure{margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3);text-align:center}.c-readme-view figcaption,.c-realm-view figcaption{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100)}.c-readme-view video,.c-realm-view video{margin-bottom:var(--g-space-4);margin-top:var(--g-space-4);max-width:100%}.c-readme-view p,.c-realm-view p{margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3)}.c-realm-view p:has(>a:only-child>img){margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4)}.c-readme-view p:has(>a:only-child>img){margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4)}.c-realm-view p:has(>a:only-child>img) img{margin-bottom:0;margin-top:0}.c-readme-view p:has(>a:only-child>img) img{margin-bottom:0;margin-top:0}.c-readme-view strong,.c-readme-view strong *,.c-realm-view strong,.c-realm-view strong *{font-weight:var(--g-font-bold)}.c-readme-view em,.c-realm-view em{font-style:var(--g-italic)}.c-readme-view blockquote,.c-realm-view blockquote{border-left:solid var(--g-space-0-5) var(--s-color-border-tertiary);color:var(--s-color-text-secondary);margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4);padding-left:var(--g-space-3)}.c-readme-view blockquote>blockquote,.c-realm-view blockquote>blockquote{margin-bottom:var(--cr-space-7);margin-top:var(--cr-space-7)}.c-readme-view caption,.c-realm-view caption{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100);margin-top:var(--cr-space-2);text-align:left}.c-readme-view q,.c-realm-view q{quotes:"“" "”"}.c-readme-view q:before,.c-realm-view q:before{content:open-quote}.c-readme-view q:after,.c-realm-view q:after{content:close-quote}.c-readme-view details,.c-realm-view details{margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3)}.c-readme-view summary,.c-realm-view summary{cursor:pointer;font-weight:var(--g-font-bold)}.c-readme-view math,.c-realm-view math{font-family:var(--g-font-family-mono)}.c-readme-view small,.c-realm-view small{font-size:var(--g-font-size-100)}.c-readme-view del,.c-realm-view del{-webkit-text-decoration:line-through;text-decoration:line-through}.c-readme-view sub,.c-realm-view sub{font-size:var(--g-font-size-50);vertical-align:sub}.c-readme-view sup,.c-realm-view sup{font-size:var(--g-font-size-50);padding-left:var(--space-px);vertical-align:middle}.c-readme-view sup>a,.c-realm-view sup>a{vertical-align:middle}.c-readme-view ol,.c-readme-view ul,.c-realm-view ol,.c-realm-view ul{margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4);padding-left:var(--g-space-4)}.c-readme-view ul,.c-realm-view ul{list-style:disc}.c-readme-view ol,.c-realm-view ol{list-style:decimal}.c-readme-view ol ol,.c-readme-view ol ul,.c-readme-view ul ol,.c-readme-view ul ul,.c-realm-view ol ol,.c-realm-view ol ul,.c-realm-view ul ol,.c-realm-view ul ul{margin-bottom:var(--cr-space-2);margin-top:var(--cr-space-2);padding-left:var(--g-space-4)}.c-readme-view li,.c-realm-view li{margin-bottom:var(--cr-space-1);margin-top:var(--cr-space-1)}.c-readme-view code,.c-readme-view pre,.c-realm-view code,.c-realm-view pre{font-family:var(--g-font-family-mono)}.c-readme-view pre,.c-readme-view pre.chroma-chroma,.c-realm-view pre,.c-realm-view pre.chroma-chroma{background-color:var(--s-color-bg-surface-primary);border-radius:var(--g-border-radius-sm);margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3);overflow-x:auto;padding:var(--cr-space-4)}.c-readme-view :not(pre)>code,.c-realm-view :not(pre)>code{background-color:var(--s-color-bg-surface-secondary);border-radius:var(--g-border-radius-sm);font-size:.96em;padding:var(--cr-space-0-5) var(--cr-space-1)}.c-readme-view a code,.c-realm-view a code{color:inherit}.c-readme-view hr,.c-realm-view hr{border-top:var(--s-border-secondary);margin-bottom:var(--cr-space-8);margin-top:var(--cr-space-8)}.c-readme-view table,.c-realm-view table{border-collapse:collapse;display:block;margin-bottom:var(--cr-space-5);margin-top:var(--cr-space-5);max-width:100%;width:100%}.c-readme-view td,.c-readme-view th,.c-realm-view td,.c-realm-view th{border:var(--s-border);padding:var(--cr-space-2) var(--cr-space-4);white-space:normal;word-break:break-word}.c-readme-view th,.c-realm-view th{background-color:var(--s-color-bg-surface-secondary);font-weight:var(--g-font-bold)}.c-readme-view button,.c-readme-view input,.c-readme-view select,.c-readme-view textarea,.c-realm-view button,.c-realm-view input,.c-realm-view select,.c-realm-view textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--s-color-bg-input);border:var(--s-border);padding:var(--cr-space-2) var(--cr-space-4)}.c-readme-view>.realm-view__btns:first-child+*,.c-readme-view>:first-child:not(.realm-view__btns),.c-realm-view>.realm-view__btns:first-child+*,.c-realm-view>:first-child:not(.realm-view__btns){margin-top:0!important}.c-readme-view .footnote-backref,.c-readme-view h1:not(.does-not-exist),.c-readme-view h2:not(.does-not-exist),.c-readme-view h3:not(.does-not-exist),.c-readme-view h4:not(.does-not-exist),.c-readme-view sup:not(.does-not-exist),.c-realm-view .footnote-backref,.c-realm-view h1:not(.does-not-exist),.c-realm-view h2:not(.does-not-exist),.c-realm-view h3:not(.does-not-exist),.c-realm-view h4:not(.does-not-exist),.c-realm-view sup:not(.does-not-exist){scroll-margin-top:var(--cr-space-24)}.c-readme-view .b-btn,.c-realm-view .b-btn{color:var(--s-color-text-secondary);display:inline-flex}.c-readme-view .b-btn:hover,.c-realm-view .b-btn:hover{-webkit-text-decoration:none;text-decoration:none}.c-readme-view .b-btn:first-child,.c-realm-view .b-btn:first-child{float:right;margin-top:var(--g-space-4)}.c-readme-view>.b-btn:first-child+*,.c-readme-view>:first-child:not(.b-btn),.c-realm-view>.b-btn:first-child+*,.c-realm-view>:first-child:not(.b-btn){margin-top:0}.c-readme-view{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius);margin-bottom:var(--g-space-6);padding:var(--g-space-6) var(--g-space-4) var(--g-space-4);width:100%}@media (min-width:calc(820 / 16 * 1rem)){.c-readme-view{grid-row-start:auto}}.b-header{background-color:var(--s-color-bg-base);border-bottom:var(--s-border);font-size:var(--g-font-size-100);position:sticky;top:0;z-index:var(--g-z-max)}.b-header nav{align-items:stretch;height:auto}.b-header .main-nav{align-items:stretch;display:flex;flex:1 1 auto;gap:var(--g-space-1);height:100%;min-width:0;padding-bottom:var(--g-space-2);padding-top:var(--g-space-2);width:100%}@media (min-width:calc(820 / 16 * 1rem)){.b-header .main-nav{grid-column:span 7}}.b-header .main-nav--explorer{grid-column:span 10}.b-header .user-picture{border:var(--s-border-secondary);border-radius:var(--s-rounded);cursor:pointer;flex-shrink:0;height:var(--g-space-10);width:var(--g-space-10)}.b-header .user-picture>svg{height:100%;width:100%}.b-main-navigation{color:var(--s-color-text-quaternary);height:auto;position:relative;width:100%}.b-main-navigation>.inner{align-items:center;background-color:var(--s-color-bg-surface-secondary);border:var(--s-border-secondary);border-radius:var(--s-rounded);height:100%;padding-left:var(--g-space-1-5);padding-right:var(--g-space-1-5);position:relative}@media (min-width:calc(640 / 16 * 1rem)){.b-main-navigation>.inner{padding-right:var(--g-space-8)}}.b-main-navigation>.inner:has([data-role=header-input-search]:focus-within){border-color:var(--s-color-border-tertiary)}.b-main-navigation .searchbar{bottom:0;color:var(--s-color-text-secondary);font-size:var(--g-font-size-200);font-weight:var(--g-font-medium);left:0;padding:var(--g-space-1-5);padding-right:var(--g-space-8);position:absolute;right:0;top:0}.b-main-navigation .searchbar>input{background-color:transparent;height:100%;outline:none;width:100%}.b-main-navigation .searchbar:focus-within+.b-breadcrumb{display:none}.b-main-navigation .network-toggle{align-items:center;background-color:var(--g-color-transparent);border-radius:var(--g-border-radius);cursor:pointer;display:none;height:calc(100% - 2px);justify-content:center;padding:var(--g-space-1-5);position:absolute;right:1px;top:1px;z-index:var(--g-z-max)}@media (min-width:calc(640 / 16 * 1rem)){.b-main-navigation .network-toggle{display:flex}}.b-main-navigation .network-toggle>svg{color:var(--s-color-text-tertiary);height:var(--g-space-5);width:var(--g-space-5)}.b-main-navigation .network-toggle:hover>svg{color:var(--s-color-text-tertiary-hover)}.b-main-navigation .b-popup-dialog>.inner{color:var(--s-color-text-tertiary);width:var(--g-space-72)}.b-main-navigation .b-popup-dialog header>span{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100);font-weight:var(--g-font-semibold)}.b-main-navigation .b-popup-dialog .item{display:flex;gap:var(--g-space-1)}.b-main-navigation .b-popup-dialog .item>svg{height:var(--g-space-4);width:var(--g-space-4)}.b-main-navigation .b-popup-dialog .item-content{display:flex;flex-direction:column}.b-main-navigation .b-popup-dialog .item-label{font-size:var(--g-font-size-50)}.b-main-navigation .b-popup-dialog .item-value{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100);font-weight:var(--g-font-semibold)}.b-main-menu{display:flex;flex:0 0 auto;grid-column:span 3;height:var(--g-space-12)}@media (min-width:calc(640 / 16 * 1rem)){.b-main-menu{height:auto}}.b-main-menu .menu-toggle{align-items:center;cursor:pointer;display:flex;margin-left:auto;order:3}.b-main-menu .menu-toggle>svg{height:var(--g-space-5);margin-left:var(--g-space-4);width:var(--g-space-5)}@media (min-width:calc(820 / 16 * 1rem)){.b-main-menu .menu-toggle>svg{margin-left:var(--g-space-2)}}.b-main-menu .menu-toggle-input~.menu-dev{display:none}.b-main-menu .menu-toggle-input:checked~.menu-dev{display:flex}.b-main-menu .menu-toggle-input:checked~.menu-general{display:none}.b-main-menu .menu-dev,.b-main-menu .menu-general{display:flex;height:100%;justify-content:flex-end}.b-menu-link:last-child,.b-menu-link:last-child .link{margin-right:0}.b-menu-link .link{align-items:center;color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-100);font-weight:var(--g-font-semibold);gap:var(--g-space-1);height:100%;margin-right:var(--g-space-3);position:relative}.b-menu-link .link:hover{color:var(--s-color-text-tertiary-hover)}.b-menu-link .link:after{background-color:var(--s-color-bg-brand-default);border-radius:var(--s-rounded) var(--s-rounded) 0 0;bottom:0;content:"";height:var(--g-space-1);left:0;position:absolute;transition:width var(--g-transition-fast);width:0}.b-menu-link .link>svg{flex-shrink:0;height:var(--g-space-5);min-width:var(--g-space-2);width:var(--g-space-5)}@media (min-width:calc(1020 / 16 * 1rem)){.b-menu-link .link>svg{display:none}}@media (min-width:calc(1366 / 16 * 1rem)){.b-menu-link .link>svg{display:inline-block;height:var(--g-space-4-5);width:var(--g-space-4-5)}}@media (min-width:calc(640 / 16 * 1rem)){.b-menu-link .link{font-weight:var(--g-font-bold)}}@media (min-width:calc(1366 / 16 * 1rem)){.b-menu-link .link{margin-right:var(--g-space-6);padding-right:var(--g-space-1)}}@media (min-width:calc(640 / 16 * 1rem)){.b-menu-link .link-label{display:none}}@media (min-width:calc(1020 / 16 * 1rem)){.b-menu-link .link-label{display:inline}}.b-menu-link .link--icon{font-weight:var(--g-font-regular);margin-right:var(--g-space-4)}@media (min-width:calc(480 / 16 * 1rem)){.b-menu-link .link--icon{margin-right:var(--g-space-6)}}.b-menu-link .link--is-active{color:var(--s-color-text-secondary)}.b-menu-link .link--is-active:after{width:100%}.b-menu-link .link--is-active>svg{color:var(--s-color-bg-brand-default)}.menu-general .link{color:var(--s-color-text-secondary)}.menu-general .link:hover{color:var(--s-color-text-link-hover)}.b-breadcrumb{display:flex}.b-breadcrumb,.b-breadcrumb:after{background-color:var(--s-color-bg-surface-secondary)}.b-breadcrumb:after{bottom:0;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:0}.b-breadcrumb>ol{color:var(--s-color-text-primary);display:flex;font-weight:var(--g-font-semibold);line-height:var(--g-line-height-snug)}.b-breadcrumb .argument,.b-breadcrumb .element,.b-breadcrumb .query{align-items:center;display:flex;white-space:nowrap;z-index:var(--g-z-1)}.b-breadcrumb .argument:not(:first-child):before,.b-breadcrumb .element:not(:first-child):before,.b-breadcrumb .query:not(:first-child):before{color:var(--s-color-text-tertiary);content:"/";line-height:var(--g-line-height-normal);padding-left:.18rem;padding-right:.18rem;padding-top:var(--g-space-px)}.b-breadcrumb .argument a,.b-breadcrumb .element a,.b-breadcrumb .query a{background-color:var(--s-color-bg-base);border:1px solid var(--s-color-border-transparent);border-radius:var(--s-rounded-sm);display:inline-block;min-width:var(--g-space-4);padding:var(--g-space-0-5);text-align:center}.b-breadcrumb .argument a:hover,.b-breadcrumb .element a:hover,.b-breadcrumb .query a:hover{background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base)}.b-breadcrumb .argument:not(:first-child):before{content:":"}.b-breadcrumb .argument a{background-color:var(--s-color-bg-surface-quaternary);color:var(--s-color-text-base)}.b-breadcrumb .query:not(:first-child):before{content:"&"}.b-breadcrumb .query:nth-child(1 of .query):before{content:"?"}.b-breadcrumb .query label{background-color:var(--s-color-bg-surface-primary);border:var(--s-border);border-radius:var(--s-rounded-sm);color:var(--s-color-text-secondary);cursor:text;display:inline-flex;height:100%;min-width:var(--g-space-4);padding:var(--g-space-0-5) var(--g-space-1);position:relative;text-align:center;width:100%}.b-breadcrumb .query label:focus-within{border-color:var(--s-color-border-quaternary)}.b-breadcrumb .query label:hover{border-color:var(--s-color-border-quaternary)}.b-breadcrumb .query input{background-color:var(--s-color-bg-surface-primary);max-width:10ch;order:3;outline:none;field-sizing:content}@supports not (field-sizing:content){.b-breadcrumb .query input{width:5rem!important}}.b-breadcrumb .query input::-moz-placeholder{opacity:0}.b-breadcrumb .query input::placeholder{opacity:0}.b-breadcrumb .query input:-moz-placeholder{width:var(--g-space-px)}.b-breadcrumb .query input:placeholder-shown{width:var(--g-space-px)}.b-breadcrumb .query input:placeholder-shown::-moz-placeholder{color:var(--g-color-transparent)}.b-breadcrumb .query input:-moz-placeholder::placeholder{color:var(--g-color-transparent)}.b-breadcrumb .query input:placeholder-shown::placeholder{color:var(--g-color-transparent)}.b-footer{border-top:var(--s-border);font-size:var(--g-font-size-100);padding-bottom:var(--g-space-4);padding-top:var(--g-space-4);width:100%}.b-footer>nav{flex-direction:column;row-gap:var(--g-space-8)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer>nav{flex-wrap:wrap}}.b-footer .logo{color:var(--s-color-text-primary);grid-column:1/-1;width:var(--g-space-44)}.b-footer .logo:hover{color:var(--s-color-text-primary);-webkit-text-decoration:none;text-decoration:none}@media (min-width:calc(1020 / 16 * 1rem)){.b-footer .logo{align-self:center;grid-column:1/3;grid-row:1/1;width:60%}}.b-footer .nav-primary{display:flex;gap:var(--g-space-10);grid-column:1/-1;grid-row:2/3}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .nav-primary{align-items:center;flex:1 0 0%;flex-direction:row;gap:var(--g-space-6);justify-content:space-between}}@media (min-width:calc(1020 / 16 * 1rem)){.b-footer .nav-primary{grid-column:2/8;grid-row:1/1}}.b-footer .nav-primary>ul{display:flex;flex:1;flex-direction:column;flex-wrap:wrap;gap:var(--g-space-1) var(--g-space-3)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .nav-primary>ul{flex:initial;flex-direction:row}.b-footer .nav-social{margin-left:auto}}@media (min-width:calc(820 / 16 * 1rem)){.b-footer .nav-social{grid-column:span 3;justify-self:end;margin-left:0}}.b-footer .nav-theme{align-items:center;display:flex;gap:var(--g-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .nav-theme{flex-basis:100%}}@media (min-width:calc(820 / 16 * 1rem)){.b-footer .nav-theme{grid-column:span 3}}.b-footer .nav-theme .nav-theme-label{color:var(--s-color-text-secondary)}.b-footer .nav-theme:has([data-theme-target=sun]:not(.u-hidden)) .nav-theme-label:before{content:"Light"}.b-footer .nav-theme:has([data-theme-target=moon]:not(.u-hidden)) .nav-theme-label:before{content:"Dark"}.b-footer .nav-theme:has([data-theme-target=system]:not(.u-hidden)) .nav-theme-label:before{content:"System"}.b-footer .legal{color:var(--s-color-text-tertiary);font-size:var(--g-font-size-50);margin-top:var(--g-space-3);padding-top:var(--g-space-3)}.b-footer .legal>nav{color:var(--s-color-text-secondary);display:flex;flex-direction:column;flex-wrap:wrap;gap:var(--g-space-1) var(--g-space-3);margin-top:var(--g-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .legal>nav{flex-direction:row}.b-footer .legal>nav>a+a:before{color:var(--s-color-text-quaternary);content:"|";margin-right:var(--g-space-3)}}.b-footer .legal>nav:nth-child(3){grid-column:span 2/span 2}.b-footer .legal>:last-child:not(ul),.b-footer .legal>nav li{margin-bottom:var(--g-space-2);margin-top:var(--g-space-2)}.b-footer .legal>:last-child:not(ul){flex-basis:100%}@media (min-width:calc(1020 / 16 * 1rem)){.b-footer .legal>:last-child:not(ul){flex-basis:auto;grid-column:span 1/span 1}}.b-footer a:hover{color:var(--s-color-text-link-hover);-webkit-text-decoration:underline;text-decoration:underline}.b-content-header{display:flex;flex-direction:column;gap:var(--g-space-3);grid-row:span 1/span 1;margin-bottom:var(--g-space-6);margin-top:var(--g-space-10)}@media (min-width:calc(820 / 16 * 1rem)){.b-content-header{grid-column:span 7/span 7;grid-row-start:1;justify-content:space-between;margin-top:var(--g-space-10)}}@media (min-width:calc(1020 / 16 * 1rem)){.b-content-header{align-items:center;flex-direction:row}}.b-content-header .title{align-items:center;display:flex;gap:var(--g-space-3)}.b-content-header .header-info{align-items:center;color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-100);gap:var(--g-space-12);justify-content:space-between}.b-content-header .b-inline-btn>span{display:none}@media (min-width:calc(1020 / 16 * 1rem)){.b-content-header .b-inline-btn>span{display:inline}}.b-content-h1{font-size:var(--g-font-size-600);text-align:center}.b-content-h1,.b-content-h2{color:var(--s-color-text-primary);font-weight:var(--g-font-bold)}.b-content-h2{font-size:var(--g-font-size-400);margin-bottom:var(--g-space-4)}.b-btns{align-items:center;display:flex;gap:var(--g-space-1)}@media (min-width:calc(1020 / 16 * 1rem)){.b-btns{gap:var(--g-space-2)}}.b-btn{border:var(--s-border);border-radius:var(--s-rounded-sm);cursor:pointer;display:inline-flex;gap:var(--g-space-1-5);min-width:-moz-max-content;min-width:max-content;padding:var(--g-space-1) var(--g-space-2)}.b-btn:hover{background-color:var(--s-color-bg-surface-primary-hover)}.b-btn .c-icon{margin-left:0;margin-right:0}.b-btn--secondary:hover{background-color:var(--s-color-bg-surface-primary)}.b-inline-btn{color:var(--s-color-text-tertiary);cursor:pointer}.b-inline-btn:hover{color:var(--s-color-text-tertiary-hover)}.b-switch input,.b-switch label:last-child{display:none}.b-switch input+label,.b-switch input:checked~label:last-child{display:block}.b-switch input:checked+label{display:none}.b-block-form,.b-inline-form{color:var(--s-color-text-tertiary);display:flex;flex-direction:column;gap:var(--g-space-2) var(--g-space-3)}@media (min-width:calc(820 / 16 * 1rem)){.b-block-form,.b-inline-form{flex-direction:row}}.b-block-form{align-items:stretch}@media (min-width:calc(820 / 16 * 1rem)){.b-block-form{flex-direction:column}}.b-input{border:var(--s-border);border-radius:var(--s-rounded-sm);color:var(--s-color-text-secondary);display:flex;font-size:var(--g-font-size-100);min-width:var(--g-space-48);overflow:hidden;position:relative}.b-input>svg{height:var(--g-space-4);pointer-events:none;position:absolute;top:50%;transform:translateY(-50%);width:var(--g-space-4)}.b-input>svg:first-child{left:var(--g-space-2)}.b-input>svg:last-child{right:var(--g-space-2)}.b-input:hover,.b-input>input:focus,.b-input>input:hover{border-color:var(--s-color-border-tertiary)}.b-input:has(input:focus),.b-input:hover,.b-input>input:focus,.b-input>input:hover{border-color:var(--s-color-border-tertiary)}.b-input:hover>label{background-color:var(--s-color-bg-surface-primary)}.b-input:has(input:focus)>label,.b-input:hover>label{background-color:var(--s-color-bg-surface-primary)}.b-input>label{align-items:center;background-color:var(--s-color-bg-surface-secondary);gap:var(--g-space-3);white-space:nowrap}.b-input>input,.b-input>label,.b-input>select{display:flex;padding:var(--g-space-1-5) var(--g-space-3)}.b-input>input,.b-input>select{color:inherit;outline:none;width:100%}@media (min-width:calc(820 / 16 * 1rem)){.b-input>input,.b-input>select{padding:var(--g-space-1-5) var(--g-space-2)}}.b-input>select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--s-color-bg-surface-secondary);cursor:pointer}.b-input>select:hover{background-color:var(--s-color-bg-surface-primary)}.b-input>input{background-color:var(--s-color-bg-base);border-left:none}.b-input>label+input{border-left:var(--s-border)}.b-list{margin-bottom:var(--g-space-10)}.b-list>li{border-bottom:var(--s-border);color:var(--s-color-text-tertiary)}.b-list>li:first-child{border-top:var(--s-border)}.b-list>li>a{align-items:center;display:flex;justify-content:space-between;padding:var(--g-space-2)}.b-list>li>a:hover{background-color:var(--s-color-bg-surface-primary-hover)}.b-list>li>a .c-icon{margin-left:0}.b-list .name{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;color:var(--s-color-text-secondary);margin-left:var(--g-space-1);max-width:100%;overflow:hidden;text-overflow:ellipsis}.b-user-sidebar{margin-top:var(--g-space-4)}.b-user-sidebar>*+*{margin-top:var(--g-space-8)}.b-user-sidebar .user-avatar{border:var(--s-border);border-radius:var(--s-rounded);height:var(--g-space-24);width:var(--g-space-24)}@media (min-width:calc(640 / 16 * 1rem)){.b-user-sidebar .user-avatar{height:var(--g-space-36);width:var(--g-space-36)}}.b-user-sidebar .user-avatar img,.b-user-sidebar .user-avatar svg{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.b-user-sidebar .user-info{align-items:flex-start;display:flex;gap:var(--g-space-6)}@media (min-width:calc(820 / 16 * 1rem)){.b-user-sidebar .user-info{flex-direction:column}}.b-user-sidebar .user-info>div:last-child{align-self:flex-end}@media (min-width:calc(820 / 16 * 1rem)){.b-user-sidebar .user-info>div:last-child{align-self:flex-start}}.b-user-sidebar .title{color:var(--s-color-text-primary);display:bock;font-size:var(--g-font-size-700);font-weight:var(--g-font-bold);line-height:var(--g-line-height-tight);text-transform:capitalize;word-break:break-all}@media (min-width:calc(640 / 16 * 1rem)){.b-user-sidebar .title{font-size:var(--g-font-size-800)}}.b-user-sidebar .subtitle{color:var(--s-color-text-secondary);display:block;font-size:var(--g-font-size-100);line-height:var(--g-line-height-tight);margin-top:var(--g-space-2)}.b-user-sidebar>a{align-items:center;display:flex;justify-content:center}@media (min-width:calc(820 / 16 * 1rem)){.b-user-sidebar>a{display:inline-flex}}.b-sidebar{border-bottom:var(--s-border);grid-column:span 1/span 1;padding-bottom:var(--g-space-10);position:relative}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar{border-bottom:none;grid-column:span 3/span 3;grid-row:span 2/span 2;grid-row-start:1;height:100%;margin-bottom:0;order:2;padding-bottom:0}.b-sidebar+md-renderer:empty+*{grid-row-start:1;padding-top:var(--g-space-6)}.b-sidebar+md-renderer:empty+*,.b-sidebar+md-renderer:has(.b-btn:only-child)+*{grid-row-start:1;padding-top:var(--g-space-6)}}.b-sidebar:first-child{margin-top:var(--g-space-8)}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar:first-child{margin-top:0}}.b-sidebar>div{padding-top:var(--g-space-2);position:sticky;top:var(--g-space-14)}.b-sidebar>div:has(.inner):not(:has(nav li)){display:none}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar>div{padding-bottom:var(--g-space-2)}}.b-sidebar .inner{background-color:var(--s-color-bg-surface-primary);border-radius:var(--s-rounded-sm);max-height:100vh;overflow:scroll;scrollbar-width:none}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar .inner{background-color:var(--g-color-transparent)}}.b-sidebar .inner>nav{display:none;font-size:var(--g-font-size-100);margin-top:var(--g-space-2);padding:var(--g-space-2) var(--g-space-4) var(--g-space-6)}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar .inner>nav{display:block;margin-top:0;padding-bottom:var(--g-space-28);padding-left:0;padding-right:0}.b-sidebar .inner>nav>*{padding-left:0}}.b-sidebar .b-expend-btn{align-items:center;background-color:var(--s-color-bg-base);border:var(--s-border);border-radius:var(--s-rounded-sm);cursor:pointer;display:flex;font-size:var(--g-font-size-100);justify-content:space-between;padding:var(--g-space-2) var(--g-space-4)}.b-sidebar .b-expend-btn:hover{background-color:var(--s-color-bg-surface-secondary)}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar .b-expend-btn{border:none;cursor:default;font-size:var(--g-font-size-200);font-weight:var(--g-font-semibold);margin-top:var(--g-space-10);padding:0}.b-sidebar .b-expend-btn,.b-sidebar .b-expend-btn:hover{background-color:var(--g-color-transparent)}}.b-sidebar .b-expend-btn:has(#toc-expend:checked)+nav{display:block}.b-sidebar .b-expend-btn>input{display:none}.b-sidebar .b-expend-btn>input:checked+.wrapper-icon:before{content:"close"}.b-sidebar .b-expend-btn>input:checked+.wrapper-icon>svg{transform:rotate(180deg)}.b-sidebar .wrapper-icon{align-items:center;display:flex;gap:var(--g-space-1-5)}.b-sidebar .wrapper-icon:before{content:"open"}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar .wrapper-icon{display:none}}.dev-mode .b-sidebar .b-expend-btn{background-color:var(--s-color-bg-surface-secondary)}@media (min-width:calc(820 / 16 * 1rem)){.dev-mode .b-sidebar .b-expend-btn{background-color:var(--g-color-transparent)}}.dev-mode .b-sidebar .b-expend-btn:hover{background-color:var(--s-color-bg-surface-primary)}.b-source-code{font-family:var(--g-font-mono)}.b-source-code>pre{background-color:var(--s-color-bg-base);border-radius:var(--s-rounded);font-size:var(--g-font-size-100);overflow:scroll;padding:var(--g-space-4) var(--g-space-1)}@media (min-width:calc(640 / 16 * 1rem)){.b-source-code>pre{font-size:var(--g-font-size-200);padding:var(--g-space-8) var(--g-space-3)}}.b-source-code>pre a:hover{-webkit-text-decoration:none;text-decoration:none}[data-theme=dark] .b-source-code>pre{background-color:var(--s-color-bg-base)}.b-toc{list-style:none;margin-top:var(--g-space-2)}.b-toc>*+*{margin-bottom:var(--g-space-1-5);margin-top:var(--g-space-1-5)}.b-toc .b-toc{border-left:1px solid var(--s-color-border-secondary);margin-bottom:var(--g-space-4);padding-left:var(--g-space-4)}.b-toc a>span{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.b-toc a:hover{color:var(--s-color-text-link-hover);-webkit-text-decoration:underline;text-decoration:underline}main.dev-mode .b-toc a{word-break:break-all}.b-source-toc>.b-toc{margin-bottom:var(--g-space-4)}.b-source-toc>*+*{margin-top:var(--g-space-1-5)}.b-source-toc .accordion summary>svg{transform:rotate(-90deg)}.b-source-toc .accordion summary:hover{color:var(--s-color-text-link-hover);-webkit-text-decoration:underline;text-decoration:underline}.b-source-toc .accordion[open] summary>svg{transform:rotate(0deg)}.b-source-toc .accordion>.b-toc{padding-left:var(--g-space-5)}.b-source-toc .accordion h3{font-size:var(--g-font-size-100);font-weight:var(--g-font-medium);margin-top:0}.b-action-overview{margin-bottom:var(--g-space-12)}.b-action-overview>p{font-size:var(--g-font-size-200)}.b-action-function{background-color:var(--s-color-bg-surface-secondary);border-radius:var(--s-rounded);margin-bottom:var(--g-space-3);padding:var(--g-space-4)}.b-action-function .title{align-items:baseline;display:flex;flex-wrap:wrap;font-size:var(--g-font-size-50);gap:var(--g-space-1) var(--g-space-4);margin-bottom:var(--g-space-1)}.b-action-function>header{align-items:flex-start;display:flex;font-size:var(--g-font-size-100);justify-content:space-between;margin-bottom:var(--g-space-4)}.b-action-function>header .signature>code{color:var(--s--text-secondary)}@media (min-width:calc(820 / 16 * 1rem)){.b-action-function>header .signature{font-size:var(--g-font-size-50)}}.b-action-function>header h2{color:var(--s-color-text-primary);font-size:var(--g-font-size-300);font-weight:var(--g-font-semibold);line-height:var(--g-line-height-tight)}.b-action-function .description{color:var(--s-color-text-secondary);font-size:var(--g-font-size-200)}.b-action-function .params{align-items:stretch;color:var(--s-color-text-tertiary);display:flex;flex-direction:column;font-size:var(--g-font-size-100);gap:var(--g-space-1);margin-bottom:var(--g-space-1);margin-top:var(--g-space-6);width:100%}.b-action-function .params label{background-color:var(--s-color-bg-surface-primary)}.b-action-function .params .b-input:has(input:focus) label{background-color:var(--s-color-bg-surface-secondary)}.b-action-function .params .b-input:has(input:hover) label{background-color:var(--s-color-bg-surface-secondary)}.b-action-function .b-alert{background-color:var(--s-color-bg-warning-weak);border-left:var(--g-space-1) solid var(--s-color-border-tertiary);border-left-color:var(--s-color-border-warning);border-radius:var(--s-rounded);color:var(--s-color-text-secondary);color:var(--s-color-text-warning);margin-bottom:var(--g-space-10);margin-top:var(--g-space-5);padding:var(--g-space-3) var(--g-space-4)}.b-action-function .b-alert>h1:first-child,.b-action-function .b-alert>h2:first-child,.b-action-function .b-alert>h3:first-child{font-size:var(--g-font-size-200);font-weight:var(--g-font-semibold);margin-bottom:var(--g-space-2)}.b-action-function .b-alert .b-btn,.b-action-function .b-alert label{background-color:var(--s-color-bg-warning-action);border:none;color:var(--s-color-bg-warning-weak);cursor:pointer}.b-action-function .b-alert .b-btn{margin-top:var(--g-space-4)}.b-code{background-color:var(--s-color-bg-base);border-radius:var(--s-rounded);font-size:var(--g-font-size-100);position:relative}.b-code pre{color:var(--s-color-text-secondary);padding:var(--g-space-4);padding-right:var(--g-space-10);white-space:pre-wrap}.b-code .btn-copy{background-color:var(--g-color-transparent);color:var(--s-color-text-tertiary);cursor:pointer;padding:0;position:absolute;right:var(--g-space-2);top:var(--g-space-2)}.b-code .btn-copy:hover{color:var(--s-color-text-primary)}.b-packages{min-height:var(--g-space-96);padding-bottom:var(--g-space-24);scroll-margin-block-start:var(--g-space-24)}@media (min-width:calc(820 / 16 * 1rem)){.b-packages{grid-column:span 7/span 7}}.b-packages .title{color:var(--s-color-text-primary);display:block;font-size:var(--g-font-size-700);font-weight:var(--g-font-bold);margin-bottom:var(--g-space-6)}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .title{font-size:var(--g-font-size-800)}}.b-packages nav{display:grid;grid-template-columns:repeat(4,1fr);grid-gap:var(--g-space-3);gap:var(--g-space-3);margin-bottom:var(--g-space-6)}@media (min-width:calc(640 / 16 * 1rem)){.b-packages nav{border-bottom:var(--s-border);padding-bottom:var(--g-space-2)}}.b-packages .packages-tabs{border-bottom:var(--s-border);color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-200);font-weight:var(--g-font-semibold);gap:var(--g-space-4);grid-column:span 4/span 4;padding-bottom:var(--g-space-2);width:auto}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .packages-tabs{border-bottom:none;font-size:var(--g-font-size-100);grid-column:span 2/span 2;padding-bottom:0;width:100%}}@media (min-width:calc(1020 / 16 * 1rem)){.b-packages .packages-tabs{gap:var(--g-space-6);margin-left:0;width:100%}}.b-packages .packages-tabs label{align-items:center;cursor:pointer;display:flex;gap:var(--g-space-1);position:relative}.b-packages .packages-tabs label:hover{color:var(--s-color-text-tertiary-hover)}.b-packages .packages-tabs label .b-tag--secondary{display:none}@media (min-width:calc(1020 / 16 * 1rem)){.b-packages .packages-tabs label .b-tag--secondary{display:inline}}.b-packages .packages-filters{align-items:center;color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-100);gap:var(--g-space-2);grid-column:span 2/span 2}@media (min-width:calc(480 / 16 * 1rem)){.b-packages .packages-filters{grid-column:span 1/span 1}}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .packages-filters{justify-content:flex-end}}.b-packages .packages-filters>div{display:grid}.b-packages .packages-filters label{align-items:center;cursor:pointer;display:flex;gap:var(--g-space-0-5);grid-column:1/1;grid-row:1/1;justify-content:space-between}.b-packages .packages-filters label:hover>*{color:var(--s-color-text-tertiary-hover)}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .packages-filters label span{display:none}}@media (min-width:calc(1366 / 16 * 1rem)){.b-packages .packages-filters label span{display:inline}}.b-packages .packages-search{display:flex;font-size:var(--g-font-size-100);grid-column:span 2/span 2;position:relative}@media (min-width:calc(480 / 16 * 1rem)){.b-packages .packages-search{grid-column:span 3/span 3}}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .packages-search{grid-column:span 1/span 1}}.b-packages .range{display:grid;grid-template-columns:repeat(1,minmax(0,1fr));grid-gap:var(--g-space-2);color:var(--s-color-text-tertiary);font-size:var(--g-font-size-100);gap:var(--g-space-2)}.b-packages .range:before{color:var(--s-color-text-tertiary);display:none;font-size:var(--g-font-size-200);font-weight:var(--g-font-weight-bold);grid-column:1/-1;padding-bottom:var(--g-space-2);padding-top:var(--g-space-2);text-align:center;width:100%}.b-packages .range:after{content:"Add a package to your namespace to get started";display:none;font-size:var(--g-font-size-100);grid-column:1/-1;text-align:center}.b-packages .range:empty:before{content:"No packages found";display:block}.b-packages .range:empty:after{content:"Add a package to your namespace to get started";display:block}.b-packages article{background-color:var(--s-color-bg-surface-primary);border-radius:var(--s-rounded);display:flex;flex-direction:column;gap:var(--g-space-6);padding:var(--g-space-1)}@media (min-width:calc(640 / 16 * 1rem)){.b-packages article{gap:var(--g-space-2)}}.b-packages article .article-content{background-color:var(--s-color-bg-base);border-radius:var(--s-rounded-sm);display:flex;flex-direction:column;height:100%;padding:var(--g-space-2);width:100%}.b-packages article .article-content .title{align-items:center;display:flex;gap:var(--g-space-2);margin-bottom:var(--g-space-1);overflow:hidden;width:100%}.b-packages article .article-content h3{font-size:var(--g-font-size-200);font-weight:var(--g-font-bold);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.b-packages article .article-content h3>a{color:var(--s-color-text-link-hover)}.b-packages article .article-content h3>a:hover{-webkit-text-decoration:underline;text-decoration:underline}.b-packages article .article-content>p{overflow:hidden;text-overflow:ellipsis;width:100%}.b-packages article .article-content>p>a:hover{-webkit-text-decoration:underline;text-decoration:underline}.b-packages article footer{display:flex;font-size:var(--g-font-size-50);gap:var(--g-space-1);justify-content:space-between;padding-bottom:var(--g-space-1);padding-left:var(--g-space-2);padding-right:var(--g-space-2)}.b-packages article footer time{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.b-packages article footer .size{text-align:right}.b-packages article,.b-packages li{display:none}.b-packages:has(input[value=packages]:checked) li{display:flex}.b-packages:has(input[value=packages]:checked) article{display:flex}.b-packages:has(input[value=realms]:checked) li[data-list-type-value=realm]{display:flex}.b-packages:has(input[value=realms]:checked) article[data-list-type-value=realm]{display:flex}.b-packages:has(input[value=pures]:checked) li[data-list-type-value=pure]{display:flex}.b-packages:has(input[value=pures]:checked) - article[data-list-type-value=pure]{display:flex}.b-packages label:has(input:checked){color:var(--s-color-text-tertiary)}.b-packages label:has(input:checked):after{background-color:var(--s-color-bg-brand-default);border-top-left-radius:var(--s-rounded-sm);border-top-right-radius:var(--s-rounded-sm);bottom:calc(var(--g-space-1)*-2);content:"";height:var(--g-space-1);left:0;position:absolute;width:100%}.b-packages:has(input[value=realms]:checked) .range:not(:has(>[data-list-type-value=realm])):before{display:block}.b-packages:has(input[value=realms]:checked) .range:not(:has(>[data-list-type-value=realm])):after{display:block}.b-packages:has(input[value=realms]:checked) .range:not(:has(>[data-list-type-value=realm])):before{content:"No realms found"}.b-packages:has(input[value=realms]:checked) .range:not(:has(>[data-list-type-value=realm])):after{content:"Add a realm to your namespace to get started"}.b-packages:has(input[value=pures]:checked) .range:not(:has(>[data-list-type-value=pure])):before{display:block}.b-packages:has(input[value=pures]:checked) .range:not(:has(>[data-list-type-value=pure])):after{display:block}.b-packages:has(input[value=pures]:checked) .range:not(:has(>[data-list-type-value=pure])):before{content:"No pures found"}.b-packages:has(input[value=pures]:checked) .range:not(:has(>[data-list-type-value=pure])):after{content:"Add a pure to your namespace to get started"}@media (min-width:calc(640 / 16 * 1rem)){.b-packages:has(input[value=display-grid]:checked) .range{gap:var(--g-space-3);grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:calc(1020 / 16 * 1rem)){.b-packages:has(input[value=display-grid]:checked) .range{grid-template-columns:repeat(4,minmax(0,1fr))}}.b-packages:has(input[value=display-grid]:checked) .range article .article-content p{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}.b-packages:has(input[value=display-list]:checked) .range article .article-content{display:flex;flex:none;flex-direction:row;gap:var(--g-space-2)}@media (min-width:calc(820 / 16 * 1rem)){.b-packages:has(input[value=display-list]:checked) .range article .article-content{flex:5;min-width:0}}.b-packages:has(input[value=display-list]:checked) .range article .article-content .title{margin-bottom:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:33.33333%}.b-packages:has(input[value=display-list]:checked) .range article .article-content p{white-space:nowrap}.b-packages:has(input[value=display-list]:checked) .range article footer{display:none;justify-content:center;min-width:0;padding-bottom:0;padding-left:0}@media (min-width:calc(640 / 16 * 1rem)){.b-packages:has(input[value=display-list]:checked) .range article footer{flex:1}}@media (min-width:calc(820 / 16 * 1rem)){.b-packages:has(input[value=display-list]:checked) .range article footer{display:flex}}.b-packages:has(input[value=display-list]:checked) .range article footer .size{display:none;min-width:0}@media (min-width:calc(1020 / 16 * 1rem)){.b-packages:has(input[value=display-list]:checked) .range article footer .size{display:block;flex:2}}.b-packages:has(input[value=display-list]:checked) .range article footer time{min-width:0}@media (min-width:calc(1020 / 16 * 1rem)){.b-packages:has(input[value=display-list]:checked) .range article footer time{flex:5}}.b-icon-action{flex-shrink:0;height:var(--g-space-5);width:var(--g-space-5)}.b-popup-bg,.b-popup-dialog{opacity:0;right:0;top:0;visibility:hidden;z-index:var(--g-z-max)}.b-popup-bg{align-items:center;bottom:0;display:flex;justify-content:center;left:0;position:fixed;right:0;top:0}.b-popup-dialog{position:absolute}.b-popup-dialog>.inner{background-color:var(--s-color-bg-base);border:var(--s-border-secondary);border-radius:var(--s-rounded);padding:var(--g-space-2-5) var(--g-space-4);position:absolute;transform:translateX(-100%);z-index:var(--g-z-max)}.b-popup-dialog>.inner>*+*{margin-top:var(--g-space-2-5)}.b-popup-dialog header{align-items:center;color:var(--s-color-text-secondary);display:flex;justify-content:space-between;width:100%}.b-popup-dialog header>svg{color:var(--s-color-text-tertiary);cursor:pointer;position:absolute;right:var(--g-space-3)}.b-popup-dialog header>svg>svg:hover{color:var(--s-color-text-primary)}.b-popup:checked+.b-popup-bg,.b-popup:checked~.b-popup-dialog{opacity:1;visibility:visible}.b-tag,.b-tag--secondary{align-items:center;border:var(--s-border);border-radius:var(--s-rounded-full);color:var(--s-color-text-secondary);display:flex;font-family:var(--g-font-family-mono);font-size:var(--g-font-size-50);gap:var(--g-space-2);padding:var(--g-space-px) var(--g-space-2)}.b-tag--secondary{background-color:var(--s-color-bg-surface-primary);border-color:transparent;color:var(--s-color-text-primary)}.c-readme-view .gno-columns,.c-realm-view .gno-columns{display:flex;flex-wrap:wrap;gap:var(--g-space-10)}@media (min-width:calc(1366 / 16 * 1rem)){.c-readme-view .gno-columns,.c-realm-view .gno-columns{gap:var(--g-space-12)}}.c-readme-view .gno-columns>*,.c-realm-view .gno-columns>*{flex-basis:var(--g-space-52);flex-grow:1;flex-shrink:1}@media (min-width:calc(820 / 16 * 1rem)){.c-readme-view .gno-columns>*,.c-realm-view .gno-columns>*{flex-basis:var(--g-space-44)}}.c-readme-view .tooltip,.c-realm-view .tooltip{--tooltip-left:0;--tooltip-right:initial;align-items:center;border:var(--s-border);border-radius:var(--s-rounded-full);color:var(--s-color-text-tertiary);display:inline-flex;height:var(--g-space-4);justify-content:center;margin-bottom:var(--g-space-px);position:relative;width:var(--g-space-4)}.c-readme-view .tooltip>svg,.c-realm-view .tooltip>svg{height:var(--g-space-3);width:var(--g-space-3)}.c-readme-view .tooltip:after,.c-realm-view .tooltip:after{background-color:var(--s-color-bg-base);border:var(--s-border-secondary);border-radius:var(--s-rounded);color:var(--s-color-text-secondary);content:attr(data-tooltip);font-size:var(--g-font-size-100);font-weight:var(--g-font-normal);left:var(--tooltip-left);max-width:var(--g-space-48);min-width:var(--g-space-32);opacity:0;padding:var(--g-space-1) var(--g-space-2);position:absolute;right:var(--tooltip-right);scale:0;text-align:center;top:100%;visibility:hidden;width:-moz-fit-content;width:fit-content;z-index:var(--g-z-max)}.c-readme-view .tooltip:hover:after,.c-realm-view .tooltip:hover:after{opacity:1;scale:1;transition-delay:var(--g-transition-fast);visibility:visible}.c-readme-view .tooltip:only-of-type,.c-realm-view .tooltip:only-of-type{margin-left:.3em;margin-right:.3em}.c-realm-view .tooltip:has(+span){margin-left:.3em}.c-readme-view .tooltip:has(+span){margin-left:.3em}.c-readme-view .link-external,.c-realm-view .link-external{font-size:.67em}.c-readme-view .link-internal,.c-realm-view .link-internal{font-size:.75em;font-weight:400}.c-readme-view .link-tx,.c-readme-view .link-user,.c-realm-view .link-tx,.c-realm-view .link-user{font-size:.75em}.c-realm-view ul:has(li>input[type=checkbox]:first-child){list-style:none;padding-left:0}.c-readme-view ul:has(li>input[type=checkbox]:first-child){list-style:none;padding-left:0}.c-realm-view li:has(>input[type=checkbox]:first-child){align-items:center;display:flex;gap:var(--g-space-2)}.c-readme-view li:has(>input[type=checkbox]:first-child){align-items:center;display:flex;gap:var(--g-space-2)}.c-readme-view li>input[type=checkbox]:first-child,.c-realm-view li>input[type=checkbox]:first-child{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:var(--s-border-secondary);border-radius:var(--s-rounded-sm);flex-shrink:0;height:var(--g-space-5);padding:0;position:relative;width:var(--g-space-5)}.c-readme-view li>input[type=checkbox]:first-child:disabled,.c-realm-view li>input[type=checkbox]:first-child:disabled{background-color:var(--s-color-bg-surface-primary);border-color:var(--s-color-border-tertiary);cursor:not-allowed}.c-readme-view li>input[type=checkbox]:first-child:disabled:after,.c-realm-view li>input[type=checkbox]:first-child:disabled:after{background-color:var(--s-color-bg-brand-default)}.c-readme-view li>input[type=checkbox]:first-child:checked:after,.c-realm-view li>input[type=checkbox]:first-child:checked:after{opacity:1}.c-readme-view li>input[type=checkbox]:first-child:after,.c-realm-view li>input[type=checkbox]:first-child:after{background-color:var(--s-color-bg-base);clip-path:polygon(25% 36%,43% 54%,76% 18%,89% 29%,44% 78%,13% 49%);content:"";height:var(--g-space-3);left:50%;margin:auto;opacity:0;position:absolute;top:50%;transform:translate(-50%,-50%);width:var(--g-space-3)}.c-readme-view .footnote-backref,.c-realm-view .footnote-backref{vertical-align:middle}.c-readme-view li[id^="fn:"],.c-realm-view li[id^="fn:"]{position:relative;z-index:var(--g-z-1)}.c-readme-view li[id^="fn:"]:before,.c-realm-view li[id^="fn:"]:before{background-color:var(--s-color-bg-brand-weak);border-radius:var(--s-rounded-sm);bottom:0;content:"";display:block;left:calc(var(--g-space-0-5)*-1);opacity:0;position:absolute;right:calc(var(--g-space-0-5)*-1);top:calc(var(--g-space-0-5)*-1);z-index:var(--g-z-min)}.c-readme-view li[id^="fn:"]:target:before,.c-realm-view li[id^="fn:"]:target:before{opacity:1;transition-delay:var(--g-duration-150);transition-duration:var(--g-duration-75)}.c-readme-view .gno-form,.c-realm-view .gno-form{border:var(--s-border-secondary);border-radius:var(--s-rounded);display:block;margin-bottom:var(--g-space-6);margin-top:var(--g-space-6)}.c-readme-view .gno-form input[type=submit],.c-realm-view .gno-form input[type=submit]{background-color:var(--s-color-bg-brand-default);border-color:var(--s-color-border-brand-default);border-radius:var(--s-rounded-sm);color:var(--s-color-text-base);cursor:pointer;margin-bottom:var(--g-space-2);margin-top:var(--g-space-4);width:100%}.c-readme-view .gno-form input[type=submit]:hover,.c-realm-view .gno-form input[type=submit]:hover{opacity:.9}.c-readme-view .gno-form .command,.c-realm-view .gno-form .command{background-color:var(--s-color-bg-base-dev);margin-top:var(--g-space-6);padding:var(--g-space-4)}.c-readme-view .gno-form .command .title,.c-realm-view .gno-form .command .title{font-size:var(--g-font-size-200);font-weight:var(--g-font-semibold);white-space:nowrap}.c-readme-view .gno-form .command>.b-code,.c-realm-view .gno-form .command>.b-code{background-color:var(--s-color-bg-base-dev)}.c-readme-view .gno-form .command>.b-code>pre,.c-realm-view .gno-form .command>.b-code>pre{background-color:var(--s-color-bg-base);margin-bottom:0}.c-readme-view .gno-form .command .c-between,.c-readme-view .gno-form .command .c-inline,.c-realm-view .gno-form .command .c-between,.c-realm-view .gno-form .command .c-inline{align-items:flex-start;flex-direction:column;gap:var(--g-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view .gno-form .command .c-between,.c-readme-view .gno-form .command .c-inline,.c-realm-view .gno-form .command .c-between,.c-realm-view .gno-form .command .c-inline{align-items:center;flex-direction:row}}.c-readme-view .gno-form .command .c-between>*,.c-readme-view .gno-form .command .c-inline>*,.c-realm-view .gno-form .command .c-between>*,.c-realm-view .gno-form .command .c-inline>*{width:100%}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view .gno-form .command .c-between>*,.c-readme-view .gno-form .command .c-inline>*,.c-realm-view .gno-form .command .c-between>*,.c-realm-view .gno-form .command .c-inline>*{width:auto}}.c-readme-view .gno-form_header,.c-realm-view .gno-form_header{color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-50);justify-content:space-between;margin-bottom:var(--g-space-6);padding:var(--g-space-2) var(--g-space-4) 0}.c-readme-view .gno-form_input,.c-readme-view .gno-form_select,.c-realm-view .gno-form_input,.c-realm-view .gno-form_select{padding-left:var(--g-space-4);padding-right:var(--g-space-4);position:relative}.c-readme-view .gno-form_input label,.c-readme-view .gno-form_select label,.c-realm-view .gno-form_input label,.c-realm-view .gno-form_select label{background-color:var(--s-color-bg-input);color:var(--s-color-text-tertiary);display:none;font-size:var(--g-font-size-50);left:var(--g-space-5);padding-left:var(--g-space-1);padding-right:var(--g-space-1);position:absolute;top:0;transform:translateY(-50%)}.c-readme-view .gno-form_input svg,.c-readme-view .gno-form_select svg,.c-realm-view .gno-form_input svg,.c-realm-view .gno-form_select svg{height:var(--g-space-4);pointer-events:none;position:absolute;right:var(--g-space-6);top:50%;transform:translateY(-50%);width:var(--g-space-4)}.c-realm-view .gno-form_input:has(input:focus) label{display:block}.c-readme-view .gno-form_input:has(input:focus) label{display:block}.c-realm-view .gno-form_input:has(input:not(:-moz-placeholder)) label{display:block}.c-realm-view .gno-form_input:has(input:not(:placeholder-shown)) label{display:block}.c-readme-view .gno-form_input:has(input:not(:-moz-placeholder)) label{display:block}.c-readme-view .gno-form_input:has(input:not(:placeholder-shown)) label{display:block}.c-realm-view .gno-form_input:has(textarea:not(:-moz-placeholder)) label{display:block}.c-realm-view .gno-form_input:has(textarea:not(:placeholder-shown)) label{display:block}.c-readme-view .gno-form_input:has(textarea:not(:-moz-placeholder)) label{display:block}.c-readme-view .gno-form_input:has(textarea:not(:placeholder-shown)) label{display:block}.c-realm-view .gno-form_input:has(textarea:focus) label{display:block}.c-readme-view .gno-form_input:has(textarea:focus) label{display:block}.c-realm-view .gno-form_select:has(select:focus) label{display:block}.c-readme-view .gno-form_select:has(select:focus) label{display:block}.c-realm-view .gno-form_select:has(select option:not([value=""]):checked) label{display:block}.c-readme-view .gno-form_select:has(select option:not([value=""]):checked) label{display:block}.c-readme-view .gno-form_input input,.c-readme-view .gno-form_input textarea,.c-readme-view .gno-form_select select,.c-realm-view .gno-form_input input,.c-realm-view .gno-form_input textarea,.c-realm-view .gno-form_select select{background-color:var(--s-color-bg-input);border:var(--g-space-px) solid var(--s-color-border-input);border-radius:var(--s-rounded-sm);color:var(--s-color-text-primary);display:block;margin-bottom:var(--g-space-4);margin-top:var(--g-space-4);outline:none;padding:var(--g-space-2);width:100%}.c-readme-view .gno-form_input input:focus,.c-readme-view .gno-form_input input:hover,.c-readme-view .gno-form_input textarea:focus,.c-readme-view .gno-form_input textarea:hover,.c-readme-view .gno-form_select select:focus,.c-readme-view .gno-form_select select:hover,.c-realm-view .gno-form_input input:focus,.c-realm-view .gno-form_input input:hover,.c-realm-view .gno-form_input textarea:focus,.c-realm-view .gno-form_input textarea:hover,.c-realm-view .gno-form_select select:focus,.c-realm-view .gno-form_select select:hover{border-color:var(--s-color-border-tertiary)}.c-realm-view .gno-form_input input::-moz-placeholder{color:var(--s-color-text-tertiary)}.c-realm-view .gno-form_input input::placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_input input::-moz-placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_input input::placeholder{color:var(--s-color-text-tertiary)}.c-realm-view .gno-form_input textarea::-moz-placeholder{color:var(--s-color-text-tertiary)}.c-realm-view .gno-form_input textarea::placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_input textarea::-moz-placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_input textarea::placeholder{color:var(--s-color-text-tertiary)}.c-realm-view .gno-form_select select::-moz-placeholder{color:var(--s-color-text-tertiary)}.c-realm-view .gno-form_select select::placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_select select::-moz-placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_select select::placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_input input:disabled,.c-readme-view .gno-form_input input[readonly],.c-readme-view .gno-form_input textarea:disabled,.c-readme-view .gno-form_input textarea[readonly],.c-readme-view .gno-form_select select:disabled,.c-readme-view .gno-form_select select[readonly],.c-realm-view .gno-form_input input:disabled,.c-realm-view .gno-form_input input[readonly],.c-realm-view .gno-form_input textarea:disabled,.c-realm-view .gno-form_input textarea[readonly],.c-realm-view .gno-form_select select:disabled,.c-realm-view .gno-form_select select[readonly]{background-color:var(--s-color-bg-secondary);color:var(--s-color-text-tertiary);cursor:not-allowed}.c-readme-view .gno-form_input input:disabled:focus,.c-readme-view .gno-form_input input:disabled:hover,.c-readme-view .gno-form_input input[readonly]:focus,.c-readme-view .gno-form_input input[readonly]:hover,.c-readme-view .gno-form_input textarea:disabled:focus,.c-readme-view .gno-form_input textarea:disabled:hover,.c-readme-view .gno-form_input textarea[readonly]:focus,.c-readme-view .gno-form_input textarea[readonly]:hover,.c-readme-view .gno-form_select select:disabled:focus,.c-readme-view .gno-form_select select:disabled:hover,.c-readme-view .gno-form_select select[readonly]:focus,.c-readme-view .gno-form_select select[readonly]:hover,.c-realm-view .gno-form_input input:disabled:focus,.c-realm-view .gno-form_input input:disabled:hover,.c-realm-view .gno-form_input input[readonly]:focus,.c-realm-view .gno-form_input input[readonly]:hover,.c-realm-view .gno-form_input textarea:disabled:focus,.c-realm-view .gno-form_input textarea:disabled:hover,.c-realm-view .gno-form_input textarea[readonly]:focus,.c-realm-view .gno-form_input textarea[readonly]:hover,.c-realm-view .gno-form_select select:disabled:focus,.c-realm-view .gno-form_select select:disabled:hover,.c-realm-view .gno-form_select select[readonly]:focus,.c-realm-view .gno-form_select select[readonly]:hover{border-color:var(--s-color-border-secondary)}.c-realm-view .gno-form_input input:user-invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input input:user-invalid{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_input textarea:user-invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input textarea:user-invalid{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_select select:user-invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_select select:user-invalid{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_input input:user-invalid:focus{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input input:user-invalid:focus{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_input textarea:user-invalid:focus{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input textarea:user-invalid:focus{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_select select:user-invalid:focus{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_select select:user-invalid:focus{border-color:var(--s-color-border-error)}@supports not selector(:user-invalid){.c-realm-view .gno-form_input input:invalid:not(:-moz-placeholder):not(:focus){border-color:var(--s-color-border-error)}.c-realm-view .gno-form_input input:invalid:not(:placeholder-shown):not(:focus){border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input input:invalid:not(:-moz-placeholder):not(:focus){border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input input:invalid:not(:placeholder-shown):not(:focus){border-color:var(--s-color-border-error)}.c-realm-view .gno-form_input textarea:invalid:not(:-moz-placeholder):not(:focus){border-color:var(--s-color-border-error)}.c-realm-view .gno-form_input textarea:invalid:not(:placeholder-shown):not(:focus){border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input textarea:invalid:not(:-moz-placeholder):not(:focus){border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input textarea:invalid:not(:placeholder-shown):not(:focus){border-color:var(--s-color-border-error)}.c-realm-view .gno-form_select select:invalid:not(:-moz-placeholder):not(:focus){border-color:var(--s-color-border-error)}.c-realm-view .gno-form_select select:invalid:not(:placeholder-shown):not(:focus){border-color:var(--s-color-border-error)}.c-readme-view .gno-form_select select:invalid:not(:-moz-placeholder):not(:focus){border-color:var(--s-color-border-error)}.c-readme-view .gno-form_select select:invalid:not(:placeholder-shown):not(:focus){border-color:var(--s-color-border-error)}}.c-realm-view .gno-form_input input:focus::-moz-placeholder{opacity:0}.c-realm-view .gno-form_input input:focus::placeholder{opacity:0}.c-readme-view .gno-form_input input:focus::-moz-placeholder{opacity:0}.c-readme-view .gno-form_input input:focus::placeholder{opacity:0}.c-realm-view .gno-form_input textarea:focus::-moz-placeholder{opacity:0}.c-realm-view .gno-form_input textarea:focus::placeholder{opacity:0}.c-readme-view .gno-form_input textarea:focus::-moz-placeholder{opacity:0}.c-readme-view .gno-form_input textarea:focus::placeholder{opacity:0}.c-readme-view .gno-form textarea,.c-realm-view .gno-form textarea{resize:none}.c-realm-view .gno-form_select select:has(option[value=""]:checked){color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_select select:has(option[value=""]:checked){color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_description,.c-realm-view .gno-form_description{color:var(--s-color-text-secondary);font-weight:var(--g-font-semibold);margin-bottom:var(--g-space-2);margin-top:var(--g-space-5);padding-left:var(--g-space-4)}.c-readme-view .gno-form_info-badge,.c-realm-view .gno-form_info-badge{color:var(--s-color-text-tertiary);font-size:var(--g-font-size-50);font-weight:var(--g-font-normal);margin-left:var(--g-space-1)}.c-readme-view .gno-form_selectable,.c-realm-view .gno-form_selectable{-moz-column-gap:var(--g-space-2);column-gap:var(--g-space-2);display:flex;margin-bottom:var(--g-space-1);padding-left:var(--g-space-4);padding-right:var(--g-space-4)}.c-readme-view .gno-form_selectable input,.c-realm-view .gno-form_selectable input{width:auto}.c-readme-view .gno-form_selectable input[type=checkbox],.c-readme-view .gno-form_selectable input[type=radio],.c-realm-view .gno-form_selectable input[type=checkbox],.c-realm-view .gno-form_selectable input[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:var(--s-border);border-radius:var(--s-rounded-full);flex-shrink:0;height:var(--g-space-5);padding:0;position:relative;width:var(--g-space-5)}.c-readme-view .gno-form_selectable input[type=checkbox]:checked,.c-readme-view .gno-form_selectable input[type=radio]:checked,.c-realm-view .gno-form_selectable input[type=checkbox]:checked,.c-realm-view .gno-form_selectable input[type=radio]:checked{background-color:var(--s-color-bg-brand-default);border-color:transparent}.c-readme-view .gno-form_selectable input[type=checkbox]:checked:after,.c-readme-view .gno-form_selectable input[type=radio]:checked:after,.c-realm-view .gno-form_selectable input[type=checkbox]:checked:after,.c-realm-view .gno-form_selectable input[type=radio]:checked:after{opacity:1}.c-readme-view .gno-form_selectable input[type=checkbox]:focus,.c-readme-view .gno-form_selectable input[type=radio]:focus,.c-realm-view .gno-form_selectable input[type=checkbox]:focus,.c-realm-view .gno-form_selectable input[type=radio]:focus{outline:none}.c-readme-view .gno-form_selectable input[type=checkbox]:focus,.c-readme-view .gno-form_selectable input[type=checkbox]:hover,.c-readme-view .gno-form_selectable input[type=radio]:focus,.c-readme-view .gno-form_selectable input[type=radio]:hover,.c-realm-view .gno-form_selectable input[type=checkbox]:focus,.c-realm-view .gno-form_selectable input[type=checkbox]:hover,.c-realm-view .gno-form_selectable input[type=radio]:focus,.c-realm-view .gno-form_selectable input[type=radio]:hover{border-color:var(--s-color-border-tertiary);color:var(--s-color-text-brand-default)}.c-readme-view .gno-form_selectable input[type=checkbox]:focus+label,.c-readme-view .gno-form_selectable input[type=checkbox]:hover+label,.c-readme-view .gno-form_selectable input[type=radio]:focus+label,.c-readme-view .gno-form_selectable input[type=radio]:hover+label,.c-realm-view .gno-form_selectable input[type=checkbox]:focus+label,.c-realm-view .gno-form_selectable input[type=checkbox]:hover+label,.c-realm-view .gno-form_selectable input[type=radio]:focus+label,.c-realm-view .gno-form_selectable input[type=radio]:hover+label{color:var(--s-color-text-brand-default);-webkit-text-decoration:underline;text-decoration:underline}.c-readme-view .gno-form_selectable input[type=checkbox]:disabled,.c-readme-view .gno-form_selectable input[type=radio]:disabled,.c-realm-view .gno-form_selectable input[type=checkbox]:disabled,.c-realm-view .gno-form_selectable input[type=radio]:disabled{cursor:not-allowed;opacity:var(--g-opacity-50);-webkit-text-decoration:line-through;text-decoration:line-through}.c-readme-view .gno-form_selectable input[type=checkbox]:disabled+label,.c-readme-view .gno-form_selectable input[type=radio]:disabled+label,.c-realm-view .gno-form_selectable input[type=checkbox]:disabled+label,.c-realm-view .gno-form_selectable input[type=radio]:disabled+label{cursor:not-allowed;opacity:var(--g-opacity-50);-webkit-text-decoration:none;text-decoration:none}.c-realm-view .gno-form_selectable input[type=radio]:user-invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_selectable input[type=radio]:user-invalid{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_selectable input[type=checkbox]:user-invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_selectable input[type=checkbox]:user-invalid{border-color:var(--s-color-border-error)}@supports not selector(:user-invalid){.c-realm-view .gno-form_selectable input[type=radio]:invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_selectable input[type=radio]:invalid{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_selectable input[type=checkbox]:invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_selectable input[type=checkbox]:invalid{border-color:var(--s-color-border-error)}}.c-readme-view .gno-form_selectable input[type=radio]:after,.c-realm-view .gno-form_selectable input[type=radio]:after{background-color:var(--s-color-bg-brand-default);border:var(--s-border-secondary);border-radius:var(--s-rounded-full);border-width:calc(var(--g-space-px)*2);content:"";height:var(--g-space-4);left:50%;opacity:0;position:absolute;top:50%;transform:translate(-50%,-50%);width:var(--g-space-4)}.c-readme-view .gno-form_selectable input[type=checkbox],.c-realm-view .gno-form_selectable input[type=checkbox]{border-radius:var(--s-rounded-sm)}.c-readme-view .gno-form_selectable input[type=checkbox]:after,.c-realm-view .gno-form_selectable input[type=checkbox]:after{background-color:var(--s-color-bg-base);clip-path:polygon(25% 36%,43% 54%,76% 18%,89% 29%,44% 78%,13% 49%);content:"";height:var(--g-space-3);left:50%;margin:auto;opacity:0;position:absolute;top:50%;transform:translate(-50%,-50%);width:var(--g-space-3)}.c-readme-view .gno-form_selectable label,.c-realm-view .gno-form_selectable label{color:var(--s-color-text-secondary);cursor:pointer;display:block;position:relative}.c-readme-view .gno-form_selectable label:first-letter,.c-realm-view .gno-form_selectable label:first-letter{text-transform:capitalize}.c-readme-view .gno-alert,.c-realm-view .gno-alert{border-left:var(--g-space-1) solid var(--s-color-border-tertiary);border-radius:var(--s-rounded);color:var(--s-color-text-secondary);margin-bottom:var(--g-space-10);margin-top:var(--g-space-5);padding:var(--g-space-3) var(--g-space-4)}.c-readme-view .gno-alert>div>:first-child,.c-realm-view .gno-alert>div>:first-child{margin-top:var(--g-space-2)}.c-readme-view .gno-alert>div>:last-child,.c-realm-view .gno-alert>div>:last-child{margin-bottom:0}.c-readme-view .gno-alert>summary,.c-realm-view .gno-alert>summary{align-items:center;display:flex;font-weight:var(--g-font-bold);gap:var(--g-space-2);list-style:none;margin-bottom:var(--g-space-1);margin-top:var(--g-space-1)}.c-readme-view .gno-alert>summary svg,.c-realm-view .gno-alert>summary svg{height:var(--g-space-6);width:var(--g-space-6)}.c-readme-view .gno-alert>summary svg:last-of-type,.c-realm-view .gno-alert>summary svg:last-of-type{height:var(--g-space-4);margin-left:auto;transform:rotate(-90deg);width:var(--g-space-4)}.c-readme-view .gno-alert>summary a,.c-readme-view .gno-alert>summary svg,.c-realm-view .gno-alert>summary a,.c-realm-view .gno-alert>summary svg{color:inherit}.c-realm-view .gno-alert>summary::marker{display:none}.c-readme-view .gno-alert>summary::marker{display:none}.c-readme-view .gno-alert>summary::-webkit-details-marker,.c-realm-view .gno-alert>summary::-webkit-details-marker{display:none}.c-readme-view .gno-alert[open]>summary svg:last-of-type,.c-realm-view .gno-alert[open]>summary svg:last-of-type{transform:rotate(0)}.c-readme-view .gno-alert.gno-alert-info,.c-realm-view .gno-alert.gno-alert-info{background-color:color-mix(in srgb,var(--s-color-bg-info-default) 10%,transparent);border-left-color:var(--s-color-border-info);color:var(--s-color-text-info)}.c-readme-view .gno-alert.gno-alert-note,.c-realm-view .gno-alert.gno-alert-note{background-color:color-mix(in srgb,var(--s-color-bg-note-default) 10%,transparent);border-left-color:var(--s-color-border-note);color:var(--s-color-text-note)}.c-readme-view .gno-alert.gno-alert-success,.c-realm-view .gno-alert.gno-alert-success{background-color:color-mix(in srgb,var(--s-color-bg-success-default) 10%,transparent);border-left-color:var(--s-color-border-success);color:var(--s-color-text-success)}.c-readme-view .gno-alert.gno-alert-warning,.c-realm-view .gno-alert.gno-alert-warning{background-color:color-mix(in srgb,var(--s-color-bg-warning-default) 10%,transparent);border-left-color:var(--s-color-border-warning);color:var(--s-color-text-warning)}.c-readme-view .gno-alert.gno-alert-caution,.c-realm-view .gno-alert.gno-alert-caution{background-color:color-mix(in srgb,var(--s-color-bg-caution-default) 10%,transparent);border-left-color:var(--s-color-border-error);color:var(--s-color-text-caution)}.c-readme-view .gno-alert.gno-alert-tip,.c-realm-view .gno-alert.gno-alert-tip{background-color:color-mix(in srgb,var(--s-color-bg-tip-default) 10%,transparent);border-left-color:var(--s-color-border-tip);color:var(--s-color-text-tip)}.u-hidden{display:none}.u-inline{display:inline}.u-sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border-width:0;white-space:nowrap}.u-color-valid{color:var(--s-color-bg-brand-default)}.u-color-danger{color:var(--s-color-text-caution)}.u-text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.u-font-mono{font-family:var(--g-font-family-mono)}.u-capitalize{text-transform:capitalize}.u-text-center{text-align:center}.u-no-scrollbar::-webkit-scrollbar{display:none}.u-no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.u-is-loading{opacity:0}.u-icon-static{height:var(--g-space-4);width:var(--g-space-4)}.u-gap-0{gap:0}.u-mt-4{margin-top:var(--g-space-4)}.u-mb-0{margin-bottom:0}.u-mb-2{margin-bottom:var(--g-space-2)}@media (min-width:calc(820 / 16 * 1rem)){.u-lg-mb-4{margin-bottom:var(--g-space-4)}}.u-grid-full{grid-column:1/-1} \ No newline at end of file + article[data-list-type-value=pure]{display:flex}.b-packages label:has(input:checked){color:var(--s-color-text-tertiary)}.b-packages label:has(input:checked):after{background-color:var(--s-color-bg-brand-default);border-top-left-radius:var(--s-rounded-sm);border-top-right-radius:var(--s-rounded-sm);bottom:calc(var(--g-space-1)*-2);content:"";height:var(--g-space-1);left:0;position:absolute;width:100%}.b-packages:has(input[value=realms]:checked) .range:not(:has(>[data-list-type-value=realm])):before{display:block}.b-packages:has(input[value=realms]:checked) .range:not(:has(>[data-list-type-value=realm])):after{display:block}.b-packages:has(input[value=realms]:checked) .range:not(:has(>[data-list-type-value=realm])):before{content:"No realms found"}.b-packages:has(input[value=realms]:checked) .range:not(:has(>[data-list-type-value=realm])):after{content:"Add a realm to your namespace to get started"}.b-packages:has(input[value=pures]:checked) .range:not(:has(>[data-list-type-value=pure])):before{display:block}.b-packages:has(input[value=pures]:checked) .range:not(:has(>[data-list-type-value=pure])):after{display:block}.b-packages:has(input[value=pures]:checked) .range:not(:has(>[data-list-type-value=pure])):before{content:"No pures found"}.b-packages:has(input[value=pures]:checked) .range:not(:has(>[data-list-type-value=pure])):after{content:"Add a pure to your namespace to get started"}@media (min-width:calc(640 / 16 * 1rem)){.b-packages:has(input[value=display-grid]:checked) .range{gap:var(--g-space-3);grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:calc(1020 / 16 * 1rem)){.b-packages:has(input[value=display-grid]:checked) .range{grid-template-columns:repeat(4,minmax(0,1fr))}}.b-packages:has(input[value=display-grid]:checked) .range article .article-content p{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}.b-packages:has(input[value=display-list]:checked) .range article .article-content{display:flex;flex:none;flex-direction:row;gap:var(--g-space-2)}@media (min-width:calc(820 / 16 * 1rem)){.b-packages:has(input[value=display-list]:checked) .range article .article-content{flex:5;min-width:0}}.b-packages:has(input[value=display-list]:checked) .range article .article-content .title{margin-bottom:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:33.33333%}.b-packages:has(input[value=display-list]:checked) .range article .article-content p{white-space:nowrap}.b-packages:has(input[value=display-list]:checked) .range article footer{display:none;justify-content:center;min-width:0;padding-bottom:0;padding-left:0}@media (min-width:calc(640 / 16 * 1rem)){.b-packages:has(input[value=display-list]:checked) .range article footer{flex:1}}@media (min-width:calc(820 / 16 * 1rem)){.b-packages:has(input[value=display-list]:checked) .range article footer{display:flex}}.b-packages:has(input[value=display-list]:checked) .range article footer .size{display:none;min-width:0}@media (min-width:calc(1020 / 16 * 1rem)){.b-packages:has(input[value=display-list]:checked) .range article footer .size{display:block;flex:2}}.b-packages:has(input[value=display-list]:checked) .range article footer time{min-width:0}@media (min-width:calc(1020 / 16 * 1rem)){.b-packages:has(input[value=display-list]:checked) .range article footer time{flex:5}}.b-icon-action{flex-shrink:0;height:var(--g-space-5);width:var(--g-space-5)}.b-popup-bg,.b-popup-dialog{opacity:0;right:0;top:0;visibility:hidden;z-index:var(--g-z-max)}.b-popup-bg{align-items:center;bottom:0;display:flex;justify-content:center;left:0;position:fixed;right:0;top:0}.b-popup-dialog{position:absolute}.b-popup-dialog>.inner{background-color:var(--s-color-bg-base);border:var(--s-border-secondary);border-radius:var(--s-rounded);padding:var(--g-space-2-5) var(--g-space-4);position:absolute;transform:translateX(-100%);z-index:var(--g-z-max)}.b-popup-dialog>.inner>*+*{margin-top:var(--g-space-2-5)}.b-popup-dialog header{align-items:center;color:var(--s-color-text-secondary);display:flex;justify-content:space-between;width:100%}.b-popup-dialog header>svg{color:var(--s-color-text-tertiary);cursor:pointer;position:absolute;right:var(--g-space-3)}.b-popup-dialog header>svg>svg:hover{color:var(--s-color-text-primary)}.b-popup:checked+.b-popup-bg,.b-popup:checked~.b-popup-dialog{opacity:1;visibility:visible}.b-tag,.b-tag--secondary{align-items:center;border:var(--s-border);border-radius:var(--s-rounded-full);color:var(--s-color-text-secondary);display:flex;font-family:var(--g-font-family-mono);font-size:var(--g-font-size-50);gap:var(--g-space-2);padding:var(--g-space-px) var(--g-space-2)}.b-tag--secondary{background-color:var(--s-color-bg-surface-primary);border-color:transparent;color:var(--s-color-text-primary)}.c-readme-view .gno-columns,.c-realm-view .gno-columns{display:flex;flex-wrap:wrap;gap:var(--g-space-10)}@media (min-width:calc(1366 / 16 * 1rem)){.c-readme-view .gno-columns,.c-realm-view .gno-columns{gap:var(--g-space-12)}}.c-readme-view .gno-columns>*,.c-realm-view .gno-columns>*{flex-basis:var(--g-space-52);flex-grow:1;flex-shrink:1}@media (min-width:calc(820 / 16 * 1rem)){.c-readme-view .gno-columns>*,.c-realm-view .gno-columns>*{flex-basis:var(--g-space-44)}}.c-readme-view .tooltip,.c-realm-view .tooltip{--tooltip-left:0;--tooltip-right:initial;align-items:center;border:var(--s-border);border-radius:var(--s-rounded-full);color:var(--s-color-text-tertiary);display:inline-flex;height:var(--g-space-4);justify-content:center;margin-bottom:var(--g-space-px);position:relative;width:var(--g-space-4)}.c-readme-view .tooltip>svg,.c-realm-view .tooltip>svg{height:var(--g-space-3);width:var(--g-space-3)}.c-readme-view .tooltip:after,.c-realm-view .tooltip:after{background-color:var(--s-color-bg-base);border:var(--s-border-secondary);border-radius:var(--s-rounded);color:var(--s-color-text-secondary);content:attr(data-tooltip);font-size:var(--g-font-size-100);font-weight:var(--g-font-normal);left:var(--tooltip-left);max-width:var(--g-space-48);min-width:var(--g-space-32);opacity:0;padding:var(--g-space-1) var(--g-space-2);position:absolute;right:var(--tooltip-right);scale:0;text-align:center;top:100%;visibility:hidden;width:-moz-fit-content;width:fit-content;z-index:var(--g-z-max)}.c-readme-view .tooltip:hover:after,.c-realm-view .tooltip:hover:after{opacity:1;scale:1;transition-delay:var(--g-transition-fast);visibility:visible}.c-readme-view .tooltip:only-of-type,.c-realm-view .tooltip:only-of-type{margin-left:.3em;margin-right:.3em}.c-realm-view .tooltip:has(+span){margin-left:.3em}.c-readme-view .tooltip:has(+span){margin-left:.3em}.c-readme-view .link-external,.c-realm-view .link-external{font-size:.67em}.c-readme-view .link-internal,.c-realm-view .link-internal{font-size:.75em;font-weight:400}.c-readme-view .link-tx,.c-readme-view .link-user,.c-realm-view .link-tx,.c-realm-view .link-user{font-size:.75em}.c-realm-view ul:has(li>input[type=checkbox]:first-child){list-style:none;padding-left:0}.c-readme-view ul:has(li>input[type=checkbox]:first-child){list-style:none;padding-left:0}.c-realm-view li:has(>input[type=checkbox]:first-child){align-items:center;display:flex;gap:var(--g-space-2)}.c-readme-view li:has(>input[type=checkbox]:first-child){align-items:center;display:flex;gap:var(--g-space-2)}.c-readme-view li>input[type=checkbox]:first-child,.c-realm-view li>input[type=checkbox]:first-child{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:var(--s-border-secondary);border-radius:var(--s-rounded-sm);flex-shrink:0;height:var(--g-space-5);padding:0;position:relative;width:var(--g-space-5)}.c-readme-view li>input[type=checkbox]:first-child:disabled,.c-realm-view li>input[type=checkbox]:first-child:disabled{background-color:var(--s-color-bg-surface-primary);border-color:var(--s-color-border-tertiary);cursor:not-allowed}.c-readme-view li>input[type=checkbox]:first-child:disabled:after,.c-realm-view li>input[type=checkbox]:first-child:disabled:after{background-color:var(--s-color-bg-brand-default)}.c-readme-view li>input[type=checkbox]:first-child:checked:after,.c-realm-view li>input[type=checkbox]:first-child:checked:after{opacity:1}.c-readme-view li>input[type=checkbox]:first-child:after,.c-realm-view li>input[type=checkbox]:first-child:after{background-color:var(--s-color-bg-base);clip-path:polygon(25% 36%,43% 54%,76% 18%,89% 29%,44% 78%,13% 49%);content:"";height:var(--g-space-3);left:50%;margin:auto;opacity:0;position:absolute;top:50%;transform:translate(-50%,-50%);width:var(--g-space-3)}.c-readme-view .footnote-backref,.c-realm-view .footnote-backref{vertical-align:middle}.c-readme-view li[id^="fn:"],.c-realm-view li[id^="fn:"]{position:relative;z-index:var(--g-z-1)}.c-readme-view li[id^="fn:"]:before,.c-realm-view li[id^="fn:"]:before{background-color:var(--s-color-bg-brand-weak);border-radius:var(--s-rounded-sm);bottom:0;content:"";display:block;left:calc(var(--g-space-0-5)*-1);opacity:0;position:absolute;right:calc(var(--g-space-0-5)*-1);top:calc(var(--g-space-0-5)*-1);z-index:var(--g-z-min)}.c-readme-view li[id^="fn:"]:target:before,.c-realm-view li[id^="fn:"]:target:before{opacity:1;transition-delay:var(--g-duration-150);transition-duration:var(--g-duration-75)}.c-readme-view .gno-form,.c-realm-view .gno-form{border:var(--s-border-secondary);border-radius:var(--s-rounded);display:block;margin-bottom:var(--g-space-6);margin-top:var(--g-space-6)}.c-readme-view .gno-form input[type=submit],.c-realm-view .gno-form input[type=submit]{background-color:var(--s-color-bg-brand-default);border-color:var(--s-color-border-brand-default);border-radius:var(--s-rounded-sm);color:var(--s-color-text-base);cursor:pointer;margin-bottom:var(--g-space-2);margin-top:var(--g-space-4);width:100%}.c-readme-view .gno-form input[type=submit]:hover,.c-realm-view .gno-form input[type=submit]:hover{opacity:.9}.c-readme-view .gno-form .command,.c-realm-view .gno-form .command{background-color:var(--s-color-bg-base-dev);margin-top:var(--g-space-6);padding:var(--g-space-4)}.c-readme-view .gno-form .command .title,.c-realm-view .gno-form .command .title{font-size:var(--g-font-size-200);font-weight:var(--g-font-semibold);white-space:nowrap}.c-readme-view .gno-form .command>.b-code,.c-realm-view .gno-form .command>.b-code{background-color:var(--s-color-bg-base-dev)}.c-readme-view .gno-form .command>.b-code>pre,.c-realm-view .gno-form .command>.b-code>pre{background-color:var(--s-color-bg-base);margin-bottom:0}.c-readme-view .gno-form .command .c-between,.c-readme-view .gno-form .command .c-inline,.c-realm-view .gno-form .command .c-between,.c-realm-view .gno-form .command .c-inline{align-items:flex-start;flex-direction:column;gap:var(--g-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view .gno-form .command .c-between,.c-readme-view .gno-form .command .c-inline,.c-realm-view .gno-form .command .c-between,.c-realm-view .gno-form .command .c-inline{align-items:center;flex-direction:row}}.c-readme-view .gno-form .command .c-between>*,.c-readme-view .gno-form .command .c-inline>*,.c-realm-view .gno-form .command .c-between>*,.c-realm-view .gno-form .command .c-inline>*{width:100%}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view .gno-form .command .c-between>*,.c-readme-view .gno-form .command .c-inline>*,.c-realm-view .gno-form .command .c-between>*,.c-realm-view .gno-form .command .c-inline>*{width:auto}}.c-readme-view .gno-form_header,.c-realm-view .gno-form_header{color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-50);justify-content:space-between;margin-bottom:var(--g-space-6);padding:var(--g-space-2) var(--g-space-4) 0}.c-readme-view .gno-form_input,.c-readme-view .gno-form_select,.c-realm-view .gno-form_input,.c-realm-view .gno-form_select{padding-left:var(--g-space-4);padding-right:var(--g-space-4);position:relative}.c-readme-view .gno-form_input label,.c-readme-view .gno-form_select label,.c-realm-view .gno-form_input label,.c-realm-view .gno-form_select label{background-color:var(--s-color-bg-input);color:var(--s-color-text-tertiary);display:none;font-size:var(--g-font-size-50);left:var(--g-space-5);padding-left:var(--g-space-1);padding-right:var(--g-space-1);position:absolute;top:0;transform:translateY(-50%)}.c-readme-view .gno-form_input svg,.c-readme-view .gno-form_select svg,.c-realm-view .gno-form_input svg,.c-realm-view .gno-form_select svg{height:var(--g-space-4);pointer-events:none;position:absolute;right:var(--g-space-6);top:50%;transform:translateY(-50%);width:var(--g-space-4)}.c-realm-view .gno-form_input:has(input:focus) label{display:block}.c-readme-view .gno-form_input:has(input:focus) label{display:block}.c-realm-view .gno-form_input:has(input:not(:-moz-placeholder)) label{display:block}.c-realm-view .gno-form_input:has(input:not(:placeholder-shown)) label{display:block}.c-readme-view .gno-form_input:has(input:not(:-moz-placeholder)) label{display:block}.c-readme-view .gno-form_input:has(input:not(:placeholder-shown)) label{display:block}.c-realm-view .gno-form_input:has(textarea:not(:-moz-placeholder)) label{display:block}.c-realm-view .gno-form_input:has(textarea:not(:placeholder-shown)) label{display:block}.c-readme-view .gno-form_input:has(textarea:not(:-moz-placeholder)) label{display:block}.c-readme-view .gno-form_input:has(textarea:not(:placeholder-shown)) label{display:block}.c-realm-view .gno-form_input:has(textarea:focus) label{display:block}.c-readme-view .gno-form_input:has(textarea:focus) label{display:block}.c-realm-view .gno-form_select:has(select:focus) label{display:block}.c-readme-view .gno-form_select:has(select:focus) label{display:block}.c-realm-view .gno-form_select:has(select option:not([value=""]):checked) label{display:block}.c-readme-view .gno-form_select:has(select option:not([value=""]):checked) label{display:block}.c-readme-view .gno-form_input input,.c-readme-view .gno-form_input textarea,.c-readme-view .gno-form_select select,.c-realm-view .gno-form_input input,.c-realm-view .gno-form_input textarea,.c-realm-view .gno-form_select select{background-color:var(--s-color-bg-input);border:var(--g-space-px) solid var(--s-color-border-input);border-radius:var(--s-rounded-sm);color:var(--s-color-text-primary);display:block;margin-bottom:var(--g-space-4);margin-top:var(--g-space-4);outline:none;padding:var(--g-space-2);width:100%}.c-readme-view .gno-form_input input:focus,.c-readme-view .gno-form_input input:hover,.c-readme-view .gno-form_input textarea:focus,.c-readme-view .gno-form_input textarea:hover,.c-readme-view .gno-form_select select:focus,.c-readme-view .gno-form_select select:hover,.c-realm-view .gno-form_input input:focus,.c-realm-view .gno-form_input input:hover,.c-realm-view .gno-form_input textarea:focus,.c-realm-view .gno-form_input textarea:hover,.c-realm-view .gno-form_select select:focus,.c-realm-view .gno-form_select select:hover{border-color:var(--s-color-border-tertiary)}.c-realm-view .gno-form_input input::-moz-placeholder{color:var(--s-color-text-tertiary)}.c-realm-view .gno-form_input input::placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_input input::-moz-placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_input input::placeholder{color:var(--s-color-text-tertiary)}.c-realm-view .gno-form_input textarea::-moz-placeholder{color:var(--s-color-text-tertiary)}.c-realm-view .gno-form_input textarea::placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_input textarea::-moz-placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_input textarea::placeholder{color:var(--s-color-text-tertiary)}.c-realm-view .gno-form_select select::-moz-placeholder{color:var(--s-color-text-tertiary)}.c-realm-view .gno-form_select select::placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_select select::-moz-placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_select select::placeholder{color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_input input:disabled,.c-readme-view .gno-form_input input[readonly],.c-readme-view .gno-form_input textarea:disabled,.c-readme-view .gno-form_input textarea[readonly],.c-readme-view .gno-form_select select:disabled,.c-readme-view .gno-form_select select[readonly],.c-realm-view .gno-form_input input:disabled,.c-realm-view .gno-form_input input[readonly],.c-realm-view .gno-form_input textarea:disabled,.c-realm-view .gno-form_input textarea[readonly],.c-realm-view .gno-form_select select:disabled,.c-realm-view .gno-form_select select[readonly]{background-color:var(--s-color-bg-secondary);color:var(--s-color-text-tertiary);cursor:not-allowed}.c-readme-view .gno-form_input input:disabled:focus,.c-readme-view .gno-form_input input:disabled:hover,.c-readme-view .gno-form_input input[readonly]:focus,.c-readme-view .gno-form_input input[readonly]:hover,.c-readme-view .gno-form_input textarea:disabled:focus,.c-readme-view .gno-form_input textarea:disabled:hover,.c-readme-view .gno-form_input textarea[readonly]:focus,.c-readme-view .gno-form_input textarea[readonly]:hover,.c-readme-view .gno-form_select select:disabled:focus,.c-readme-view .gno-form_select select:disabled:hover,.c-readme-view .gno-form_select select[readonly]:focus,.c-readme-view .gno-form_select select[readonly]:hover,.c-realm-view .gno-form_input input:disabled:focus,.c-realm-view .gno-form_input input:disabled:hover,.c-realm-view .gno-form_input input[readonly]:focus,.c-realm-view .gno-form_input input[readonly]:hover,.c-realm-view .gno-form_input textarea:disabled:focus,.c-realm-view .gno-form_input textarea:disabled:hover,.c-realm-view .gno-form_input textarea[readonly]:focus,.c-realm-view .gno-form_input textarea[readonly]:hover,.c-realm-view .gno-form_select select:disabled:focus,.c-realm-view .gno-form_select select:disabled:hover,.c-realm-view .gno-form_select select[readonly]:focus,.c-realm-view .gno-form_select select[readonly]:hover{border-color:var(--s-color-border-secondary)}.c-realm-view .gno-form_input input:user-invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input input:user-invalid{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_input textarea:user-invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input textarea:user-invalid{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_select select:user-invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_select select:user-invalid{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_input input:user-invalid:focus{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input input:user-invalid:focus{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_input textarea:user-invalid:focus{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input textarea:user-invalid:focus{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_select select:user-invalid:focus{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_select select:user-invalid:focus{border-color:var(--s-color-border-error)}@supports not selector(:user-invalid){.c-realm-view .gno-form_input input:invalid:not(:-moz-placeholder):not(:focus){border-color:var(--s-color-border-error)}.c-realm-view .gno-form_input input:invalid:not(:placeholder-shown):not(:focus){border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input input:invalid:not(:-moz-placeholder):not(:focus){border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input input:invalid:not(:placeholder-shown):not(:focus){border-color:var(--s-color-border-error)}.c-realm-view .gno-form_input textarea:invalid:not(:-moz-placeholder):not(:focus){border-color:var(--s-color-border-error)}.c-realm-view .gno-form_input textarea:invalid:not(:placeholder-shown):not(:focus){border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input textarea:invalid:not(:-moz-placeholder):not(:focus){border-color:var(--s-color-border-error)}.c-readme-view .gno-form_input textarea:invalid:not(:placeholder-shown):not(:focus){border-color:var(--s-color-border-error)}.c-realm-view .gno-form_select select:invalid:not(:-moz-placeholder):not(:focus){border-color:var(--s-color-border-error)}.c-realm-view .gno-form_select select:invalid:not(:placeholder-shown):not(:focus){border-color:var(--s-color-border-error)}.c-readme-view .gno-form_select select:invalid:not(:-moz-placeholder):not(:focus){border-color:var(--s-color-border-error)}.c-readme-view .gno-form_select select:invalid:not(:placeholder-shown):not(:focus){border-color:var(--s-color-border-error)}}.c-realm-view .gno-form_input input:focus::-moz-placeholder{opacity:0}.c-realm-view .gno-form_input input:focus::placeholder{opacity:0}.c-readme-view .gno-form_input input:focus::-moz-placeholder{opacity:0}.c-readme-view .gno-form_input input:focus::placeholder{opacity:0}.c-realm-view .gno-form_input textarea:focus::-moz-placeholder{opacity:0}.c-realm-view .gno-form_input textarea:focus::placeholder{opacity:0}.c-readme-view .gno-form_input textarea:focus::-moz-placeholder{opacity:0}.c-readme-view .gno-form_input textarea:focus::placeholder{opacity:0}.c-readme-view .gno-form textarea,.c-realm-view .gno-form textarea{resize:none}.c-realm-view .gno-form_select select:has(option[value=""]:checked){color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_select select:has(option[value=""]:checked){color:var(--s-color-text-tertiary)}.c-readme-view .gno-form_description,.c-realm-view .gno-form_description{color:var(--s-color-text-secondary);font-weight:var(--g-font-semibold);margin-bottom:var(--g-space-2);margin-top:var(--g-space-5);padding-left:var(--g-space-4)}.c-readme-view .gno-form_info-badge,.c-realm-view .gno-form_info-badge{color:var(--s-color-text-tertiary);font-size:var(--g-font-size-50);font-weight:var(--g-font-normal);margin-left:var(--g-space-1)}.c-readme-view .gno-form_selectable,.c-realm-view .gno-form_selectable{-moz-column-gap:var(--g-space-2);column-gap:var(--g-space-2);display:flex;margin-bottom:var(--g-space-1);padding-left:var(--g-space-4);padding-right:var(--g-space-4)}.c-readme-view .gno-form_selectable input,.c-realm-view .gno-form_selectable input{width:auto}.c-readme-view .gno-form_selectable input[type=checkbox],.c-readme-view .gno-form_selectable input[type=radio],.c-realm-view .gno-form_selectable input[type=checkbox],.c-realm-view .gno-form_selectable input[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:var(--s-border);border-radius:var(--s-rounded-full);flex-shrink:0;height:var(--g-space-5);padding:0;position:relative;width:var(--g-space-5)}.c-readme-view .gno-form_selectable input[type=checkbox]:checked,.c-readme-view .gno-form_selectable input[type=radio]:checked,.c-realm-view .gno-form_selectable input[type=checkbox]:checked,.c-realm-view .gno-form_selectable input[type=radio]:checked{background-color:var(--s-color-bg-brand-default);border-color:transparent}.c-readme-view .gno-form_selectable input[type=checkbox]:checked:after,.c-readme-view .gno-form_selectable input[type=radio]:checked:after,.c-realm-view .gno-form_selectable input[type=checkbox]:checked:after,.c-realm-view .gno-form_selectable input[type=radio]:checked:after{opacity:1}.c-readme-view .gno-form_selectable input[type=checkbox]:focus,.c-readme-view .gno-form_selectable input[type=radio]:focus,.c-realm-view .gno-form_selectable input[type=checkbox]:focus,.c-realm-view .gno-form_selectable input[type=radio]:focus{outline:none}.c-readme-view .gno-form_selectable input[type=checkbox]:focus,.c-readme-view .gno-form_selectable input[type=checkbox]:hover,.c-readme-view .gno-form_selectable input[type=radio]:focus,.c-readme-view .gno-form_selectable input[type=radio]:hover,.c-realm-view .gno-form_selectable input[type=checkbox]:focus,.c-realm-view .gno-form_selectable input[type=checkbox]:hover,.c-realm-view .gno-form_selectable input[type=radio]:focus,.c-realm-view .gno-form_selectable input[type=radio]:hover{border-color:var(--s-color-border-tertiary);color:var(--s-color-text-brand-default)}.c-readme-view .gno-form_selectable input[type=checkbox]:focus+label,.c-readme-view .gno-form_selectable input[type=checkbox]:hover+label,.c-readme-view .gno-form_selectable input[type=radio]:focus+label,.c-readme-view .gno-form_selectable input[type=radio]:hover+label,.c-realm-view .gno-form_selectable input[type=checkbox]:focus+label,.c-realm-view .gno-form_selectable input[type=checkbox]:hover+label,.c-realm-view .gno-form_selectable input[type=radio]:focus+label,.c-realm-view .gno-form_selectable input[type=radio]:hover+label{color:var(--s-color-text-brand-default);-webkit-text-decoration:underline;text-decoration:underline}.c-readme-view .gno-form_selectable input[type=checkbox]:disabled,.c-readme-view .gno-form_selectable input[type=radio]:disabled,.c-realm-view .gno-form_selectable input[type=checkbox]:disabled,.c-realm-view .gno-form_selectable input[type=radio]:disabled{cursor:not-allowed;opacity:var(--g-opacity-50);-webkit-text-decoration:line-through;text-decoration:line-through}.c-readme-view .gno-form_selectable input[type=checkbox]:disabled+label,.c-readme-view .gno-form_selectable input[type=radio]:disabled+label,.c-realm-view .gno-form_selectable input[type=checkbox]:disabled+label,.c-realm-view .gno-form_selectable input[type=radio]:disabled+label{cursor:not-allowed;opacity:var(--g-opacity-50);-webkit-text-decoration:none;text-decoration:none}.c-realm-view .gno-form_selectable input[type=radio]:user-invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_selectable input[type=radio]:user-invalid{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_selectable input[type=checkbox]:user-invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_selectable input[type=checkbox]:user-invalid{border-color:var(--s-color-border-error)}@supports not selector(:user-invalid){.c-realm-view .gno-form_selectable input[type=radio]:invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_selectable input[type=radio]:invalid{border-color:var(--s-color-border-error)}.c-realm-view .gno-form_selectable input[type=checkbox]:invalid{border-color:var(--s-color-border-error)}.c-readme-view .gno-form_selectable input[type=checkbox]:invalid{border-color:var(--s-color-border-error)}}.c-readme-view .gno-form_selectable input[type=radio]:after,.c-realm-view .gno-form_selectable input[type=radio]:after{background-color:var(--s-color-bg-brand-default);border:var(--s-border-secondary);border-radius:var(--s-rounded-full);border-width:calc(var(--g-space-px)*2);content:"";height:var(--g-space-4);left:50%;opacity:0;position:absolute;top:50%;transform:translate(-50%,-50%);width:var(--g-space-4)}.c-readme-view .gno-form_selectable input[type=checkbox],.c-realm-view .gno-form_selectable input[type=checkbox]{border-radius:var(--s-rounded-sm)}.c-readme-view .gno-form_selectable input[type=checkbox]:after,.c-realm-view .gno-form_selectable input[type=checkbox]:after{background-color:var(--s-color-bg-base);clip-path:polygon(25% 36%,43% 54%,76% 18%,89% 29%,44% 78%,13% 49%);content:"";height:var(--g-space-3);left:50%;margin:auto;opacity:0;position:absolute;top:50%;transform:translate(-50%,-50%);width:var(--g-space-3)}.c-readme-view .gno-form_selectable label,.c-realm-view .gno-form_selectable label{color:var(--s-color-text-secondary);cursor:pointer;display:block;position:relative}.c-readme-view .gno-form_selectable label:first-letter,.c-realm-view .gno-form_selectable label:first-letter{text-transform:capitalize}.c-readme-view .gno-alert,.c-realm-view .gno-alert{border-left:var(--g-space-1) solid var(--s-color-border-tertiary);border-radius:var(--s-rounded);color:var(--s-color-text-secondary);margin-bottom:var(--g-space-10);margin-top:var(--g-space-5);padding:var(--g-space-3) var(--g-space-4)}.c-readme-view .gno-alert>div>:first-child,.c-realm-view .gno-alert>div>:first-child{margin-top:var(--g-space-2)}.c-readme-view .gno-alert>div>:last-child,.c-realm-view .gno-alert>div>:last-child{margin-bottom:0}.c-readme-view .gno-alert>summary,.c-realm-view .gno-alert>summary{align-items:center;display:flex;font-weight:var(--g-font-bold);gap:var(--g-space-2);list-style:none;margin-bottom:var(--g-space-1);margin-top:var(--g-space-1)}.c-readme-view .gno-alert>summary svg,.c-realm-view .gno-alert>summary svg{height:var(--g-space-6);width:var(--g-space-6)}.c-readme-view .gno-alert>summary svg:last-of-type,.c-realm-view .gno-alert>summary svg:last-of-type{height:var(--g-space-4);margin-left:auto;transform:rotate(-90deg);width:var(--g-space-4)}.c-readme-view .gno-alert>summary a,.c-readme-view .gno-alert>summary svg,.c-realm-view .gno-alert>summary a,.c-realm-view .gno-alert>summary svg{color:inherit}.c-realm-view .gno-alert>summary::marker{display:none}.c-readme-view .gno-alert>summary::marker{display:none}.c-readme-view .gno-alert>summary::-webkit-details-marker,.c-realm-view .gno-alert>summary::-webkit-details-marker{display:none}.c-readme-view .gno-alert[open]>summary svg:last-of-type,.c-realm-view .gno-alert[open]>summary svg:last-of-type{transform:rotate(0)}.c-readme-view .gno-alert.gno-alert-info,.c-realm-view .gno-alert.gno-alert-info{background-color:color-mix(in srgb,var(--s-color-bg-info-default) 10%,transparent);border-left-color:var(--s-color-border-info);color:var(--s-color-text-info)}.c-readme-view .gno-alert.gno-alert-note,.c-realm-view .gno-alert.gno-alert-note{background-color:color-mix(in srgb,var(--s-color-bg-note-default) 10%,transparent);border-left-color:var(--s-color-border-note);color:var(--s-color-text-note)}.c-readme-view .gno-alert.gno-alert-success,.c-realm-view .gno-alert.gno-alert-success{background-color:color-mix(in srgb,var(--s-color-bg-success-default) 10%,transparent);border-left-color:var(--s-color-border-success);color:var(--s-color-text-success)}.c-readme-view .gno-alert.gno-alert-warning,.c-realm-view .gno-alert.gno-alert-warning{background-color:color-mix(in srgb,var(--s-color-bg-warning-default) 10%,transparent);border-left-color:var(--s-color-border-warning);color:var(--s-color-text-warning)}.c-readme-view .gno-alert.gno-alert-caution,.c-realm-view .gno-alert.gno-alert-caution{background-color:color-mix(in srgb,var(--s-color-bg-caution-default) 10%,transparent);border-left-color:var(--s-color-border-error);color:var(--s-color-text-caution)}.c-readme-view .gno-alert.gno-alert-tip,.c-realm-view .gno-alert.gno-alert-tip{background-color:color-mix(in srgb,var(--s-color-bg-tip-default) 10%,transparent);border-left-color:var(--s-color-border-tip);color:var(--s-color-text-tip)}.b-state-explorer{grid-column:1/-1;margin-top:var(--g-space-4);padding-bottom:var(--g-space-24);padding-top:var(--g-space-4)}@media (min-width:calc(820 / 16 * 1rem)){.b-state-explorer{margin-top:0;padding-top:var(--g-space-6)}}.b-state-explorer__header{margin-bottom:var(--g-space-4)}.b-state-explorer__path{color:var(--s-color-text-tertiary);font-size:var(--g-font-size-90);margin-bottom:var(--g-spacing-050)}.b-state-explorer__path:empty{display:none}.b-state-explorer__path-link{color:var(--s-color-text-link)}.b-state-explorer__path-link:hover{color:var(--s-color-text-link-hover)}.b-state-explorer__count{color:var(--s-color-text-secondary);font-size:var(--g-font-size-90);white-space:nowrap}.b-state-tree{font-family:var(--g-font-family-mono);font-size:var(--g-font-size-90);line-height:1}.b-state-row__line{align-items:baseline;border-radius:3px;display:flex;flex-wrap:nowrap;gap:0 .4em;padding:.2rem 0}.b-state-row__line:hover{background-color:var(--s-color-bg-alt)}.b-state-toggle{align-items:center;color:var(--s-color-text-secondary);cursor:pointer;display:inline-flex;flex-shrink:0;font-size:.65em;justify-content:center;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:1em}.b-state-toggle:hover{color:var(--s-color-text-primary)}.b-state-toggle:empty{cursor:default}.b-state-toggle--loading{animation:state-pulse .8s ease-in-out infinite}@keyframes state-pulse{0%,to{opacity:.3}50%{opacity:1}}.b-state-name{color:var(--s-color-text-primary);font-weight:600}.b-state-sep{color:var(--s-color-text-secondary);opacity:.5}.b-state-type{font-style:italic}.b-state-kind--struct{color:var(--s-color-text-tip)}.b-state-kind--map{color:var(--s-color-text-caution)}.b-state-kind--array,.b-state-kind--slice{color:var(--s-color-text-success)}.b-state-kind--pointer{color:var(--s-color-text-warning)}.b-state-kind--func{color:var(--s-color-text-tip)}.b-state-kind--closure{color:var(--s-color-text-info)}.b-state-kind--nil,.b-state-kind--primitive,.b-state-meta{color:var(--s-color-text-secondary)}.b-state-meta{font-size:.85em}.b-state-val{word-break:break-all}.b-state-val--primitive{color:var(--s-color-text-info)}.b-state-val--nil{color:var(--s-color-text-secondary);font-style:italic}.b-state-val--func{color:var(--s-color-text-tip)}.b-state-val--closure{color:var(--s-color-text-info)}.b-state-oid{color:var(--s-color-text-secondary);cursor:pointer;font-size:.75em;opacity:0;transition:opacity .15s}.b-state-row__line:hover .b-state-oid{opacity:.5}.b-state-oid:hover{opacity:1!important;-webkit-text-decoration:underline;text-decoration:underline}.b-state-source{border-radius:var(--s-rounded);font-size:var(--g-font-size-90);line-height:1.5;margin:.25rem 0;overflow-x:auto;position:relative;-moz-tab-size:4;-o-tab-size:4;tab-size:4}.b-state-source__link{background-color:var(--s-color-bg-surface-secondary);border-radius:var(--s-rounded);color:var(--s-color-text-tertiary);font-size:var(--g-font-size-80);padding:var(--g-space-1) var(--g-space-2);position:absolute;right:var(--g-space-2);-webkit-text-decoration:none;text-decoration:none;top:var(--g-space-2);z-index:1}.b-state-source__link:hover{background-color:var(--s-color-bg-surface-tertiary);color:var(--s-color-text-link)}.b-state-source pre{background-color:var(--s-color-bg-base);border-radius:var(--s-rounded);margin:0;overflow-x:auto;padding:var(--g-space-4) var(--g-space-1)}.b-state-source code{font-family:var(--g-font-family-mono)}.b-state-srclink{color:var(--s-color-text-secondary);display:inline-block;font-size:var(--g-font-size-80);margin-top:.125rem;-webkit-text-decoration:none;text-decoration:none}.b-state-srclink:hover{color:var(--s-color-text-link);-webkit-text-decoration:underline;text-decoration:underline}.b-state-captures-label{color:var(--s-color-text-info);font-size:.85em;font-style:italic;margin-bottom:var(--g-space-1);margin-top:var(--g-space-2)}.b-state-err{color:var(--s-color-text-caution);font-style:italic;padding-left:1.5rem}.u-hidden{display:none}.u-inline{display:inline}.u-sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border-width:0;white-space:nowrap}.u-color-valid{color:var(--s-color-bg-brand-default)}.u-color-danger{color:var(--s-color-text-caution)}.u-text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.u-font-mono{font-family:var(--g-font-family-mono)}.u-capitalize{text-transform:capitalize}.u-text-center{text-align:center}.u-no-scrollbar::-webkit-scrollbar{display:none}.u-no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.u-is-loading{opacity:0}.u-icon-static{height:var(--g-space-4);width:var(--g-space-4)}.u-gap-0{gap:0}.u-mt-4{margin-top:var(--g-space-4)}.u-mb-0{margin-bottom:0}.u-mb-2{margin-bottom:var(--g-space-2)}@media (min-width:calc(820 / 16 * 1rem)){.u-lg-mb-4{margin-bottom:var(--g-space-4)}}.u-grid-full{grid-column:1/-1} \ No newline at end of file diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index f5f15de7ebf..45d7de6f5e5 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -81,6 +81,8 @@ const ( QueryDoc = "qdoc" QueryPaths = "qpaths" QueryStorage = "qstorage" + QueryPkgJSON = "qpkg_json" + QueryTypeJSON = "qtype_json" ) func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { @@ -110,6 +112,10 @@ func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) (res abci.Resp res = vh.queryPaths(ctx, req) case QueryStorage: res = vh.queryStorage(ctx, req) + case QueryPkgJSON: + res = vh.queryPkg(ctx, req) + case QueryTypeJSON: + res = vh.queryType(ctx, req) default: return sdk.ABCIResponseQueryFromError( std.ErrUnknownRequest(fmt.Sprintf( @@ -300,6 +306,39 @@ func (vh vmHandler) queryStorage(ctx sdk.Context, req abci.RequestQuery) (res ab return } +// queryPkg returns the named block variables of a package as Amino JSON. +func (vh vmHandler) queryPkg(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { + pkgPath := string(req.Data) + result, err := vh.vm.QueryPkg(ctx, pkgPath) + if err != nil { + res = sdk.ABCIResponseQueryFromError(err) + return + } + res.Data = []byte(result) + return +} + +// queryType returns a type definition by TypeID as Amino JSON. +func (vh vmHandler) queryType(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { + // Recover from panics (e.g. stack overflow from circular type references + // like time.Time) so the server stays alive. + defer func() { + if r := recover(); r != nil { + res = sdk.ABCIResponseQueryFromError( + fmt.Errorf("queryType panic for %q: %v", string(req.Data), r)) + } + }() + + tidStr := string(req.Data) + result, err := vh.vm.QueryType(ctx, tidStr) + if err != nil { + res = sdk.ABCIResponseQueryFromError(err) + return + } + res.Data = []byte(result) + return +} + // ---------------------------------------- // misc diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index ddeb8357f5b..8483f0e0bc0 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -1265,6 +1265,183 @@ func (vm *VMKeeper) QueryObjectBinary(ctx sdk.Context, oidStr string) (res []byt return amino.MarshalAny(exported) } +// QueryPkg returns the named block variables of a package as Amino JSON. +// This is the entry point for the state explorer: given a package path, +// return variable names alongside their exported Amino JSON values. +func (vm *VMKeeper) QueryPkg(ctx sdk.Context, pkgPath string) (res string, err error) { + ctx = ctx.WithGasMeter(store.NewGasMeter(maxGasQuery)) + gnostore := vm.newGnoTransactionStore(ctx) // throwaway (never committed) + + pv := gnostore.GetPackage(pkgPath, false) + if pv == nil { + return "", ErrInvalidPkgPath(fmt.Sprintf("package not found: %s", pkgPath)) + } + + block := resolveBlock(gnostore, pv.Block) + if block == nil { + return "", fmt.Errorf("package block not found for %s", pkgPath) + } + + // Resolve Source: it may be a RefNode (lazy reference to the PackageNode). + source := resolveBlockNode(gnostore, block.Source) + if source == nil { + return "", fmt.Errorf("block source not found for %s", pkgPath) + } + sb := source.GetStaticBlock() + names := sb.Names + + // Collect variable names and their exported values. + varNames := make([]string, 0, len(block.Values)) + varValues := make([]gno.TypedValue, 0, len(block.Values)) + for i, tv := range block.Values { + if i >= len(names) { + break + } + name := string(names[i]) + if name == "" || name == "_" { + continue + } + // Unwrap heap items. + if tv.T != nil && tv.T.Kind() == gno.HeapItemKind { + if hiv, ok := tv.V.(*gno.HeapItemValue); ok { + tv = hiv.Value + } + } + varNames = append(varNames, name) + varValues = append(varValues, tv) + } + + // Export values (replace persisted objects with RefValues, etc.) + exported := gno.ExportValues(varValues) + + // Serialize values with Amino JSON. + valuesJSON, err := amino.MarshalJSON(exported) + if err != nil { + return "", fmt.Errorf("failed to marshal values: %w", err) + } + + // Serialize names. + namesJSON, err := amino.MarshalJSON(varNames) + if err != nil { + return "", fmt.Errorf("failed to marshal names: %w", err) + } + + result := fmt.Sprintf(`{"names":%s,"values":%s}`, string(namesJSON), string(valuesJSON)) + return result, nil +} + +// QueryType retrieves a type by TypeID and returns its Amino JSON representation. +// This resolves RefType references in exported values: given a TypeID like +// "gno.land/r/demo/boards.Board", return the full type definition with field names. +func (vm *VMKeeper) QueryType(ctx sdk.Context, tidStr string) (res string, err error) { + ctx = ctx.WithGasMeter(store.NewGasMeter(maxGasQuery)) + gnostore := vm.newGnoTransactionStore(ctx) // throwaway (never committed) + + tid := gno.TypeID(tidStr) + tt := gnostore.GetTypeSafe(tid) + if tt == nil { + return "", ErrInvalidExpr(fmt.Sprintf("type not found: %s", tidStr)) + } + + // Use a custom serializer instead of amino.MarshalJSON to avoid fatal + // stack overflow from circular type references (e.g. time.Time). + var buf bytes.Buffer + marshalTypeJSON(&buf, tt, 0) + + result := fmt.Sprintf(`{"typeid":%q,"type":%s}`, tidStr, buf.String()) + return result, nil +} + +const maxTypeDepth = 8 + +// marshalTypeJSON writes a safe JSON representation of a gno.Type. +// It limits recursion depth to avoid stack overflow from circular references. +func marshalTypeJSON(buf *bytes.Buffer, t gno.Type, depth int) { + if t == nil || depth > maxTypeDepth { + buf.WriteString("null") + return + } + switch ct := t.(type) { + case gno.PrimitiveType: + fmt.Fprintf(buf, `{"@type":"/gno.PrimitiveType","value":"%d"}`, int(ct)) + case *gno.PointerType: + buf.WriteString(`{"@type":"/gno.PointerType","Elt":`) + marshalTypeJSON(buf, ct.Elt, depth+1) + buf.WriteByte('}') + case *gno.ArrayType: + fmt.Fprintf(buf, `{"@type":"/gno.ArrayType","Len":"%d","Elt":`, ct.Len) + marshalTypeJSON(buf, ct.Elt, depth+1) + buf.WriteByte('}') + case *gno.SliceType: + buf.WriteString(`{"@type":"/gno.SliceType","Elt":`) + marshalTypeJSON(buf, ct.Elt, depth+1) + buf.WriteByte('}') + case *gno.StructType: + buf.WriteString(`{"@type":"/gno.StructType","Fields":[`) + for i, f := range ct.Fields { + if i > 0 { + buf.WriteByte(',') + } + fmt.Fprintf(buf, `{"Name":%q,"Type":`, string(f.Name)) + marshalTypeJSON(buf, f.Type, depth+1) + buf.WriteByte('}') + } + buf.WriteString("]}") + case *gno.MapType: + buf.WriteString(`{"@type":"/gno.MapType","Key":`) + marshalTypeJSON(buf, ct.Key, depth+1) + buf.WriteString(`,"Value":`) + marshalTypeJSON(buf, ct.Value, depth+1) + buf.WriteByte('}') + case *gno.FuncType: + buf.WriteString(`{"@type":"/gno.FuncType"}`) + case *gno.InterfaceType: + buf.WriteString(`{"@type":"/gno.InterfaceType"}`) + case *gno.DeclaredType: + fmt.Fprintf(buf, `{"@type":"/gno.DeclaredType","PkgPath":%q,"Name":%q,"Base":`, + ct.PkgPath, string(ct.Name)) + marshalTypeJSON(buf, ct.Base, depth+1) + buf.WriteByte('}') + case *gno.PackageType: + buf.WriteString(`{"@type":"/gno.PackageType"}`) + case *gno.ChanType: + buf.WriteString(`{"@type":"/gno.ChanType","Elt":`) + marshalTypeJSON(buf, ct.Elt, depth+1) + buf.WriteByte('}') + default: + // RefType or unknown — emit type ID if available + fmt.Fprintf(buf, `{"@type":"/gno.RefType","ID":%q}`, t.TypeID()) + } +} + +// resolveBlockNode resolves a BlockNode that may be a RefNode (lazy reference). +func resolveBlockNode(store gno.Store, bn gno.BlockNode) gno.BlockNode { + if bn == nil { + return nil + } + if _, ok := bn.(gno.RefNode); ok { + loc := bn.GetLocation() + return store.GetBlockNodeSafe(loc) + } + return bn +} + +// resolveBlock extracts a *Block from a Value which may be a RefValue. +func resolveBlock(store gno.Store, v gno.Value) *gno.Block { + switch cv := v.(type) { + case *gno.Block: + return cv + case gno.RefValue: + obj := store.GetObject(cv.ObjectID) + if b, ok := obj.(*gno.Block); ok { + return b + } + return nil + default: + return nil + } +} + // processStorageDeposit processes storage deposit adjustments for package realms based on // storage size changes tracked within the gnoStore. // diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index db1ada52591..099f4df7ef3 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -2339,3 +2339,135 @@ func extractNestedRefValueObjectID(t *testing.T, jsonStr string) string { return searchFrom[start : start+end] } + +func TestQueryPkg(t *testing.T) { + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + addr := crypto.AddressFromPreimage([]byte("qpkg-test")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + env.bankk.SetCoins(ctx, addr, std.MustParseCoins(ugnot.ValueString(10_000_000))) + + const pkgPath = "gno.land/r/test/qpkg" + files := []*std.MemFile{ + {Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(pkgPath)}, + { + Name: "pkg.gno", + Body: `package qpkg + +type MyStruct struct { + Name string + Age int +} + +var ( + myInt int = 42 + myStr string = "hello" + myStruct MyStruct +) + +func init() { + myStruct = MyStruct{Name: "Alice", Age: 30} +} + +func Render(path string) string { return "qpkg test" } +`, + }, + } + + msg := NewMsgAddPackage(addr, pkgPath, files) + err := env.vmk.AddPackage(ctx, msg) + require.NoError(t, err) + env.vmk.CommitGnoTransactionStore(ctx) + + // Query via qpkg + res, err := env.vmk.QueryPkg(env.ctx, pkgPath) + require.NoError(t, err) + t.Logf("QueryPkg result:\n%s\n", res) + + // Verify it's valid JSON with names and values + assert.Contains(t, res, `"names"`) + assert.Contains(t, res, `"values"`) + assert.Contains(t, res, `"myInt"`) + assert.Contains(t, res, `"myStr"`) + assert.Contains(t, res, `"myStruct"`) + + // Parse and verify structure + var parsed struct { + Names []string `json:"names"` + Values []json.RawMessage `json:"values"` + } + err = json.Unmarshal([]byte(res), &parsed) + require.NoError(t, err) + assert.Equal(t, len(parsed.Names), len(parsed.Values)) + assert.Contains(t, parsed.Names, "myInt") + assert.Contains(t, parsed.Names, "myStr") + assert.Contains(t, parsed.Names, "myStruct") +} + +func TestQueryPkgNotFound(t *testing.T) { + env := setupTestEnv() + + _, err := env.vmk.QueryPkg(env.ctx, "gno.land/r/nonexistent/pkg") + assert.Error(t, err) +} + +func TestQueryType(t *testing.T) { + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + addr := crypto.AddressFromPreimage([]byte("qtype-test")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + env.bankk.SetCoins(ctx, addr, std.MustParseCoins(ugnot.ValueString(10_000_000))) + + const pkgPath = "gno.land/r/test/qtype" + files := []*std.MemFile{ + {Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(pkgPath)}, + { + Name: "types.gno", + Body: `package qtype + +type Board struct { + Name string + Creator string + Posts int +} + +var board Board + +func init() { + board = Board{Name: "general", Creator: "alice", Posts: 5} +} + +func Render(path string) string { return "qtype test" } +`, + }, + } + + msg := NewMsgAddPackage(addr, pkgPath, files) + err := env.vmk.AddPackage(ctx, msg) + require.NoError(t, err) + env.vmk.CommitGnoTransactionStore(ctx) + + // Query type by TypeID + tidStr := pkgPath + ".Board" + res, err := env.vmk.QueryType(env.ctx, tidStr) + require.NoError(t, err) + t.Logf("QueryType result:\n%s\n", res) + + // Verify it contains the type definition with field names + assert.Contains(t, res, `"typeid"`) + assert.Contains(t, res, tidStr) + assert.Contains(t, res, `"Name"`) + assert.Contains(t, res, `"Creator"`) + assert.Contains(t, res, `"Posts"`) +} + +func TestQueryTypeNotFound(t *testing.T) { + env := setupTestEnv() + + _, err := env.vmk.QueryType(env.ctx, "gno.land/r/nonexistent.FakeType") + assert.Error(t, err) +} diff --git a/misc/gnojs/README.md b/misc/gnojs/README.md new file mode 100644 index 00000000000..bf9a540ed40 --- /dev/null +++ b/misc/gnojs/README.md @@ -0,0 +1,166 @@ +# @gnojs/amino + +JavaScript library for decoding Gno's Amino JSON wire format into clean, UI-friendly data structures. + +Decodes responses from GnoVM's query endpoints (`vm/qpkg_json`, `vm/qobject_json`, `vm/qtype_json`) into typed `StateNode` trees that any frontend can render — tree views, tables, inspectors, CLI tools. + +![Architecture Diagram](diagram.svg) + +## What it does + +GnoVM persists all realm state as `TypedValue` objects, serialized with Amino JSON. The wire format is rich but complex: + +- **Primitives** are stored in a base64-encoded 8-byte `N` field (little-endian), not as plain numbers +- **Strings** are in `V` as `{"@type": "/gno.StringValue", "value": "hello"}` +- **Structs, arrays, maps** are nested `TypedValue` trees with `@type` discriminators +- **Persisted objects** appear as `RefValue{ObjectID}` — must be fetched separately via `qobject_json` +- **Declared types** appear as `RefType{ID}` — field names must be fetched via `qtype_json` +- **Cycles** are broken with `ExportRefValue{":N"}` synthetic references +- **Heap items** wrap values transparently and must be unwrapped + +This library handles all of that and produces flat `StateNode` objects: + +```typescript +interface StateNode { + name: string; // "myVar", "Name", "0", '"key"' + type: string; // "int", "boards.Board", "*string" + kind: string; // "primitive", "struct", "map", "pointer", "func", "closure", ... + value?: string; // "42", '"hello"', "true" + expandable: boolean; // true if children can be loaded + children?: StateNode[]; + objectId?: string; // for lazy-loading via qobject_json + typeId?: string; // for resolving field names via qtype_json + length?: number; // for collections + source?: { file: string; startLine: number; endLine: number }; // for functions +} +``` + +## Usage + +```typescript +import { decodePkg, decodeObject, decodeTypedValue } from "@gnojs/amino"; + +// Decode a qpkg_json response (package entry point) +const pkgResponse = await fetch("/vm/qpkg_json?data=gno.land/r/demo/boards"); +const pkgData = await pkgResponse.json(); +const nodes = decodePkg(pkgData); +// → [{name: "boardTree", type: "avl.Tree", kind: "struct", ...}, ...] + +// Decode a qobject_json response (drill into a persisted object) +const objResponse = await fetch("/vm/qobject_json?data=abc123:5"); +const objData = await objResponse.json(); +const children = decodeObject(objData); +// → [{name: "0", type: "string", kind: "primitive", value: '"hello"'}, ...] + +// Decode a single TypedValue (from any Amino JSON source) +const node = decodeTypedValue("myField", typedValue); +``` + +## Modules + +### `primitives.ts` + +PrimitiveType enum values matching GnoVM's `1 << iota` constants, and the `decodeN()` function for base64 N-field decoding. + +```typescript +import { PrimitiveTypes, decodeN, primitiveTypeName } from "@gnojs/amino"; + +decodeN("KgAAAAAAAAA=", PrimitiveTypes.Int); // → "42" +decodeN("AQAAAAAAAAA=", PrimitiveTypes.Bool); // → "true" +primitiveTypeName(32); // → "int" +``` + +### `type-utils.ts` + +Utilities for working with Amino type descriptors. + +```typescript +import { typeName, typeKind, baseType, structFieldNames } from "@gnojs/amino"; + +typeName({ "@type": "/gno.RefType", ID: "gno.land/r/demo/boards.Board" }); +// → "boards.Board" + +typeKind({ "@type": "/gno.MapType", Key: ..., Value: ... }); +// → "map" + +structFieldNames({ "@type": "/gno.StructType", Fields: [...] }); +// → ["Name", "Creator", "Posts"] +``` + +### `decode.ts` + +Core decoder: `decodePkg()`, `decodeObject()`, `decodeTypedValue()`. + +### `types.ts` + +Full TypeScript type definitions for all Amino JSON wire types — `AminoTypedValue`, `AminoType` (all variants), `AminoValue` (all variants), endpoint response types. + +## The three endpoints + +| Endpoint | Input | Output | Purpose | +|---|---|---|---| +| `vm/qpkg_json` | package path | `{names, values}` | Entry point: all named variables in a package | +| `vm/qobject_json` | ObjectID | `{objectid, value}` | Drill-down: expand a persisted object | +| `vm/qtype_json` | TypeID | `{typeid, type}` | Type resolution: get struct field names | + +### Data flow + +1. **Start** with `qpkg_json` → get all package variables as `TypedValue[]` with names +2. **Structs** with `RefType` in `T` → use `qtype_json` to get field names +3. **Persisted objects** with `RefValue` in `V` → use `qobject_json` to expand children +4. **Repeat** step 2–3 for each level of the tree (lazy loading) + +## Functions and closures + +Functions decode to kind `"func"` with an optional `source` location (file, start/end line). When stored as a `RefValue` at the package level, they are expandable — fetching the object returns the full `FuncValue` with source info. + +Closures are detected by the presence of a non-empty `Captures` array in the `FuncValue` (not by the `IsClosure` boolean, which is unreliable in persisted state). When captures are present, the decoder assigns kind `"closure"` and includes the captured variables as `children`: + +```typescript +const node = decodeTypedValue("stepper", closureTv); +// → { +// name: "stepper", +// type: "func() int", +// kind: "closure", // not "func" +// expandable: true, +// source: { file: "main.gno", startLine: 17, endLine: 20 }, +// children: [ +// { name: "value", type: "heapItemType", expandable: true, objectId: "abc:13" } +// ] +// } +``` + +Each capture is a `TypedValue` with `T: heapItemType` and `V: RefValue` pointing to the heap item holding the captured variable's value. + +## Amino JSON format reference + +A `TypedValue` has three fields: + +```json +{ + "T": { "@type": "/gno.PrimitiveType", "value": "32" }, + "V": { "@type": "/gno.StringValue", "value": "hello" }, + "N": "KgAAAAAAAAA=" +} +``` + +- **T** — Type descriptor with `@type` discriminator. Key types: + - `/gno.PrimitiveType` — `value` is numeric (4=bool, 16=string, 32=int, 1024=int64, ...) + - `/gno.RefType` — `ID` is a TypeID like `"gno.land/r/demo/boards.Board"` + - `/gno.StructType` — `Fields` array with `Name`, `Type`, `Embedded`, `Tag` + - `/gno.PointerType`, `/gno.SliceType`, `/gno.MapType`, `/gno.ArrayType`, ... + +- **V** — Value with `@type` discriminator. Key values: + - `/gno.StringValue` — `value` is the string content + - `/gno.RefValue` — `ObjectID` points to a persisted object (fetch via `qobject_json`) + - `/gno.StructValue` — `Fields` array of nested TypedValues + - `/gno.PointerValue` — `Base` (often RefValue) + `Index` + - `/gno.HeapItemValue` — wrapper, unwrap to get inner `Value` + +- **N** — base64-encoded 8-byte little-endian value for numeric primitives and bools + +## Running tests + +```bash +npx tsx src/decode.test.ts +``` diff --git a/misc/gnojs/diagram.svg b/misc/gnojs/diagram.svg new file mode 100644 index 00000000000..2a2a45ad6f7 --- /dev/null +++ b/misc/gnojs/diagram.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + @gnojs/amino — Data Flow + + + + GnoVM + + + + vm/qpkg_json + + + vm/qobject_json + + + vm/qtype_json + + + entry point + drill-down · type resolve + + + + Amino JSON + + TypedValue { T, V, N } + T: @type discriminator + V: StringValue, RefValue, + StructValue, MapValue… + N: base64 8-byte LE + (primitives) + RefValue → qobject_json + RefType → qtype_json + + + + @gnojs/amino + + decodePkg() + decodeObject() + decodeTypedValue() + decodeN() · typeName() + structFieldNames() + + + + StateNode + + name: "myVar" + type: "boards.Board" + kind: "struct" + value?: "42" + children?: [...] + + + + Consumers + + State Explorer UI + CLI tools + Custom dashboards + + + + + + + + + + + + + + + + + + lazy fetch: RefValue/RefType → drill-down + + + Gno.land State Explorer Architecture + diff --git a/misc/gnojs/package.json b/misc/gnojs/package.json new file mode 100644 index 00000000000..7323ad1324a --- /dev/null +++ b/misc/gnojs/package.json @@ -0,0 +1,11 @@ +{ + "name": "@gnojs/amino", + "version": "0.1.0", + "description": "JavaScript library for decoding Gno Amino JSON (TypedValues, Types, Objects)", + "type": "module", + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "license": "MIT" +} diff --git a/misc/gnojs/src/decode.test.ts b/misc/gnojs/src/decode.test.ts new file mode 100644 index 00000000000..33b0173f48f --- /dev/null +++ b/misc/gnojs/src/decode.test.ts @@ -0,0 +1,356 @@ +// decode.test.ts — Tests for the Amino JSON decoder. +// Uses real output from vm/qpkg_json and vm/qobject_json Go tests. + +import { decodePkg, decodeObject, decodeTypedValue } from "./decode.js"; +import type { QpkgResponse, QobjectResponse } from "./types.js"; + +// ---- Test helpers ---- + +function assert(condition: boolean, msg: string): void { + if (!condition) throw new Error(`FAIL: ${msg}`); +} + +function assertEqual(actual: T, expected: T, msg: string): void { + if (actual !== expected) { + throw new Error(`FAIL: ${msg}\n expected: ${JSON.stringify(expected)}\n actual: ${JSON.stringify(actual)}`); + } +} + +// ---- Test fixtures (real Go test output) ---- + +const qpkgFixture: QpkgResponse = { + names: ["MyStruct", "myInt", "myStr", "myStruct", "init.4", "Render"], + values: [ + { T: { "@type": "/gno.TypeType" }, V: { "@type": "/gno.TypeValue", Type: { "@type": "/gno.DeclaredType", PkgPath: "gno.land/r/test/qpkg", Name: "MyStruct", Base: { "@type": "/gno.StructType", PkgPath: "gno.land/r/test/qpkg", Fields: [{ Name: "Name", Type: { "@type": "/gno.PrimitiveType", value: "16" }, Embedded: false, Tag: "" }, { Name: "Age", Type: { "@type": "/gno.PrimitiveType", value: "32" }, Embedded: false, Tag: "" }] }, Methods: [] } } }, + { T: { "@type": "/gno.PrimitiveType", value: "32" }, N: "KgAAAAAAAAA=" }, + { T: { "@type": "/gno.PrimitiveType", value: "16" }, V: { "@type": "/gno.StringValue", value: "hello" } }, + { T: { "@type": "/gno.RefType", ID: "gno.land/r/test/qpkg.MyStruct" }, V: { "@type": "/gno.RefValue", ObjectID: "715383ba05505afed61caa873216e2ee896bede9:10", Hash: "abc" } }, + { T: { "@type": "/gno.FuncType", Params: [], Results: [] }, V: { "@type": "/gno.RefValue", ObjectID: "715383ba05505afed61caa873216e2ee896bede9:7", Hash: "def" } }, + { T: { "@type": "/gno.FuncType", Params: [{ Name: "path", Type: { "@type": "/gno.PrimitiveType", value: "16" }, Embedded: false, Tag: "" }], Results: [{ Name: ".res.0", Type: { "@type": "/gno.PrimitiveType", value: "16" }, Embedded: false, Tag: "" }] }, V: { "@type": "/gno.RefValue", ObjectID: "715383ba05505afed61caa873216e2ee896bede9:9", Hash: "ghi" } }, + ], +}; + +// ---- Tests ---- + +function testDecodePkg(): void { + const nodes = decodePkg(qpkgFixture); + assertEqual(nodes.length, 6, "should decode 6 nodes"); + + // MyStruct — TypeValue + assertEqual(nodes[0].name, "MyStruct", "first node name"); + assertEqual(nodes[0].kind, "type", "TypeValue kind"); + assert(nodes[0].value !== undefined, "TypeValue should have value"); + + // myInt — int = 42 (N: "KgAAAAAAAAA=" is base64 of 42 as int64 LE) + assertEqual(nodes[1].name, "myInt", "second node name"); + assertEqual(nodes[1].type, "int", "myInt type"); + assertEqual(nodes[1].kind, "primitive", "myInt kind"); + assertEqual(nodes[1].value, "42", "myInt value decoded from N"); + + // myStr — string = "hello" + assertEqual(nodes[2].name, "myStr", "third node name"); + assertEqual(nodes[2].type, "string", "myStr type"); + assertEqual(nodes[2].kind, "primitive", "myStr kind"); + assertEqual(nodes[2].value, '"hello"', "myStr value"); + + // myStruct — RefValue (persisted struct) + assertEqual(nodes[3].name, "myStruct", "fourth node name"); + assertEqual(nodes[3].kind, "ref", "myStruct kind is ref (RefType)"); + assert(nodes[3].expandable, "myStruct should be expandable"); + assertEqual(nodes[3].objectId, "715383ba05505afed61caa873216e2ee896bede9:10", "myStruct objectId"); + assertEqual(nodes[3].typeId, "gno.land/r/test/qpkg.MyStruct", "myStruct typeId"); + + // init.4 — func (RefValue) + assertEqual(nodes[4].name, "init.4", "fifth node name"); + // Func values stored as RefValue are not expandable in a useful way + // but the kind comes from FuncType + + // Render — func (RefValue) + assertEqual(nodes[5].name, "Render", "sixth node name"); + + console.log(" testDecodePkg: PASS"); +} + +function testDecodeObject(): void { + // Simulate qobject_json response for a StructValue + const fixture: QobjectResponse = { + objectid: "abc123:8", + value: { + "@type": "/gno.StructValue", + ObjectInfo: { ID: "abc123:8" }, + Fields: [ + { T: { "@type": "/gno.PrimitiveType", value: "32" }, N: "AQAAAAAAAAA=" }, + { T: { "@type": "/gno.PrimitiveType", value: "16" }, V: { "@type": "/gno.StringValue", value: "test" } }, + ], + }, + }; + + const nodes = decodeObject(fixture); + assertEqual(nodes.length, 2, "should decode 2 fields"); + + // Field 0: int = 1 + assertEqual(nodes[0].name, "0", "field names are indices from qobject"); + assertEqual(nodes[0].type, "int", "field 0 type"); + assertEqual(nodes[0].value, "1", "field 0 value"); + + // Field 1: string = "test" + assertEqual(nodes[1].name, "1", "field 1 name"); + assertEqual(nodes[1].value, '"test"', "field 1 value"); + + console.log(" testDecodeObject: PASS"); +} + +function testDecodeHeapItem(): void { + // HeapItemValue should be unwrapped transparently + const node = decodeTypedValue("ptr", { + T: { "@type": "/gno.PointerType", Elt: { "@type": "/gno.PrimitiveType", value: "32" } }, + V: { + "@type": "/gno.HeapItemValue", + Value: { + T: { "@type": "/gno.PrimitiveType", value: "32" }, + N: "BwAAAAAAAAA=", // 7 + }, + }, + }); + + // HeapItemValue is unwrapped, so we get the inner primitive + assertEqual(node.name, "ptr", "heap item preserves name"); + assertEqual(node.value, "7", "heap item unwraps to inner value"); + assertEqual(node.kind, "primitive", "heap item unwraps to primitive kind"); + + console.log(" testDecodeHeapItem: PASS"); +} + +function testDecodeMap(): void { + const node = decodeTypedValue("myMap", { + T: { "@type": "/gno.MapType", Key: { "@type": "/gno.PrimitiveType", value: "16" }, Value: { "@type": "/gno.PrimitiveType", value: "32" } }, + V: { + "@type": "/gno.MapValue", + List: { + List: [ + { + Key: { T: { "@type": "/gno.PrimitiveType", value: "16" }, V: { "@type": "/gno.StringValue", value: "alice" } }, + Value: { T: { "@type": "/gno.PrimitiveType", value: "32" }, N: "ZAAAAAAAAAA=" }, // 100 + }, + ], + }, + }, + }); + + assertEqual(node.kind, "map", "map kind"); + assertEqual(node.length, 1, "map length"); + assert(node.children !== undefined && node.children.length === 1, "map should have 1 child"); + assertEqual(node.children![0].name, '"alice"', "map key as child name"); + assertEqual(node.children![0].value, "100", "map value"); + + console.log(" testDecodeMap: PASS"); +} + +function testDecodePointerRefValue(): void { + const node = decodeTypedValue("ptr", { + T: { "@type": "/gno.PointerType", Elt: { "@type": "/gno.RefType", ID: "gno.land/r/test.Foo" } }, + V: { + "@type": "/gno.PointerValue", + TV: null, + Base: { "@type": "/gno.RefValue", ObjectID: "abc:5", Hash: "xyz" }, + Index: "0", + }, + }); + + assertEqual(node.kind, "pointer", "pointer kind"); + assert(node.expandable, "pointer with RefValue base should be expandable"); + assertEqual(node.objectId, "abc:5", "pointer objectId from Base"); + + console.log(" testDecodePointerRefValue: PASS"); +} + +function testDecodeInlineStruct(): void { + const node = decodeTypedValue("s", { + T: { "@type": "/gno.StructType", PkgPath: "gno.land/r/test", Fields: [ + { Name: "X", Type: { "@type": "/gno.PrimitiveType", value: "32" }, Embedded: false, Tag: "" }, + { Name: "Y", Type: { "@type": "/gno.PrimitiveType", value: "16" }, Embedded: false, Tag: "" }, + ]}, + V: { + "@type": "/gno.StructValue", + Fields: [ + { T: { "@type": "/gno.PrimitiveType", value: "32" }, N: "CQAAAAAAAAA=" }, // 9 + { T: { "@type": "/gno.PrimitiveType", value: "16" }, V: { "@type": "/gno.StringValue", value: "hi" } }, + ], + }, + }); + + assertEqual(node.kind, "struct", "struct kind"); + assertEqual(node.length, 2, "struct length"); + assert(node.children !== undefined && node.children.length === 2, "struct has 2 children"); + assertEqual(node.children![0].name, "X", "field name from StructType"); + assertEqual(node.children![0].value, "9", "field X value"); + assertEqual(node.children![1].name, "Y", "field name Y"); + assertEqual(node.children![1].value, '"hi"', "field Y value"); + + console.log(" testDecodeInlineStruct: PASS"); +} + +function testDecodeCycleRef(): void { + const node = decodeTypedValue("self", { + T: { "@type": "/gno.PointerType", Elt: { "@type": "/gno.RefType", ID: "gno.land/r/test.Node" } }, + V: { "@type": "/gno.ExportRefValue", ObjectID: ":1" }, + }); + + assertEqual(node.kind, "pointer", "cycle ref kind"); + assertEqual(node.value, "", "cycle ref value"); + assert(!node.expandable, "cycle ref should not be expandable"); + + console.log(" testDecodeCycleRef: PASS"); +} + +function testDecodeNil(): void { + const node = decodeTypedValue("x", {}); + assertEqual(node.kind, "nil", "nil kind"); + assertEqual(node.value, "nil", "nil value"); + + console.log(" testDecodeNil: PASS"); +} + +function testDecodeSliceRefBase(): void { + const node = decodeTypedValue("items", { + T: { "@type": "/gno.SliceType", Elt: { "@type": "/gno.PrimitiveType", value: "32" }, Vrd: false }, + V: { + "@type": "/gno.SliceValue", + Base: { "@type": "/gno.RefValue", ObjectID: "abc:3", Hash: "def" }, + Offset: "0", + Length: "5", + Maxcap: "8", + }, + }); + + assertEqual(node.kind, "slice", "slice kind"); + assertEqual(node.length, 5, "slice length"); + assert(node.expandable, "slice with RefValue base should be expandable"); + assertEqual(node.objectId, "abc:3", "slice objectId from Base"); + + console.log(" testDecodeSliceRefBase: PASS"); +} + +function testDecodeFuncInline(): void { + const node = decodeTypedValue("myFunc", { + T: { "@type": "/gno.FuncType", Params: [{ Name: "x", Type: { "@type": "/gno.PrimitiveType", value: "32" }, Embedded: false, Tag: "" }], Results: [{ Name: ".res.0", Type: { "@type": "/gno.PrimitiveType", value: "32" }, Embedded: false, Tag: "" }] }, + V: { + "@type": "/gno.FuncValue", + Type: { "@type": "/gno.FuncType", Params: [{ Name: "x", Type: { "@type": "/gno.PrimitiveType", value: "32" }, Embedded: false, Tag: "" }], Results: [{ Name: ".res.0", Type: { "@type": "/gno.PrimitiveType", value: "32" }, Embedded: false, Tag: "" }] }, + Name: "myFunc", + Source: { "@type": "/gno.RefNode", Location: { PkgPath: "gno.land/r/test", File: "test.gno", Span: { Pos: { Line: "5", Column: "1" }, End: { Line: "7", Column: "1" }, Num: "0" } }, BlockNode: null }, + }, + }); + + assertEqual(node.kind, "func", "inline func kind"); + assert(node.source !== undefined, "inline func should have source"); + assertEqual(node.source!.file, "test.gno", "func source file"); + assertEqual(node.source!.startLine, 5, "func source start line"); + + console.log(" testDecodeFuncInline: PASS"); +} + +function testDecodeFuncRefValue(): void { + // Func stored as RefValue (top-level package func) — expandable to show source + const node = decodeTypedValue("Render", { + T: { "@type": "/gno.FuncType", Params: [{ Name: "path", Type: { "@type": "/gno.PrimitiveType", value: "16" }, Embedded: false, Tag: "" }], Results: [{ Name: ".res.0", Type: { "@type": "/gno.PrimitiveType", value: "16" }, Embedded: false, Tag: "" }] }, + V: { "@type": "/gno.RefValue", ObjectID: "abc:9", Hash: "xyz" }, + }); + + assertEqual(node.kind, "func", "func ref kind"); + assert(node.expandable, "func ref should be expandable"); + assertEqual(node.objectId, "abc:9", "func ref objectId"); + assert(node.type.includes("func("), "func type should include signature"); + + console.log(" testDecodeFuncRefValue: PASS"); +} + +function testDecodeClosureWithCaptures(): void { + // Closure with captures — the FuncValue has Captures field + const node = decodeTypedValue("stepper", { + T: { "@type": "/gno.FuncType", Params: [], Results: [{ Name: ".res.0", Type: { "@type": "/gno.PrimitiveType", value: "32" }, Embedded: false, Tag: "" }] }, + V: { + "@type": "/gno.FuncValue", + Type: { "@type": "/gno.FuncType", Params: [], Results: [{ Name: ".res.0", Type: { "@type": "/gno.PrimitiveType", value: "32" }, Embedded: false, Tag: "" }] }, + Name: "", + IsClosure: false, + Captures: [ + { T: { "@type": "/gno.heapItemType" }, V: { "@type": "/gno.RefValue", ObjectID: "abc:13", Hash: "def" } }, + ], + Source: { "@type": "/gno.RefNode", Location: { PkgPath: "gno.land/r/test", File: "test.gno", Span: { Pos: { Line: "17", Column: "12" }, End: { Line: "20", Column: "3" }, Num: "0" } }, BlockNode: null }, + }, + }); + + assertEqual(node.kind, "closure", "closure kind"); + assert(node.expandable, "closure should be expandable"); + assert(node.source !== undefined, "closure should have source"); + assert(node.children !== undefined, "closure should have children from captures"); + assertEqual(node.children!.length, 1, "closure should have 1 capture"); + assertEqual(node.children![0].name, "value", "capture child name"); + assert(node.children![0].expandable, "capture with RefValue should be expandable"); + assertEqual(node.children![0].objectId, "abc:13", "capture objectId"); + + console.log(" testDecodeClosureWithCaptures: PASS"); +} + +function testDecodeFuncNoCapturesNotClosure(): void { + // Regular func with no captures — should be "func" not "closure" + const node = decodeTypedValue("init", { + T: { "@type": "/gno.FuncType", Params: [], Results: [] }, + V: { + "@type": "/gno.FuncValue", + Type: { "@type": "/gno.FuncType", Params: [], Results: [] }, + Name: "init", + Captures: [], + Source: { "@type": "/gno.RefNode", Location: { PkgPath: "gno.land/r/test", File: "test.gno", Span: { Pos: { Line: "1", Column: "1" }, End: { Line: "3", Column: "1" }, Num: "0" } }, BlockNode: null }, + }, + }); + + assertEqual(node.kind, "func", "func without captures is not closure"); + assert(node.children === undefined || node.children.length === 0, "no children for non-closure"); + + console.log(" testDecodeFuncNoCapturesNotClosure: PASS"); +} + +function testDecodeClosureMultipleCaptures(): void { + // Closure with multiple captures + const node = decodeTypedValue("accumulator", { + T: { "@type": "/gno.FuncType", Params: [{ Name: "val", Type: { "@type": "/gno.PrimitiveType", value: "32" }, Embedded: false, Tag: "" }], Results: [] }, + V: { + "@type": "/gno.FuncValue", + Type: { "@type": "/gno.FuncType", Params: [{ Name: "val", Type: { "@type": "/gno.PrimitiveType", value: "32" }, Embedded: false, Tag: "" }], Results: [] }, + Name: "", + Captures: [ + { T: { "@type": "/gno.heapItemType" }, V: { "@type": "/gno.RefValue", ObjectID: "abc:16", Hash: "aaa" } }, + { T: { "@type": "/gno.heapItemType" }, V: { "@type": "/gno.RefValue", ObjectID: "abc:17", Hash: "bbb" } }, + ], + Source: { "@type": "/gno.RefNode", Location: { PkgPath: "gno.land/r/test", File: "test.gno", Span: { Pos: { Line: "23", Column: "16" }, End: { Line: "25", Column: "3" }, Num: "0" } }, BlockNode: null }, + }, + }); + + assertEqual(node.kind, "closure", "multi-capture closure kind"); + assertEqual(node.children!.length, 2, "should have 2 captures"); + assertEqual(node.children![0].objectId, "abc:16", "first capture objectId"); + assertEqual(node.children![1].objectId, "abc:17", "second capture objectId"); + + console.log(" testDecodeClosureMultipleCaptures: PASS"); +} + +// ---- Run all tests ---- + +console.log("decode.test.ts:"); +testDecodePkg(); +testDecodeObject(); +testDecodeHeapItem(); +testDecodeMap(); +testDecodePointerRefValue(); +testDecodeInlineStruct(); +testDecodeCycleRef(); +testDecodeNil(); +testDecodeSliceRefBase(); +testDecodeFuncInline(); +testDecodeFuncRefValue(); +testDecodeClosureWithCaptures(); +testDecodeFuncNoCapturesNotClosure(); +testDecodeClosureMultipleCaptures(); +console.log("All tests passed."); diff --git a/misc/gnojs/src/decode.ts b/misc/gnojs/src/decode.ts new file mode 100644 index 00000000000..df3b0c3d8f5 --- /dev/null +++ b/misc/gnojs/src/decode.ts @@ -0,0 +1,344 @@ +// decode.ts — Decode Amino JSON TypedValues into a flat, UI-friendly format. +// +// This is the core decoder: it takes raw Amino JSON (as returned by +// vm/qpkg_json, vm/qobject_json) and produces StateNode trees that +// any UI can render (tree view, table, inspector, etc.). + +import type { + AminoTypedValue, + AminoType, + AminoValue, + AminoStructValue, + AminoArrayValue, + AminoSliceValue, + AminoPointerValue, + AminoMapValue, + AminoFuncValue, + AminoHeapItemValue, + AminoTypeValue, + AminoRefValue, + AminoExportRefValue, + AminoBlockValue, + AminoStringValue, + QpkgResponse, + QobjectResponse, +} from "./types.js"; +import { PrimitiveTypes, decodeN } from "./primitives.js"; +import { typeName, typeKind, baseType, structFieldNames, getTypeId, funcSignature } from "./type-utils.js"; + +// ---- Output model ---- + +/** A decoded node suitable for rendering in any UI. */ +export interface StateNode { + /** Display name (variable name, field name, map key, index). */ + name: string; + /** Human-readable type name. */ + type: string; + /** Simplified kind: struct, map, array, slice, pointer, func, primitive, nil, type, package. */ + kind: string; + /** Display value for leaf nodes. */ + value?: string; + /** Whether this node can be expanded (has or can fetch children). */ + expandable: boolean; + /** Inline children (already decoded). */ + children?: StateNode[]; + /** ObjectID for lazy-loading via vm/qobject_json. */ + objectId?: string; + /** TypeID (RefType.ID) for resolving struct field names via vm/qtype_json. */ + typeId?: string; + /** Length for collections. */ + length?: number; + /** Source location for functions. */ + source?: { file: string; startLine: number; endLine: number }; +} + +// ---- Decoder ---- + +/** Decode a qpkg_json response into StateNodes. */ +export function decodePkg(data: QpkgResponse): StateNode[] { + const nodes: StateNode[] = []; + for (let i = 0; i < data.names.length; i++) { + const tv = data.values[i]; + if (!tv) continue; + nodes.push(decodeTypedValue(data.names[i], tv)); + } + return nodes; +} + +/** Decode a qobject_json response into child StateNodes. */ +export function decodeObject(data: QobjectResponse): StateNode[] { + return decodeValueChildren(data.value); +} + +/** Decode a single AminoTypedValue into a StateNode. */ +export function decodeTypedValue(name: string, tv: AminoTypedValue): StateNode { + const t = tv.T; + const v = tv.V; + const n = tv.N; + + if (!t) { + return { name, type: "", kind: "nil", value: "nil", expandable: false }; + } + + const tName = typeName(t); + const kind = typeKind(t); + const bt = baseType(t); + const typeId = getTypeId(t); + + // ---- FuncType stored as RefValue: expandable to show source ---- + if (kind === "func" && v && v["@type"] === "/gno.RefValue") { + const rv = v as AminoRefValue; + const sig = funcSignature(t); + return { name, type: sig, kind: "func", expandable: true, objectId: rv.ObjectID }; + } + + // ---- RefValue: persisted object reference ---- + if (v && v["@type"] === "/gno.RefValue") { + const rv = v as AminoRefValue; + if (rv.PkgPath) { + return { name, type: tName, kind: "package", value: rv.PkgPath, expandable: false }; + } + return { name, type: tName, kind, expandable: true, objectId: rv.ObjectID, typeId }; + } + + // ---- ExportRefValue: cycle-breaking reference ---- + if (v && v["@type"] === "/gno.ExportRefValue") { + const erv = v as AminoExportRefValue; + return { name, type: tName, kind, value: ``, expandable: false }; + } + + // ---- HeapItemValue: unwrap transparently ---- + if (v && v["@type"] === "/gno.HeapItemValue") { + const hiv = v as AminoHeapItemValue; + return decodeTypedValue(name, hiv.Value); + } + + // ---- TypeValue: type definition as a value ---- + if (v && v["@type"] === "/gno.TypeValue") { + const tv2 = v as AminoTypeValue; + return { name, type: "type", kind: "type", value: typeName(tv2.Type), expandable: false }; + } + + // ---- Primitives ---- + if (bt && bt["@type"] === "/gno.PrimitiveType") { + const primVal = parseInt(bt.value); + // String: value in V + if ((primVal === PrimitiveTypes.String || primVal === PrimitiveTypes.UntypedString) + && v && v["@type"] === "/gno.StringValue") { + const s = (v as AminoStringValue).value; + const display = s.length > 256 + ? JSON.stringify(s.substring(0, 256)) + "..." + : JSON.stringify(s); + return { name, type: tName, kind: "primitive", value: display, expandable: false }; + } + // Numeric/bool: value in N + if (n) { + return { name, type: tName, kind: "primitive", value: decodeN(n, primVal), expandable: false }; + } + // Zero value + if (primVal === PrimitiveTypes.Bool || primVal === PrimitiveTypes.UntypedBool) { + return { name, type: tName, kind: "primitive", value: "false", expandable: false }; + } + if (primVal === PrimitiveTypes.String || primVal === PrimitiveTypes.UntypedString) { + return { name, type: tName, kind: "primitive", value: '""', expandable: false }; + } + return { name, type: tName, kind: "primitive", value: "0", expandable: false }; + } + + // ---- Struct (inline) ---- + if (v && v["@type"] === "/gno.StructValue") { + const sv = v as AminoStructValue; + const objectId = sv.ObjectInfo?.ID; + const fieldNames = structFieldNames(bt); + const children = sv.Fields.map((ftv, i) => { + const fname = fieldNames && i < fieldNames.length ? fieldNames[i] : String(i); + return decodeTypedValue(fname, ftv); + }); + return { + name, type: tName, kind: "struct", expandable: children.length > 0, + children, objectId, typeId, length: sv.Fields.length, + }; + } + + // ---- Array (inline) ---- + if (v && v["@type"] === "/gno.ArrayValue") { + const av = v as AminoArrayValue; + const objectId = av.ObjectInfo?.ID; + if (av.Data) { + const len = atob(av.Data).length; + return { name, type: tName, kind: "array", value: `[${len}]byte{...}`, expandable: false, objectId, length: len }; + } + const list = av.List || []; + const children = list.map((etv, i) => decodeTypedValue(String(i), etv)); + return { + name, type: tName, kind: "array", expandable: list.length > 0, + children, objectId, length: list.length, + }; + } + + // ---- Slice ---- + if (v && v["@type"] === "/gno.SliceValue") { + const sv = v as AminoSliceValue; + const length = parseInt(sv.Length) || 0; + // Base is a RefValue — lazy + if (sv.Base && sv.Base["@type"] === "/gno.RefValue") { + const rv = sv.Base as AminoRefValue; + return { name, type: tName, kind: "slice", expandable: length > 0, objectId: rv.ObjectID, length }; + } + // Base is inline ArrayValue + if (sv.Base && sv.Base["@type"] === "/gno.ArrayValue") { + const av = sv.Base as AminoArrayValue; + const offset = parseInt(sv.Offset) || 0; + if (av.Data) { + return { name, type: tName, kind: "slice", value: `[]byte (len=${length})`, expandable: false, length }; + } + const list = (av.List || []).slice(offset, offset + length); + const children = list.map((etv, i) => decodeTypedValue(String(i), etv)); + return { name, type: tName, kind: "slice", expandable: children.length > 0, children, length }; + } + return { name, type: tName, kind: "slice", expandable: length > 0, length }; + } + + // ---- Map (inline) ---- + if (v && v["@type"] === "/gno.MapValue") { + const mv = v as AminoMapValue; + const objectId = mv.ObjectInfo?.ID; + const items = mv.List?.List || []; + const children = items.map(item => { + const keyStr = previewTypedValue(item.Key); + return decodeTypedValue(keyStr, item.Value); + }); + return { + name, type: tName, kind: "map", expandable: children.length > 0, + children, objectId, length: items.length, + }; + } + + // ---- Pointer (inline) ---- + if (v && v["@type"] === "/gno.PointerValue") { + const pv = v as AminoPointerValue; + if (pv.Base && pv.Base["@type"] === "/gno.RefValue") { + const rv = pv.Base as AminoRefValue; + return { name, type: tName, kind: "pointer", expandable: true, objectId: rv.ObjectID }; + } + if (pv.TV) { + const child = decodeTypedValue("*", pv.TV); + return { name, type: tName, kind: "pointer", expandable: true, children: [child] }; + } + return { name, type: tName, kind: "pointer", value: "nil", expandable: false }; + } + + // ---- Func / Closure ---- + if (v && v["@type"] === "/gno.FuncValue") { + const fv = v as AminoFuncValue; + const sig = fv.Type ? funcSignature(fv.Type) : `func ${fv.Name}()`; + const source = extractFuncSource(fv); + const hasCaps = fv.Captures && fv.Captures.length > 0; + const kind = hasCaps ? "closure" : "func"; + + // Decode closure captures as inline children + if (hasCaps) { + const children = fv.Captures.map((cap, i) => + decodeTypedValue(`value`, cap), + ); + return { name, type: sig, kind, expandable: true, source, children }; + } + + return { name, type: sig, kind, expandable: !!source, source }; + } + + // ---- Zero value (type but no value) ---- + if (!v && !n) { + return { name, type: tName, kind, value: "", expandable: false }; + } + + // ---- Fallback ---- + return { name, type: tName, kind, value: v ? `<${v["@type"]}>` : "", expandable: false }; +} + +// ---- Internal helpers ---- + +/** Extract source location from a FuncValue. */ +function extractFuncSource(fv: AminoFuncValue): StateNode["source"] | undefined { + const loc = fv.Source?.Location; + if (!loc?.File || !loc?.Span) return undefined; + return { + file: loc.File, + startLine: parseInt(loc.Span.Pos.Line) || 0, + endLine: parseInt(loc.Span.End.Line) || 0, + }; +} + +/** Decode a FuncValue (from qobject_json) into a StateNode with source info. */ +export function decodeFuncObject(v: AminoFuncValue): StateNode { + const sig = v.Type ? funcSignature(v.Type) : `func ${v.Name}()`; + const source = extractFuncSource(v); + const hasCaps = v.Captures && v.Captures.length > 0; + const kind = hasCaps ? "closure" : "func"; + + if (hasCaps) { + const children = v.Captures.map((cap, i) => + decodeTypedValue(`value`, cap), + ); + return { name: v.Name, type: sig, kind, expandable: true, source, children }; + } + + return { name: v.Name, type: sig, kind, expandable: false, source }; +} + +/** Decode the children of a raw Amino Value (from qobject_json). */ +function decodeValueChildren(v: AminoValue): StateNode[] { + if (!v) return []; + + switch (v["@type"]) { + case "/gno.StructValue": { + const sv = v as AminoStructValue; + return sv.Fields.map((ftv, i) => decodeTypedValue(String(i), ftv)); + } + case "/gno.ArrayValue": { + const av = v as AminoArrayValue; + if (av.Data) { + return [{ name: "data", type: "[]byte", kind: "primitive", value: `[${atob(av.Data).length}]byte{...}`, expandable: false }]; + } + return (av.List || []).map((etv, i) => decodeTypedValue(String(i), etv)); + } + case "/gno.MapValue": { + const mv = v as AminoMapValue; + return (mv.List?.List || []).map(item => { + const keyStr = previewTypedValue(item.Key); + return decodeTypedValue(keyStr, item.Value); + }); + } + case "/gno.HeapItemValue": { + const hiv = v as AminoHeapItemValue; + return [decodeTypedValue("value", hiv.Value)]; + } + case "/gno.Block": { + const block = v as AminoBlockValue; + return (block.Values || []).map((tv, i) => decodeTypedValue(String(i), tv)); + } + default: + return []; + } +} + +/** Short preview string for a TypedValue (used for map keys). */ +function previewTypedValue(tv: AminoTypedValue): string { + const t = tv.T; + const v = tv.V; + const n = tv.N; + + if (!t) return "nil"; + + const bt = baseType(t); + if (bt && bt["@type"] === "/gno.PrimitiveType") { + const primVal = parseInt(bt.value); + if ((primVal === PrimitiveTypes.String || primVal === PrimitiveTypes.UntypedString) + && v && v["@type"] === "/gno.StringValue") { + const s = (v as AminoStringValue).value; + return s.length > 64 ? JSON.stringify(s.substring(0, 64)) + "..." : JSON.stringify(s); + } + if (n) return decodeN(n, primVal); + } + return typeName(t); +} diff --git a/misc/gnojs/src/index.ts b/misc/gnojs/src/index.ts new file mode 100644 index 00000000000..f27770151cf --- /dev/null +++ b/misc/gnojs/src/index.ts @@ -0,0 +1,44 @@ +// @gnojs/amino — JavaScript library for decoding Gno Amino JSON. +// +// Decodes the wire format from vm/qpkg_json, vm/qobject_json, vm/qtype_json +// into clean, UI-friendly StateNode trees. + +export { PrimitiveTypes, primitiveTypeName, decodeN } from "./primitives.js"; +export { typeName, typeKind, baseType, structFieldNames, getTypeId, isPrimitive, funcSignature } from "./type-utils.js"; +export { decodePkg, decodeObject, decodeTypedValue, decodeFuncObject } from "./decode.js"; +export type { StateNode } from "./decode.js"; +export type { + AminoTypedValue, + AminoType, + AminoValue, + AminoFieldType, + AminoPrimitiveType, + AminoPointerType, + AminoArrayType, + AminoSliceType, + AminoStructType, + AminoMapType, + AminoFuncType, + AminoInterfaceType, + AminoRefType, + AminoDeclaredType, + AminoTypeType, + AminoPackageType, + AminoChanType, + AminoStringValue, + AminoRefValue, + AminoObjectInfo, + AminoStructValue, + AminoArrayValue, + AminoSliceValue, + AminoPointerValue, + AminoMapValue, + AminoFuncValue, + AminoHeapItemValue, + AminoTypeValue, + AminoExportRefValue, + AminoBlockValue, + QpkgResponse, + QobjectResponse, + QtypeResponse, +} from "./types.js"; diff --git a/misc/gnojs/src/primitives.ts b/misc/gnojs/src/primitives.ts new file mode 100644 index 00000000000..3894ee036fd --- /dev/null +++ b/misc/gnojs/src/primitives.ts @@ -0,0 +1,137 @@ +// primitives.ts — PrimitiveType enum values and N-field decoder. +// +// GnoVM PrimitiveType uses `1 << iota` starting from InvalidType. +// The N field in TypedValue is an 8-byte little-endian value, base64-encoded. + +export const PrimitiveTypes = { + Invalid: 1, + UntypedBool: 2, + Bool: 4, + UntypedString: 8, + String: 16, + Int: 32, + Int8: 64, + Int16: 128, + UntypedRune: 256, + Int32: 512, + Int64: 1024, + Uint: 2048, + Uint8: 4096, + DataByte: 8192, + Uint16: 16384, + Uint32: 32768, + Uint64: 65536, + Float32: 131072, + Float64: 262144, + UntypedBigint: 524288, + UntypedBigdec: 1048576, +} as const; + +export type PrimitiveTypeValue = typeof PrimitiveTypes[keyof typeof PrimitiveTypes]; + +const primNames: Record = { + [PrimitiveTypes.Bool]: "bool", + [PrimitiveTypes.UntypedBool]: "untyped bool", + [PrimitiveTypes.String]: "string", + [PrimitiveTypes.UntypedString]: "untyped string", + [PrimitiveTypes.Int]: "int", + [PrimitiveTypes.Int8]: "int8", + [PrimitiveTypes.Int16]: "int16", + [PrimitiveTypes.UntypedRune]: "rune", + [PrimitiveTypes.Int32]: "int32", + [PrimitiveTypes.Int64]: "int64", + [PrimitiveTypes.Uint]: "uint", + [PrimitiveTypes.Uint8]: "uint8", + [PrimitiveTypes.DataByte]: "databyte", + [PrimitiveTypes.Uint16]: "uint16", + [PrimitiveTypes.Uint32]: "uint32", + [PrimitiveTypes.Uint64]: "uint64", + [PrimitiveTypes.Float32]: "float32", + [PrimitiveTypes.Float64]: "float64", + [PrimitiveTypes.UntypedBigint]: "untyped bigint", + [PrimitiveTypes.UntypedBigdec]: "untyped bigdec", +}; + +/** Returns the Go-style name for a PrimitiveType numeric value. */ +export function primitiveTypeName(value: number): string { + return primNames[value] ?? `prim(${value})`; +} + +/** True if this primitive stores its value in the N field (not V). */ +export function isNFieldPrimitive(value: number): boolean { + return value !== PrimitiveTypes.String + && value !== PrimitiveTypes.UntypedString + && value !== PrimitiveTypes.UntypedBigint + && value !== PrimitiveTypes.UntypedBigdec; +} + +/** True if this is a signed integer type. */ +export function isSignedInt(value: number): boolean { + return value === PrimitiveTypes.Int + || value === PrimitiveTypes.Int8 + || value === PrimitiveTypes.Int16 + || value === PrimitiveTypes.Int32 + || value === PrimitiveTypes.Int64 + || value === PrimitiveTypes.UntypedRune; +} + +/** + * Decode the base64-encoded N field into a human-readable string. + * N is an 8-byte little-endian value. + */ +export function decodeN(base64: string, primValue: number): string { + const raw = atob(base64); + const buf = new Uint8Array(8); + for (let i = 0; i < raw.length && i < 8; i++) { + buf[i] = raw.charCodeAt(i); + } + const view = new DataView(buf.buffer); + + switch (primValue) { + case PrimitiveTypes.Bool: + case PrimitiveTypes.UntypedBool: + return buf[0] !== 0 ? "true" : "false"; + + case PrimitiveTypes.Int: + case PrimitiveTypes.Int64: + return view.getBigInt64(0, true).toString(); + + case PrimitiveTypes.Int8: + return view.getInt8(0).toString(); + + case PrimitiveTypes.Int16: + return view.getInt16(0, true).toString(); + + case PrimitiveTypes.UntypedRune: + case PrimitiveTypes.Int32: + return view.getInt32(0, true).toString(); + + case PrimitiveTypes.Uint: + case PrimitiveTypes.Uint64: + return view.getBigUint64(0, true).toString(); + + case PrimitiveTypes.Uint8: + case PrimitiveTypes.DataByte: + return buf[0].toString(); + + case PrimitiveTypes.Uint16: + return view.getUint16(0, true).toString(); + + case PrimitiveTypes.Uint32: + return view.getUint32(0, true).toString(); + + case PrimitiveTypes.Float32: { + // N stores the raw bits as uint32 LE + const bits = view.getUint32(0, true); + const f32buf = new DataView(new ArrayBuffer(4)); + f32buf.setUint32(0, bits); + return f32buf.getFloat32(0).toString(); + } + + case PrimitiveTypes.Float64: + return view.getFloat64(0, true).toString(); + + default: + return ``; + } +} diff --git a/misc/gnojs/src/type-utils.ts b/misc/gnojs/src/type-utils.ts new file mode 100644 index 00000000000..5d8e2ba5914 --- /dev/null +++ b/misc/gnojs/src/type-utils.ts @@ -0,0 +1,132 @@ +// type-utils.ts — Utilities for working with Amino-encoded Gno types. + +import type { + AminoType, + AminoFieldType, + AminoPrimitiveType, + AminoDeclaredType, + AminoStructType, +} from "./types.js"; +import { primitiveTypeName } from "./primitives.js"; + +/** Returns a human-readable display name for a type. */ +export function typeName(t: AminoType | undefined): string { + if (!t) return ""; + switch (t["@type"]) { + case "/gno.PrimitiveType": + return primitiveTypeName(parseInt(t.value)); + case "/gno.PointerType": + return "*" + typeName(t.Elt); + case "/gno.ArrayType": + return `[${t.Len}]${typeName(t.Elt)}`; + case "/gno.SliceType": + return "[]" + typeName(t.Elt); + case "/gno.MapType": + return `map[${typeName(t.Key)}]${typeName(t.Value)}`; + case "/gno.StructType": + return "struct{...}"; + case "/gno.FuncType": + return "func(...)"; + case "/gno.InterfaceType": + return "interface{...}"; + case "/gno.RefType": { + const id = t.ID; + const dot = id.lastIndexOf("."); + if (dot >= 0) { + const pkgPath = id.substring(0, dot); + const parts = pkgPath.split("/"); + return parts[parts.length - 1] + id.substring(dot); + } + return id; + } + case "/gno.DeclaredType": { + const parts = (t.PkgPath || "").split("/"); + return parts[parts.length - 1] + "." + t.Name; + } + case "/gno.TypeType": + return "type"; + case "/gno.PackageType": + return "package"; + case "/gno.ChanType": + return `chan ${typeName(t.Elt)}`; + default: + return t["@type"].replace("/gno.", ""); + } +} + +/** Returns a simplified kind string for a type. */ +export function typeKind(t: AminoType | undefined): string { + if (!t) return "nil"; + switch (t["@type"]) { + case "/gno.PrimitiveType": return "primitive"; + case "/gno.PointerType": return "pointer"; + case "/gno.ArrayType": return "array"; + case "/gno.SliceType": return "slice"; + case "/gno.StructType": return "struct"; + case "/gno.MapType": return "map"; + case "/gno.FuncType": return "func"; + case "/gno.InterfaceType": return "interface"; + case "/gno.RefType": return "ref"; + case "/gno.DeclaredType": return typeKind((t as AminoDeclaredType).Base); + case "/gno.TypeType": return "type"; + case "/gno.PackageType": return "package"; + case "/gno.ChanType": return "chan"; + default: return "unknown"; + } +} + +/** Unwrap DeclaredType to its base type. Returns t unchanged if not declared. */ +export function baseType(t: AminoType | undefined): AminoType | undefined { + if (!t) return undefined; + if (t["@type"] === "/gno.DeclaredType") return (t as AminoDeclaredType).Base; + return t; +} + +/** Extract field names from a StructType. */ +export function structFieldNames(t: AminoType | undefined): string[] | undefined { + if (!t) return undefined; + if (t["@type"] === "/gno.StructType") { + return (t as AminoStructType).Fields.map((f: AminoFieldType) => f.Name); + } + if (t["@type"] === "/gno.DeclaredType") { + return structFieldNames((t as AminoDeclaredType).Base); + } + return undefined; +} + +/** Extract the RefType ID from a type if it is one, or from a DeclaredType. */ +export function getTypeId(t: AminoType | undefined): string | undefined { + if (!t) return undefined; + if (t["@type"] === "/gno.RefType") return t.ID; + if (t["@type"] === "/gno.DeclaredType") { + return (t as AminoDeclaredType).PkgPath + "." + (t as AminoDeclaredType).Name; + } + return undefined; +} + +/** Check if a type is a PrimitiveType with the given numeric value. */ +export function isPrimitive(t: AminoType | undefined): t is AminoPrimitiveType { + return t !== undefined && t["@type"] === "/gno.PrimitiveType"; +} + +/** Build a human-readable function signature from a FuncType. */ +export function funcSignature(t: AminoType | undefined): string { + if (!t || t["@type"] !== "/gno.FuncType") return "func()"; + const ft = t as import("./types.js").AminoFuncType; + const params = (ft.Params || []) + .filter(p => !(p.Name.startsWith("cur") && p.Type?.["@type"] === "/gno.RefType")) + .map(p => { + const tn = typeName(p.Type); + return p.Name && !p.Name.startsWith(".") ? `${p.Name} ${tn}` : tn; + }) + .join(", "); + const results = (ft.Results || []) + .map(r => { + const tn = typeName(r.Type); + return r.Name && !r.Name.startsWith(".") ? `${r.Name} ${tn}` : tn; + }); + const retStr = results.length === 0 ? "" + : results.length === 1 ? ` ${results[0]}` + : ` (${results.join(", ")})`; + return `func(${params})${retStr}`; +} diff --git a/misc/gnojs/src/types.ts b/misc/gnojs/src/types.ts new file mode 100644 index 00000000000..030aa96f46a --- /dev/null +++ b/misc/gnojs/src/types.ts @@ -0,0 +1,271 @@ +// types.ts — Amino JSON wire types as they come from vm/qpkg_json, vm/qobject_json, vm/qtype_json. + +// ---- TypedValue ---- + +export interface AminoTypedValue { + T?: AminoType; + V?: AminoValue; + N?: string; // base64-encoded 8-byte little-endian primitive value +} + +// ---- Types (discriminated by @type) ---- + +export type AminoType = + | AminoPrimitiveType + | AminoPointerType + | AminoArrayType + | AminoSliceType + | AminoStructType + | AminoMapType + | AminoFuncType + | AminoInterfaceType + | AminoRefType + | AminoDeclaredType + | AminoTypeType + | AminoPackageType + | AminoChanType + | AminoUnknownType; + +export interface AminoPrimitiveType { + "@type": "/gno.PrimitiveType"; + value: string; // numeric string, e.g. "32" for IntType +} + +export interface AminoPointerType { + "@type": "/gno.PointerType"; + Elt: AminoType; +} + +export interface AminoArrayType { + "@type": "/gno.ArrayType"; + Len: number; + Elt: AminoType; + Vrd: boolean; +} + +export interface AminoSliceType { + "@type": "/gno.SliceType"; + Elt: AminoType; + Vrd: boolean; +} + +export interface AminoStructType { + "@type": "/gno.StructType"; + PkgPath: string; + Fields: AminoFieldType[]; +} + +export interface AminoMapType { + "@type": "/gno.MapType"; + Key: AminoType; + Value: AminoType; +} + +export interface AminoFuncType { + "@type": "/gno.FuncType"; + Params: AminoFieldType[]; + Results: AminoFieldType[]; +} + +export interface AminoInterfaceType { + "@type": "/gno.InterfaceType"; + PkgPath: string; + Methods: AminoFieldType[]; + Generic: string; +} + +export interface AminoRefType { + "@type": "/gno.RefType"; + ID: string; // TypeID, e.g. "gno.land/r/demo/boards.Board" +} + +export interface AminoDeclaredType { + "@type": "/gno.DeclaredType"; + PkgPath: string; + Name: string; + Base: AminoType; + Methods: AminoTypedValue[]; + ParentLoc?: unknown; +} + +export interface AminoTypeType { + "@type": "/gno.TypeType"; +} + +export interface AminoPackageType { + "@type": "/gno.PackageType"; +} + +export interface AminoChanType { + "@type": "/gno.ChanType"; + Dir: number; + Elt: AminoType; +} + +export interface AminoUnknownType { + "@type": string; + [key: string]: unknown; +} + +// ---- Field type ---- + +export interface AminoFieldType { + Name: string; + Type: AminoType; + Embedded: boolean; + Tag: string; +} + +// ---- Values (discriminated by @type) ---- + +export type AminoValue = + | AminoStringValue + | AminoRefValue + | AminoStructValue + | AminoArrayValue + | AminoSliceValue + | AminoPointerValue + | AminoMapValue + | AminoFuncValue + | AminoHeapItemValue + | AminoTypeValue + | AminoExportRefValue + | AminoBlockValue + | AminoUnknownValue; + +export interface AminoStringValue { + "@type": "/gno.StringValue"; + value: string; +} + +export interface AminoRefValue { + "@type": "/gno.RefValue"; + ObjectID: string; + Hash?: string; + PkgPath?: string; + Escaped?: boolean; +} + +export interface AminoObjectInfo { + ID: string; + Hash?: string; + OwnerID?: string; + ModTime?: string; + RefCount?: string; + LastObjectSize?: string; +} + +export interface AminoStructValue { + "@type": "/gno.StructValue"; + ObjectInfo?: AminoObjectInfo; + Fields: AminoTypedValue[]; +} + +export interface AminoArrayValue { + "@type": "/gno.ArrayValue"; + ObjectInfo?: AminoObjectInfo; + List?: AminoTypedValue[]; + Data?: string; // base64 for byte arrays +} + +export interface AminoSliceValue { + "@type": "/gno.SliceValue"; + Base: AminoValue; + Offset: string; + Length: string; + Maxcap: string; +} + +export interface AminoPointerValue { + "@type": "/gno.PointerValue"; + TV: AminoTypedValue | null; + Base: AminoValue; + Index: string; +} + +export interface AminoMapList { + List?: AminoMapItem[]; +} + +export interface AminoMapItem { + Key: AminoTypedValue; + Value: AminoTypedValue; +} + +export interface AminoMapValue { + "@type": "/gno.MapValue"; + ObjectInfo?: AminoObjectInfo; + List: AminoMapList; +} + +export interface AminoFuncValue { + "@type": "/gno.FuncValue"; + ObjectInfo?: AminoObjectInfo; + Type?: AminoFuncType; + Name: string; + IsClosure?: boolean; + Captures?: AminoTypedValue[]; + PkgPath?: string; + FileName?: string; + NativePkg?: string; + NativeName?: string; + Source?: { + "@type": string; + Location?: { + PkgPath: string; + File: string; + Span: { + Pos: { Line: string; Column: string }; + End: { Line: string; Column: string }; + Num: string; + }; + }; + }; + [key: string]: unknown; +} + +export interface AminoHeapItemValue { + "@type": "/gno.HeapItemValue"; + ObjectInfo?: AminoObjectInfo; + Value: AminoTypedValue; +} + +export interface AminoTypeValue { + "@type": "/gno.TypeValue"; + Type: AminoType; +} + +export interface AminoExportRefValue { + "@type": "/gno.ExportRefValue"; + ObjectID: string; // ":1", ":2", etc. +} + +export interface AminoBlockValue { + "@type": "/gno.Block"; + ObjectInfo?: AminoObjectInfo; + Source?: unknown; + Values?: AminoTypedValue[]; + Parent?: AminoValue; +} + +export interface AminoUnknownValue { + "@type": string; + [key: string]: unknown; +} + +// ---- Endpoint response types ---- + +export interface QpkgResponse { + names: string[]; + values: AminoTypedValue[]; +} + +export interface QobjectResponse { + objectid: string; + value: AminoValue; +} + +export interface QtypeResponse { + typeid: string; + type: AminoType; +} diff --git a/misc/gnojs/tsconfig.json b/misc/gnojs/tsconfig.json new file mode 100644 index 00000000000..a65aa8b41f6 --- /dev/null +++ b/misc/gnojs/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "outDir": "dist", + "declaration": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +}