From b3cb9f8d19dec1652c5380299b466d21df8ab4af Mon Sep 17 00:00:00 2001 From: Anish Sahoo Date: Tue, 14 Jul 2026 16:07:33 -0500 Subject: [PATCH 01/10] 1. Add Go playground server and handlers (#3109) * feat: add Go playground server * fix: strip protocol-relative playground redirects * fix: strip all playground proxy redirects --- pkg/cli/playground.go | 525 ++++++++++++++++++++++++ pkg/cli/playground_assets_stub.go | 11 + pkg/cli/playground_test.go | 644 ++++++++++++++++++++++++++++++ 3 files changed, 1180 insertions(+) create mode 100644 pkg/cli/playground.go create mode 100644 pkg/cli/playground_assets_stub.go create mode 100644 pkg/cli/playground_test.go diff --git a/pkg/cli/playground.go b/pkg/cli/playground.go new file mode 100644 index 0000000000..203fc968f1 --- /dev/null +++ b/pkg/cli/playground.go @@ -0,0 +1,525 @@ +package cli + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "os/exec" + "os/signal" + "runtime" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/spf13/cobra" + + "github.com/replicate/cog/pkg/global" + "github.com/replicate/cog/pkg/util/console" +) + +const ( + // maxWebhookBody caps a single webhook payload relayed to the browser. + maxWebhookBody = 10 * 1024 * 1024 + // maxPlaygroundHeaderMetadata keeps inspector metadata below browser header limits. + maxPlaygroundHeaderMetadata = 32 * 1024 + // playgroundUpstreamHeaders identifies model response headers for the request inspector. + playgroundUpstreamHeaders = "X-Cog-Upstream-Headers" +) + +var ( + playgroundPort = 0 + playgroundTarget = "http://localhost:8393" + playgroundHost = "127.0.0.1" + playgroundWebhookHost = "host.docker.internal" + playgroundNoOpen = false +) + +func newPlaygroundCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "playground", + Short: "Open a browser playground for talking to a running model", + Long: `Open a browser playground for talking to a running model. + +Starts a local web server that serves a schema-driven UI (a Postman-like tool +for Cog models). Point it at any running Cog HTTP API -- for example one started +with 'cog serve' -- and the playground reflects that model's inputs and outputs +from its OpenAPI schema in real time. + +Requests are reverse-proxied through this server, so the target API does not +need to set CORS headers. The server also hosts a webhook sink so async +predictions can be observed in the browser. + +Async/webhook testing against a containerized model requires the webhook URL to +be reachable from inside the container. On Docker Desktop the default +'host.docker.internal' works once the server listens on a reachable interface +(e.g. --host 0.0.0.0).`, + Example: ` # Start a model API in one terminal + cog serve -p 8393 + + # Open the playground pointing at it + cog playground --target http://localhost:8393`, + RunE: cmdPlayground, + Args: cobra.MaximumNArgs(0), + SuggestFor: []string{"ui", "gui"}, + } + + cmd.Flags().IntVarP(&playgroundPort, "port", "p", playgroundPort, "Port to listen on (0 picks a free port)") + cmd.Flags().StringVar(&playgroundTarget, "target", playgroundTarget, "Default target model API URL") + cmd.Flags().StringVar(&playgroundHost, "host", playgroundHost, "Address to bind (use 0.0.0.0 to receive webhooks from containers)") + cmd.Flags().StringVar(&playgroundWebhookHost, "webhook-host", playgroundWebhookHost, "Hostname the model uses to reach this server for webhooks") + cmd.Flags().BoolVar(&playgroundNoOpen, "no-open", playgroundNoOpen, "Do not open the browser automatically") + + return cmd +} + +// playgroundServer holds the runtime state for a playground instance. +type playgroundServer struct { + hub *eventHub + webhookBase string + defaultTarget string +} + +// newPlaygroundServer builds a playground server with an initialized event hub. +func newPlaygroundServer(webhookBase, defaultTarget string) *playgroundServer { + return &playgroundServer{ + hub: newEventHub(), + webhookBase: webhookBase, + defaultTarget: defaultTarget, + } +} + +func cmdPlayground(cmd *cobra.Command, _ []string) error { + if !playgroundAssetsBuilt { + return errors.New("playground assets are not included; build Cog with 'mise run build:cog'") + } + + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) + defer stop() + + uiFS, err := fs.Sub(playgroundUI, "playground") + if err != nil { + return fmt.Errorf("loading playground assets: %w", err) + } + + ln, err := net.Listen("tcp", playgroundAddress(playgroundHost, playgroundPort)) + if err != nil { + return fmt.Errorf("starting playground server: %w", err) + } + port := ln.Addr().(*net.TCPAddr).Port + + srvState := newPlaygroundServer( + "http://"+playgroundAddress(playgroundWebhookHost, port), + playgroundTarget, + ) + + mux := srvState.routes(uiFS) + + browserHost := playgroundBrowserHost(playgroundHost) + uiURL := "http://" + playgroundAddress(browserHost, port) + "/" + console.Infof("Cog playground running at %s", uiURL) + console.Info("Press Ctrl+C to stop.") + if !playgroundNoOpen { + maybeOpenBrowser(uiURL) + } + + srv := &http.Server{ + Handler: mux, + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 5 * time.Minute, + IdleTimeout: 60 * time.Second, + MaxHeaderBytes: 64 * 1024, + BaseContext: func(net.Listener) context.Context { return ctx }, + } + + return servePlayground(ctx, srv, ln, 5*time.Second) +} + +func servePlayground(ctx context.Context, srv *http.Server, ln net.Listener, timeout time.Duration) error { + serveErr := make(chan error, 1) + go func() { serveErr <- srv.Serve(ln) }() + + select { + case err := <-serveErr: + if err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil + case <-ctx.Done(): + shutdownCtx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + if err := srv.Shutdown(shutdownCtx); err != nil { + _ = srv.Close() + <-serveErr + return fmt.Errorf("shutting down playground server: %w", err) + } + if err := <-serveErr; err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil + } +} + +func playgroundAddress(host string, port int) string { + return net.JoinHostPort(normalizePlaygroundHost(host), strconv.Itoa(port)) +} + +func playgroundBrowserHost(host string) string { + host = normalizePlaygroundHost(host) + if host == "" { + return "127.0.0.1" + } + ip := net.ParseIP(host) + if ip == nil || !ip.IsUnspecified() { + return host + } + if ip.To4() == nil { + return "::1" + } + return "127.0.0.1" +} + +func normalizePlaygroundHost(host string) string { + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + return host[1 : len(host)-1] + } + return host +} + +// routes builds the HTTP handler: static UI, the reverse proxy, and the webhook +// sink + event relay. +func (s *playgroundServer) routes(uiFS fs.FS) http.Handler { + mux := http.NewServeMux() + mux.Handle("/", http.FileServerFS(uiFS)) + mux.HandleFunc("/proxy/", handlePlaygroundProxy) + mux.HandleFunc("/webhook/", s.handleWebhook) + mux.HandleFunc("/events", s.handleEvents) + mux.HandleFunc("/config", s.handleConfig) + return protectPlayground(mux) +} + +// protectPlayground keeps the UI and user-directed proxy private even when the +// server listens on 0.0.0.0 for container webhook callbacks. Remote webhook +// deliveries are the only requests exempt from the browser-origin checks. +func protectPlayground(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + setPlaygroundSecurityHeaders(w, r.URL.Path) + if strings.HasPrefix(r.URL.Path, "/webhook/") { + next.ServeHTTP(w, r) + return + } + if !isLoopbackRemote(r.RemoteAddr) || !isLoopbackHost(r.Host) || isCrossSiteRequest(r) { + http.Error(w, "playground UI and proxy are only available from this browser", http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) +} + +func setPlaygroundSecurityHeaders(w http.ResponseWriter, path string) { + csp := "default-src 'self'; connect-src 'self'; img-src 'self' data: http: https:; media-src 'self' data: http: https:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'" + if strings.HasPrefix(path, "/assets/validation.worker-") && strings.HasSuffix(path, ".js") { + // Ajv compiles dynamic model schemas. Confine the required code generation + // to this network-isolated worker rather than relaxing the page policy. + csp = "default-src 'none'; script-src 'self' 'unsafe-eval'; connect-src 'none'; base-uri 'none'; form-action 'none'" + } + w.Header().Set("Content-Security-Policy", csp) + w.Header().Set("Referrer-Policy", "no-referrer") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") +} + +func isLoopbackHost(hostport string) bool { + host := hostport + if parsed, _, err := net.SplitHostPort(hostport); err == nil { + host = parsed + } + host = strings.Trim(host, "[]") + if strings.EqualFold(host, "localhost") { + return true + } + ip := net.ParseIP(host) + return ip != nil && ip.IsLoopback() +} + +func isCrossSiteRequest(r *http.Request) bool { + if strings.EqualFold(r.Header.Get("Sec-Fetch-Site"), "cross-site") { + return true + } + origin := r.Header.Get("Origin") + if origin == "" { + return false + } + parsed, err := url.Parse(origin) + return err != nil || !strings.EqualFold(parsed.Host, r.Host) || (parsed.Scheme != "http" && parsed.Scheme != "https") +} + +func isLoopbackRemote(remoteAddr string) bool { + host, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + return false + } + ip := net.ParseIP(host) + return ip != nil && ip.IsLoopback() +} + +// handleConfig reports runtime configuration the UI needs, notably the webhook +// base URL the model should call back on. +func (s *playgroundServer) handleConfig(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{ + "target": s.defaultTarget, + "webhookBase": s.webhookBase, + "cogVersion": global.Version, + }) +} + +// handleWebhook receives a webhook delivery from a model and relays its body to +// any browser subscribed to the matching token's event stream. +func (s *playgroundServer) handleWebhook(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.Header().Set("Allow", http.MethodPost) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + token := strings.TrimPrefix(r.URL.Path, "/webhook/") + if token == "" || strings.Contains(token, "/") || len(token) > 128 { + http.Error(w, "missing token", http.StatusNotFound) + return + } + if !s.hub.hasSubscribers(token) { + http.Error(w, "no browser is ready to receive this webhook", http.StatusServiceUnavailable) + return + } + body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxWebhookBody)) + if err != nil { + var maxBytesError *http.MaxBytesError + if errors.As(err, &maxBytesError) { + http.Error(w, "webhook body too large", http.StatusRequestEntityTooLarge) + return + } + http.Error(w, "cannot read webhook body", http.StatusBadRequest) + return + } + if !s.hub.publish(token, body) { + http.Error(w, "no browser is ready to receive this webhook", http.StatusServiceUnavailable) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, "{}") +} + +// handleEvents streams relayed webhook payloads to the browser over SSE. +func (s *playgroundServer) handleEvents(w http.ResponseWriter, r *http.Request) { + token := r.URL.Query().Get("token") + if token == "" { + http.Error(w, "missing token", http.StatusBadRequest) + return + } + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "streaming unsupported", http.StatusInternalServerError) + return + } + + // Subscribe before flushing headers so the caller is guaranteed to be + // receiving by the time it observes the response (no missed events). + ch := s.hub.subscribe(token) + defer s.hub.unsubscribe(token, ch) + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + flusher.Flush() + + keepAlive := time.NewTicker(15 * time.Second) + defer keepAlive.Stop() + ctx := r.Context() + for { + select { + case <-ctx.Done(): + return + case msg := <-ch: + writeSSEData(w, msg) + flusher.Flush() + case <-keepAlive.C: + _, _ = fmt.Fprint(w, ": keep-alive\n\n") + flusher.Flush() + } + } +} + +// writeSSEData emits a payload as a single SSE event, prefixing every line with +// "data: ". This preserves embedded newlines without letting them terminate the +// event early or inject additional SSE fields (e.g. a spoofed "event:" line). +func writeSSEData(w io.Writer, msg []byte) { + normalized := strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(string(msg)) + for line := range strings.SplitSeq(normalized, "\n") { + _, _ = fmt.Fprintf(w, "data: %s\n", line) + } + _, _ = fmt.Fprint(w, "\n") +} + +// handlePlaygroundProxy reverse-proxies /proxy/* to the target model API. The +// target origin is taken from the X-Cog-Target header set by the playground UI. +// Proxying keeps the browser same-origin, sidestepping CORS, and streams SSE +// responses through unbuffered. +func handlePlaygroundProxy(w http.ResponseWriter, r *http.Request) { + rawTarget := r.Header.Get("X-Cog-Target") + if rawTarget == "" { + writeProxyError(w, http.StatusBadRequest, "no target API set") + return + } + + target, err := url.Parse(strings.TrimRight(rawTarget, "/")) + if err != nil || target.Host == "" || target.User != nil || target.Fragment != "" || (target.Scheme != "http" && target.Scheme != "https") { + writeProxyError(w, http.StatusBadRequest, "invalid target API URL") + return + } + + proxy := &httputil.ReverseProxy{ + FlushInterval: -1, // flush immediately so SSE streams in real time + Rewrite: func(pr *httputil.ProxyRequest) { + // Forward the path after /proxy. + escapedPath := strings.TrimPrefix(pr.In.URL.EscapedPath(), "/proxy") + if escapedPath == "" { + escapedPath = "/" + } + path, err := url.PathUnescape(escapedPath) + if err != nil { + path = strings.TrimPrefix(pr.In.URL.Path, "/proxy") + } + pr.Out.URL.Path = path + pr.Out.URL.RawPath = escapedPath + pr.SetURL(target) + pr.Out.Host = target.Host + pr.Out.Header.Del("X-Cog-Target") + pr.Out.Header.Del("Authorization") + pr.Out.Header.Del("Cookie") + pr.Out.Header.Del("Origin") + pr.Out.Header.Del("Referer") + }, + ModifyResponse: func(resp *http.Response) error { + resp.Header.Del("Clear-Site-Data") + resp.Header.Del("Service-Worker-Allowed") + resp.Header.Del("Set-Cookie") + // Drop redirects so browser URL normalization cannot pivot off the + // intended target (fetch uses redirect: "manual" as a second line). + if resp.StatusCode >= 300 && resp.StatusCode < 400 { + resp.Header.Del("Location") + } + upstreamHeaders := resp.Header.Clone() + upstreamHeaders.Del(playgroundUpstreamHeaders) + resp.Header.Del(playgroundUpstreamHeaders) + encodedHeaders, err := json.Marshal(upstreamHeaders) + if err != nil { + return fmt.Errorf("encode upstream response headers: %w", err) + } + metadata := base64.RawURLEncoding.EncodeToString(encodedHeaders) + if len(metadata) <= maxPlaygroundHeaderMetadata { + resp.Header.Set(playgroundUpstreamHeaders, metadata) + } + return nil + }, + ErrorHandler: func(w http.ResponseWriter, _ *http.Request, err error) { + writeProxyError(w, http.StatusBadGateway, "cannot reach target API: "+err.Error()) + }, + } + // The proxy target is user-specified by design (a local model API); SSRF to + // it is the intended behavior of this dev tool, not a vulnerability. + proxy.ServeHTTP(w, r) //nolint:gosec // user-directed proxy target is intentional +} + +func writeProxyError(w http.ResponseWriter, status int, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + // message is server-controlled text; encode minimally as a JSON string. + _, _ = fmt.Fprintf(w, `{"error":%q}`, message) +} + +// maybeOpenBrowser best-effort opens a URL in the default browser. +func maybeOpenBrowser(target string) { + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", target) + case "windows": + cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", target) + default: + cmd = exec.Command("xdg-open", target) + } + if err := cmd.Start(); err == nil { + go func() { _ = cmd.Wait() }() + } +} + +// eventHub fans out relayed webhook payloads to browser SSE subscribers keyed +// by an opaque token. +type eventHub struct { + mu sync.Mutex + subs map[string]map[chan []byte]struct{} +} + +func newEventHub() *eventHub { + return &eventHub{subs: make(map[string]map[chan []byte]struct{})} +} + +func (h *eventHub) subscribe(token string) chan []byte { + h.mu.Lock() + defer h.mu.Unlock() + ch := make(chan []byte, 4) + if h.subs[token] == nil { + h.subs[token] = make(map[chan []byte]struct{}) + } + h.subs[token][ch] = struct{}{} + return ch +} + +func (h *eventHub) hasSubscribers(token string) bool { + h.mu.Lock() + defer h.mu.Unlock() + return len(h.subs[token]) > 0 +} + +func (h *eventHub) unsubscribe(token string, ch chan []byte) { + h.mu.Lock() + defer h.mu.Unlock() + subs := h.subs[token] + if subs == nil { + return + } + delete(subs, ch) + if len(subs) == 0 { + delete(h.subs, token) + } +} + +// publish delivers msg to every subscriber of token, best-effort. It reports +// whether at least one subscriber received it. Delivery is non-blocking: a +// subscriber whose buffer is momentarily full is skipped rather than stalling +// the webhook. The depth-buffered channels absorb any realistic burst for a +// single-user tool. +func (h *eventHub) publish(token string, msg []byte) bool { + h.mu.Lock() + defer h.mu.Unlock() + + delivered := false + for ch := range h.subs[token] { + select { + case ch <- msg: + delivered = true + default: + } + } + return delivered +} diff --git a/pkg/cli/playground_assets_stub.go b/pkg/cli/playground_assets_stub.go new file mode 100644 index 0000000000..ef129f9a4c --- /dev/null +++ b/pkg/cli/playground_assets_stub.go @@ -0,0 +1,11 @@ +//go:build !playground_assets + +package cli + +import "testing/fstest" + +var playgroundUI = fstest.MapFS{ + "playground/index.html": {Data: []byte("Cog Playground")}, +} + +const playgroundAssetsBuilt = false diff --git a/pkg/cli/playground_test.go b/pkg/cli/playground_test.go new file mode 100644 index 0000000000..464fb059c2 --- /dev/null +++ b/pkg/cli/playground_test.go @@ -0,0 +1,644 @@ +package cli + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/fs" + "net" + "net/http" + "net/http/httptest" + "net/url" + "regexp" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/replicate/cog/pkg/global" +) + +var _ = newPlaygroundCommand + +func newTestPlayground(t *testing.T) *httptest.Server { + t.Helper() + uiFS, err := fs.Sub(playgroundUI, "playground") + require.NoError(t, err) + s := newPlaygroundServer("http://wh.example/cb", "http://localhost:8393") + ts := httptest.NewServer(s.routes(uiFS)) + t.Cleanup(ts.Close) + return ts +} + +// echoServer reports the received path and (forwarded) query as JSON. +func echoServer(t *testing.T) *httptest.Server { + t.Helper() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"path":%q,"escaped_path":%q,"query":%q}`, r.URL.Path, r.URL.EscapedPath(), r.URL.RawQuery) + })) + t.Cleanup(ts.Close) + return ts +} + +func TestPlaygroundServesUI(t *testing.T) { + ts := newTestPlayground(t) + + resp, err := http.Get(ts.URL + "/") + require.NoError(t, err) + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Contains(t, string(body), "Cog Playground") + + assets := regexp.MustCompile(`(?:src|href)="(/?assets/[^"]+)"`).FindAllStringSubmatch(string(body), -1) + if !playgroundAssetsBuilt { + assert.Empty(t, assets, "stub index should not reference generated assets") + return + } + require.NotEmpty(t, assets, "generated index should reference bundled assets") + for _, match := range assets { + path := "/" + strings.TrimPrefix(match[1], "/") + assetResp, err := http.Get(ts.URL + path) + require.NoError(t, err, "requesting %s", path) + assetResp.Body.Close() + assert.Equal(t, http.StatusOK, assetResp.StatusCode, "%s should be served", path) + if strings.HasSuffix(path, ".css") { + assert.Contains(t, assetResp.Header.Get("Content-Type"), "text/css") + } else if strings.HasSuffix(path, ".js") { + assert.Contains(t, assetResp.Header.Get("Content-Type"), "javascript") + } + } +} + +func TestServePlaygroundWaitsForHandlers(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + started := make(chan struct{}) + canceled := make(chan struct{}) + release := make(chan struct{}) + srv := &http.Server{ + Handler: http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + close(started) + <-r.Context().Done() + close(canceled) + <-release + }), + BaseContext: func(net.Listener) context.Context { return ctx }, + } + serveDone := make(chan error, 1) + go func() { serveDone <- servePlayground(ctx, srv, ln, time.Second) }() + requestDone := make(chan struct{}) + go func() { + defer close(requestDone) + resp, requestErr := http.Get("http://" + ln.Addr().String()) + if requestErr == nil { + resp.Body.Close() + } + }() + + <-started + cancel() + <-canceled + select { + case err := <-serveDone: + require.FailNow(t, "server returned before the active handler finished", "%v", err) + default: + } + close(release) + require.NoError(t, <-serveDone) + <-requestDone +} + +func TestPlaygroundConfig(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Get(ts.URL + "/config") + require.NoError(t, err) + defer resp.Body.Close() + + var config map[string]string + require.NoError(t, json.NewDecoder(resp.Body).Decode(&config)) + assert.Equal(t, "http://wh.example/cb", config["webhookBase"]) + assert.Equal(t, "http://localhost:8393", config["target"]) + assert.Equal(t, global.Version, config["cogVersion"]) +} + +func TestPlaygroundRejectsRemoteUIAndProxyRequests(t *testing.T) { + uiFS, err := fs.Sub(playgroundUI, "playground") + require.NoError(t, err) + s := newPlaygroundServer("http://wh.example/cb", "") + + for _, path := range []string{"/", "/config", "/proxy/health-check"} { + req := httptest.NewRequest(http.MethodGet, "http://playground.example"+path, nil) + req.RemoteAddr = "192.0.2.10:1234" + resp := httptest.NewRecorder() + s.routes(uiFS).ServeHTTP(resp, req) + assert.Equal(t, http.StatusForbidden, resp.Code, path) + } +} + +func TestPlaygroundAllowsRemoteWebhookRequests(t *testing.T) { + uiFS, err := fs.Sub(playgroundUI, "playground") + require.NoError(t, err) + s := newPlaygroundServer("http://wh.example/cb", "") + ch := s.hub.subscribe("token") + defer s.hub.unsubscribe("token", ch) + + req := httptest.NewRequest(http.MethodPost, "http://playground.example/webhook/token", strings.NewReader(`{"status":"succeeded"}`)) + req.RemoteAddr = "192.0.2.10:1234" + resp := httptest.NewRecorder() + s.routes(uiFS).ServeHTTP(resp, req) + + assert.Equal(t, http.StatusOK, resp.Code) +} + +func TestPlaygroundProxyHeaderTarget(t *testing.T) { + ts := newTestPlayground(t) + stub := echoServer(t) + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/openapi.json?foo=bar", nil) + require.NoError(t, err) + req.Header.Set("X-Cog-Target", stub.URL) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var got map[string]string + require.NoError(t, json.NewDecoder(resp.Body).Decode(&got)) + assert.Equal(t, "/openapi.json", got["path"], "/proxy prefix should be stripped") + assert.Equal(t, "foo=bar", got["query"]) +} + +func TestPlaygroundProxyRejectsQueryTarget(t *testing.T) { + ts := newTestPlayground(t) + stub := echoServer(t) + + u := ts.URL + "/proxy/health-check?x=1&cog_target=" + url.QueryEscape(stub.URL) + resp, err := http.Get(u) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) +} + +func TestPlaygroundProxyPreservesEscapedPathSegments(t *testing.T) { + ts := newTestPlayground(t) + stub := echoServer(t) + + req, err := http.NewRequest(http.MethodPost, ts.URL+"/proxy/predictions/custom%2Fid/cancel", nil) + require.NoError(t, err) + req.Header.Set("X-Cog-Target", stub.URL+"/api") + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var got map[string]string + require.NoError(t, json.NewDecoder(resp.Body).Decode(&got)) + assert.Equal(t, "/api/predictions/custom/id/cancel", got["path"]) + assert.Equal(t, "/api/predictions/custom%2Fid/cancel", got["escaped_path"]) +} + +func TestPlaygroundProxyForwardsRequestAndResponse(t *testing.T) { + ts := newTestPlayground(t) + stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, http.MethodPut, r.Method) + assert.Equal(t, "/api/predictions/p1", r.URL.Path) + assert.Equal(t, `{"input":{"prompt":"hello"}}`, string(body)) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.Empty(t, r.Header.Get("X-Cog-Target")) + assert.Empty(t, r.Header.Get("Authorization")) + assert.Empty(t, r.Header.Get("Cookie")) + w.Header().Set("X-Upstream", "preserved") + w.Header().Set("X-Frame-Options", "SAMEORIGIN") + w.Header().Set("Set-Cookie", "session=upstream") + w.WriteHeader(http.StatusUnprocessableEntity) + _, _ = w.Write([]byte(`{"detail":"invalid input"}`)) + })) + t.Cleanup(stub.Close) + + req, err := http.NewRequest(http.MethodPut, ts.URL+"/proxy/predictions/p1", bytes.NewBufferString(`{"input":{"prompt":"hello"}}`)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer local-secret") + req.Header.Set("Cookie", "session=local-secret") + req.Header.Set("X-Cog-Target", stub.URL+"/api") + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + assert.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) + assert.Equal(t, "preserved", resp.Header.Get("X-Upstream")) + assert.Empty(t, resp.Header.Get("Set-Cookie")) + encodedHeaders, err := base64.RawURLEncoding.DecodeString(resp.Header.Get(playgroundUpstreamHeaders)) + require.NoError(t, err) + var upstreamHeaders http.Header + require.NoError(t, json.Unmarshal(encodedHeaders, &upstreamHeaders)) + assert.Equal(t, "preserved", upstreamHeaders.Get("X-Upstream")) + assert.Equal(t, "SAMEORIGIN", upstreamHeaders.Get("X-Frame-Options")) + assert.Empty(t, upstreamHeaders.Get("Set-Cookie")) + assert.JSONEq(t, `{"detail":"invalid input"}`, string(body)) +} + +func TestPlaygroundProxyRoutesConcurrentWorkspacesIndependently(t *testing.T) { + ts := newTestPlayground(t) + models := map[string]*httptest.Server{} + for _, name := range []string{"first", "second"} { + models[name] = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = fmt.Fprint(w, name) + })) + t.Cleanup(models[name].Close) + } + + errs := make(chan error, 40) + var requests sync.WaitGroup + for name, model := range models { + for range 20 { + requests.Go(func() { + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/identity", nil) + if err != nil { + errs <- err + return + } + req.Header.Set("X-Cog-Target", model.URL) + resp, err := http.DefaultClient.Do(req) + if err != nil { + errs <- err + return + } + body, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if readErr != nil { + errs <- readErr + return + } + if string(body) != name { + errs <- fmt.Errorf("request for %s reached %q", name, body) + } + }) + } + } + requests.Wait() + close(errs) + for err := range errs { + assert.NoError(t, err) + } +} + +func TestPlaygroundProxyOmitsOversizedHeaderMetadata(t *testing.T) { + ts := newTestPlayground(t) + stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("X-Large", strings.Repeat("x", maxPlaygroundHeaderMetadata)) + w.Header().Set(playgroundUpstreamHeaders, "spoofed") + w.WriteHeader(http.StatusNoContent) + })) + t.Cleanup(stub.Close) + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/health-check", nil) + require.NoError(t, err) + req.Header.Set("X-Cog-Target", stub.URL) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, strings.Repeat("x", maxPlaygroundHeaderMetadata), resp.Header.Get("X-Large")) + assert.Empty(t, resp.Header.Get(playgroundUpstreamHeaders)) +} + +func TestPlaygroundProxyMissingTarget(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Get(ts.URL + "/proxy/health-check") + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) +} + +func TestPlaygroundProxyInvalidTarget(t *testing.T) { + ts := newTestPlayground(t) + for _, target := range []string{"ftp://example.com", "garbage", "", "http://user@example.com", "http://example.com/#fragment"} { + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/health-check", nil) + require.NoError(t, err) + if target != "" { + req.Header.Set("X-Cog-Target", target) + } + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "target %q should be rejected", target) + } +} + +func TestPlaygroundWebhookRejectsNonPost(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Get(ts.URL + "/webhook/token") + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) + assert.Equal(t, http.MethodPost, resp.Header.Get("Allow")) +} + +func TestPlaygroundRecognizesIPv6Loopback(t *testing.T) { + assert.True(t, isLoopbackRemote("[::1]:8080")) + assert.False(t, isLoopbackRemote("not-an-address")) + assert.True(t, isLoopbackHost("[::1]:8080")) + assert.True(t, isLoopbackHost("localhost:8080")) + assert.False(t, isLoopbackHost("playground.example:8080")) +} + +func TestPlaygroundFormatsIPv6Address(t *testing.T) { + assert.Equal(t, "[::1]:8080", playgroundAddress("::1", 8080)) + assert.Equal(t, "[::1]:8080", playgroundAddress("[::1]", 8080)) +} + +func TestPlaygroundUsesLoopbackBrowserHostForWildcard(t *testing.T) { + assert.Equal(t, "127.0.0.1", playgroundBrowserHost("0.0.0.0")) + assert.Equal(t, "::1", playgroundBrowserHost("::")) + assert.Equal(t, "::1", playgroundBrowserHost("[::]")) +} + +func TestPlaygroundRejectsCrossSiteBrowserRequests(t *testing.T) { + ts := newTestPlayground(t) + + for name, mutate := range map[string]func(*http.Request){ + "host": func(req *http.Request) { req.Host = "attacker.example" }, + "origin": func(req *http.Request) { + req.Header.Set("Origin", "https://attacker.example") + }, + "fetch metadata": func(req *http.Request) { + req.Header.Set("Sec-Fetch-Site", "cross-site") + }, + } { + t.Run(name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, ts.URL+"/config", nil) + require.NoError(t, err) + mutate(req) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + }) + } +} + +func TestPlaygroundSetsBrowserSecurityHeaders(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Get(ts.URL + "/") + require.NoError(t, err) + defer resp.Body.Close() + + assert.Contains(t, resp.Header.Get("Content-Security-Policy"), "frame-ancestors 'none'") + assert.NotContains(t, resp.Header.Get("Content-Security-Policy"), "'unsafe-eval'") + assert.Equal(t, "no-referrer", resp.Header.Get("Referrer-Policy")) + assert.Equal(t, "nosniff", resp.Header.Get("X-Content-Type-Options")) + + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/assets/validation.worker-test.js", nil) + request.RemoteAddr = "127.0.0.1:1234" + request.Host = "127.0.0.1" + protectPlayground(http.NotFoundHandler()).ServeHTTP(recorder, request) + workerCSP := recorder.Header().Get("Content-Security-Policy") + assert.Contains(t, workerCSP, "script-src 'self' 'unsafe-eval'") + assert.Contains(t, workerCSP, "connect-src 'none'") +} + +func TestPlaygroundProxyUnreachableTarget(t *testing.T) { + ts := newTestPlayground(t) + dead := httptest.NewServer(http.NotFoundHandler()) + deadURL := dead.URL + dead.Close() // now refuses connections + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/health-check", nil) + require.NoError(t, err) + req.Header.Set("X-Cog-Target", deadURL) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadGateway, resp.StatusCode) +} + +func TestPlaygroundProxyStreamsSSE(t *testing.T) { + ts := newTestPlayground(t) + sse := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + w.WriteHeader(http.StatusOK) + w.(http.Flusher).Flush() + fmt.Fprint(w, "event: start\ndata: {\"a\":1}\n\n") + w.(http.Flusher).Flush() + })) + t.Cleanup(sse.Close) + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/predictions", nil) + require.NoError(t, err) + req.Header.Set("X-Cog-Target", sse.URL) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, "text/event-stream", resp.Header.Get("Content-Type")) + body, _ := io.ReadAll(resp.Body) + assert.Contains(t, string(body), "event: start") + assert.Contains(t, string(body), `data: {"a":1}`) +} + +func TestPlaygroundWebhookRelay(t *testing.T) { + ts := newTestPlayground(t) + const token = "tok123" + + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, ts.URL+"/events?token="+token, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, "text/event-stream", resp.Header.Get("Content-Type")) + + // The subscription is registered before headers are flushed, so by now the + // hub has our channel; deliver a webhook and expect it relayed. + got := make(chan string, 1) + go func() { + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + if line := scanner.Text(); strings.HasPrefix(line, "data: ") { + got <- strings.TrimPrefix(line, "data: ") + return + } + } + }() + + whResp, err := http.Post(ts.URL+"/webhook/"+token, "application/json", + strings.NewReader(`{"status":"succeeded","id":"p1"}`)) + require.NoError(t, err) + whResp.Body.Close() + assert.Equal(t, http.StatusOK, whResp.StatusCode) + + select { + case data := <-got: + assert.JSONEq(t, `{"status":"succeeded","id":"p1"}`, data) + case <-time.After(2 * time.Second): + require.FailNow(t, "timed out waiting for relayed webhook event") + } +} + +// A payload containing newlines must be framed as one SSE event with a +// "data: " prefix per line, not terminate the event early or inject fields. +func TestPlaygroundWebhookRelayPreservesNewlines(t *testing.T) { + ts := newTestPlayground(t) + const token = "tok-nl" + + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, ts.URL+"/events?token="+token, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + got := make(chan []string, 1) + go func() { + scanner := bufio.NewScanner(resp.Body) + var data []string + for scanner.Scan() { + line := scanner.Text() + if after, ok := strings.CutPrefix(line, "data: "); ok { + data = append(data, after) + } else if line == "" && len(data) > 0 { + got <- data + return + } + } + }() + + whResp, err := http.Post(ts.URL+"/webhook/"+token, "application/json", + strings.NewReader("{\"a\":1}\n{\"b\":2}")) + require.NoError(t, err) + whResp.Body.Close() + + select { + case data := <-got: + assert.Equal(t, []string{`{"a":1}`, `{"b":2}`}, data) + case <-time.After(2 * time.Second): + require.FailNow(t, "timed out waiting for relayed webhook event") + } +} + +func TestWriteSSEDataNormalizesLineEndings(t *testing.T) { + for name, input := range map[string]string{ + "LF": "first\nsecond", + "CRLF": "first\r\nsecond", + "CR": "first\rsecond", + } { + t.Run(name, func(t *testing.T) { + var output strings.Builder + writeSSEData(&output, []byte(input)) + assert.Equal(t, "data: first\ndata: second\n\n", output.String()) + }) + } +} + +func TestPlaygroundEventsMissingToken(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Get(ts.URL + "/events") + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) +} + +func TestPlaygroundWebhookMissingToken(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Post(ts.URL+"/webhook/", "application/json", strings.NewReader("{}")) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusNotFound, resp.StatusCode) +} + +func TestPlaygroundWebhookRejectsOversizedBody(t *testing.T) { + uiFS, err := fs.Sub(playgroundUI, "playground") + require.NoError(t, err) + // Keep a subscriber active so the handler reaches its body-size check. + s := newPlaygroundServer("http://wh.example/cb", "") + ch := s.hub.subscribe("token") + defer s.hub.unsubscribe("token", ch) + ts := httptest.NewServer(s.routes(uiFS)) + t.Cleanup(ts.Close) + resp, err := http.Post(ts.URL+"/webhook/token", "application/json", + strings.NewReader(strings.Repeat("x", maxWebhookBody+1))) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusRequestEntityTooLarge, resp.StatusCode) +} + +func TestPlaygroundWebhookRejectsMissingSubscriber(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Post(ts.URL+"/webhook/token", "application/json", strings.NewReader("{}")) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) +} + +func TestPlaygroundProxyStripsRedirectLocation(t *testing.T) { + for name, location := range map[string]string{ + "absolute": "http://169.254.169.254/latest/meta-data/", + "protocol-relative": "//169.254.169.254/latest/meta-data/", + "triple-slash": "///169.254.169.254/latest/meta-data/", + "backslash": `\\169.254.169.254\latest\meta-data\`, + } { + t.Run(name, func(t *testing.T) { + ts := newTestPlayground(t) + stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Location", location) + w.WriteHeader(http.StatusFound) + })) + t.Cleanup(stub.Close) + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/health-check", nil) + require.NoError(t, err) + req.Header.Set("X-Cog-Target", stub.URL) + // Do not follow redirects; we want the proxy response as the browser would with redirect:manual. + client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + }} + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Empty(t, resp.Header.Get("Location")) + }) + } +} + +// A stalled subscriber (full buffer) must not block or deny delivery to a +// healthy one: publish is best-effort and non-blocking. +func TestEventHubPublishDeliversDespiteStalledSubscriber(t *testing.T) { + hub := newEventHub() + stalled := hub.subscribe("token") + healthy := hub.subscribe("token") + // Fill the stalled subscriber's buffer so any send to it would block. + for len(stalled) < cap(stalled) { + stalled <- []byte("fill") + } + + assert.True(t, hub.publish("token", []byte("msg")), "healthy subscriber should receive the message") + + select { + case msg := <-healthy: + assert.Equal(t, []byte("msg"), msg) + case <-time.After(time.Second): + require.FailNow(t, "healthy subscriber did not receive the message") + } +} + +func TestEventHubPublishReturnsFalseWithoutSubscribers(t *testing.T) { + hub := newEventHub() + assert.False(t, hub.publish("token", []byte("msg"))) +} From 6feed4a5c8ddc99d14f51f995d2cd26ced8ab070 Mon Sep 17 00:00:00 2001 From: Anish Sahoo Date: Tue, 14 Jul 2026 16:09:32 -0500 Subject: [PATCH 02/10] feat(playground): add frontend scaffold and CI (#3108) --- .github/dependabot.yml | 4 + .github/workflows/ci.yaml | 175 +- .github/workflows/release-build.yaml | 31 +- .gitignore | 7 + .golangci.yaml | 2 + .goreleaser.yaml | 2 + .markdownlint-cli2.yaml | 2 + mise.lock | 312 ++ mise.toml | 86 +- pkg/cli/playground_assets.go | 10 + playground/.gitignore | 5 + playground/.oxfmtrc.json | 5 + playground/.oxlintrc.json | 11 + playground/index.html | 21 + playground/package.json | 56 + playground/pnpm-lock.yaml | 3345 +++++++++++++++++++ playground/scripts/check-bundle-size.ts | 17 + playground/scripts/merge-licenses.ts | 30 + playground/scripts/worker-license-plugin.ts | 74 + playground/src/main.tsx | 3 + playground/tsconfig.json | 31 + playground/vite.config.ts | 33 + playground/vitest.config.ts | 29 + 23 files changed, 4256 insertions(+), 35 deletions(-) create mode 100644 pkg/cli/playground_assets.go create mode 100644 playground/.gitignore create mode 100644 playground/.oxfmtrc.json create mode 100644 playground/.oxlintrc.json create mode 100644 playground/index.html create mode 100644 playground/package.json create mode 100644 playground/pnpm-lock.yaml create mode 100644 playground/scripts/check-bundle-size.ts create mode 100644 playground/scripts/merge-licenses.ts create mode 100644 playground/scripts/worker-license-plugin.ts create mode 100644 playground/src/main.tsx create mode 100644 playground/tsconfig.json create mode 100644 playground/vite.config.ts create mode 100644 playground/vitest.config.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6056ed25cb..83c8dbde64 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,6 +10,10 @@ updates: interval: "weekly" allow: - dependency-type: "direct" + - package-ecosystem: "npm" + directory: "/playground" + schedule: + interval: "weekly" - package-ecosystem: "cargo" directory: "/crates" schedule: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 142080868b..2c3c04354e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -63,6 +63,103 @@ jobs: steps: - run: echo "supported_pythons=$SUPPORTED_PYTHONS" + playground-changes: + name: Detect playground changes + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + changed: ${{ steps.changes.outputs.changed }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Check playground sources + id: changes + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before }} + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" || -z "$BASE_SHA" || "$BASE_SHA" =~ ^0+$ ]]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + elif git diff --quiet "$BASE_SHA" "$GITHUB_SHA" -- \ + playground \ + 'pkg/cli/playground*' \ + pkg/cli/root.go \ + cmd/cog \ + .goreleaser.yaml \ + mise.toml \ + mise.lock \ + go.mod \ + go.sum \ + .github/workflows/ci.yaml \ + .github/workflows/release-build.yaml; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + fmt-playground: + name: Format playground + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: jdx/mise-action@v4 + with: + version: 2026.4.27 + cache_key_prefix: mise-ci-${{ github.job }} + - name: Check formatting + run: mise run fmt:playground + + lint-playground: + name: Lint playground + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: jdx/mise-action@v4 + with: + version: 2026.4.27 + cache_key_prefix: mise-ci-${{ github.job }} + - name: Lint + run: mise run lint:playground + + test-playground: + name: Test playground + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: jdx/mise-action@v4 + with: + version: 2026.4.27 + cache_key_prefix: mise-ci-${{ github.job }} + - name: Install dependencies + run: mise run playground:install + - name: Test + run: pnpm --dir playground exec vitest run --passWithNoTests + + playground-check: + name: Check playground + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: jdx/mise-action@v4 + with: + version: 2026.4.27 + cache_key_prefix: mise-ci-${{ github.job }} + - name: Type check + run: mise run typecheck:playground + - name: Build embedded assets + run: mise run --force build:playground + - name: Test embedded assets + run: go test -tags playground_assets ./pkg/cli -run '^TestPlaygroundServesUI$' + - name: Upload playground assets + uses: actions/upload-artifact@v6 + with: + name: PlaygroundAssets + path: pkg/cli/playground + # ============================================================================= # Version Check - Validates VERSION.txt format and sync # ============================================================================= @@ -183,6 +280,7 @@ jobs: build-cog: name: Build cog CLI + needs: [playground-changes, playground-check] runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -193,6 +291,11 @@ jobs: with: version: 2026.4.27 cache_key_prefix: mise-ci-${{ github.job }} + - name: Download verified playground assets + uses: actions/download-artifact@v8 + with: + name: PlaygroundAssets + path: pkg/cli/playground - name: Get version from VERSION.txt id: version run: echo "version=$(tr -d '[:space:]' < VERSION.txt)" >> "$GITHUB_OUTPUT" @@ -696,6 +799,11 @@ jobs: name: CI Complete needs: - setup + - playground-changes + - fmt-playground + - lint-playground + - test-playground + - playground-check - version-check - build-cog - build-sdk @@ -725,6 +833,11 @@ jobs: - name: Check job results run: | echo "Job results:" + echo " playground-changes: ${{ needs.playground-changes.result }}" + echo " fmt-playground: ${{ needs.fmt-playground.result }}" + echo " lint-playground: ${{ needs.lint-playground.result }}" + echo " test-playground: ${{ needs.test-playground.result }}" + echo " playground-check: ${{ needs.playground-check.result }}" echo " version-check: ${{ needs.version-check.result }}" echo " build-cog: ${{ needs.build-cog.result }}" echo " build-sdk: ${{ needs.build-sdk.result }}" @@ -748,38 +861,42 @@ jobs: echo " integration-shards: ${{ needs.integration-shards.result }}" echo " test-integration: ${{ needs.test-integration.result }}" - # Fail if any required job failed. - # lint-rust-advisories is excluded: it uses continue-on-error because - # advisory DB updates shouldn't block unrelated PRs. FAILED=false - for result in \ - "${{ needs.setup.result }}" \ - "${{ needs.version-check.result }}" \ - "${{ needs.build-cog.result }}" \ - "${{ needs.build-sdk.result }}" \ - "${{ needs.build-rust.result }}" \ - "${{ needs.fmt-go.result }}" \ - "${{ needs.fmt-rust.result }}" \ - "${{ needs.fmt-python.result }}" \ - "${{ needs.check-llm-docs.result }}" \ - "${{ needs.check-stubs.result }}" \ - "${{ needs.lint-go.result }}" \ - "${{ needs.lint-rust.result }}" \ - "${{ needs.lint-rust-deny.result }}" \ - "${{ needs.lint-python.result }}" \ - "${{ needs.lint-docs.result }}" \ - "${{ needs.test-go.result }}" \ - "${{ needs.fuzz-go.result }}" \ - "${{ needs.test-rust.result }}" \ - "${{ needs.test-python.result }}" \ - "${{ needs.test-coglet-python.result }}" \ - "${{ needs.integration-shards.result }}" \ - "${{ needs.test-integration.result }}" - do - if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then + check_success() { + if [ "$2" != "success" ]; then + echo "::error::$1 finished with unexpected result: $2" FAILED=true fi - done + } + + # lint-rust-advisories is informational and intentionally excluded. + check_success setup "${{ needs.setup.result }}" + check_success playground-changes "${{ needs.playground-changes.result }}" + check_success fmt-playground "${{ needs.fmt-playground.result }}" + check_success lint-playground "${{ needs.lint-playground.result }}" + check_success test-playground "${{ needs.test-playground.result }}" + check_success playground-check "${{ needs.playground-check.result }}" + check_success version-check "${{ needs.version-check.result }}" + check_success build-cog "${{ needs.build-cog.result }}" + check_success build-sdk "${{ needs.build-sdk.result }}" + check_success build-rust "${{ needs.build-rust.result }}" + check_success fmt-go "${{ needs.fmt-go.result }}" + check_success fmt-rust "${{ needs.fmt-rust.result }}" + check_success fmt-python "${{ needs.fmt-python.result }}" + check_success check-llm-docs "${{ needs.check-llm-docs.result }}" + check_success check-stubs "${{ needs.check-stubs.result }}" + check_success lint-go "${{ needs.lint-go.result }}" + check_success lint-rust "${{ needs.lint-rust.result }}" + check_success lint-rust-deny "${{ needs.lint-rust-deny.result }}" + check_success lint-python "${{ needs.lint-python.result }}" + check_success lint-docs "${{ needs.lint-docs.result }}" + check_success test-go "${{ needs.test-go.result }}" + check_success fuzz-go "${{ needs.fuzz-go.result }}" + check_success test-rust "${{ needs.test-rust.result }}" + check_success test-python "${{ needs.test-python.result }}" + check_success test-coglet-python "${{ needs.test-coglet-python.result }}" + check_success integration-shards "${{ needs.integration-shards.result }}" + check_success test-integration "${{ needs.test-integration.result }}" if [ "$FAILED" = "true" ]; then echo "::error::Some jobs failed or were cancelled" diff --git a/.github/workflows/release-build.yaml b/.github/workflows/release-build.yaml index 015bc34dc2..fa27317f3a 100644 --- a/.github/workflows/release-build.yaml +++ b/.github/workflows/release-build.yaml @@ -25,7 +25,7 @@ on: tags: ["v[0-9]+.[0-9]+.[0-9]+*"] permissions: - contents: write + contents: read env: CARGO_TERM_COLOR: always @@ -248,6 +248,25 @@ jobs: name: sdk-dist path: dist/* + build-playground: + name: Build playground assets + needs: verify-tag + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: jdx/mise-action@v4 + with: + version: 2026.4.27 + cache_key_prefix: mise-rel-${{ github.job }} + - name: Build embedded playground assets + run: mise run --force build:playground + - name: Upload playground assets + uses: actions/upload-artifact@v6 + with: + name: playground-assets + path: pkg/cli/playground + build-coglet-wheels: name: Build coglet wheel (${{ matrix.target }}) needs: verify-tag @@ -298,7 +317,9 @@ jobs: create-release: name: Create release - needs: [verify-tag, build-sdk, build-coglet-wheels] + permissions: + contents: write + needs: [verify-tag, build-sdk, build-coglet-wheels, build-playground] # macOS arm64 runner: native clang for darwin targets, zig for linux targets. # CGo required for go-tree-sitter (static Python schema parser). runs-on: macos-14 @@ -327,6 +348,12 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Download verified playground assets + uses: actions/download-artifact@v8 + with: + name: playground-assets + path: pkg/cli/playground + # Goreleaser builds CLI binaries and creates draft release - name: Build CLI and create draft release uses: goreleaser/goreleaser-action@v7 diff --git a/.gitignore b/.gitignore index 4c1c6b6b9d..35ee93229c 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,10 @@ target # Generated docs /site + +# Playground frontend +/playground/node_modules/ +/playground/coverage/ +/playground/playwright-report/ +/playground/test-results/ +/pkg/cli/playground/ diff --git a/.golangci.yaml b/.golangci.yaml index 17b97cfad8..0d795ff98a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -64,6 +64,8 @@ linters: - weakCond exclusions: generated: strict + paths: + - playground/node_modules rules: # Exclude some linters from running on test files - path: '(.+)_test\.go' diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 33a9dd13cc..bdb5eb5b14 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -18,6 +18,8 @@ builds: - amd64 - arm64 main: ./cmd/cog + tags: + - playground_assets ldflags: # COG_VERSION (from VERSION.txt via mise build:cog or CI) overrides git-derived version for snapshots. # For tagged releases, COG_VERSION is unset and envOrDefault falls back to .Version (from tag). diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 18d9b90fc6..5396b71f52 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -7,6 +7,8 @@ ignores: - "site/**" - "docs/llms.txt" - "CLAUDE.md" + - "playground/node_modules/**" + - "playground/coverage/**" config: default: false diff --git a/mise.lock b/mise.lock index 3fe3fb9ec2..f26db49f81 100644 --- a/mise.lock +++ b/mise.lock @@ -16,10 +16,18 @@ url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/carg checksum = "sha256:0e8c2aa59128612c90d9e09c02204e912f29a5b8d9a64671b94608cbe09e064f" url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-unknown-linux-musl.tar.gz" +[tools."aqua:EmbarkStudios/cargo-deny"."platforms.linux-x64-baseline"] +checksum = "sha256:0e8c2aa59128612c90d9e09c02204e912f29a5b8d9a64671b94608cbe09e064f" +url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-unknown-linux-musl.tar.gz" + [tools."aqua:EmbarkStudios/cargo-deny"."platforms.linux-x64-musl"] checksum = "sha256:0e8c2aa59128612c90d9e09c02204e912f29a5b8d9a64671b94608cbe09e064f" url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-unknown-linux-musl.tar.gz" +[tools."aqua:EmbarkStudios/cargo-deny"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:0e8c2aa59128612c90d9e09c02204e912f29a5b8d9a64671b94608cbe09e064f" +url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-unknown-linux-musl.tar.gz" + [tools."aqua:EmbarkStudios/cargo-deny"."platforms.macos-arm64"] checksum = "sha256:a22f2023c06f3eefd099a5d42dd828fd4fa74d1e1c167bd1dbc3cf59ad62ded0" url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-aarch64-apple-darwin.tar.gz" @@ -28,10 +36,18 @@ url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/carg checksum = "sha256:c42163655413f7e872638cd8c4345a327b512ef0ab99109e9cced691b95af5fb" url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-apple-darwin.tar.gz" +[tools."aqua:EmbarkStudios/cargo-deny"."platforms.macos-x64-baseline"] +checksum = "sha256:c42163655413f7e872638cd8c4345a327b512ef0ab99109e9cced691b95af5fb" +url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-apple-darwin.tar.gz" + [tools."aqua:EmbarkStudios/cargo-deny"."platforms.windows-x64"] checksum = "sha256:413f0e2ce780d0d14ba8e9339f9fb033419a8a971ec7714faec518e4a664bdb0" url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-pc-windows-msvc.tar.gz" +[tools."aqua:EmbarkStudios/cargo-deny"."platforms.windows-x64-baseline"] +checksum = "sha256:413f0e2ce780d0d14ba8e9339f9fb033419a8a971ec7714faec518e4a664bdb0" +url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-pc-windows-msvc.tar.gz" + [[tools."aqua:golangci/golangci-lint"]] version = "2.10.1" backend = "aqua:golangci/golangci-lint" @@ -51,11 +67,21 @@ checksum = "sha256:dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09 url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-amd64.tar.gz" provenance = "github-attestations" +[tools."aqua:golangci/golangci-lint"."platforms.linux-x64-baseline"] +checksum = "sha256:dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09c99" +url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-amd64.tar.gz" +provenance = "github-attestations" + [tools."aqua:golangci/golangci-lint"."platforms.linux-x64-musl"] checksum = "sha256:dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09c99" url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-amd64.tar.gz" provenance = "github-attestations" +[tools."aqua:golangci/golangci-lint"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09c99" +url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-amd64.tar.gz" +provenance = "github-attestations" + [tools."aqua:golangci/golangci-lint"."platforms.macos-arm64"] checksum = "sha256:03bfadf67e52b441b7ec21305e501c717df93c959836d66c7f97312654acb297" url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-darwin-arm64.tar.gz" @@ -66,11 +92,21 @@ checksum = "sha256:66fb0da81b8033b477f97eea420d4b46b230ca172b8bb87c6610109f3772b url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-darwin-amd64.tar.gz" provenance = "github-attestations" +[tools."aqua:golangci/golangci-lint"."platforms.macos-x64-baseline"] +checksum = "sha256:66fb0da81b8033b477f97eea420d4b46b230ca172b8bb87c6610109f3772b6b6" +url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-darwin-amd64.tar.gz" +provenance = "github-attestations" + [tools."aqua:golangci/golangci-lint"."platforms.windows-x64"] checksum = "sha256:c60c87695e79db8e320f0e5be885059859de52bb5ee5f11be5577828570bc2a3" url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-windows-amd64.zip" provenance = "github-attestations" +[tools."aqua:golangci/golangci-lint"."platforms.windows-x64-baseline"] +checksum = "sha256:c60c87695e79db8e320f0e5be885059859de52bb5ee5f11be5577828570bc2a3" +url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-windows-amd64.zip" +provenance = "github-attestations" + [[tools."aqua:gotestyourself/gotestsum"]] version = "1.13.0" backend = "aqua:gotestyourself/gotestsum" @@ -87,10 +123,18 @@ url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/got checksum = "sha256:11ccddeaf708ef228889f9fe2f68291a75b27013ddfc3b18156e094f5f40e8ee" url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz" +[tools."aqua:gotestyourself/gotestsum"."platforms.linux-x64-baseline"] +checksum = "sha256:11ccddeaf708ef228889f9fe2f68291a75b27013ddfc3b18156e094f5f40e8ee" +url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz" + [tools."aqua:gotestyourself/gotestsum"."platforms.linux-x64-musl"] checksum = "sha256:11ccddeaf708ef228889f9fe2f68291a75b27013ddfc3b18156e094f5f40e8ee" url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz" +[tools."aqua:gotestyourself/gotestsum"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:11ccddeaf708ef228889f9fe2f68291a75b27013ddfc3b18156e094f5f40e8ee" +url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz" + [tools."aqua:gotestyourself/gotestsum"."platforms.macos-arm64"] checksum = "sha256:509cb27aef747f48faf9bce424f59dcf79572c905204b990ee935bbfcc7fa0e9" url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_darwin_arm64.tar.gz" @@ -99,10 +143,18 @@ url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/got checksum = "sha256:99529350f4c7b780b1efc543ca0d9721b09f0a4228f0efa9281261f58fefa05a" url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_darwin_amd64.tar.gz" +[tools."aqua:gotestyourself/gotestsum"."platforms.macos-x64-baseline"] +checksum = "sha256:99529350f4c7b780b1efc543ca0d9721b09f0a4228f0efa9281261f58fefa05a" +url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_darwin_amd64.tar.gz" + [tools."aqua:gotestyourself/gotestsum"."platforms.windows-x64"] checksum = "sha256:fd5a6dc69e46a0970593e70d85a7e75f16714e9c61d6d72ccc324eb82df5bb8a" url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_windows_amd64.tar.gz" +[tools."aqua:gotestyourself/gotestsum"."platforms.windows-x64-baseline"] +checksum = "sha256:fd5a6dc69e46a0970593e70d85a7e75f16714e9c61d6d72ccc324eb82df5bb8a" +url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_windows_amd64.tar.gz" + [[tools."aqua:jqlang/jq"]] version = "1.8.1" backend = "aqua:jqlang/jq" @@ -122,11 +174,21 @@ checksum = "sha256:020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2 url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" provenance = "github-attestations" +[tools."aqua:jqlang/jq"."platforms.linux-x64-baseline"] +checksum = "sha256:020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d" +url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" +provenance = "github-attestations" + [tools."aqua:jqlang/jq"."platforms.linux-x64-musl"] checksum = "sha256:020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" provenance = "github-attestations" +[tools."aqua:jqlang/jq"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d" +url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" +provenance = "github-attestations" + [tools."aqua:jqlang/jq"."platforms.macos-arm64"] checksum = "sha256:a9fe3ea2f86dfc72f6728417521ec9067b343277152b114f4e98d8cb0e263603" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-macos-arm64" @@ -137,11 +199,21 @@ checksum = "sha256:e80dbe0d2a2597e3c11c404f03337b981d74b4a8504b70586c354b7697a7c url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-macos-amd64" provenance = "github-attestations" +[tools."aqua:jqlang/jq"."platforms.macos-x64-baseline"] +checksum = "sha256:e80dbe0d2a2597e3c11c404f03337b981d74b4a8504b70586c354b7697a7c27f" +url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-macos-amd64" +provenance = "github-attestations" + [tools."aqua:jqlang/jq"."platforms.windows-x64"] checksum = "sha256:23cb60a1354eed6bcc8d9b9735e8c7b388cd1fdcb75726b93bc299ef22dd9334" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-windows-amd64.exe" provenance = "github-attestations" +[tools."aqua:jqlang/jq"."platforms.windows-x64-baseline"] +checksum = "sha256:23cb60a1354eed6bcc8d9b9735e8c7b388cd1fdcb75726b93bc299ef22dd9334" +url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-windows-amd64.exe" +provenance = "github-attestations" + [[tools."aqua:mitsuhiko/insta"]] version = "1.46.0" backend = "aqua:mitsuhiko/insta" @@ -150,10 +222,18 @@ backend = "aqua:mitsuhiko/insta" checksum = "sha256:592f7f8ddd465f0b8f2e7121bbe6bace97b475330b7f3c0ac62e504c1de6b967" url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-unknown-linux-musl.tar.xz" +[tools."aqua:mitsuhiko/insta"."platforms.linux-x64-baseline"] +checksum = "sha256:592f7f8ddd465f0b8f2e7121bbe6bace97b475330b7f3c0ac62e504c1de6b967" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-unknown-linux-musl.tar.xz" + [tools."aqua:mitsuhiko/insta"."platforms.linux-x64-musl"] checksum = "sha256:592f7f8ddd465f0b8f2e7121bbe6bace97b475330b7f3c0ac62e504c1de6b967" url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-unknown-linux-musl.tar.xz" +[tools."aqua:mitsuhiko/insta"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:592f7f8ddd465f0b8f2e7121bbe6bace97b475330b7f3c0ac62e504c1de6b967" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-unknown-linux-musl.tar.xz" + [tools."aqua:mitsuhiko/insta"."platforms.macos-arm64"] checksum = "sha256:c32a785806a7b329330fefced808c0ba7017416c8a7ea24c0a8363ad66d1aeed" url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-aarch64-apple-darwin.tar.xz" @@ -162,10 +242,18 @@ url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-a checksum = "sha256:4a2e8bc9b3e7591fd96580cbb4c79cae062060f9482719bb32bc3932eff08fba" url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-apple-darwin.tar.xz" +[tools."aqua:mitsuhiko/insta"."platforms.macos-x64-baseline"] +checksum = "sha256:4a2e8bc9b3e7591fd96580cbb4c79cae062060f9482719bb32bc3932eff08fba" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-apple-darwin.tar.xz" + [tools."aqua:mitsuhiko/insta"."platforms.windows-x64"] checksum = "sha256:d13a207264e10644d6995bdb332d7cc7353ffc53a0199f4e20376923016247ab" url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-pc-windows-msvc.zip" +[tools."aqua:mitsuhiko/insta"."platforms.windows-x64-baseline"] +checksum = "sha256:d13a207264e10644d6995bdb332d7cc7353ffc53a0199f4e20376923016247ab" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-pc-windows-msvc.zip" + [[tools."aqua:rust-cross/cargo-zigbuild"]] version = "0.20.1" backend = "aqua:rust-cross/cargo-zigbuild" @@ -182,10 +270,18 @@ url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/ca checksum = "sha256:742ed281f15fef6eaf49535ac10ffe98fb57913d0c4c88d6888d794043c05618" url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.x86_64-unknown-linux-musl.tar.gz" +[tools."aqua:rust-cross/cargo-zigbuild"."platforms.linux-x64-baseline"] +checksum = "sha256:742ed281f15fef6eaf49535ac10ffe98fb57913d0c4c88d6888d794043c05618" +url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.x86_64-unknown-linux-musl.tar.gz" + [tools."aqua:rust-cross/cargo-zigbuild"."platforms.linux-x64-musl"] checksum = "sha256:742ed281f15fef6eaf49535ac10ffe98fb57913d0c4c88d6888d794043c05618" url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.x86_64-unknown-linux-musl.tar.gz" +[tools."aqua:rust-cross/cargo-zigbuild"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:742ed281f15fef6eaf49535ac10ffe98fb57913d0c4c88d6888d794043c05618" +url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.x86_64-unknown-linux-musl.tar.gz" + [tools."aqua:rust-cross/cargo-zigbuild"."platforms.macos-arm64"] checksum = "sha256:d5c7ac2e6f25fb76083dff84640cdf679c5da858b9c97631555c3185be2fbf35" url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.apple-darwin.tar.gz" @@ -194,10 +290,18 @@ url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/ca checksum = "sha256:d5c7ac2e6f25fb76083dff84640cdf679c5da858b9c97631555c3185be2fbf35" url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.apple-darwin.tar.gz" +[tools."aqua:rust-cross/cargo-zigbuild"."platforms.macos-x64-baseline"] +checksum = "sha256:d5c7ac2e6f25fb76083dff84640cdf679c5da858b9c97631555c3185be2fbf35" +url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.apple-darwin.tar.gz" + [tools."aqua:rust-cross/cargo-zigbuild"."platforms.windows-x64"] checksum = "sha256:b0bb728ba068ee61342f40a2124b3d8d234af8f716dd416b7c1f794dfeb4e478" url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.windows-x64.zip" +[tools."aqua:rust-cross/cargo-zigbuild"."platforms.windows-x64-baseline"] +checksum = "sha256:b0bb728ba068ee61342f40a2124b3d8d234af8f716dd416b7c1f794dfeb4e478" +url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.windows-x64.zip" + [[tools."aqua:rust-lang/rustup"]] version = "1.28.2" backend = "aqua:rust-lang/rustup" @@ -214,10 +318,18 @@ url = "https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-unknown-linux- checksum = "sha256:20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init" +[tools."aqua:rust-lang/rustup"."platforms.linux-x64-baseline"] +checksum = "sha256:20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init" + [tools."aqua:rust-lang/rustup"."platforms.linux-x64-musl"] checksum = "sha256:e6599a1c7be58a2d8eaca66a80e0dc006d87bbcf780a58b7343d6e14c1605cb2" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-musl/rustup-init" +[tools."aqua:rust-lang/rustup"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:e6599a1c7be58a2d8eaca66a80e0dc006d87bbcf780a58b7343d6e14c1605cb2" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-musl/rustup-init" + [tools."aqua:rust-lang/rustup"."platforms.macos-arm64"] checksum = "sha256:20ef5516c31b1ac2290084199ba77dbbcaa1406c45c1d978ca68558ef5964ef5" url = "https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-apple-darwin/rustup-init" @@ -226,10 +338,18 @@ url = "https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-apple-darwin/r checksum = "sha256:9c331076f62b4d0edeae63d9d1c9442d5fe39b37b05025ec8d41c5ed35486496" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-apple-darwin/rustup-init" +[tools."aqua:rust-lang/rustup"."platforms.macos-x64-baseline"] +checksum = "sha256:9c331076f62b4d0edeae63d9d1c9442d5fe39b37b05025ec8d41c5ed35486496" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-apple-darwin/rustup-init" + [tools."aqua:rust-lang/rustup"."platforms.windows-x64"] checksum = "sha256:88d8258dcf6ae4f7a80c7d1088e1f36fa7025a1cfd1343731b4ee6f385121fc0" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-pc-windows-msvc/rustup-init.exe" +[tools."aqua:rust-lang/rustup"."platforms.windows-x64-baseline"] +checksum = "sha256:88d8258dcf6ae4f7a80c7d1088e1f36fa7025a1cfd1343731b4ee6f385121fc0" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-pc-windows-msvc/rustup-init.exe" + [[tools."aqua:rust-lang/rustup/rustup-init"]] version = "1.28.2" backend = "aqua:rust-lang/rustup/rustup-init" @@ -246,10 +366,18 @@ url = "https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-unknown-linux- checksum = "sha256:20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init" +[tools."aqua:rust-lang/rustup/rustup-init"."platforms.linux-x64-baseline"] +checksum = "sha256:20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init" + [tools."aqua:rust-lang/rustup/rustup-init"."platforms.linux-x64-musl"] checksum = "sha256:e6599a1c7be58a2d8eaca66a80e0dc006d87bbcf780a58b7343d6e14c1605cb2" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-musl/rustup-init" +[tools."aqua:rust-lang/rustup/rustup-init"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:e6599a1c7be58a2d8eaca66a80e0dc006d87bbcf780a58b7343d6e14c1605cb2" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-musl/rustup-init" + [tools."aqua:rust-lang/rustup/rustup-init"."platforms.macos-arm64"] checksum = "sha256:20ef5516c31b1ac2290084199ba77dbbcaa1406c45c1d978ca68558ef5964ef5" url = "https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-apple-darwin/rustup-init" @@ -258,10 +386,18 @@ url = "https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-apple-darwin/r checksum = "sha256:9c331076f62b4d0edeae63d9d1c9442d5fe39b37b05025ec8d41c5ed35486496" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-apple-darwin/rustup-init" +[tools."aqua:rust-lang/rustup/rustup-init"."platforms.macos-x64-baseline"] +checksum = "sha256:9c331076f62b4d0edeae63d9d1c9442d5fe39b37b05025ec8d41c5ed35486496" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-apple-darwin/rustup-init" + [tools."aqua:rust-lang/rustup/rustup-init"."platforms.windows-x64"] checksum = "sha256:88d8258dcf6ae4f7a80c7d1088e1f36fa7025a1cfd1343731b4ee6f385121fc0" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-pc-windows-msvc/rustup-init.exe" +[tools."aqua:rust-lang/rustup/rustup-init"."platforms.windows-x64-baseline"] +checksum = "sha256:88d8258dcf6ae4f7a80c7d1088e1f36fa7025a1cfd1343731b4ee6f385121fc0" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-pc-windows-msvc/rustup-init.exe" + [[tools."aqua:vektra/mockery"]] version = "3.7.0" backend = "aqua:vektra/mockery" @@ -278,10 +414,18 @@ url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_ checksum = "sha256:5e10597d9741d7b8cda699f044b24c9e8b51467dce413e79516d7cd9373c2896" url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Linux_x86_64.tar.gz" +[tools."aqua:vektra/mockery"."platforms.linux-x64-baseline"] +checksum = "sha256:5e10597d9741d7b8cda699f044b24c9e8b51467dce413e79516d7cd9373c2896" +url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Linux_x86_64.tar.gz" + [tools."aqua:vektra/mockery"."platforms.linux-x64-musl"] checksum = "sha256:5e10597d9741d7b8cda699f044b24c9e8b51467dce413e79516d7cd9373c2896" url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Linux_x86_64.tar.gz" +[tools."aqua:vektra/mockery"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:5e10597d9741d7b8cda699f044b24c9e8b51467dce413e79516d7cd9373c2896" +url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Linux_x86_64.tar.gz" + [tools."aqua:vektra/mockery"."platforms.macos-arm64"] checksum = "sha256:c0a8ea54d602c071775d1692321d385903ccc80bd2d1ae9f230539a195643d7d" url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Darwin_arm64.tar.gz" @@ -290,10 +434,18 @@ url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_ checksum = "sha256:36e7357fd160689a9a8ae57726e099761159845e2749ba31091589853a5a894d" url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Darwin_x86_64.tar.gz" +[tools."aqua:vektra/mockery"."platforms.macos-x64-baseline"] +checksum = "sha256:36e7357fd160689a9a8ae57726e099761159845e2749ba31091589853a5a894d" +url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Darwin_x86_64.tar.gz" + [tools."aqua:vektra/mockery"."platforms.windows-x64"] checksum = "sha256:76d524ad1740cd02ed621d90015f538fdb53cf6cd4a1ad4d289db017fb69bd0e" url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Windows_x86_64.tar.gz" +[tools."aqua:vektra/mockery"."platforms.windows-x64-baseline"] +checksum = "sha256:76d524ad1740cd02ed621d90015f538fdb53cf6cd4a1ad4d289db017fb69bd0e" +url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Windows_x86_64.tar.gz" + [[tools.cargo-binstall]] version = "1.16.6" backend = "aqua:cargo-bins/cargo-binstall" @@ -310,10 +462,18 @@ url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/ca checksum = "sha256:3225eea8041c30d7462761a481883e3aa8fe31c58def4b6c8dd91b7c80973df0" url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-unknown-linux-musl.tgz" +[tools.cargo-binstall."platforms.linux-x64-baseline"] +checksum = "sha256:3225eea8041c30d7462761a481883e3aa8fe31c58def4b6c8dd91b7c80973df0" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-unknown-linux-musl.tgz" + [tools.cargo-binstall."platforms.linux-x64-musl"] checksum = "sha256:3225eea8041c30d7462761a481883e3aa8fe31c58def4b6c8dd91b7c80973df0" url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-unknown-linux-musl.tgz" +[tools.cargo-binstall."platforms.linux-x64-musl-baseline"] +checksum = "sha256:3225eea8041c30d7462761a481883e3aa8fe31c58def4b6c8dd91b7c80973df0" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-unknown-linux-musl.tgz" + [tools.cargo-binstall."platforms.macos-arm64"] checksum = "sha256:30543b378b96fbddabee1edfaccde7914dd2f851f02c560de859f81a21ab665b" url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-aarch64-apple-darwin.zip" @@ -322,10 +482,18 @@ url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/ca checksum = "sha256:633dc2f381f7000d8ba3c02eb24b2f290bf0154372bafe8d8094d777f129f21d" url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-apple-darwin.zip" +[tools.cargo-binstall."platforms.macos-x64-baseline"] +checksum = "sha256:633dc2f381f7000d8ba3c02eb24b2f290bf0154372bafe8d8094d777f129f21d" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-apple-darwin.zip" + [tools.cargo-binstall."platforms.windows-x64"] checksum = "sha256:fca962c3d12ae6192280111074db073c15abad3ba162a1a5a2af0f6f01872114" url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-pc-windows-msvc.zip" +[tools.cargo-binstall."platforms.windows-x64-baseline"] +checksum = "sha256:fca962c3d12ae6192280111074db073c15abad3ba162a1a5a2af0f6f01872114" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-pc-windows-msvc.zip" + [[tools."cargo:cargo-nextest"]] version = "0.9.120" backend = "cargo:cargo-nextest" @@ -350,10 +518,18 @@ url = "https://dl.google.com/go/go1.25.6.linux-arm64.tar.gz" checksum = "sha256:f022b6aad78e362bcba9b0b94d09ad58c5a70c6ba3b7582905fababf5fe0181a" url = "https://dl.google.com/go/go1.25.6.linux-amd64.tar.gz" +[tools.go."platforms.linux-x64-baseline"] +checksum = "sha256:f022b6aad78e362bcba9b0b94d09ad58c5a70c6ba3b7582905fababf5fe0181a" +url = "https://dl.google.com/go/go1.25.6.linux-amd64.tar.gz" + [tools.go."platforms.linux-x64-musl"] checksum = "sha256:f022b6aad78e362bcba9b0b94d09ad58c5a70c6ba3b7582905fababf5fe0181a" url = "https://dl.google.com/go/go1.25.6.linux-amd64.tar.gz" +[tools.go."platforms.linux-x64-musl-baseline"] +checksum = "sha256:f022b6aad78e362bcba9b0b94d09ad58c5a70c6ba3b7582905fababf5fe0181a" +url = "https://dl.google.com/go/go1.25.6.linux-amd64.tar.gz" + [tools.go."platforms.macos-arm64"] checksum = "sha256:984521ae978a5377c7d782fd2dd953291840d7d3d0bd95781a1f32f16d94a006" url = "https://dl.google.com/go/go1.25.6.darwin-arm64.tar.gz" @@ -362,18 +538,78 @@ url = "https://dl.google.com/go/go1.25.6.darwin-arm64.tar.gz" checksum = "sha256:e2b5b237f5c262931b8e280ac4b8363f156e19bfad5270c099998932819670b7" url = "https://dl.google.com/go/go1.25.6.darwin-amd64.tar.gz" +[tools.go."platforms.macos-x64-baseline"] +checksum = "sha256:e2b5b237f5c262931b8e280ac4b8363f156e19bfad5270c099998932819670b7" +url = "https://dl.google.com/go/go1.25.6.darwin-amd64.tar.gz" + [tools.go."platforms.windows-x64"] checksum = "sha256:19b4733b727ba5c611b5656187f3ac367d278d64c3d4199a845e39c0fdac5335" url = "https://dl.google.com/go/go1.25.6.windows-amd64.zip" +[tools.go."platforms.windows-x64-baseline"] +checksum = "sha256:19b4733b727ba5c611b5656187f3ac367d278d64c3d4199a845e39c0fdac5335" +url = "https://dl.google.com/go/go1.25.6.windows-amd64.zip" + [[tools."go:golang.org/x/tools/cmd/goimports"]] version = "0.44.0" backend = "go:golang.org/x/tools/cmd/goimports" +[[tools.node]] +version = "24.18.0" +backend = "core:node" + +[tools.node."platforms.linux-arm64"] +checksum = "sha256:6b4484c2190274175df9aa8f28e2d758a819cb1c1fe6ab481e2f95b463ab8508" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-linux-arm64.tar.gz" + +[tools.node."platforms.linux-arm64-musl"] +checksum = "sha256:b1c6c2dc31b46dd8fb2322f4fe75b07e775c5120bc37251deeea28f529d4567b" +url = "https://unofficial-builds.nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-arm64-musl.tar.gz" + +[tools.node."platforms.linux-x64"] +checksum = "sha256:783130984963db7ba9cbd01089eaf2c2efb055c7c1693c943174b967b3050cb8" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-linux-x64.tar.gz" + +[tools.node."platforms.linux-x64-baseline"] +checksum = "sha256:783130984963db7ba9cbd01089eaf2c2efb055c7c1693c943174b967b3050cb8" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-linux-x64.tar.gz" + +[tools.node."platforms.linux-x64-musl"] +checksum = "sha256:ea58409911e141ec6b19d9178efa2d9185a13295005b1cbf5521b3157eed1d95" +url = "https://unofficial-builds.nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-x64-musl.tar.gz" + +[tools.node."platforms.linux-x64-musl-baseline"] +checksum = "sha256:ea58409911e141ec6b19d9178efa2d9185a13295005b1cbf5521b3157eed1d95" +url = "https://unofficial-builds.nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-x64-musl.tar.gz" + +[tools.node."platforms.macos-arm64"] +checksum = "sha256:e1a97e14c99c803e96c7339403282ea05a499c32f8d83defe9ef5ec66f979ed1" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-darwin-arm64.tar.gz" + +[tools.node."platforms.macos-x64"] +checksum = "sha256:dfd0dbd3e721503434df7b7205e719f61b3a3a31b2bcf9729b8b91fea240f080" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-darwin-x64.tar.gz" + +[tools.node."platforms.macos-x64-baseline"] +checksum = "sha256:dfd0dbd3e721503434df7b7205e719f61b3a3a31b2bcf9729b8b91fea240f080" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-darwin-x64.tar.gz" + +[tools.node."platforms.windows-x64"] +checksum = "sha256:0ae68406b42d7725661da979b1403ec9926da205c6770827f33aac9d8f26e821" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-win-x64.zip" + +[tools.node."platforms.windows-x64-baseline"] +checksum = "sha256:0ae68406b42d7725661da979b1403ec9926da205c6770827f33aac9d8f26e821" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-win-x64.zip" + [[tools."npm:markdownlint-cli2"]] version = "0.22.0" backend = "npm:markdownlint-cli2" +[[tools."npm:pnpm"]] +version = "11.11.0" +backend = "npm:pnpm" + [[tools."npm:prettier"]] version = "3.6.2" backend = "npm:prettier" @@ -402,10 +638,18 @@ url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-aarch64- checksum = "sha256:2fe394f493318551f271277a2228e56b31ca69a4842483e5709af4548f342bc3" url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-unknown-linux-musl.tar.gz" +[tools.ruff."platforms.linux-x64-baseline"] +checksum = "sha256:2fe394f493318551f271277a2228e56b31ca69a4842483e5709af4548f342bc3" +url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-unknown-linux-musl.tar.gz" + [tools.ruff."platforms.linux-x64-musl"] checksum = "sha256:2fe394f493318551f271277a2228e56b31ca69a4842483e5709af4548f342bc3" url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-unknown-linux-musl.tar.gz" +[tools.ruff."platforms.linux-x64-musl-baseline"] +checksum = "sha256:2fe394f493318551f271277a2228e56b31ca69a4842483e5709af4548f342bc3" +url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-unknown-linux-musl.tar.gz" + [tools.ruff."platforms.macos-arm64"] checksum = "sha256:067d1a90da8add55614eff91990425883a092d8279d9e503258ff8be0f8e9c18" url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-aarch64-apple-darwin.tar.gz" @@ -414,10 +658,26 @@ url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-aarch64- checksum = "sha256:69e424a42ac3a7c6c7032ad96deb757c35c93c848b6ea329a3f4c605e6d89ef9" url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-apple-darwin.tar.gz" +[tools.ruff."platforms.macos-x64-baseline"] +checksum = "sha256:69e424a42ac3a7c6c7032ad96deb757c35c93c848b6ea329a3f4c605e6d89ef9" +url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-apple-darwin.tar.gz" + [tools.ruff."platforms.windows-x64"] checksum = "sha256:d2af4376053458f283d74980b49dc0d61d3ef9d9b8684c5b25bd64b73e11634a" url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-pc-windows-msvc.zip" +[tools.ruff."platforms.windows-x64-baseline"] +checksum = "sha256:d2af4376053458f283d74980b49dc0d61d3ef9d9b8684c5b25bd64b73e11634a" +url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-pc-windows-msvc.zip" + +[[tools.rust]] +version = "1.93.0" +backend = "core:rust" + +[tools.rust.options] +components = "clippy,rust-analyzer,rustfmt" +targets = "aarch64-apple-darwin,aarch64-unknown-linux-gnu,x86_64-unknown-linux-gnu" + [[tools.rust]] version = "1.93.0" backend = "core:rust" @@ -438,10 +698,18 @@ url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-aarch64-unkno checksum = "sha256:870e0f634d740df2a543dc977d68cae9bb073f1e70da93278b78622962a51189" url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-unknown-linux-musl.tar.gz" +[tools.ty."platforms.linux-x64-baseline"] +checksum = "sha256:870e0f634d740df2a543dc977d68cae9bb073f1e70da93278b78622962a51189" +url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-unknown-linux-musl.tar.gz" + [tools.ty."platforms.linux-x64-musl"] checksum = "sha256:870e0f634d740df2a543dc977d68cae9bb073f1e70da93278b78622962a51189" url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-unknown-linux-musl.tar.gz" +[tools.ty."platforms.linux-x64-musl-baseline"] +checksum = "sha256:870e0f634d740df2a543dc977d68cae9bb073f1e70da93278b78622962a51189" +url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-unknown-linux-musl.tar.gz" + [tools.ty."platforms.macos-arm64"] checksum = "sha256:5c256844da0401534908535d3c744ffb28db823e2e21ebc2aa45fa4b0599f6f5" url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-aarch64-apple-darwin.tar.gz" @@ -450,10 +718,18 @@ url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-aarch64-apple checksum = "sha256:eb839e72d317e381ad7c5804d61a6098b041b0b7bd0a207f429b8bfe9f445be0" url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-apple-darwin.tar.gz" +[tools.ty."platforms.macos-x64-baseline"] +checksum = "sha256:eb839e72d317e381ad7c5804d61a6098b041b0b7bd0a207f429b8bfe9f445be0" +url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-apple-darwin.tar.gz" + [tools.ty."platforms.windows-x64"] checksum = "sha256:d7cd33adf0113decee579fbf31f58b55e44195571043b22ed0e653bf60062d4a" url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-pc-windows-msvc.zip" +[tools.ty."platforms.windows-x64-baseline"] +checksum = "sha256:d7cd33adf0113decee579fbf31f58b55e44195571043b22ed0e653bf60062d4a" +url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-pc-windows-msvc.zip" + [[tools.uv]] version = "0.9.26" backend = "aqua:astral-sh/uv" @@ -473,11 +749,21 @@ checksum = "sha256:708b752876aeeb753257e1d55470569789e465684c1d3bc1760db26360b6c url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-musl.tar.gz" provenance = "github-attestations" +[tools.uv."platforms.linux-x64-baseline"] +checksum = "sha256:708b752876aeeb753257e1d55470569789e465684c1d3bc1760db26360b6c28b" +url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-musl.tar.gz" +provenance = "github-attestations" + [tools.uv."platforms.linux-x64-musl"] checksum = "sha256:708b752876aeeb753257e1d55470569789e465684c1d3bc1760db26360b6c28b" url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-musl.tar.gz" provenance = "github-attestations" +[tools.uv."platforms.linux-x64-musl-baseline"] +checksum = "sha256:708b752876aeeb753257e1d55470569789e465684c1d3bc1760db26360b6c28b" +url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-musl.tar.gz" +provenance = "github-attestations" + [tools.uv."platforms.macos-arm64"] checksum = "sha256:fcf0a9ea6599c6ae28a4c854ac6da76f2c889354d7c36ce136ef071f7ab9721f" url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.tar.gz" @@ -488,11 +774,21 @@ checksum = "sha256:171eb8c518313e157c5b4cec7b4f743bc6bab1bd23e09b646679a02d096a0 url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-apple-darwin.tar.gz" provenance = "github-attestations" +[tools.uv."platforms.macos-x64-baseline"] +checksum = "sha256:171eb8c518313e157c5b4cec7b4f743bc6bab1bd23e09b646679a02d096a047f" +url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-apple-darwin.tar.gz" +provenance = "github-attestations" + [tools.uv."platforms.windows-x64"] checksum = "sha256:eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036" url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-pc-windows-msvc.zip" provenance = "github-attestations" +[tools.uv."platforms.windows-x64-baseline"] +checksum = "sha256:eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036" +url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-pc-windows-msvc.zip" +provenance = "github-attestations" + [[tools.zig]] version = "0.15.2" backend = "core:zig" @@ -509,10 +805,18 @@ url = "https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz" checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" +[tools.zig."platforms.linux-x64-baseline"] +checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" + [tools.zig."platforms.linux-x64-musl"] checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" +[tools.zig."platforms.linux-x64-musl-baseline"] +checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" + [tools.zig."platforms.macos-arm64"] checksum = "sha256:3cc2bab367e185cdfb27501c4b30b1b0653c28d9f73df8dc91488e66ece5fa6b" url = "https://ziglang.org/download/0.15.2/zig-aarch64-macos-0.15.2.tar.xz" @@ -522,6 +826,14 @@ provenance = "minisign" checksum = "sha256:375b6909fc1495d16fc2c7db9538f707456bfc3373b14ee83fdd3e22b3d43f7f" url = "https://ziglang.org/download/0.15.2/zig-x86_64-macos-0.15.2.tar.xz" +[tools.zig."platforms.macos-x64-baseline"] +checksum = "sha256:375b6909fc1495d16fc2c7db9538f707456bfc3373b14ee83fdd3e22b3d43f7f" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-macos-0.15.2.tar.xz" + [tools.zig."platforms.windows-x64"] checksum = "sha256:3a0ed1e8799a2f8ce2a6e6290a9ff22e6906f8227865911fb7ddedc3cc14cb0c" url = "https://ziglang.org/download/0.15.2/zig-x86_64-windows-0.15.2.zip" + +[tools.zig."platforms.windows-x64-baseline"] +checksum = "sha256:3a0ed1e8799a2f8ce2a6e6290a9ff22e6906f8227865911fb7ddedc3cc14cb0c" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-windows-0.15.2.zip" diff --git a/mise.toml b/mise.toml index bace737d06..8a15051cf2 100644 --- a/mise.toml +++ b/mise.toml @@ -33,6 +33,8 @@ config_roots = ["."] [tools] go = "latest" +node = "24.18.0" +"npm:pnpm" = "11.11.0" uv = "0.9.26" "pipx:nox" = { version = "2025.11.12", uvx = true, uvx_args = "--python-preference=managed -p 3.13" } "rust" = { version = "1.93.0", components = "rustfmt,clippy,rust-analyzer", targets = "x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,aarch64-apple-darwin" } @@ -148,7 +150,8 @@ echo "Installed $DEST/cog -> $BINARY" [tasks."build:cog"] description = "Build cog CLI (development)" -sources = ["cmd/**/*.go", "pkg/**/*.go", "go.mod", "go.sum", "VERSION.txt"] +depends = ["build:playground"] +sources = ["cmd/**/*.go", "pkg/**/*.go", "pkg/cli/playground/**", ".goreleaser.yaml", "go.mod", "go.sum", "mise.toml", "mise.lock", "VERSION.txt"] outputs = ["dist/go/*/cog"] run = """ #!/usr/bin/env bash @@ -158,8 +161,27 @@ set -e GOFLAGS=-buildvcs=false go run github.com/goreleaser/goreleaser/v2@latest build --clean --snapshot --single-target --id cog --output cog """ +[tasks."playground:install"] +description = "Install playground dependencies" +dir = "playground" +sources = ["package.json", "pnpm-lock.yaml", "../mise.toml", "../mise.lock"] +outputs = ["node_modules/.cog-playground-installed"] +run = "pnpm install --frozen-lockfile && touch node_modules/.cog-playground-installed" + +[tasks."build:playground"] +description = "Build embedded playground assets" +depends = ["playground:install"] +dir = "playground" +sources = ["src/**/*", "scripts/**/*", "index.html", "package.json", "pnpm-lock.yaml", "tsconfig.json", "vite.config.ts", "../mise.toml", "../mise.lock"] +outputs = ["../pkg/cli/playground/index.html", "../pkg/cli/playground/THIRD_PARTY_LICENSES.md", "../pkg/cli/playground/assets/*"] +run = [ + "pnpm run build", + "test -s ../pkg/cli/playground/THIRD_PARTY_LICENSES.md", +] + [tasks."build:cog:release"] description = "Build cog CLI (release)" +depends = ["build:playground"] run = "go run github.com/goreleaser/goreleaser/v2@latest build --clean --single-target --id cog --output cog" [tasks."build:rust"] @@ -292,6 +314,7 @@ set -e mise run test:go mise run test:rust mise run test:python +mise run test:playground if [ "${INTEGRATION_TESTS:-}" = "1" ]; then mise run test:integration fi @@ -310,6 +333,27 @@ description = "Run Python SDK tests (latest supported Python)" depends = ["build:coglet:wheel"] run = "nox -s tests -p 3.13" +[tasks."test:playground"] +description = "Run playground tests with coverage" +depends = ["playground:install"] +dir = "playground" +run = "pnpm run test:coverage" + +[tasks."test:playground:e2e"] +description = "Run playground browser tests against a real Cog server" +depends = ["build:cog", "build:sdk", "build:coglet:wheel:linux-x64", "playground:install"] +dir = "playground" +run = """ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd .. && pwd)" +BINARY="${COG_BINARY:-$(ls "$ROOT"/dist/go/*/cog | head -1)}" +COG_BINARY="$BINARY" \ +COG_SDK_WHEEL="${COG_SDK_WHEEL:-$ROOT/dist}" \ +COGLET_WHEEL="${COGLET_WHEEL:-$ROOT/dist}" \ +pnpm run test:e2e +""" + [tasks."test:python:all"] description = "Run Python SDK tests on all supported Python versions" depends = ["build:coglet:wheel"] @@ -388,7 +432,7 @@ fi [tasks.fmt] alias = ["format", "fmt:check", "format:check"] description = "Check formatting for all languages (non-destructive)" -run = [{ tasks = ["fmt:go", "fmt:rust", "fmt:python", "fmt:docs"] }] +run = [{ tasks = ["fmt:go", "fmt:rust", "fmt:python", "fmt:playground", "fmt:docs"] }] [tasks."fmt:fix"] alias = "format:fix" @@ -398,6 +442,7 @@ run = [ "fmt:go:fix", "fmt:rust:fix", "fmt:python:fix", + "fmt:playground:fix", "fmt:docs:fix", ] }, ] @@ -437,6 +482,19 @@ run = "ruff format --check ." description = "Fix Python formatting" run = "ruff format ." +[tasks."fmt:playground"] +alias = "fmt:playground:check" +description = "Check playground formatting" +depends = ["playground:install"] +dir = "playground" +run = "pnpm run fmt:check" + +[tasks."fmt:playground:fix"] +description = "Fix playground formatting" +depends = ["playground:install"] +dir = "playground" +run = "pnpm run fmt" + # Docs formatting [tasks."fmt:docs"] alias = "fmt:docs:check" @@ -454,7 +512,7 @@ run = ["bash ./tools/format-markdown.sh --write", "markdownlint-cli2 --fix"] [tasks.lint] alias = "lint:check" description = "Run linters for all languages (non-destructive)" -run = [{ tasks = ["lint:go", "lint:rust", "lint:python", "lint:docs"] }] +run = [{ tasks = ["lint:go", "lint:rust", "lint:python", "lint:playground", "lint:docs"] }] [tasks."lint:fix"] description = "Fix lint issues for all languages" @@ -463,6 +521,7 @@ run = [ "lint:go:fix", "lint:rust:fix", "lint:python:fix", + "lint:playground:fix", "lint:docs:fix", ] }, ] @@ -520,13 +579,26 @@ mise run typecheck:python description = "Fix Python lint issues" run = "ruff check --fix ." +[tasks."lint:playground"] +alias = "lint:playground:check" +description = "Lint playground code" +depends = ["playground:install"] +dir = "playground" +run = "pnpm run lint" + +[tasks."lint:playground:fix"] +description = "Fix playground lint issues" +depends = ["playground:install"] +dir = "playground" +run = "pnpm run lint:fix" + # ============================================================================= # Typecheck tasks # ============================================================================= [tasks.typecheck] description = "Run type checking for all languages" -run = [{ tasks = ["typecheck:rust", "typecheck:python"] }] +run = [{ tasks = ["typecheck:rust", "typecheck:python", "typecheck:playground"] }] [tasks."typecheck:rust"] description = "Type check Rust code (cargo check)" @@ -536,6 +608,12 @@ run = "cargo check --manifest-path crates/Cargo.toml --workspace" description = "Type check Python code" run = "nox -s typecheck" +[tasks."typecheck:playground"] +description = "Type check playground code" +depends = ["playground:install"] +dir = "playground" +run = "pnpm run typecheck" + # ============================================================================= # Generate tasks # ============================================================================= diff --git a/pkg/cli/playground_assets.go b/pkg/cli/playground_assets.go new file mode 100644 index 0000000000..97dcd472e5 --- /dev/null +++ b/pkg/cli/playground_assets.go @@ -0,0 +1,10 @@ +//go:build playground_assets + +package cli + +import "embed" + +//go:embed playground +var playgroundUI embed.FS + +const playgroundAssetsBuilt = true diff --git a/playground/.gitignore b/playground/.gitignore new file mode 100644 index 0000000000..a55b3fb16e --- /dev/null +++ b/playground/.gitignore @@ -0,0 +1,5 @@ +**/node_modules/ +**/coverage/ +**/playwright-report/ +**/test-results/ +*.log diff --git a/playground/.oxfmtrc.json b/playground/.oxfmtrc.json new file mode 100644 index 0000000000..600e409925 --- /dev/null +++ b/playground/.oxfmtrc.json @@ -0,0 +1,5 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 100, + "ignorePatterns": ["coverage", "playwright-report", "test-results"] +} diff --git a/playground/.oxlintrc.json b/playground/.oxlintrc.json new file mode 100644 index 0000000000..ee4fa674b3 --- /dev/null +++ b/playground/.oxlintrc.json @@ -0,0 +1,11 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "import", "vitest", "jsx-a11y"], + "rules": { + "vitest/require-mock-type-parameters": "off" + }, + "env": { + "browser": true + }, + "ignorePatterns": ["coverage", "playwright-report", "test-results"] +} diff --git a/playground/index.html b/playground/index.html new file mode 100644 index 0000000000..67a622b658 --- /dev/null +++ b/playground/index.html @@ -0,0 +1,21 @@ + + + + + + Cog Playground + + + +
+ + + diff --git a/playground/package.json b/playground/package.json new file mode 100644 index 0000000000..9445043c8f --- /dev/null +++ b/playground/package.json @@ -0,0 +1,56 @@ +{ + "name": "@replicate/cog-playground", + "private": true, + "type": "module", + "scripts": { + "build": "vite build && node ./scripts/merge-licenses.ts && pnpm run check:bundle-size", + "check:bundle-size": "node ./scripts/check-bundle-size.ts", + "dev": "vite", + "fmt": "oxfmt", + "fmt:check": "oxfmt --check", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "test:e2e": "playwright test", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@cloudflare/kumo": "^2.7.0", + "@codemirror/commands": "^6.10.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/language": "^6.12.4", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.6", + "@lezer/highlight": "^1.2.3", + "@phosphor-icons/react": "^2.1.10", + "ajv": "^8.20.0", + "ajv-draft-04": "^1.0.0", + "ajv-formats": "^3.0.1", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@playwright/test": "^1.55.0", + "@tailwindcss/vite": "^4.1.17", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^24.10.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.2", + "@vitest/coverage-v8": "^4.0.8", + "jsdom": "^27.2.0", + "oxfmt": "^0.58.0", + "oxlint": "^1.50.0", + "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", + "vite": "^8.1.4", + "vitest": "^4.0.8" + }, + "engines": { + "node": "24.18.0" + }, + "packageManager": "pnpm@11.11.0" +} diff --git a/playground/pnpm-lock.yaml b/playground/pnpm-lock.yaml new file mode 100644 index 0000000000..fe15af9d2e --- /dev/null +++ b/playground/pnpm-lock.yaml @@ -0,0 +1,3345 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@cloudflare/kumo': + specifier: ^2.7.0 + version: 2.7.0(@date-fns/tz@1.5.0)(@phosphor-icons/react@2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@codemirror/commands': + specifier: ^6.10.4 + version: 6.10.4 + '@codemirror/lang-json': + specifier: ^6.0.2 + version: 6.0.2 + '@codemirror/language': + specifier: ^6.12.4 + version: 6.12.4 + '@codemirror/state': + specifier: ^6.7.1 + version: 6.7.1 + '@codemirror/view': + specifier: ^6.43.6 + version: 6.43.6 + '@lezer/highlight': + specifier: ^1.2.3 + version: 1.2.3 + '@phosphor-icons/react': + specifier: ^2.1.10 + version: 2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + ajv: + specifier: ^8.20.0 + version: 8.20.0 + ajv-draft-04: + specifier: ^1.0.0 + version: 1.0.0(ajv@8.20.0) + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.20.0) + react: + specifier: ^19.2.4 + version: 19.2.7 + react-dom: + specifier: ^19.2.4 + version: 19.2.7(react@19.2.7) + devDependencies: + '@playwright/test': + specifier: ^1.55.0 + version: 1.61.1 + '@tailwindcss/vite': + specifier: ^4.1.17 + version: 4.3.2(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0)) + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/node': + specifier: ^24.10.1 + version: 24.13.3 + '@types/react': + specifier: ^19.2.2 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.2 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^5.1.2 + version: 5.2.0(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0)) + '@vitest/coverage-v8': + specifier: ^4.0.8 + version: 4.1.10(vitest@4.1.10) + jsdom: + specifier: ^27.2.0 + version: 27.4.0 + oxfmt: + specifier: ^0.58.0 + version: 0.58.0 + oxlint: + specifier: ^1.50.0 + version: 1.73.0 + tailwindcss: + specifier: ^4.1.17 + version: 4.3.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^8.1.4 + version: 8.1.4(@types/node@24.13.3)(jiti@2.7.0) + vitest: + specifier: ^4.0.8 + version: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@27.4.0)(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0)) + +packages: + + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@asamuzakjp/css-color@4.1.2': + resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@base-ui/react@1.6.0': + resolution: {integrity: sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@date-fns/tz': ^1.2.0 + '@types/react': ^17 || ^18 || ^19 + date-fns: ^4.0.0 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@date-fns/tz': + optional: true + '@types/react': + optional: true + date-fns: + optional: true + + '@base-ui/utils@0.3.1': + resolution: {integrity: sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@cloudflare/kumo@2.7.0': + resolution: {integrity: sha512-KniieEmj/ZwqffqmLIVW76JnMuG5aeKJrs1nmSMME3U2wFo4yEzQtbLrLujNR2xmiDed5qm2PKpO03BltQa2mw==, tarball: https://registry.npmjs.org/@cloudflare/kumo/-/kumo-2.7.0.tgz} + hasBin: true + peerDependencies: + '@phosphor-icons/react': ^2.1.10 + echarts: ^6.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + zod: ^4.0.0 + peerDependenciesMeta: + echarts: + optional: true + zod: + optional: true + + '@codemirror/commands@6.10.4': + resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/state@6.7.1': + resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} + + '@codemirror/view@6.43.6': + resolution: {integrity: sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==} + + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.9': + resolution: {integrity: sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.6': + resolution: {integrity: sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@date-fns/tz@1.5.0': + resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@marijn/find-cluster-break@1.0.3': + resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@oxfmt/binding-android-arm-eabi@0.58.0': + resolution: {integrity: sha512-Uz62sHduGGPftXtILGyxdSW4PX82rUg+rfdNqhsgxe881g4rIoXlIqmZQ6HVKcF4f+F8qMhdD03Bx5u7gmeTdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.58.0': + resolution: {integrity: sha512-rD0lRaJp1b+9vw6X4A2dJWKukd6X8yxiicN4JxXcXayolmUypRZxk+lKR+fVOu5q/iYc0fh5fR4bgmfOfVlbaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.58.0': + resolution: {integrity: sha512-uzbPPk7O6M+w2K65vcQ1woga3wgP8zghjL1KOG5b6qJ8dvYHZJ1VShaslg2KOK6yQIwCQtcMCXqLBM6sqXUNTg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.58.0': + resolution: {integrity: sha512-L0nKYDxU32oxeQqJj21W9SlIMnf81VZEhyah6iDvFhf5q0oynq498Fopth7blErUJVBpVtxQ98RMCfMPqpJX6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.58.0': + resolution: {integrity: sha512-woNwfD58dC5PGS9LSLSD5JYfo/EFK5iG9vhDWkcCg3q78ag7KC8bpDqgvPHrMoXpx83OLXxoSOhu6z8FsVTHlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.58.0': + resolution: {integrity: sha512-Sqs8nMLxuQpY21NKJ1u4stPDmO5hskBCNNh2E3AdCfI1QqWtf4m+Qn4mGEIUO4KGmuq3SWc/SZ80uy5IiwTCDw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.58.0': + resolution: {integrity: sha512-Vd4exzBI5B5hB9m22JiTQzIL23WvHo/Pe+sNXPNeBLXSP9swCBPKCEBRwKpmpQzYhlgYaCgfPcGXPKAJBRIiZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.58.0': + resolution: {integrity: sha512-bUWi5mHV+4Vi56RLHE1h6q/HHfwAIT3XoB9vJAVeRzfu5NriXM8y6eeJu0vlKa0C9kq2rq1sOWRClhdLHPocrg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.58.0': + resolution: {integrity: sha512-2ZHxemzgHcjtktAuVUwSoyXmGo/t+aF5tS1ciPpPei4rhSyrz3JOqDosXXrmhN/yLUSzJjtuW7ToTWqfQpCj2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.58.0': + resolution: {integrity: sha512-AwKkVwjVmFQ3bcO7j0McGYAqCKH2a326fswfofng/E8VewCT/raeeGQr4huVhY704deK8AWASSTlxzMj0eZc6Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.58.0': + resolution: {integrity: sha512-xsRpTxfUnJF8D3AUKko/qyWdjw4GZVHlCVFuGlzSCTeewLmykKINW8em1+wx+axsDVtJJcMtvsiaXggXxrlHgw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.58.0': + resolution: {integrity: sha512-Z4AYOTcy7nYEIiXwD62PlerimyYRcfJOgUbQAEBjXz098kxKuERBlRntofGy69HHhe9E0TLVNMl1yspVNu+efw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.58.0': + resolution: {integrity: sha512-A3nhhtZPC/TKVWOPj9q/H3p2znJDCcHWYlJBhWL8hGq/bFmBaNBHC8Np6E581yVq1w9Mi3rMDNzDalWvtUfJtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.58.0': + resolution: {integrity: sha512-2g+tVkgwqphw8R4hgo+kF4oz8+P5RwVOtr9+irsC7uwEp0e9j7Crw8kDGKL20uYlLPD7g02DqA61mC/UNYx98A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.58.0': + resolution: {integrity: sha512-rc15P6AbyyB7426aN8AakLd02Trb3a6ML/mmfAQeVHJEfVofWLcWIrBdy6zDEY+DIaL/s8E4GGPboVw+oP3+EA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.58.0': + resolution: {integrity: sha512-ZWoTM27/HYPOh9iq86DAbhPu9nXb8qKvvGU/h8OfliyVUFAMMNTLDkGsWDKKnDqIkqvZ9+dXlgUOsH1LYO3O7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.58.0': + resolution: {integrity: sha512-LHZnqFXe2dEfkRI4XdZS/57nEOT/I4UCRX5IyM9v4GYW9XwQCjGe1IUK59SuKw3POwvcgWQ4pme2cYXmNqTNPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.58.0': + resolution: {integrity: sha512-mZKpg20TpheCJym1rarcZCUJeW1sSruw8zAAaCYWvuVfwIUDN1CXdrPU/JgCWReXTCTrEfCB8Wyo3hh9jSZ2EA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.58.0': + resolution: {integrity: sha512-N/wUU4N5PZ2orBtI+Ko7MnMfYLfE7K91UrGMY/c/pYyHR3lA9kwst1XugkZx+92YcRh/Eo+iv2eTESSWXfiZPA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.73.0': + resolution: {integrity: sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.73.0': + resolution: {integrity: sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.73.0': + resolution: {integrity: sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.73.0': + resolution: {integrity: sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.73.0': + resolution: {integrity: sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.73.0': + resolution: {integrity: sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.73.0': + resolution: {integrity: sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.73.0': + resolution: {integrity: sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.73.0': + resolution: {integrity: sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.73.0': + resolution: {integrity: sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.73.0': + resolution: {integrity: sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.73.0': + resolution: {integrity: sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.73.0': + resolution: {integrity: sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.73.0': + resolution: {integrity: sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.73.0': + resolution: {integrity: sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.73.0': + resolution: {integrity: sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.73.0': + resolution: {integrity: sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.73.0': + resolution: {integrity: sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.73.0': + resolution: {integrity: sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@phosphor-icons/react@2.1.10': + resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8' + react-dom: '>= 16.8' + + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@shikijs/core@4.3.1': + resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.3.1': + resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.3.1': + resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.3.1': + resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.3.1': + resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} + engines: {node: '>=20'} + + '@shikijs/themes@4.3.1': + resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} + engines: {node: '>=20'} + + '@shikijs/types@4.3.1': + resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tabby_ai/hijri-converter@1.0.5': + resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==} + engines: {node: '>=16.0.0'} + + '@tailwindcss/node@4.3.2': + resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} + + '@tailwindcss/oxide-android-arm64@4.3.2': + resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.2': + resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.2': + resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.2': + resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': + resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': + resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.2': + resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.2': + resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.2': + resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.2': + resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': + resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.2': + resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.2': + resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.2': + resolution: {integrity: sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + + '@vitejs/plugin-react@5.2.0': + resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@vitest/coverage-v8@4.1.10': + resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} + peerDependencies: + '@vitest/browser': 4.1.10 + vitest: 4.1.10 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.4: + resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==} + + baseline-browser-mapping@2.10.42: + resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + browserslist@4.28.5: + resolution: {integrity: sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001803: + resolution: {integrity: sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + cnfast@0.0.8: + resolution: {integrity: sha512-EjXKMfGfdwtV4AcNSQ6AwQaVzpC1B7IxeiwA3FlhTXz+YFlMKVi4c1JX9tgD2QOlahQXjB8KUXrBaYG+3v871Q==} + hasBin: true + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} + + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + electron-to-chromium@1.5.389: + resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + + enhanced-resolve@5.21.6: + resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} + engines: {node: '>=10.13.0'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + es-module-lexer@2.3.0: + resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + framer-motion@12.42.2: + resolution: {integrity: sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + motion-dom@12.42.2: + resolution: {integrity: sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==} + + motion-utils@12.39.0: + resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + + motion@12.42.2: + resolution: {integrity: sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + oxfmt@0.58.0: + resolution: {integrity: sha512-8feG/7NVEHDVwc1OUpP6Pks+TnaDFUw2jLLFIMi5bcmmwxAX2wBQvjSzj62RRTYBf2Op1Wt8xbkmagmPTR5ETg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint@1.73.0: + resolution: {integrity: sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.24.0' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-day-picker@9.14.0: + resolution: {integrity: sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==} + engines: {node: '>=18'} + peerDependencies: + react: '>=16.8.0' + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shiki@4.3.1: + resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} + engines: {node: '>=20'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tailwindcss@4.3.2: + resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.8: + resolution: {integrity: sha512-c1P7u0EhACHj7lPy4MJm8iTFEU8+nB0LCtddH0fhP7noaVoXAqafMtOOeX+ulpuPBqnrRgRhw494RICT3mbhnw==} + + tldts@7.4.8: + resolution: {integrity: sha512-htwgN/8KRB3z3vnC0BOETVh2m499g5GmyTK9Wq5JBLX3FNz6tSBveAd+fQhzy9hkjif8vy2jwDMR1sGhLtZl2A==} + hasBin: true + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@8.1.4: + resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@acemir/cssom@0.9.31': {} + + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@4.1.2': + dependencies: + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.2 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.5 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@base-ui/react@1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@base-ui/utils': 0.3.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@floating-ui/utils': 0.2.11 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + '@date-fns/tz': 1.5.0 + '@types/react': 19.2.17 + date-fns: 4.4.0 + + '@base-ui/utils@0.3.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@floating-ui/utils': 0.2.11 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + reselect: 5.2.0 + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + '@bcoe/v8-coverage@1.0.2': {} + + '@cloudflare/kumo@2.7.0(@date-fns/tz@1.5.0)(@phosphor-icons/react@2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@base-ui/react': 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@phosphor-icons/react': 2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@shikijs/langs': 4.3.1 + '@shikijs/themes': 4.3.1 + cnfast: 0.0.8 + d3-geo: 3.1.1 + motion: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-day-picker: 9.14.0(react@19.2.7) + react-dom: 19.2.7(react@19.2.7) + shiki: 4.3.1 + transitivePeerDependencies: + - '@date-fns/tz' + - '@emotion/is-prop-valid' + - '@types/react' + - date-fns + + '@codemirror/commands@6.10.4': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + '@lezer/common': 1.5.2 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/json': 1.0.3 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/state@6.7.1': + dependencies: + '@marijn/find-cluster-break': 1.0.3 + + '@codemirror/view@6.43.6': + dependencies: + '@codemirror/state': 6.7.1 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + + '@csstools/color-helpers@6.1.0': {} + + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.6(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@date-fns/tz@1.5.0': {} + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@exodus/bytes@1.15.1': {} + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@floating-ui/utils@0.2.11': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@marijn/find-cluster-break@1.0.3': {} + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.139.0': {} + + '@oxfmt/binding-android-arm-eabi@0.58.0': + optional: true + + '@oxfmt/binding-android-arm64@0.58.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.58.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.58.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.58.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.58.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.58.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.58.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.58.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.58.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.58.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.58.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.58.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.58.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.73.0': + optional: true + + '@oxlint/binding-android-arm64@1.73.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.73.0': + optional: true + + '@oxlint/binding-darwin-x64@1.73.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.73.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.73.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.73.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.73.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.73.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.73.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.73.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.73.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.73.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.73.0': + optional: true + + '@phosphor-icons/react@2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.3': {} + + '@rolldown/pluginutils@1.0.1': {} + + '@shikijs/core@4.3.1': + dependencies: + '@shikijs/primitive': 4.3.1 + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + + '@shikijs/primitive@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/themes@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + + '@shikijs/types@4.3.1': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@standard-schema/spec@1.1.0': {} + + '@tabby_ai/hijri-converter@1.0.5': {} + + '@tailwindcss/node@4.3.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.6 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.2 + + '@tailwindcss/oxide-android-arm64@4.3.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.2': + optional: true + + '@tailwindcss/oxide@4.3.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.2 + '@tailwindcss/oxide-darwin-arm64': 4.3.2 + '@tailwindcss/oxide-darwin-x64': 4.3.2 + '@tailwindcss/oxide-freebsd-x64': 4.3.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.2 + '@tailwindcss/oxide-linux-x64-musl': 4.3.2 + '@tailwindcss/oxide-wasm32-wasi': 4.3.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 + + '@tailwindcss/vite@4.3.2(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.2 + '@tailwindcss/oxide': 4.3.2 + tailwindcss: 4.3.2 + vite: 8.1.4(@types/node@24.13.3)(jiti@2.7.0) + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.5.0 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.3': {} + + '@vitejs/plugin-react@5.2.0(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 8.1.4(@types/node@24.13.3)(jiti@2.7.0) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.10 + ast-v8-to-istanbul: 1.0.4 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.3 + std-env: 4.2.0 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@27.4.0)(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0)) + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.4(@types/node@24.13.3)(jiti@2.7.0) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + agent-base@7.1.4: {} + + ajv-draft-04@1.0.0(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.4: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + baseline-browser-mapping@2.10.42: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + browserslist@4.28.5: + dependencies: + baseline-browser-mapping: 2.10.42 + caniuse-lite: 1.0.30001803 + electron-to-chromium: 1.5.389 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.5) + + caniuse-lite@1.0.30001803: {} + + ccount@2.0.1: {} + + chai@6.2.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + cnfast@0.0.8: {} + + comma-separated-tokens@2.0.3: {} + + convert-source-map@2.0.0: {} + + crelt@1.0.7: {} + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.6(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.5.2 + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + data-urls@6.0.1: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + + date-fns-jalali@4.1.0-0: {} + + date-fns@4.4.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + electron-to-chromium@1.5.389: {} + + enhanced-resolve@5.21.6: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@8.0.0: {} + + es-module-lexer@2.3.0: {} + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.3: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + framer-motion@12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + motion-dom: 12.42.2 + motion-utils: 12.39.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + + html-escaper@2.0.2: {} + + html-void-elements@3.0.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + indent-string@4.0.0: {} + + internmap@2.0.3: {} + + is-potential-custom-element-name@1.0.1: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jiti@2.7.0: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + jsdom@27.4.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@exodus/bytes': 1.15.1 + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.21.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lru-cache@11.5.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdn-data@2.27.1: {} + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + min-indent@1.0.1: {} + + motion-dom@12.42.2: + dependencies: + motion-utils: 12.39.0 + + motion-utils@12.39.0: {} + + motion@12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + framer-motion: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + ms@2.1.3: {} + + nanoid@3.3.15: {} + + node-releases@2.0.51: {} + + obug@2.1.3: {} + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + oxfmt@0.58.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.58.0 + '@oxfmt/binding-android-arm64': 0.58.0 + '@oxfmt/binding-darwin-arm64': 0.58.0 + '@oxfmt/binding-darwin-x64': 0.58.0 + '@oxfmt/binding-freebsd-x64': 0.58.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.58.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.58.0 + '@oxfmt/binding-linux-arm64-gnu': 0.58.0 + '@oxfmt/binding-linux-arm64-musl': 0.58.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.58.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.58.0 + '@oxfmt/binding-linux-riscv64-musl': 0.58.0 + '@oxfmt/binding-linux-s390x-gnu': 0.58.0 + '@oxfmt/binding-linux-x64-gnu': 0.58.0 + '@oxfmt/binding-linux-x64-musl': 0.58.0 + '@oxfmt/binding-openharmony-arm64': 0.58.0 + '@oxfmt/binding-win32-arm64-msvc': 0.58.0 + '@oxfmt/binding-win32-ia32-msvc': 0.58.0 + '@oxfmt/binding-win32-x64-msvc': 0.58.0 + + oxlint@1.73.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.73.0 + '@oxlint/binding-android-arm64': 1.73.0 + '@oxlint/binding-darwin-arm64': 1.73.0 + '@oxlint/binding-darwin-x64': 1.73.0 + '@oxlint/binding-freebsd-x64': 1.73.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.73.0 + '@oxlint/binding-linux-arm-musleabihf': 1.73.0 + '@oxlint/binding-linux-arm64-gnu': 1.73.0 + '@oxlint/binding-linux-arm64-musl': 1.73.0 + '@oxlint/binding-linux-ppc64-gnu': 1.73.0 + '@oxlint/binding-linux-riscv64-gnu': 1.73.0 + '@oxlint/binding-linux-riscv64-musl': 1.73.0 + '@oxlint/binding-linux-s390x-gnu': 1.73.0 + '@oxlint/binding-linux-x64-gnu': 1.73.0 + '@oxlint/binding-linux-x64-musl': 1.73.0 + '@oxlint/binding-openharmony-arm64': 1.73.0 + '@oxlint/binding-win32-arm64-msvc': 1.73.0 + '@oxlint/binding-win32-ia32-msvc': 1.73.0 + '@oxlint/binding-win32-x64-msvc': 1.73.0 + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + + postcss@8.5.16: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + property-information@7.2.0: {} + + punycode@2.3.1: {} + + react-day-picker@9.14.0(react@19.2.7): + dependencies: + '@date-fns/tz': 1.5.0 + '@tabby_ai/hijri-converter': 1.0.5 + date-fns: 4.4.0 + date-fns-jalali: 4.1.0-0 + react: 19.2.7 + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react-refresh@0.18.0: {} + + react@19.2.7: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + require-from-string@2.0.2: {} + + reselect@5.2.0: {} + + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + shiki@4.3.1: + dependencies: + '@shikijs/core': 4.3.1 + '@shikijs/engine-javascript': 4.3.1 + '@shikijs/engine-oniguruma': 4.3.1 + '@shikijs/langs': 4.3.1 + '@shikijs/themes': 4.3.1 + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + style-mod@4.1.3: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + symbol-tree@3.2.4: {} + + tailwindcss@4.3.2: {} + + tapable@2.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@2.1.0: {} + + tinyrainbow@3.1.0: {} + + tldts-core@7.4.8: {} + + tldts@7.4.8: + dependencies: + tldts-core: 7.4.8 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.8 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + trim-lines@3.0.1: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + update-browserslist-db@1.2.3(browserslist@4.28.5): + dependencies: + browserslist: 4.28.5 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.6.0(react@19.2.7): + dependencies: + react: 19.2.7 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.16 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitest@4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@27.4.0)(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.4(@types/node@24.13.3)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + jsdom: 27.4.0 + transitivePeerDependencies: + - msw + + w3c-keyname@2.2.8: {} + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@4.0.0: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + zwitch@2.0.4: {} diff --git a/playground/scripts/check-bundle-size.ts b/playground/scripts/check-bundle-size.ts new file mode 100644 index 0000000000..fe95929c4f --- /dev/null +++ b/playground/scripts/check-bundle-size.ts @@ -0,0 +1,17 @@ +import { readdir, stat } from "node:fs/promises"; + +const assets = new URL("../../pkg/cli/playground/assets/", import.meta.url); +const maxJavaScriptBytes = 1024 * 1024; +const files = (await readdir(assets)).filter((file) => file.endsWith(".js")); +if (files.length === 0) throw new Error("No built playground JavaScript assets found"); +const sizes = await Promise.all( + files.map(async (file) => (await stat(new URL(file, assets))).size), +); +const total = sizes.reduce((sum, size) => sum + size, 0); + +console.log(`Playground JavaScript: ${(total / 1024).toFixed(1)} KiB`); +if (total > maxJavaScriptBytes) { + throw new Error( + `Playground JavaScript exceeds the ${maxJavaScriptBytes / 1024 / 1024} MiB JavaScript budget`, + ); +} diff --git a/playground/scripts/merge-licenses.ts b/playground/scripts/merge-licenses.ts new file mode 100644 index 0000000000..7299af70eb --- /dev/null +++ b/playground/scripts/merge-licenses.ts @@ -0,0 +1,30 @@ +import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; + +// THIRD_PARTY_LICENSES.md is not used at runtime. Playground JS (React, editor +// deps, ajv in the validation worker, etc.) is bundled into assets embedded in +// the cog binary and redistributed to users; many OSS licenses require shipping +// those notices with the software. This file is that compliance record. +const outputDirectory = new URL("../../pkg/cli/playground/", import.meta.url); +const mainPath = new URL("THIRD_PARTY_LICENSES.md", outputDirectory); +const workerPath = new URL("WORKER_LICENSES.md", outputDirectory); +const main = readFileSync(mainPath, "utf8").trimEnd(); +if (!existsSync(workerPath)) process.exit(0); +const worker = readFileSync(workerPath, "utf8"); +const existing = new Set([...main.matchAll(/^## (.+?) - .+$/gm)].map((match) => match[1])); +const workerSections = worker + .split(/(?=^## )/m) + .slice(1) + .filter((section) => { + const name = /^## (.+?) - /m.exec(section)?.[1]; + return name && !existing.has(name); + }); +const merged = `${main}\n\n${workerSections.join("\n").trim()}\n`; + +for (const dependency of ["ajv", "ajv-draft-04", "ajv-formats", "fast-uri"]) { + if (!merged.includes(`## ${dependency} - `)) { + throw new Error(`Worker dependency ${dependency} is missing from THIRD_PARTY_LICENSES.md`); + } +} + +writeFileSync(mainPath, merged); +unlinkSync(workerPath); diff --git a/playground/scripts/worker-license-plugin.ts b/playground/scripts/worker-license-plugin.ts new file mode 100644 index 0000000000..d0ec51811b --- /dev/null +++ b/playground/scripts/worker-license-plugin.ts @@ -0,0 +1,74 @@ +import { readFileSync, readdirSync } from "node:fs"; + +import type { Plugin } from "vite"; + +type PackageLicense = { + license: string; + name: string; + text: string; + version: string; +}; + +/** Creates a Vite plugin that emits license notices for dependencies bundled into workers. */ +export function workerLicensePlugin(): Plugin { + return { + name: "worker-license-file", + /** Scans emitted worker chunks and writes a sorted Markdown license asset. */ + generateBundle(_options, bundle) { + const packages = new Map(); + for (const output of Object.values(bundle)) { + if (output.type !== "chunk") continue; + for (const moduleId of Object.keys(output.modules)) { + const packageRoot = packageRootForModule(moduleId); + if (!packageRoot || packages.has(packageRoot)) continue; + const license = readPackageLicense(packageRoot); + if (license) packages.set(packageRoot, license); + } + } + const sections = [...packages.values()] + .sort((left, right) => left.name.localeCompare(right.name)) + .map( + ({ license, name, text, version }) => + `## ${name} - ${version} (${license})\n\n${text.trim()}\n`, + ); + this.emitFile({ + type: "asset", + fileName: "WORKER_LICENSES.md", + source: `# Worker licenses\n\n${sections.join("\n")}`, + }); + }, + }; +} + +function packageRootForModule(moduleId: string): string | undefined { + const cleanId = moduleId.split("?", 1)[0]; + const marker = "/node_modules/"; + const start = cleanId.lastIndexOf(marker); + if (start < 0) return undefined; + const packagePath = cleanId.slice(start + marker.length).split("/"); + const name = packagePath[0].startsWith("@") + ? `${packagePath[0]}/${packagePath[1]}` + : packagePath[0]; + return cleanId.slice(0, start + marker.length) + name; +} + +function readPackageLicense(root: string): PackageLicense | undefined { + try { + const metadata = JSON.parse(readFileSync(`${root}/package.json`, "utf8")) as { + license?: string; + name?: string; + version?: string; + }; + if (!metadata.name || !metadata.version) return undefined; + const licenseFile = readdirSync(root).find((name) => /^licen[cs]e(?:\..+)?$/i.test(name)); + if (!licenseFile) return undefined; + return { + license: metadata.license ?? "Unknown", + name: metadata.name, + text: readFileSync(`${root}/${licenseFile}`, "utf8"), + version: metadata.version, + }; + } catch { + return undefined; + } +} diff --git a/playground/src/main.tsx b/playground/src/main.tsx new file mode 100644 index 0000000000..341ef8415b --- /dev/null +++ b/playground/src/main.tsx @@ -0,0 +1,3 @@ +import { createRoot } from "react-dom/client"; + +createRoot(document.getElementById("root")!).render("Cog Playground"); diff --git a/playground/tsconfig.json b/playground/tsconfig.json new file mode 100644 index 0000000000..dc467d5683 --- /dev/null +++ b/playground/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "paths": { + "@/*": ["./src/*"] + }, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vitest/globals", "@testing-library/jest-dom"] + }, + "include": [ + "src", + "e2e/**/*.ts", + "scripts/**/*.ts", + "playwright.config.ts", + "vite.config.ts", + "vitest.config.ts" + ] +} diff --git a/playground/vite.config.ts b/playground/vite.config.ts new file mode 100644 index 0000000000..e7ee0c254f --- /dev/null +++ b/playground/vite.config.ts @@ -0,0 +1,33 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +import { workerLicensePlugin } from "./scripts/worker-license-plugin"; + +const root = path.dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(root, "src"), + }, + }, + build: { + outDir: path.resolve(root, "../pkg/cli/playground"), + emptyOutDir: true, + // License notices for deps bundled into the embedded playground UI. + // Redistributed with the cog binary; not loaded at runtime. + license: { fileName: "THIRD_PARTY_LICENSES.md" }, + }, + worker: { + /** Collects licenses for worker-only deps (e.g. ajv) for THIRD_PARTY_LICENSES.md. */ + plugins: () => [workerLicensePlugin()], + rollupOptions: { + output: { entryFileNames: "assets/validation.worker-[hash].js" }, + }, + }, +}); diff --git a/playground/vitest.config.ts b/playground/vitest.config.ts new file mode 100644 index 0000000000..fd1949ab74 --- /dev/null +++ b/playground/vitest.config.ts @@ -0,0 +1,29 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { defineConfig } from "vitest/config"; + +const root = path.dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + resolve: { + alias: { + "@": path.resolve(root, "src"), + }, + }, + test: { + environment: "jsdom", + include: ["src/**/*.test.{ts,tsx}"], + setupFiles: "./src/test/setup.ts", + coverage: { + include: ["src/**/*.{ts,tsx}"], + exclude: ["src/**/*.d.ts", "src/main.tsx", "src/test/**"], + thresholds: { + statements: 40, + branches: 40, + functions: 40, + lines: 40, + }, + }, + }, +}); From 6145b13a5b469bb880959d1cdbbb22120007b9df Mon Sep 17 00:00:00 2001 From: Anish Sahoo Date: Tue, 14 Jul 2026 16:10:01 -0500 Subject: [PATCH 03/10] feat(playground): add core frontend types and utilities (#3114) --- playground/src/config/theme.ts | 12 +++ playground/src/test/deferred.ts | 14 ++++ playground/src/test/setup.ts | 27 +++++++ playground/src/types/health.ts | 6 ++ playground/src/types/json.ts | 8 ++ playground/src/types/openapi.ts | 42 ++++++++++ playground/src/types/prediction.ts | 40 ++++++++++ playground/src/utils/openapi.test.ts | 108 ++++++++++++++++++++++++++ playground/src/utils/openapi.ts | 104 +++++++++++++++++++++++++ playground/src/utils/terminal.test.ts | 15 ++++ playground/src/utils/terminal.ts | 4 + 11 files changed, 380 insertions(+) create mode 100644 playground/src/config/theme.ts create mode 100644 playground/src/test/deferred.ts create mode 100644 playground/src/test/setup.ts create mode 100644 playground/src/types/health.ts create mode 100644 playground/src/types/json.ts create mode 100644 playground/src/types/openapi.ts create mode 100644 playground/src/types/prediction.ts create mode 100644 playground/src/utils/openapi.test.ts create mode 100644 playground/src/utils/openapi.ts create mode 100644 playground/src/utils/terminal.test.ts create mode 100644 playground/src/utils/terminal.ts diff --git a/playground/src/config/theme.ts b/playground/src/config/theme.ts new file mode 100644 index 0000000000..3bac34ff27 --- /dev/null +++ b/playground/src/config/theme.ts @@ -0,0 +1,12 @@ +export type ThemeMode = "light" | "dark"; + +/** Returns light only when explicitly selected; the playground otherwise defaults to dark. */ +export function currentTheme(): ThemeMode { + return document.documentElement.dataset.mode === "light" ? "light" : "dark"; +} + +/** Updates Kumo's root color mode and persists it in local storage. */ +export function setTheme(mode: ThemeMode) { + document.documentElement.dataset.mode = mode; + localStorage.setItem("cog-playground-theme", mode); +} diff --git a/playground/src/test/deferred.ts b/playground/src/test/deferred.ts new file mode 100644 index 0000000000..da0b897aad --- /dev/null +++ b/playground/src/test/deferred.ts @@ -0,0 +1,14 @@ +/** Creates a test promise whose resolve and reject callbacks are controlled externally. */ +export function deferred(): { + promise: Promise; + resolve: (value: Value) => void; + reject: (reason?: unknown) => void; +} { + let resolve!: (value: Value) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +} diff --git a/playground/src/test/setup.ts b/playground/src/test/setup.ts new file mode 100644 index 0000000000..0a032b32b6 --- /dev/null +++ b/playground/src/test/setup.ts @@ -0,0 +1,27 @@ +import "@testing-library/jest-dom/vitest"; +import { cleanup } from "@testing-library/react"; +import { afterEach, vi } from "vitest"; + +afterEach(cleanup); + +vi.stubGlobal( + "ResizeObserver", + class { + observe(): void {} + unobserve(): void {} + disconnect(): void {} + }, +); + +HTMLElement.prototype.scrollIntoView = vi.fn(); +Object.defineProperty(Range.prototype, "getClientRects", { + configurable: true, + value: () => [] as unknown as DOMRectList, +}); +Object.defineProperty(Range.prototype, "getBoundingClientRect", { + configurable: true, + value: () => new DOMRect(), +}); +window.requestAnimationFrame = (callback) => + window.setTimeout(() => callback(performance.now()), 0); +window.cancelAnimationFrame = window.clearTimeout; diff --git a/playground/src/types/health.ts b/playground/src/types/health.ts new file mode 100644 index 0000000000..0f9f2f567a --- /dev/null +++ b/playground/src/types/health.ts @@ -0,0 +1,6 @@ +export type HealthResponse = { + status?: string; + user_healthcheck_error?: string; + setup?: { status?: string; logs?: string }; + version?: { coglet?: string; python_sdk?: string; python?: string }; +}; diff --git a/playground/src/types/json.ts b/playground/src/types/json.ts new file mode 100644 index 0000000000..0675efc22e --- /dev/null +++ b/playground/src/types/json.ts @@ -0,0 +1,8 @@ +export type JsonPrimitive = boolean | null | number | string; +export type JsonValue = JsonObject | JsonPrimitive | JsonValue[]; +export type JsonObject = { [key: string]: JsonValue }; + +/** Narrows to a plain JSON object, excluding arrays and null. */ +export function isJsonObject(value: unknown): value is JsonObject { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/playground/src/types/openapi.ts b/playground/src/types/openapi.ts new file mode 100644 index 0000000000..a82e2a0732 --- /dev/null +++ b/playground/src/types/openapi.ts @@ -0,0 +1,42 @@ +import type { JsonObject } from "@/types/json"; + +export type OpenAPISchema = { + $ref?: string; + type?: string; + format?: string; + title?: string; + description?: string; + default?: unknown; + enum?: unknown[]; + allOf?: OpenAPISchema[]; + anyOf?: OpenAPISchema[]; + oneOf?: OpenAPISchema[]; + items?: OpenAPISchema; + properties?: Record; + additionalProperties?: boolean | OpenAPISchema; + required?: string[]; + nullable?: boolean; + minimum?: number; + maximum?: number; + exclusiveMinimum?: boolean; + exclusiveMaximum?: boolean; + multipleOf?: number; + minLength?: number; + maxLength?: number; + pattern?: string; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + minProperties?: number; + maxProperties?: number; + deprecated?: boolean; + [key: `x-${string}`]: unknown; +}; + +export type OpenAPIDocument = { + components?: { schemas?: Record }; + paths?: Record; +}; + +export type OpenAPIOperation = JsonObject & { "x-cog-streaming"?: boolean }; +export type OpenAPIPathItem = Record; diff --git a/playground/src/types/prediction.ts b/playground/src/types/prediction.ts new file mode 100644 index 0000000000..709a3c2fb7 --- /dev/null +++ b/playground/src/types/prediction.ts @@ -0,0 +1,40 @@ +import type { JsonObject } from "@/types/json"; + +export type PredictionEnvelope = { + id?: string; + status?: string; + output?: unknown; + error?: string; + logs?: string; + metrics?: Record; + [key: string]: unknown; +}; + +export type StreamEvent = { + type: string; + data: JsonObject; + raw: string; +}; + +export type TraceEventKind = "request" | "response" | "sse" | "webhook" | "cancel" | "error"; + +export type TraceEvent = { + id: string; + elapsedMs: number; + kind: TraceEventKind; + label: string; + data?: unknown; + count?: number; +}; + +export type RequestTrace = { + startedAtLabel: string; + method: "POST" | "PUT"; + endpoint: string; + requestHeaders: Record; + requestBody: unknown; + responseStatus?: number; + responseHeaders?: Record; + responseBody?: unknown; + events: TraceEvent[]; +}; diff --git a/playground/src/utils/openapi.test.ts b/playground/src/utils/openapi.test.ts new file mode 100644 index 0000000000..3be8289d76 --- /dev/null +++ b/playground/src/utils/openapi.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "vitest"; + +import { + constraintText, + defaultInput, + effectiveSchema, + enumValues, + playgroundCapabilities, + orderedProperties, +} from "@/utils/openapi"; +import type { OpenAPIDocument } from "@/types/openapi"; + +const document: OpenAPIDocument = { + components: { + schemas: { + Choice: { enum: [1, 2] }, + Input: { + required: ["prompt", "enabled", "choice"], + properties: { + late: { type: "string", "x-order": 2 }, + prompt: { type: "string", default: "hello", "x-order": 1 }, + optional: { type: "boolean", default: false }, + nullable: { type: "string", nullable: true, default: null }, + enabled: { type: "boolean" }, + choice: { allOf: [{ $ref: "#/components/schemas/Choice" }] }, + }, + }, + Output: { type: "string" }, + }, + }, + paths: { + "/predictions": { post: { "x-cog-streaming": true } }, + "/predictions/{prediction_id}/cancel": {}, + }, +}; + +describe("schema helpers", () => { + it("resolves nullable references without losing metadata", () => { + expect( + effectiveSchema(document, { + title: "Nullable choice", + anyOf: [{ $ref: "#/components/schemas/Choice" }, { type: "null" }], + }), + ).toEqual({ title: "Nullable choice", enum: [1, 2] }); + }); + + it("initializes explicit defaults and required controls", () => { + const input = document.components?.schemas?.Input ?? {}; + expect(orderedProperties(input).map(([name]) => name)).toEqual([ + "prompt", + "late", + "optional", + "nullable", + "enabled", + "choice", + ]); + expect(defaultInput(document, input)).toEqual({ + prompt: "hello", + optional: false, + nullable: null, + enabled: false, + choice: 1, + }); + }); + + it("discovers enums from direct and composed schemas", () => { + expect(enumValues(document, { $ref: "#/components/schemas/Choice" })).toEqual([1, 2]); + expect(enumValues(document, { allOf: [{ $ref: "#/components/schemas/Choice" }] })).toEqual([ + 1, 2, + ]); + }); + + it("derives prediction capabilities", () => { + expect(playgroundCapabilities(document)).toMatchObject({ + endpoint: "/predictions", + streaming: true, + async: true, + input: document.components?.schemas?.Input, + }); + }); + + it("derives training capabilities", () => { + const training: OpenAPIDocument = { + components: { + schemas: { TrainingInput: { type: "object" }, TrainingOutput: { type: "object" } }, + }, + paths: { "/trainings": { post: {} } }, + }; + expect(playgroundCapabilities(training)).toMatchObject({ + endpoint: "/trainings", + streaming: false, + async: false, + }); + }); + + it("formats constraints", () => { + expect( + constraintText({ + default: 5, + minimum: 1, + maximum: 9, + minLength: 2, + maxLength: 10, + pattern: "^[a-z]+$", + }), + ).toBe("default 5 · min 1 · max 9 · min 2 chars · max 10 chars · pattern: ^[a-z]+$"); + }); +}); diff --git a/playground/src/utils/openapi.ts b/playground/src/utils/openapi.ts new file mode 100644 index 0000000000..864c0caf74 --- /dev/null +++ b/playground/src/utils/openapi.ts @@ -0,0 +1,104 @@ +import type { OpenAPIDocument, OpenAPISchema } from "@/types/openapi"; + +const REF_PREFIX = "#/components/schemas/"; + +/** Resolves a local component-schema reference, leaving unsupported references unchanged. */ +export function resolveRef(root: OpenAPIDocument, value: OpenAPISchema): OpenAPISchema { + if (!value.$ref?.startsWith(REF_PREFIX)) return value; + return root.components?.schemas?.[value.$ref.slice(REF_PREFIX.length)] ?? value; +} + +/** Resolves local references and unwraps a nullable `anyOf` with one non-null branch. */ +export function effectiveSchema(root: OpenAPIDocument, value: OpenAPISchema): OpenAPISchema { + let schema = resolveRef(root, value); + if (!schema.anyOf) return schema; + const nonNull = schema.anyOf + .map((item) => resolveRef(root, item)) + .filter((item) => item.type !== "null"); + if (nonNull.length !== 1) return schema; + const merged: OpenAPISchema = { ...schema, ...nonNull[0] }; + delete merged.anyOf; + return merged; +} + +/** Finds enum choices declared directly or through a referenced `allOf` branch. */ +export function enumValues(root: OpenAPIDocument, value: OpenAPISchema): unknown[] | undefined { + const schema = effectiveSchema(root, value); + if (schema.enum) return schema.enum; + for (const item of schema.allOf ?? []) { + const resolved = resolveRef(root, item); + if (resolved.enum) return resolved.enum; + } + return undefined; +} + +/** Builds initial input from explicit defaults and required enum or Boolean properties. */ +export function defaultInput( + root: OpenAPIDocument, + schema: OpenAPISchema, +): Record { + const result: Record = {}; + const required = new Set(schema.required ?? []); + for (const [name, raw] of orderedProperties(schema)) { + const property = effectiveSchema(root, raw); + if (Object.hasOwn(property, "default")) { + result[name] = property.default; + continue; + } + if (!required.has(name)) continue; + const choices = enumValues(root, property); + if (choices?.length) result[name] = choices[0]; + else if (property.type === "boolean") result[name] = false; + } + return result; +} + +/** Orders object properties by Cog's `x-order` extension, with unspecified entries last. */ +export function orderedProperties(schema: OpenAPISchema): [string, OpenAPISchema][] { + return Object.entries(schema.properties ?? {}).sort(([, left], [, right]) => { + const leftOrder = typeof left["x-order"] === "number" ? left["x-order"] : 999; + const rightOrder = typeof right["x-order"] === "number" ? right["x-order"] : 999; + return leftOrder - rightOrder; + }); +} + +/** Formats an explicit default and supported constraints as field guidance. */ +export function constraintText(schema: OpenAPISchema): string { + const parts: string[] = []; + if (Object.hasOwn(schema, "default")) parts.push(`default ${formatSchemaValue(schema.default)}`); + if (schema.minimum !== undefined) parts.push(`min ${schema.minimum}`); + if (schema.maximum !== undefined) parts.push(`max ${schema.maximum}`); + if (schema.minLength !== undefined) parts.push(`min ${schema.minLength} chars`); + if (schema.maxLength !== undefined) parts.push(`max ${schema.maxLength} chars`); + if (schema.pattern) parts.push(`pattern: ${schema.pattern}`); + return parts.join(" · "); +} + +function formatSchemaValue(value: unknown): string { + if (typeof value === "string") return value; + return JSON.stringify(value); +} + +export type PlaygroundCapabilities = { + endpoint: "/predictions" | "/trainings"; + input: OpenAPISchema; + streaming: boolean; + async: boolean; +}; + +/** Derives the model endpoint, input schema, streaming support, and cancellation capability. */ +export function playgroundCapabilities(document: OpenAPIDocument): PlaygroundCapabilities { + const schemas = document.components?.schemas ?? {}; + const training = Boolean(document.paths?.["/trainings"]); + const endpoint = training ? "/trainings" : "/predictions"; + const input = resolveRef(document, schemas[training ? "TrainingInput" : "Input"] ?? {}); + const operation = document.paths?.[endpoint]?.post; + return { + endpoint, + input, + streaming: operation?.["x-cog-streaming"] === true, + async: Boolean( + document.paths?.[`${endpoint}/{${training ? "training" : "prediction"}_id}/cancel`], + ), + }; +} diff --git a/playground/src/utils/terminal.test.ts b/playground/src/utils/terminal.test.ts new file mode 100644 index 0000000000..d5c57a8112 --- /dev/null +++ b/playground/src/utils/terminal.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; + +import { renderTerminalText } from "@/utils/terminal"; + +describe("renderTerminalText", () => { + it("renders carriage-return progress updates as separate lines", () => { + expect(renderTerminalText("starting\n\r 0%| | 0/28\r 50%|#| 14/28\r100%|#| 28/28\n")).toBe( + "starting\n\n 0%| | 0/28\n 50%|#| 14/28\n100%|#| 28/28\n", + ); + }); + + it("normalizes CRLF without adding an extra line", () => { + expect(renderTerminalText("first\rsecond\r\nthird")).toBe("first\nsecond\nthird"); + }); +}); diff --git a/playground/src/utils/terminal.ts b/playground/src/utils/terminal.ts new file mode 100644 index 0000000000..94a4d9c955 --- /dev/null +++ b/playground/src/utils/terminal.ts @@ -0,0 +1,4 @@ +/** Displays terminal carriage-return updates as separate lines without changing captured text. */ +export function renderTerminalText(value: string): string { + return value.replaceAll("\r\n", "\n").replaceAll("\r", "\n"); +} From e2227d0ca41a99d3fd1066ddd2b71d65fe62f738 Mon Sep 17 00:00:00 2001 From: Anish Sahoo Date: Wed, 15 Jul 2026 15:08:32 -0500 Subject: [PATCH 04/10] feat(playground): add shared UI and JSON editor (#3110) --- .../src/components/SegmentedTabs.test.tsx | 33 + playground/src/components/SegmentedTabs.tsx | 65 ++ .../src/components/StatusBadge.test.tsx | 16 + playground/src/components/StatusBadge.tsx | 28 + .../src/components/editor/JsonEditor.test.tsx | 157 ++++ .../src/components/editor/JsonEditor.tsx | 217 +++++ .../src/components/editor/LazyJsonEditor.tsx | 19 + playground/src/components/editor/config.ts | 107 +++ playground/src/styles/app.css | 5 + playground/src/styles/playground.css | 869 ++++++++++++++++++ playground/src/test/FakeJsonEditor.tsx | 39 + 11 files changed, 1555 insertions(+) create mode 100644 playground/src/components/SegmentedTabs.test.tsx create mode 100644 playground/src/components/SegmentedTabs.tsx create mode 100644 playground/src/components/StatusBadge.test.tsx create mode 100644 playground/src/components/StatusBadge.tsx create mode 100644 playground/src/components/editor/JsonEditor.test.tsx create mode 100644 playground/src/components/editor/JsonEditor.tsx create mode 100644 playground/src/components/editor/LazyJsonEditor.tsx create mode 100644 playground/src/components/editor/config.ts create mode 100644 playground/src/styles/app.css create mode 100644 playground/src/styles/playground.css create mode 100644 playground/src/test/FakeJsonEditor.tsx diff --git a/playground/src/components/SegmentedTabs.test.tsx b/playground/src/components/SegmentedTabs.test.tsx new file mode 100644 index 0000000000..4b2a81f13b --- /dev/null +++ b/playground/src/components/SegmentedTabs.test.tsx @@ -0,0 +1,33 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { SegmentedTabs } from "@/components/SegmentedTabs"; + +describe("SegmentedTabs", () => { + it("reports the selected Kumo tab", () => { + const onChange = vi.fn(); + render( + , + ); + + const form = screen.getByRole("tab", { name: "Form" }); + const json = screen.getByRole("tab", { name: "JSON" }); + expect(screen.getByRole("tablist", { name: "Modes" })).toBeVisible(); + expect(form).toHaveAttribute("aria-selected", "true"); + expect(form).toHaveAttribute("id", "modes-tab-form"); + expect(form).toHaveAttribute("aria-controls", "modes-panel-form"); + expect(json).toHaveAttribute("aria-selected", "false"); + + fireEvent.click(json); + expect(onChange).toHaveBeenCalledWith("json"); + }); +}); diff --git a/playground/src/components/SegmentedTabs.tsx b/playground/src/components/SegmentedTabs.tsx new file mode 100644 index 0000000000..17c49d7993 --- /dev/null +++ b/playground/src/components/SegmentedTabs.tsx @@ -0,0 +1,65 @@ +import { Tabs } from "@cloudflare/kumo/components/tabs"; +import { useEffect, useRef } from "react"; + +type TabItem = { + value: Value; + label: string; +}; + +type Props = { + id: string; + label: string; + items: readonly TabItem[]; + value: Value; + onChange: (value: Value) => void; +}; + +/** Generates the ID referenced by a matching panel's `aria-labelledby`. */ +export function tabId(id: string, value: string): string { + return `${id}-tab-${value}`; +} + +/** Generates the panel ID referenced by a matching tab's `aria-controls`. */ +export function tabPanelId(id: string, value: string): string { + return `${id}-panel-${value}`; +} + +/** Adds stable ARIA tab/panel relationships around Kumo's controlled tabs. */ +export function SegmentedTabs({ + id, + label, + items, + value, + onChange, +}: Props) { + const root = useRef(null); + useEffect(() => { + root.current?.querySelector('[role="tablist"]')?.setAttribute("aria-label", label); + }, [label]); + + return ( +
+ ({ + ...item, + render: ( +
+ ); +} diff --git a/playground/src/components/StatusBadge.test.tsx b/playground/src/components/StatusBadge.test.tsx new file mode 100644 index 0000000000..1b28bbe3d0 --- /dev/null +++ b/playground/src/components/StatusBadge.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { StatusBadge } from "@/components/StatusBadge"; + +describe("StatusBadge", () => { + it.each([ + ["READY", "ready"], + ["SETUP_FAILED", "setup_failed"], + ["canceled", "canceled"], + [undefined, "unknown"], + ])("normalizes %s", (status, label) => { + render(); + expect(screen.getByText(label)).toBeVisible(); + }); +}); diff --git a/playground/src/components/StatusBadge.tsx b/playground/src/components/StatusBadge.tsx new file mode 100644 index 0000000000..3b5e2b9a67 --- /dev/null +++ b/playground/src/components/StatusBadge.tsx @@ -0,0 +1,28 @@ +import { Badge, type BadgeVariant } from "@cloudflare/kumo/components/badge"; + +/** Maps known Cog statuses to semantic colors and renders unknown values neutrally. */ +export function StatusBadge({ status }: { status?: string }) { + const normalized = status?.toLowerCase() || "unknown"; + const variants: Record = { + ready: "success", + succeeded: "success", + busy: "warning", + starting: "warning", + processing: "warning", + defunct: "error", + failed: "error", + error: "error", + setup_failed: "error", + unhealthy: "error", + unreachable: "error", + canceled: "neutral", + unknown: "neutral", + }; + return ( + + + {normalized} + + + ); +} diff --git a/playground/src/components/editor/JsonEditor.test.tsx b/playground/src/components/editor/JsonEditor.test.tsx new file mode 100644 index 0000000000..e3963551eb --- /dev/null +++ b/playground/src/components/editor/JsonEditor.test.tsx @@ -0,0 +1,157 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { JsonEditor } from "@/components/editor/JsonEditor"; + +describe("JsonEditor", () => { + it("renders highlighted read-only JSON and follows controlled updates", async () => { + const { container, rerender } = render( + , + ); + + const editor = screen.getByRole("textbox", { name: "Response JSON" }); + expect(editor).toHaveAttribute("aria-readonly", "true"); + expect(editor).toHaveAttribute("contenteditable", "false"); + expect(container.querySelector(".cm-gutters")).toBeInTheDocument(); + expect(container.querySelector(".cm-foldGutter")).toBeInTheDocument(); + expect(container.querySelector(".cm-content span")).toBeInTheDocument(); + + rerender(); + await waitFor(() => expect(editor).toHaveTextContent('"answer":43')); + }); + + it("follows the tail until the user scrolls away", async () => { + const { container, rerender } = render( + , + ); + const scroller = container.querySelector(".cm-scroller"); + if (!scroller) throw new Error("CodeMirror scroller was not rendered"); + + let scrollHeight = 1_000; + Object.defineProperties(scroller, { + clientHeight: { configurable: true, value: 200 }, + scrollHeight: { configurable: true, get: () => scrollHeight }, + scrollTop: { configurable: true, value: 0, writable: true }, + }); + + rerender(); + await waitFor(() => expect(scroller.scrollTop).toBe(scrollHeight)); + await nextAnimationFrame(); + + scroller.scrollTop = 300; + fireEvent.scroll(scroller); + scrollHeight = 1_200; + rerender(); + await nextAnimationFrame(); + + expect(scroller.scrollTop).toBe(300); + + scrollHeight = 1_300; + rerender( + , + ); + rerender( + , + ); + await nextAnimationFrame(); + + expect(scroller.scrollTop).toBe(300); + + rerender( + , + ); + scrollHeight = 1_400; + rerender( + , + ); + + await waitFor(() => expect(scroller.scrollTop).toBe(scrollHeight)); + }); + + it("does not override a user scroll while a tail measurement is queued", async () => { + const { container, rerender } = render( + , + ); + const scroller = container.querySelector(".cm-scroller"); + if (!scroller) throw new Error("CodeMirror scroller was not rendered"); + Object.defineProperties(scroller, { + clientHeight: { configurable: true, value: 200 }, + scrollHeight: { configurable: true, value: 1_000 }, + scrollTop: { configurable: true, value: 0, writable: true }, + }); + + rerender(); + fireEvent.wheel(scroller, { deltaY: -100 }); + scroller.scrollTop = 300; + fireEvent.scroll(scroller); + await nextAnimationFrame(); + + expect(scroller.scrollTop).toBe(300); + }); + + it("copies the complete document", async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: { writeText }, + }); + render(); + + fireEvent.click(screen.getByRole("button", { name: "Copy Copy JSON" })); + + await waitFor(() => expect(writeText).toHaveBeenCalledWith('{"answer":42}')); + }); + + it("removes disabled editors and copy controls from keyboard navigation", () => { + render( + , + ); + + const editor = screen.getByRole("textbox", { name: "Disabled JSON" }); + expect(editor).toHaveAttribute("tabindex", "-1"); + expect(editor).toHaveAttribute("aria-disabled", "true"); + expect(editor).toHaveAttribute("aria-invalid", "true"); + expect(editor).toHaveAttribute("aria-describedby", "disabled-help"); + expect(screen.getByRole("button", { name: "Copy Disabled JSON" })).toBeDisabled(); + }); + + it("leaves Tab available for keyboard navigation", () => { + render(); + const editor = screen.getByRole("textbox", { name: "Input JSON" }); + editor.focus(); + + expect(fireEvent.keyDown(editor, { key: "Tab", code: "Tab" })).toBe(true); + }); + + it("does not add controlled updates to undo history", async () => { + const { rerender } = render(); + const editor = screen.getByRole("textbox", { name: "Updated JSON" }); + + rerender(); + await waitFor(() => expect(editor).toHaveTextContent('"answer":43')); + editor.focus(); + fireEvent.keyDown(editor, { key: "z", code: "KeyZ", metaKey: true }); + + expect(editor).toHaveTextContent('"answer":43'); + }); +}); + +function nextAnimationFrame(): Promise { + return new Promise((resolve) => window.requestAnimationFrame(() => resolve())); +} diff --git a/playground/src/components/editor/JsonEditor.tsx b/playground/src/components/editor/JsonEditor.tsx new file mode 100644 index 0000000000..3c778afa46 --- /dev/null +++ b/playground/src/components/editor/JsonEditor.tsx @@ -0,0 +1,217 @@ +import { Compartment, EditorState, Transaction } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import { useEffect, useRef } from "react"; + +import { editorBehavior, jsonEditorExtensions } from "@/components/editor/config"; + +export type JsonEditorProps = { + value: string; + onChange?: (value: string) => void; + readOnly?: boolean; + disabled?: boolean; + invalid?: boolean; + describedBy?: string; + followTail?: boolean; + active?: boolean; + className?: string; + label: string; + autoHeight?: boolean; +}; + +/** + * Renders a controlled CodeMirror JSON editor with accessible read-only and disabled modes, + * copying, optional content-based sizing, and live-output tail following. + */ +export function JsonEditor({ + value, + onChange, + readOnly = false, + disabled = false, + invalid = false, + describedBy, + followTail = false, + active = true, + className = "", + label, + autoHeight = false, +}: JsonEditorProps) { + const hostRef = useRef(null); + const editorRef = useRef(undefined); + const initialValue = useRef(value); + const initialLabel = useRef(label); + const initialReadOnly = useRef(readOnly); + const initialDisabled = useRef(disabled); + const initialInvalid = useRef(invalid); + const initialDescribedBy = useRef(describedBy); + const onChangeRef = useRef(onChange); + const updatingValue = useRef(false); + const followRef = useRef(true); + const followEnabled = useRef(followTail); + const scrollingToTail = useRef(false); + const scrollRequest = useRef(0); + const activeRef = useRef(active); + const followTailRef = useRef(followTail); + const behavior = useRef(new Compartment()); + onChangeRef.current = onChange; + activeRef.current = active; + followTailRef.current = followTail; + + useEffect(() => { + if (!hostRef.current) return; + const editor = new EditorView({ + parent: hostRef.current, + state: EditorState.create({ + doc: initialValue.current, + extensions: [ + ...jsonEditorExtensions(), + behavior.current.of( + editorBehavior( + initialReadOnly.current, + initialDisabled.current, + initialInvalid.current, + initialLabel.current, + initialDescribedBy.current, + ), + ), + EditorView.updateListener.of((update) => { + if (update.docChanged && !updatingValue.current) { + onChangeRef.current?.(update.state.doc.toString()); + } + }), + ], + }), + }); + editorRef.current = editor; + const updateFollow = () => { + if (scrollingToTail.current) return; + const { clientHeight, scrollHeight, scrollTop } = editor.scrollDOM; + followRef.current = scrollHeight - scrollTop - clientHeight < 48; + }; + const stopFollowing = () => { + followRef.current = false; + scrollingToTail.current = false; + scrollRequest.current += 1; + }; + const stopFollowingFromWheel = (event: WheelEvent) => { + if (event.deltaY < 0) stopFollowing(); + }; + const stopFollowingFromPointer = (event: PointerEvent) => { + if (event.target === editor.scrollDOM) stopFollowing(); + }; + const stopFollowingFromKey = (event: KeyboardEvent) => { + if (["ArrowUp", "Home", "PageUp"].includes(event.key)) stopFollowing(); + }; + editor.scrollDOM.addEventListener("scroll", updateFollow, { passive: true }); + editor.scrollDOM.addEventListener("wheel", stopFollowingFromWheel, { passive: true }); + editor.scrollDOM.addEventListener("touchstart", stopFollowing, { passive: true }); + editor.scrollDOM.addEventListener("pointerdown", stopFollowingFromPointer, { passive: true }); + editor.scrollDOM.addEventListener("keydown", stopFollowingFromKey); + return () => { + editor.scrollDOM.removeEventListener("scroll", updateFollow); + editor.scrollDOM.removeEventListener("wheel", stopFollowingFromWheel); + editor.scrollDOM.removeEventListener("touchstart", stopFollowing); + editor.scrollDOM.removeEventListener("pointerdown", stopFollowingFromPointer); + editor.scrollDOM.removeEventListener("keydown", stopFollowingFromKey); + editor.destroy(); + editorRef.current = undefined; + }; + }, []); + + useEffect(() => { + editorRef.current?.dispatch({ + effects: behavior.current.reconfigure( + editorBehavior(readOnly, disabled, invalid, label, describedBy), + ), + }); + }, [describedBy, disabled, invalid, label, readOnly]); + + useEffect(() => { + const editor = editorRef.current; + if (!editor) return; + const current = editor.state.doc.toString(); + if (followTail && !followEnabled.current) followRef.current = true; + followEnabled.current = followTail; + const scroll = followTail && active && followRef.current; + if (current !== value) { + updatingValue.current = true; + editor.dispatch({ + changes: changedRange(current, value), + annotations: Transaction.addToHistory.of(false), + selection: readOnly + ? undefined + : { anchor: Math.min(editor.state.selection.main.head, value.length) }, + }); + updatingValue.current = false; + } + if (!scroll) return; + + const request = ++scrollRequest.current; + scrollingToTail.current = true; + editor.requestMeasure({ + read(view) { + return view.scrollDOM.scrollHeight; + }, + write(scrollHeight, view) { + if ( + scrollRequest.current !== request || + !activeRef.current || + !followTailRef.current || + !followRef.current + ) { + if (scrollRequest.current === request) scrollingToTail.current = false; + return; + } + view.scrollDOM.scrollTop = scrollHeight; + window.requestAnimationFrame(() => { + if (scrollRequest.current === request) scrollingToTail.current = false; + }); + }, + }); + }, [active, followTail, readOnly, value]); + + const copy = async () => { + try { + await navigator.clipboard.writeText(editorRef.current?.state.doc.toString() ?? value); + } catch { + const editor = editorRef.current; + if (!editor) return; + editor.dispatch({ selection: { anchor: 0, head: editor.state.doc.length } }); + editor.focus(); + } + }; + + return ( +
+
+ +
+ ); +} + +function editorHeight(value: string): number { + return Math.min(320, Math.max(80, value.split("\n").length * 18 + 18)); +} + +function changedRange(current: string, value: string) { + let from = 0; + while (from < current.length && from < value.length && current[from] === value[from]) from += 1; + + let currentTo = current.length; + let valueTo = value.length; + while (currentTo > from && valueTo > from && current[currentTo - 1] === value[valueTo - 1]) { + currentTo -= 1; + valueTo -= 1; + } + return { from, to: currentTo, insert: value.slice(from, valueTo) }; +} diff --git a/playground/src/components/editor/LazyJsonEditor.tsx b/playground/src/components/editor/LazyJsonEditor.tsx new file mode 100644 index 0000000000..4780a1efec --- /dev/null +++ b/playground/src/components/editor/LazyJsonEditor.tsx @@ -0,0 +1,19 @@ +import { Loader } from "@cloudflare/kumo/components/loader"; +import { lazy, Suspense } from "react"; + +import type { JsonEditorProps } from "@/components/editor/JsonEditor"; + +const JsonEditor = lazy(async () => ({ + default: (await import("@/components/editor/JsonEditor")).JsonEditor, +})); + +/** Keeps CodeMirror out of the initial bundle until an editor is first displayed. */ +export function LazyJsonEditor(props: JsonEditorProps) { + return ( + } + > + + + ); +} diff --git a/playground/src/components/editor/config.ts b/playground/src/components/editor/config.ts new file mode 100644 index 0000000000..beea25adbd --- /dev/null +++ b/playground/src/components/editor/config.ts @@ -0,0 +1,107 @@ +import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; +import { json } from "@codemirror/lang-json"; +import { + bracketMatching, + foldGutter, + foldKeymap, + HighlightStyle, + syntaxHighlighting, +} from "@codemirror/language"; +import { EditorState, type Extension } from "@codemirror/state"; +import { + drawSelection, + EditorView, + highlightActiveLine, + highlightActiveLineGutter, + highlightSpecialChars, + keymap, + lineNumbers, +} from "@codemirror/view"; +import { tags } from "@lezer/highlight"; + +/** Builds the shared JSON language, editing, highlighting, and theme extensions. */ +export function jsonEditorExtensions(): Extension[] { + return [ + lineNumbers(), + foldGutter(), + highlightSpecialChars(), + history(), + drawSelection(), + bracketMatching(), + highlightActiveLine(), + highlightActiveLineGutter(), + keymap.of([...defaultKeymap, ...historyKeymap, ...foldKeymap]), + json(), + syntaxHighlighting(kumoHighlightStyle), + kumoEditorTheme, + EditorView.lineWrapping, + ]; +} + +/** Maps editor mode and accessibility state into reconfigurable CodeMirror extensions. */ +export function editorBehavior( + readOnly: boolean, + disabled: boolean, + invalid: boolean, + label: string, + describedBy?: string, +): Extension[] { + const attributes: Record = { + "aria-label": label, + "aria-readonly": String(readOnly || disabled), + spellcheck: "false", + }; + if (describedBy) attributes["aria-describedby"] = describedBy; + if (invalid) attributes["aria-invalid"] = "true"; + if (disabled) { + attributes["aria-disabled"] = "true"; + attributes.tabindex = "-1"; + } else if (readOnly) { + attributes.tabindex = "0"; + } + return [ + EditorState.readOnly.of(readOnly || disabled), + EditorView.editable.of(!readOnly && !disabled), + EditorView.contentAttributes.of(attributes), + ]; +} + +const kumoEditorTheme = EditorView.theme({ + "&": { + height: "100%", + backgroundColor: "var(--color-kumo-base)", + color: "var(--text-color-kumo-default)", + }, + "&.cm-focused": { outline: "none" }, + ".cm-scroller": { + fontFamily: '"SF Mono", Monaco, Consolas, "Liberation Mono", monospace', + fontSize: "12px", + lineHeight: "1.5", + }, + ".cm-content": { caretColor: "var(--text-color-kumo-default)", padding: "8px 0" }, + ".cm-line": { padding: "0 10px" }, + ".cm-gutters": { + backgroundColor: "var(--color-kumo-elevated)", + borderRight: "1px solid var(--color-kumo-hairline)", + color: "var(--text-color-kumo-subtle)", + }, + ".cm-activeLine, .cm-activeLineGutter": { + backgroundColor: "color-mix(in srgb, var(--color-kumo-fill) 45%, transparent)", + }, + ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { + backgroundColor: "var(--color-kumo-info-tint) !important", + }, + ".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--text-color-kumo-default)" }, +}); + +const kumoHighlightStyle = HighlightStyle.define([ + { tag: tags.propertyName, color: "var(--text-color-kumo-link)" }, + { tag: [tags.string, tags.special(tags.string)], color: "var(--text-color-kumo-success)" }, + { tag: tags.number, color: "var(--text-color-kumo-warning)" }, + { tag: [tags.bool, tags.null], color: "var(--text-color-kumo-brand)" }, + { tag: tags.escape, color: "var(--text-color-kumo-info)" }, + { + tag: [tags.brace, tags.squareBracket, tags.separator], + color: "var(--text-color-kumo-subtle)", + }, +]); diff --git a/playground/src/styles/app.css b/playground/src/styles/app.css new file mode 100644 index 0000000000..ac83ce2dcb --- /dev/null +++ b/playground/src/styles/app.css @@ -0,0 +1,5 @@ +@source "../../node_modules/@cloudflare/kumo/dist/**/*.{js,jsx,ts,tsx}"; +@import "@cloudflare/kumo/styles/tailwind"; +@import "tailwindcss"; + +/* Playground-specific layout extends Kumo's semantic tokens and components. */ diff --git a/playground/src/styles/playground.css b/playground/src/styles/playground.css new file mode 100644 index 0000000000..aa1a706295 --- /dev/null +++ b/playground/src/styles/playground.css @@ -0,0 +1,869 @@ +/* + * Cog Playground styles. + * + * Colors come from Kumo's semantic design tokens. Light/dark is driven by the + * data-mode attribute on , which switches the token values automatically. + */ + +:root { + /* Surfaces */ + --bg: var(--color-kumo-canvas); + --surface: var(--color-kumo-elevated); + --surface-hover: var(--color-kumo-fill-hover); + --border: var(--color-kumo-hairline); + + /* Text */ + --text: var(--text-color-kumo-default); + --text-muted: var(--text-color-kumo-subtle); + + /* Brand / actions */ + --accent: var(--color-kumo-brand); + --on-accent: #fff; + + /* Status (text-optimized tokens for good contrast on surfaces) */ + --green: var(--text-color-kumo-success); + --yellow: var(--text-color-kumo-warning); + --red: var(--text-color-kumo-danger); +} + +* { + box-sizing: border-box; +} +/* Ensure the hidden attribute wins over author display rules below. */ +[hidden] { + display: none !important; +} +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + background: var(--bg); + color: var(--text); + font-size: 14px; + line-height: 1.5; +} +.muted { + color: var(--text-muted); + font-size: 12px; +} +.spacer { + flex: 1; +} + +header { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 20px; + background: var(--surface); + border-bottom: 1px solid var(--border); +} +header h1 { + font-size: 16px; + margin: 0; + font-weight: 600; + white-space: nowrap; +} +#playground-toolbar { + display: flex; + align-items: flex-start; + gap: 12px 24px; + padding: 10px 20px; + background: var(--surface); + border-bottom: 1px solid var(--border); + flex-wrap: wrap; + position: sticky; + top: 0; + z-index: 20; +} +#target-bar, +#action-bar { + position: relative; + display: flex; + align-items: flex-end; + gap: 8px; + min-width: 0; + margin: 0; + padding: 0; + border: 0; +} +#target-bar { + flex: 1 1 360px; + max-width: 640px; + align-items: flex-end; + flex-wrap: wrap; +} +.target-input { + flex: 1 1 280px; + min-width: 220px; +} +#action-bar { + flex: 0 1 auto; + align-items: center; + flex-wrap: wrap; + margin-left: auto; +} +#target-bar > output { + flex-basis: 100%; + min-width: 0; + overflow-wrap: anywhere; +} +#target-bar > output:empty { + display: none; +} +.run-actions { + display: flex; + align-items: center; + gap: 8px; + padding-left: 12px; + border-left: 1px solid var(--border); +} +#target-bar label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.playground-segmented { + flex: none; +} +.playground-segmented > div { + border-radius: 8px; + box-shadow: none; +} +.playground-segmented [role="tablist"], +.playground-options { + display: flex; + width: max-content; + max-width: 100%; + gap: 2px; + padding: 3px; + background: var(--color-kumo-fill); + border: 1px solid var(--border); + border-radius: 8px; +} +.playground-segmented [role="tablist"] { + align-items: stretch; + height: 34px; + overflow-x: auto; + white-space: nowrap; +} +.playground-segmented [role="tab"], +.playground-options button { + min-height: 26px; + margin: 0; + padding: 4px 12px; + background: transparent; + border-radius: 5px; + color: var(--text-muted); + font-size: 12px; + font-weight: 600; +} +.playground-segmented [role="tab"][aria-selected="true"], +.playground-options button[aria-pressed="true"] { + background: var(--accent); + color: var(--on-accent); + box-shadow: 0 1px 2px color-mix(in srgb, var(--text) 20%, transparent); +} +.playground-segmented [role="tab"][aria-selected="false"]:hover, +.playground-options button[aria-pressed="false"]:hover { + background: var(--surface-hover); + color: var(--text); +} +.playground-segmented [role="presentation"] { + display: none; +} +.playground-options { + margin: 0; +} +.playground-options button { + border: 0; + cursor: pointer; +} + +#setup-panel { + position: relative; + flex: 0 0 auto; + margin-left: 0; +} +#setup-panel summary { + padding: 6px 0; + cursor: pointer; + font-size: 13px; + color: var(--text-muted); + white-space: nowrap; +} +#setup-logs { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 30; + width: min(720px, calc(100vw - 40px)); + margin: 0; + padding: 12px; + max-height: 200px; + overflow: auto; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 8px 24px color-mix(in srgb, var(--text) 18%, transparent); + font-family: "SF Mono", Monaco, Consolas, monospace; + font-size: 12px; + white-space: pre-wrap; + color: var(--text-muted); +} + +main { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1px; + background: var(--border); + min-height: 60vh; +} +@media (max-width: 900px) { + main { + grid-template-columns: 1fr; + } +} +@media (min-width: 601px) and (max-width: 1100px) { + #target-bar { + order: 1; + max-width: none; + } + #action-bar { + order: 2; + flex-basis: auto; + } + .setup-label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } +} +section { + background: var(--bg); + padding: 16px 20px; +} +#json-container { + display: flex; + flex-direction: column; + gap: 6px; +} +.panel-head { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 14px; +} +.panel-head h2 { + font-size: 13px; + margin: 0; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} +.response-title { + display: flex; + align-items: center; + gap: 8px; +} + +/* Inputs */ +input[type="text"], +input[type="password"], +input[type="url"], +input[type="number"], +select, +textarea { + width: 100%; + padding: 6px 10px; + background: var(--color-kumo-control); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text); + font-size: 13px; + font-family: inherit; +} +textarea { + resize: vertical; +} +input:focus, +select:focus, +textarea:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); +} +input[type="checkbox"] { + width: auto; + margin-right: 6px; +} +input[type="file"] { + font-size: 12px; + color: var(--text-muted); + width: auto; +} +input[type="file"]::file-selector-button { + padding: 4px 10px; + background: var(--surface-hover); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text); + cursor: pointer; + font-size: 12px; + margin-right: 8px; +} + +.field { + margin-bottom: 14px; +} +.field label { + display: block; + font-size: 13px; + font-weight: 500; + margin-bottom: 4px; +} +.field label .req { + color: var(--red); +} +.field .desc { + display: block; + font-size: 12px; + color: var(--text-muted); + margin-bottom: 4px; +} +.deprecated-tag { + color: var(--yellow); + font-weight: 400; + font-size: 12px; +} +.field-error { + display: block; + color: var(--red); + font-size: 12px; + margin-top: 4px; +} +.field-errors { + margin: 4px 0 0; + padding-left: 18px; + color: var(--red); + font-size: 12px; +} +.validation-summary { + margin: 6px 0 0; + padding-left: 18px; +} + +.file-widget { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; +} +.file-widget .file-name { + font-size: 12px; + color: var(--green); +} + +#webhook-options { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 20px; + background: var(--surface); + border-bottom: 1px solid var(--border); + flex-wrap: wrap; + margin: 0; +} +.webhook-title { + color: var(--text-muted); + font-size: 12px; + font-weight: 600; +} +#webhook-options label { + font-size: 12px; + color: var(--text-muted); + display: flex; + align-items: center; +} +.json-editor { + position: relative; + border: 1px solid var(--border); + border-radius: 6px; + width: 100%; + min-height: 120px; + overflow: hidden; +} +.json-editor:focus-within { + border-color: var(--accent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); +} +.json-editor > div:first-child, +.json-editor .cm-editor { + width: 100%; + height: 100%; +} +.editor-copy { + position: absolute; + top: 6px; + right: 14px; + z-index: 6; + padding: 2px 8px; + font-size: 11px; + font-weight: 500; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text-muted); + cursor: pointer; + opacity: 0; + transition: opacity 0.12s ease; +} +.json-editor:hover .editor-copy { + opacity: 0.95; +} +.editor-copy:focus-visible { + opacity: 0.95; +} +.editor-copy:hover { + color: var(--text); + background: var(--surface-hover); +} +.response-editor { + min-height: 80px; +} +.json-input { + height: min(55vh, 520px); + min-height: 280px; +} +.viewer-inspector { + min-height: 80px; +} +.viewer-timeline { + margin-top: 6px; +} +.json-field { + height: 140px; +} +.metrics-table { + border-collapse: collapse; + font-size: 12px; + width: 100%; +} +.metrics-table td, +.metrics-table th { + padding: 3px 10px 3px 0; + border-bottom: 1px solid var(--border); + font-weight: normal; + text-align: left; +} +.metrics-table th { + color: var(--text-muted); +} + +.notice { + display: block; + background: var(--color-kumo-warning-tint); + border: 1px solid var(--color-kumo-warning); + border-radius: 6px; + padding: 8px 12px; + font-size: 13px; + color: var(--text-color-kumo-warning); + margin-bottom: 12px; +} +.error-container { + background: var(--color-kumo-danger-tint); + border: 1px solid var(--color-kumo-danger); + border-radius: 6px; + padding: 10px 14px; + margin-bottom: 12px; + font-size: 13px; + color: var(--text-color-kumo-danger); +} + +.field-heading { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; +} +.field-heading label { + margin: 0; +} +.field-disabled { + opacity: 0.55; + pointer-events: none; +} +.input-media { + display: block; + max-width: 100%; + max-height: 240px; + margin-top: 8px; + border: 1px solid var(--border); + border-radius: 6px; +} +.empty-output { + padding: 32px 16px; + border: 1px dashed var(--border); + border-radius: 6px; + color: var(--text-muted); + text-align: center; +} +.live-output { + min-height: 280px; + max-height: 60vh; + overflow: auto; + padding: 16px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; +} +.live-output .empty-output { + border: 0; +} +.live-output .metrics-table { + margin-bottom: 14px; +} +.text-output { + min-height: 120px; + margin: 0; + padding: 0; + background: transparent; + border: 0; + font-family: inherit; + font-size: 14px; + line-height: 1.7; + white-space: pre-wrap; + word-break: break-word; +} +.inspector-logs { + min-height: 220px; + max-height: 60vh; + margin: 0; + overflow: auto; + padding: 12px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + font-family: "SF Mono", Monaco, Consolas, monospace; + font-size: 12px; + white-space: pre-wrap; + word-break: break-word; +} +.request-summary { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin: 0 0 16px; +} +.request-summary > div { + min-width: 0; + padding: 10px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; +} +.request-summary dt { + color: var(--text-muted); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.request-summary dd { + margin: 4px 0 0; + overflow-wrap: anywhere; + font-family: "SF Mono", Monaco, Consolas, monospace; + font-size: 12px; +} +.method-badge { + display: inline-block; + padding: 1px 5px; + background: var(--color-kumo-info-tint); + border-radius: 4px; + color: var(--text-color-kumo-info); + font-size: 10px; + font-weight: 700; +} +.inspector-document { + margin-bottom: 10px; +} +.inspector-document > summary { + margin-bottom: 6px; + color: var(--text-muted); + cursor: pointer; + font-size: 12px; + font-weight: 600; +} +.trace-timeline { + --trace-background: color-mix(in srgb, var(--accent) 12%, transparent); + --trace-color: var(--accent); + + position: relative; + max-height: 60vh; + margin: 0; + padding: 2px 8px 2px 92px; + overflow: auto; + list-style: none; +} +.trace-timeline::before { + position: absolute; + top: 8px; + bottom: 8px; + left: 70px; + width: 1px; + background: var(--border); + content: ""; +} +.trace-timeline > li { + position: relative; + display: grid; + grid-template-columns: 70px minmax(0, 1fr); + gap: 8px; + min-height: 42px; + padding: 0 0 14px; +} +.trace-timeline > li::before { + position: absolute; + top: 5px; + left: -26px; + width: 9px; + height: 9px; + background: var(--trace-color); + border: 2px solid var(--bg); + border-radius: 50%; + box-shadow: 0 0 0 1px var(--border); + content: ""; +} +.trace-timeline time { + position: absolute; + top: 1px; + left: -92px; + width: 58px; + color: var(--text-muted); + font-family: "SF Mono", Monaco, Consolas, monospace; + font-size: 11px; + text-align: right; +} +.trace-kind { + width: fit-content; + height: fit-content; + padding: 1px 5px; + background: var(--trace-background); + border-radius: 4px; + color: var(--trace-color); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; +} +.trace-timeline strong { + font-size: 12px; + font-weight: 600; +} +.trace-timeline details { + margin-top: 4px; +} +.trace-timeline summary { + color: var(--text-muted); + cursor: pointer; + font-size: 11px; +} +.trace-request { + --trace-background: var(--color-kumo-info-tint); + --trace-color: var(--text-color-kumo-info); +} +.trace-response { + --trace-background: var(--color-kumo-success-tint); + --trace-color: var(--green); +} +.trace-webhook { + --trace-background: var(--color-kumo-warning-tint); + --trace-color: var(--yellow); +} +.trace-cancel { + --trace-background: var(--surface-hover); + --trace-color: var(--text-muted); +} +.trace-error { + --trace-background: var(--color-kumo-danger-tint); + --trace-color: var(--red); +} + +@media (max-width: 600px) { + header { + display: grid; + grid-template-columns: auto auto 1fr auto auto; + padding: 8px 12px; + } + header .spacer { + display: none; + } + header > .muted { + grid-row: 2; + grid-column: 1 / 5; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + header > #setup-panel { + grid-row: 2; + grid-column: 5; + justify-self: end; + } + header > button:first-of-type { + grid-column: 4; + } + header > button:last-of-type { + grid-column: 5; + } + #playground-toolbar, + #webhook-options { + padding-inline: 12px; + } + #playground-toolbar { + gap: 12px; + } + section { + padding-inline: 12px; + } + #playground-toolbar { + position: relative; + top: auto; + } + #target-bar, + #action-bar { + flex: 1 1 100%; + } + #action-bar { + margin-left: 0; + } + #target-bar { + max-width: none; + } + .target-input { + flex-basis: 190px; + min-width: 190px; + } + .playground-options button { + padding-inline: 9px; + } + #action-bar > input { + flex: 1 1 130px; + width: auto; + min-width: 130px; + } + .setup-label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + #setup-logs { + right: -4px; + width: calc(100vw - 24px); + } + .panel-head > * { + min-width: 0; + } + .playground-segmented { + max-width: 100%; + } +} +@media (max-width: 360px) { + header { + grid-template-columns: auto 1fr auto auto; + } + header > output { + grid-row: 2; + grid-column: 1; + } + header > .muted { + grid-row: 3; + grid-column: 1 / -1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + header > #setup-panel { + grid-row: 2; + grid-column: 3 / 5; + justify-self: end; + } + header > button:first-of-type { + grid-column: 3; + } + header > button:last-of-type { + grid-column: 4; + } +} + +@media (prefers-reduced-motion: reduce) { + .live-output { + scroll-behavior: auto; + } + .streaming-cursor { + animation: none; + } +} + +.output-item { + margin-bottom: 10px; +} +.output-item img { + max-width: 100%; + border-radius: 6px; + border: 1px solid var(--border); +} +.output-item audio, +.output-item video { + width: 100%; + max-width: 500px; +} +.output-item pre { + margin: 0; + padding: 10px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + font-family: "SF Mono", Monaco, Consolas, monospace; + font-size: 12px; + white-space: pre-wrap; + word-break: break-word; + overflow-x: auto; +} +.output-item a { + color: var(--text-color-kumo-link); + font-size: 13px; +} +.streaming-cursor { + display: inline-block; + width: 8px; + height: 14px; + background: var(--accent); + animation: blink 1s step-end infinite; + vertical-align: text-bottom; + margin-left: 2px; +} +@keyframes blink { + 50% { + opacity: 0; + } +} diff --git a/playground/src/test/FakeJsonEditor.tsx b/playground/src/test/FakeJsonEditor.tsx new file mode 100644 index 0000000000..6604130a0c --- /dev/null +++ b/playground/src/test/FakeJsonEditor.tsx @@ -0,0 +1,39 @@ +import type { JsonEditorProps } from "@/components/editor/JsonEditor"; + +export function FakeJsonEditor({ + label, + onChange, + value, + describedBy, + disabled, + invalid, + readOnly, + autoHeight, + followTail, + active, +}: JsonEditorProps) { + if (onChange) { + return ( +