From b456f36984d033bf2d4fd6a30c44e69c4ed32cfd Mon Sep 17 00:00:00 2001 From: Alejandro Perez Gancedo Date: Sun, 21 Sep 2025 22:50:15 +0100 Subject: [PATCH 1/6] feat: render docs --- README.md | 4 - TODO.md | 10 ++ cmd/server/main.go | 3 + docs/sample.md | 36 ++++ docs/sample.mdx | 58 +++++++ go.mod | 9 + go.sum | 28 ++++ internal/handlers/document.go | 308 ++++++++++++++++++++++++++++++++++ internal/service/document.go | 254 ++++++++++++++++++++++++++++ 9 files changed, 706 insertions(+), 4 deletions(-) create mode 100644 TODO.md create mode 100644 docs/sample.md create mode 100644 docs/sample.mdx create mode 100644 internal/handlers/document.go create mode 100644 internal/service/document.go diff --git a/README.md b/README.md index 542eb5e..4a30326 100644 --- a/README.md +++ b/README.md @@ -203,10 +203,6 @@ export ENVIRONMENT=production 4. Add tests 5. Submit a pull request -## License - -MIT License - see LICENSE file for details. - ## Acknowledgments - Inspired by Google's internal golinks system diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..4eddbba --- /dev/null +++ b/TODO.md @@ -0,0 +1,10 @@ +- [ ] Support Postgres as a DB not just SQLite +- [ ] Add authentication and login, whether propietary or integration with 3rd parties +- [ ] Add authorization, different users can have different roles +- [ ] Support for theme customization +- [ ] Support for user administration +- [ ] Add golang migrations +- [ ] Consider the use of an ORM +- [ ] Statistics of most accessed docs +- [ ] Support to render Markdown docs and MDX docs +- [ ] Deployment in google app engine \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 12a816c..7850b5d 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -44,13 +44,16 @@ func main() { // Initialize services linkService := service.NewLinkService(shortcutRepo, queryRepo) + docService := service.NewDocumentService("docs") // Initialize handlers handler := handlers.NewHandler(linkService, cfg) + docHandler := handlers.NewDocumentHandler(docService) // Setup router router := mux.NewRouter() handler.RegisterRoutes(router) + docHandler.RegisterRoutes(router) // Setup server server := &http.Server{ diff --git a/docs/sample.md b/docs/sample.md new file mode 100644 index 0000000..fa32d6c --- /dev/null +++ b/docs/sample.md @@ -0,0 +1,36 @@ +# Sample Documentation + +This is a **sample markdown document** to test the rendering functionality. + +## Features + +- Markdown rendering with [Goldmark](https://github.com/yuin/goldmark) +- Syntax highlighting for code blocks +- Support for tables, lists, and more + +## Code Example + +```go +func main() { + fmt.Println("Hello, GoLinks!") +} +``` + +## Table Example + +| Feature | Status | +|---------|--------| +| Markdown | ✅ Working | +| MDX | 🚧 In Progress | +| Syntax Highlighting | ✅ Working | + +> This is a blockquote to test styling. + +### Links and Images + +- [GoLinks Repository](https://github.com/example/golinks) +- ![Sample Image](https://via.placeholder.com/300x200?text=Sample+Image) + +--- + +*Last updated: 2024 \ No newline at end of file diff --git a/docs/sample.mdx b/docs/sample.mdx new file mode 100644 index 0000000..b851e7d --- /dev/null +++ b/docs/sample.mdx @@ -0,0 +1,58 @@ +# Sample MDX Documentation + +This is a **sample MDX document** that combines markdown with JSX components. + +## Interactive Elements + +
+ Info: This is an MDX component example. +
+ +## Code with Syntax Highlighting + +```javascript +const greeting = (name) => { + return `Hello, ${name}!`; +}; + +console.log(greeting("GoLinks")); +``` + +## Custom Components (Future) + +In the future, we could add custom React components: + +```jsx + + func renderMDX() string { + return "Awesome!" + } + +``` + +## Standard Markdown Features + +All standard markdown features work: + +- **Bold text** +- *Italic text* +- `Inline code` +- [Links](https://example.com) + +### Lists + +1. First item +2. Second item +3. Third item + +### Tables + +| Component | Status | Notes | +|-----------|--------|-------| +| Markdown | ✅ | Fully supported | +| MDX | 🚧 | Basic support | +| React Components | ⏳ | Planned | + +--- + +*This MDX file demonstrates the potential for interactive documentation.* diff --git a/go.mod b/go.mod index 2282faf..201cd99 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,13 @@ require ( github.com/gorilla/mux v1.8.1 github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.18 + github.com/yuin/goldmark v1.7.8 + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc + github.com/yuin/goldmark-meta v1.1.0 +) + +require ( + github.com/alecthomas/chroma/v2 v2.2.0 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index 3838145..5aadec7 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,34 @@ +github.com/alecthomas/chroma/v2 v2.2.0 h1:Aten8jfQwUqEdadVFFjNyjx7HTexhKP0XuqBG67mRDY= +github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= +github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae h1:zzGwJfFlFGD94CyyYwCJeSuD32Gj9GTaSi5y9hoVzdY= +github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= +github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= +github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/handlers/document.go b/internal/handlers/document.go new file mode 100644 index 0000000..1d5a862 --- /dev/null +++ b/internal/handlers/document.go @@ -0,0 +1,308 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "html/template" + "net/http" + "path/filepath" + "strings" + + "golinks/internal/service" + + "github.com/gorilla/mux" +) + +// DocumentHandler handles document-related HTTP requests +type DocumentHandler struct { + docService *service.DocumentService +} + +// NewDocumentHandler creates a new document handler +func NewDocumentHandler(docService *service.DocumentService) *DocumentHandler { + return &DocumentHandler{ + docService: docService, + } +} + +// RegisterRoutes registers document-related routes +func (h *DocumentHandler) RegisterRoutes(router *mux.Router) { + router.HandleFunc("/docs/{filename}", h.ServeDocument).Methods("GET") + router.HandleFunc("/docs/", h.ListDocuments).Methods("GET") + router.HandleFunc("/api/docs/upload", h.UploadDocument).Methods("POST") + router.HandleFunc("/api/docs/{filename}", h.DeleteDocument).Methods("DELETE") +} + +// ServeDocument renders and serves a document +func (h *DocumentHandler) ServeDocument(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + filename := vars["filename"] + + if filename == "" { + http.Error(w, "Filename is required", http.StatusBadRequest) + return + } + + // Add extension if not provided + if !strings.HasSuffix(filename, ".md") && !strings.HasSuffix(filename, ".mdx") { + // Try .md first, then .mdx + if result, err := h.docService.GetDocument(r.Context(), filename+".md"); err == nil { + h.renderDocumentHTML(w, result) + return + } + filename = filename + ".mdx" + } + + result, err := h.docService.GetDocument(r.Context(), filename) + if err != nil { + http.Error(w, fmt.Sprintf("Document not found: %v", err), http.StatusNotFound) + return + } + + h.renderDocumentHTML(w, result) +} + +// UploadDocument handles document upload +func (h *DocumentHandler) UploadDocument(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse multipart form + if err := r.ParseMultipartForm(10 << 20); err != nil { // 10 MB max + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + // Get file from form + file, header, err := r.FormFile("file") + if err != nil { + http.Error(w, "No file provided", http.StatusBadRequest) + return + } + defer file.Close() + + // Validate file extension + filename := header.Filename + if !strings.HasSuffix(filename, ".md") && !strings.HasSuffix(filename, ".mdx") { + http.Error(w, "Only .md and .mdx files are allowed", http.StatusBadRequest) + return + } + + // Save document + if err := h.docService.SaveDocument(r.Context(), filename, file); err != nil { + http.Error(w, fmt.Sprintf("Failed to save document: %v", err), http.StatusInternalServerError) + return + } + + // Return success response + response := map[string]interface{}{ + "success": true, + "filename": filename, + "message": "Document uploaded successfully", + "url": fmt.Sprintf("/docs/%s", strings.TrimSuffix(filename, filepath.Ext(filename))), + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(response) +} + +// ListDocuments returns a list of available documents +func (h *DocumentHandler) ListDocuments(w http.ResponseWriter, r *http.Request) { + docs, err := h.docService.ListDocuments(r.Context()) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to list documents: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "documents": docs, + }) +} + +// DeleteDocument removes a document +func (h *DocumentHandler) DeleteDocument(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + vars := mux.Vars(r) + filename := vars["filename"] + + if filename == "" { + http.Error(w, "Filename is required", http.StatusBadRequest) + return + } + + if err := h.docService.DeleteDocument(r.Context(), filename); err != nil { + http.Error(w, fmt.Sprintf("Failed to delete document: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "Document deleted successfully", + }) +} + +// renderDocumentHTML renders the document using HTML template +func (h *DocumentHandler) renderDocumentHTML(w http.ResponseWriter, result *service.RenderResult) { + // Create template data + data := struct { + Title string + Description string + Type string + Content template.HTML + Metadata map[string]interface{} + }{ + Title: result.Metadata.Title, + Description: result.Metadata.Description, + Type: result.Metadata.Type, + Content: template.HTML(result.HTML), + Metadata: result.Metadata.Metadata, + } + + // Simple HTML template (we'll create a proper template file later) + tmpl := ` + + + + + {{.Title}} - GoLinks Docs + + + + + + + + +
+
+
{{.Type}}
+

{{.Title}}

+ {{if .Description}}

{{.Description}}

{{end}} +
+
+ {{.Content}} +
+ +
+ +` + + t, err := template.New("document").Parse(tmpl) + if err != nil { + http.Error(w, "Template error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html") + if err := t.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + return + } +} diff --git a/internal/service/document.go b/internal/service/document.go new file mode 100644 index 0000000..cbc9f12 --- /dev/null +++ b/internal/service/document.go @@ -0,0 +1,254 @@ +package service + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/yuin/goldmark" + meta "github.com/yuin/goldmark-meta" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer/html" +) + +// DocumentService handles document rendering and management +type DocumentService struct { + docsPath string + markdown goldmark.Markdown +} + +// DocumentInfo contains metadata about a document +type DocumentInfo struct { + Title string `json:"title"` + Description string `json:"description"` + Type string `json:"type"` // "markdown" or "mdx" + Path string `json:"path"` + Metadata map[string]interface{} `json:"metadata"` +} + +// RenderResult contains the rendered document and metadata +type RenderResult struct { + HTML string `json:"html"` + Metadata DocumentInfo `json:"metadata"` +} + +// NewDocumentService creates a new document service +func NewDocumentService(docsPath string) *DocumentService { + // Configure Goldmark with extensions + md := goldmark.New( + goldmark.WithExtensions( + extension.GFM, // GitHub Flavored Markdown + extension.Table, // Tables + extension.Strikethrough, // ~~strikethrough~~ + extension.TaskList, // - [x] task lists + meta.Meta, // Frontmatter support + ), + goldmark.WithParserOptions( + parser.WithAutoHeadingID(), // Auto-generate heading IDs + ), + goldmark.WithRendererOptions( + html.WithHardWraps(), // Convert line breaks to
+ html.WithXHTML(), // XHTML-compliant output + html.WithUnsafe(), // Allow raw HTML (be careful!) + ), + ) + + return &DocumentService{ + docsPath: docsPath, + markdown: md, + } +} + +// GetDocument retrieves and renders a document by filename +func (s *DocumentService) GetDocument(ctx context.Context, filename string) (*RenderResult, error) { + // Sanitize filename to prevent directory traversal + filename = filepath.Base(filename) + filePath := filepath.Join(s.docsPath, filename) + + // Check if file exists + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return nil, fmt.Errorf("document not found: %s", filename) + } + + // Read file content + content, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read document: %w", err) + } + + // Determine document type + docType := "markdown" + if strings.HasSuffix(filename, ".mdx") { + docType = "mdx" + } + + // Render based on type + switch docType { + case "mdx": + return s.renderMDX(ctx, filename, content) + default: + return s.renderMarkdown(ctx, filename, content) + } +} + +// renderMarkdown renders a markdown document +func (s *DocumentService) renderMarkdown(ctx context.Context, filename string, content []byte) (*RenderResult, error) { + var buf bytes.Buffer + context := parser.NewContext() + + // Parse and render + if err := s.markdown.Convert(content, &buf, parser.WithContext(context)); err != nil { + return nil, fmt.Errorf("failed to render markdown: %w", err) + } + + // Extract metadata from frontmatter + metaData := meta.Get(context) + if metaData == nil { + metaData = make(map[string]interface{}) + } + + // Create document info + docInfo := DocumentInfo{ + Title: getStringFromMeta(metaData, "title", strings.TrimSuffix(filename, filepath.Ext(filename))), + Description: getStringFromMeta(metaData, "description", ""), + Type: "markdown", + Path: filename, + Metadata: metaData, + } + + return &RenderResult{ + HTML: buf.String(), + Metadata: docInfo, + }, nil +} + +// renderMDX renders an MDX document (simplified for now) +func (s *DocumentService) renderMDX(ctx context.Context, filename string, content []byte) (*RenderResult, error) { + // For now, treat MDX as enhanced markdown + // In the future, we could add proper MDX compilation with esbuild + + // Remove JSX-like syntax for basic rendering + processedContent := s.preprocessMDX(content) + + var buf bytes.Buffer + context := parser.NewContext() + + // Parse and render as markdown + if err := s.markdown.Convert(processedContent, &buf, parser.WithContext(context)); err != nil { + return nil, fmt.Errorf("failed to render MDX: %w", err) + } + + // Extract metadata + metaData := meta.Get(context) + if metaData == nil { + metaData = make(map[string]interface{}) + } + + // Create document info + docInfo := DocumentInfo{ + Title: getStringFromMeta(metaData, "title", strings.TrimSuffix(filename, filepath.Ext(filename))), + Description: getStringFromMeta(metaData, "description", ""), + Type: "mdx", + Path: filename, + Metadata: metaData, + } + + return &RenderResult{ + HTML: buf.String(), + Metadata: docInfo, + }, nil +} + +// preprocessMDX does basic preprocessing of MDX content +func (s *DocumentService) preprocessMDX(content []byte) []byte { + contentStr := string(content) + + // Convert simple JSX-like elements to HTML + // This is a very basic implementation - in production, use proper MDX compiler + contentStr = strings.ReplaceAll(contentStr, `
`, `
`) + contentStr = strings.ReplaceAll(contentStr, `className=`, `class=`) + + return []byte(contentStr) +} + +// SaveDocument saves a document to the filesystem +func (s *DocumentService) SaveDocument(ctx context.Context, filename string, content io.Reader) error { + // Sanitize filename + filename = filepath.Base(filename) + filePath := filepath.Join(s.docsPath, filename) + + // Create file + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create document file: %w", err) + } + defer file.Close() + + // Copy content + if _, err := io.Copy(file, content); err != nil { + return fmt.Errorf("failed to write document content: %w", err) + } + + return nil +} + +// ListDocuments returns a list of available documents +func (s *DocumentService) ListDocuments(ctx context.Context) ([]DocumentInfo, error) { + entries, err := os.ReadDir(s.docsPath) + if err != nil { + return nil, fmt.Errorf("failed to read docs directory: %w", err) + } + + var docs []DocumentInfo + for _, entry := range entries { + if entry.IsDir() { + continue + } + + name := entry.Name() + if !strings.HasSuffix(name, ".md") && !strings.HasSuffix(name, ".mdx") { + continue + } + + docType := "markdown" + if strings.HasSuffix(name, ".mdx") { + docType = "mdx" + } + + docs = append(docs, DocumentInfo{ + Title: strings.TrimSuffix(name, filepath.Ext(name)), + Type: docType, + Path: name, + }) + } + + return docs, nil +} + +// DeleteDocument removes a document from the filesystem +func (s *DocumentService) DeleteDocument(ctx context.Context, filename string) error { + // Sanitize filename + filename = filepath.Base(filename) + filePath := filepath.Join(s.docsPath, filename) + + if err := os.Remove(filePath); err != nil { + return fmt.Errorf("failed to delete document: %w", err) + } + + return nil +} + +// Helper function to safely get string values from metadata +func getStringFromMeta(meta map[string]interface{}, key, defaultValue string) string { + if value, ok := meta[key]; ok { + if str, ok := value.(string); ok { + return str + } + } + return defaultValue +} From 13bbf6aebb235f25be611323bbbe08247d11a0b5 Mon Sep 17 00:00:00 2001 From: Alejandro Perez Gancedo Date: Wed, 24 Sep 2025 23:32:51 +0100 Subject: [PATCH 2/6] feat: logger, 404 page and layout improvements --- cmd/server/main.go | 38 +++- env.example | 5 + internal/config/config.go | 15 +- internal/handlers/document.go | 6 +- internal/handlers/handler.go | 95 +++++++-- internal/logger/logger.go | 93 +++++++++ internal/repository/query.go | 30 ++- internal/repository/query_test.go | 22 ++- internal/repository/shortcut.go | 37 +++- internal/repository/shortcut_test.go | 16 +- internal/service/document.go | 10 +- internal/service/link.go | 47 ++++- internal/service/link_test.go | 13 +- web/static/styles.css | 282 +++++++++++++++++++++++++++ web/templates/404.html | 56 ++++++ web/templates/homepage.html | 14 +- web/templates/setup.html | 14 +- 17 files changed, 726 insertions(+), 67 deletions(-) create mode 100644 internal/logger/logger.go create mode 100644 web/templates/404.html diff --git a/cmd/server/main.go b/cmd/server/main.go index 7850b5d..87fda32 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -13,6 +13,7 @@ import ( "golinks/internal/config" "golinks/internal/database" "golinks/internal/handlers" + "golinks/internal/logger" "golinks/internal/repository" "golinks/internal/service" @@ -26,32 +27,48 @@ func main() { log.Fatalf("Failed to load configuration: %v", err) } + // Initialize simple logging + logger.Initialize(cfg.Logging) + appLogger := logger.Default() + + appLogger.Info("Starting GoLinks application on port %d (env: %s)", cfg.Port, cfg.Environment) + // Initialize database + appLogger.Info("Initializing database: %s", cfg.DatabasePath) db, err := database.NewSQLiteDB(cfg.DatabasePath) if err != nil { + appLogger.Error("Failed to initialize database: %v", err) log.Fatalf("Failed to initialize database: %v", err) } defer db.Close() // Run migrations + appLogger.Info("Running database migrations") if err := database.Migrate(db); err != nil { + appLogger.Error("Failed to run migrations: %v", err) log.Fatalf("Failed to run migrations: %v", err) } + appLogger.Info("Database migrations completed successfully") // Initialize repositories - shortcutRepo := repository.NewShortcutRepository(db) - queryRepo := repository.NewQueryRepository(db) + appLogger.Info("Initializing repositories") + shortcutRepo := repository.NewShortcutRepository(db, appLogger) + queryRepo := repository.NewQueryRepository(db, appLogger) // Initialize services - linkService := service.NewLinkService(shortcutRepo, queryRepo) - docService := service.NewDocumentService("docs") + appLogger.Info("Initializing services") + linkService := service.NewLinkService(shortcutRepo, queryRepo, appLogger) + docService := service.NewDocumentService("docs", appLogger) // Initialize handlers - handler := handlers.NewHandler(linkService, cfg) - docHandler := handlers.NewDocumentHandler(docService) + appLogger.Info("Initializing handlers") + handler := handlers.NewHandler(linkService, cfg, appLogger) + docHandler := handlers.NewDocumentHandler(docService, appLogger) // Setup router + appLogger.Info("Setting up HTTP router") router := mux.NewRouter() + handler.RegisterRoutes(router) docHandler.RegisterRoutes(router) @@ -66,8 +83,9 @@ func main() { // Start server in a goroutine go func() { - log.Printf("Starting server on port %d", cfg.Port) + appLogger.Info("Starting HTTP server on %s", server.Addr) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + appLogger.Error("Server failed to start: %v", err) log.Fatalf("Server failed to start: %v", err) } }() @@ -76,15 +94,17 @@ func main() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit - log.Println("Shutting down server...") + appLogger.Info("Received shutdown signal, initiating graceful shutdown") // Graceful shutdown with timeout ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + appLogger.Info("Shutting down HTTP server (timeout: 30s)") if err := server.Shutdown(ctx); err != nil { + appLogger.Error("Server forced to shutdown: %v", err) log.Fatalf("Server forced to shutdown: %v", err) } - log.Println("Server exited") + appLogger.Info("Server shutdown completed successfully") } diff --git a/env.example b/env.example index bf201f4..ab1d315 100644 --- a/env.example +++ b/env.example @@ -7,4 +7,9 @@ BASE_URL=http://localhost:8080 # Database Configuration DATABASE_PATH=golinks.db +# Environment Configuration ENVIRONMENT=development + +# Logging Configuration +# LOG_LEVEL: debug, info, warn, error (default: info) +LOG_LEVEL=debug diff --git a/internal/config/config.go b/internal/config/config.go index ae61c68..540bcf0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,15 +4,18 @@ import ( "os" "strconv" + "golinks/internal/logger" + "github.com/joho/godotenv" ) // Config holds all configuration for the application type Config struct { - Port int `json:"port"` - DatabasePath string `json:"database_path"` - BaseURL string `json:"base_url"` - Environment string `json:"environment"` + Port int `json:"port"` + DatabasePath string `json:"database_path"` + BaseURL string `json:"base_url"` + Environment string `json:"environment"` + Logging logger.Config `json:"logging"` } // Load loads configuration from environment variables and .env file @@ -25,6 +28,10 @@ func Load() (*Config, error) { DatabasePath: getEnv("DATABASE_PATH", "golinks.db"), BaseURL: getEnv("BASE_URL", "http://localhost:8080"), Environment: getEnv("ENVIRONMENT", "development"), + Logging: logger.Config{ + Level: getEnv("LOG_LEVEL", "info"), + Format: "text", // Not used in simple logger + }, } return cfg, nil diff --git a/internal/handlers/document.go b/internal/handlers/document.go index 1d5a862..88bef2b 100644 --- a/internal/handlers/document.go +++ b/internal/handlers/document.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" + "golinks/internal/logger" "golinks/internal/service" "github.com/gorilla/mux" @@ -16,12 +17,15 @@ import ( // DocumentHandler handles document-related HTTP requests type DocumentHandler struct { docService *service.DocumentService + logger *logger.Logger } // NewDocumentHandler creates a new document handler -func NewDocumentHandler(docService *service.DocumentService) *DocumentHandler { +func NewDocumentHandler(docService *service.DocumentService, log *logger.Logger) *DocumentHandler { + log.Info("Document handler initialized") return &DocumentHandler{ docService: docService, + logger: log, } } diff --git a/internal/handlers/handler.go b/internal/handlers/handler.go index 2134af2..401f480 100644 --- a/internal/handlers/handler.go +++ b/internal/handlers/handler.go @@ -2,15 +2,14 @@ package handlers import ( "context" - "encoding/json" "fmt" "html/template" - "log" "net/http" "strings" "golinks/internal/config" "golinks/internal/domain" + "golinks/internal/logger" "golinks/internal/service" "github.com/gorilla/mux" @@ -29,10 +28,13 @@ type Handler struct { linkService LinkService config *config.Config templates *template.Template + logger *logger.Logger } // NewHandler creates a new handler -func NewHandler(linkService LinkService, cfg *config.Config) *Handler { +func NewHandler(linkService LinkService, cfg *config.Config, log *logger.Logger) *Handler { + log.Info("Loading HTML templates from web/templates/*.html") + // Load templates templates := template.Must(template.New("").Funcs(template.FuncMap{ "urlify": func(url string) template.HTML { @@ -43,10 +45,13 @@ func NewHandler(linkService LinkService, cfg *config.Config) *Handler { }, }).ParseGlob("web/templates/*.html")) + log.Info("Handler initialized successfully") + return &Handler{ linkService: linkService, config: cfg, templates: templates, + logger: log, } } @@ -65,6 +70,9 @@ func (h *Handler) RegisterRoutes(router *mux.Router) { router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/homepage/", http.StatusFound) }).Methods("GET") + + // 404 handler for all other routes + router.NotFoundHandler = http.HandlerFunc(h.NotFoundHandler) } // RedirectHandler handles golink redirects @@ -77,20 +85,24 @@ func (h *Handler) RedirectHandler(w http.ResponseWriter, r *http.Request) { userID := h.getUserID(r) + h.logger.Info("Processing golink redirect: %s (user: %s)", queryPath, userID) + targetURL, err := h.linkService.GetLink(ctx, queryPath, "") if err != nil { if _, ok := err.(service.InvalidQueryError); ok { + h.logger.Warn("Invalid query '%s' - redirecting to homepage: %v", queryPath, err) // Redirect to homepage with missing query parameter redirectURL := fmt.Sprintf("%s/homepage/?missing=%s", h.config.BaseURL, queryPath) http.Redirect(w, r, redirectURL, http.StatusFound) return } + h.logger.Error("Failed to get link for query '%s': %v", queryPath, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - log.Printf("query word=%s user=%s response=%s", queryPath, userID, targetURL) + h.logger.Info("Redirecting '%s' to '%s' (user: %s)", queryPath, targetURL, userID) http.Redirect(w, r, targetURL, http.StatusFound) } @@ -99,29 +111,40 @@ func (h *Handler) UpdateLinkHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req domain.LinkRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid JSON", http.StatusBadRequest) + + // Parse form data + if err := r.ParseForm(); err != nil { + h.logger.Warn("Invalid form data in update request: %v", err) + http.Error(w, "Invalid form data", http.StatusBadRequest) return } + req.Word = strings.TrimSpace(r.FormValue("word")) + req.Link = strings.TrimSpace(r.FormValue("link")) + + h.logger.Info("Parsed form data: word='%s' link='%s'", req.Word, req.Link) + userID := h.getUserID(r) + h.logger.Info("Processing link update: word='%s' link='%s' user='%s'", req.Word, req.Link, userID) + if err := h.linkService.UpdateLink(ctx, req, userID); err != nil { if _, ok := err.(service.InvalidQueryError); ok { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]string{"detail": err.Error()}) + h.logger.Warn("Invalid link update request for word='%s': %v", req.Word, err) + http.Error(w, err.Error(), http.StatusBadRequest) return } + h.logger.Error("Failed to update link word='%s': %v", req.Word, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - log.Printf("update word=%s user=%s link=%s", req.Word, userID, req.Link) + h.logger.Info("Link updated successfully: word='%s' link='%s' user='%s'", req.Word, req.Link, userID) - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + // Return success message for HTMX + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte("Link added successfully!")) } // HomepageHandler handles the homepage @@ -136,20 +159,25 @@ func (h *Handler) HomepageHandler(w http.ResponseWriter, r *http.Request) { reason := r.URL.Query().Get("reason") missing := r.URL.Query().Get("missing") + h.logger.Info("Rendering homepage for user '%s'", userID) + if missing != "" { + h.logger.Info("Homepage showing missing query: %s", missing) + } + // Get recent queries and keywords recentQueries, err := h.linkService.GetRecentQueries(ctx) if err != nil { - log.Printf("Failed to get recent queries: %v", err) + h.logger.Error("Failed to get recent queries: %v", err) recentQueries = []domain.PopularQuery{} } allKeywords, err := h.linkService.GetAllKeywords(ctx) if err != nil { - log.Printf("Failed to get all keywords: %v", err) + h.logger.Error("Failed to get all keywords: %v", err) allKeywords = []domain.KeywordInfo{} } - log.Printf("homepage user=%s", userID) + h.logger.Debug("Homepage data loaded: %d recent queries, %d keywords", len(recentQueries), len(allKeywords)) data := struct { Success string @@ -171,16 +199,19 @@ func (h *Handler) HomepageHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") if err := h.templates.ExecuteTemplate(w, "homepage.html", data); err != nil { - log.Printf("Failed to execute template: %v", err) + h.logger.Error("Failed to execute homepage template: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) + return } + + h.logger.Debug("Homepage rendered successfully") } // SetupHandler handles the setup page func (h *Handler) SetupHandler(w http.ResponseWriter, r *http.Request) { userID := h.getUserID(r) - log.Printf("setup user=%s", userID) + h.logger.Info("Rendering setup page for user '%s'", userID) data := struct { BaseURL string @@ -190,9 +221,37 @@ func (h *Handler) SetupHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") if err := h.templates.ExecuteTemplate(w, "setup.html", data); err != nil { - log.Printf("Failed to execute template: %v", err) + h.logger.Error("Failed to execute setup template: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) + return } + + h.logger.Debug("Setup page rendered successfully") +} + +// NotFoundHandler handles 404 errors +func (h *Handler) NotFoundHandler(w http.ResponseWriter, r *http.Request) { + userID := h.getUserID(r) + + h.logger.Info("404 page requested for path '%s' by user '%s'", r.URL.Path, userID) + + data := struct { + BaseURL string + Path string + }{ + BaseURL: h.config.BaseURL, + Path: r.URL.Path, + } + + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusNotFound) + if err := h.templates.ExecuteTemplate(w, "404.html", data); err != nil { + h.logger.Error("Failed to execute 404 template: %v", err) + http.Error(w, "Page not found", http.StatusNotFound) + return + } + + h.logger.Debug("404 page rendered successfully") } // getUserID extracts user ID from request (simplified - no OAuth2 for now) diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..271ddef --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,93 @@ +package logger + +import ( + "fmt" + "log/slog" + "os" +) + +// Logger is a simple wrapper around slog +type Logger struct { + *slog.Logger +} + +// Config holds logger configuration +type Config struct { + Level string `json:"level"` // debug, info, warn, error + Format string `json:"format"` // not used in simple logger +} + +// New creates a new simple slog logger +func New(cfg Config) *Logger { + var level slog.Level + switch cfg.Level { + case "debug": + level = slog.LevelDebug + case "info": + level = slog.LevelInfo + case "warn": + level = slog.LevelWarn + case "error": + level = slog.LevelError + default: + level = slog.LevelInfo + } + + opts := &slog.HandlerOptions{ + Level: level, + AddSource: true, + } + + handler := slog.NewTextHandler(os.Stdout, opts) + logger := slog.New(handler) + + return &Logger{ + Logger: logger, + } +} + +// Debug logs debug messages +func (l *Logger) Debug(msg string, args ...interface{}) { + l.Logger.Debug(formatMessage(msg, args...)) +} + +// Info logs info messages +func (l *Logger) Info(msg string, args ...interface{}) { + l.Logger.Info(formatMessage(msg, args...)) +} + +// Warn logs warning messages +func (l *Logger) Warn(msg string, args ...interface{}) { + l.Logger.Warn(formatMessage(msg, args...)) +} + +// Error logs error messages +func (l *Logger) Error(msg string, args ...interface{}) { + l.Logger.Error(formatMessage(msg, args...)) +} + +// formatMessage formats the message with args using Printf-style formatting +func formatMessage(msg string, args ...interface{}) string { + if len(args) == 0 { + return msg + } + // Use Go's fmt package for printf-style formatting + return fmt.Sprintf(msg, args...) +} + +// Global logger instance +var defaultLogger *Logger + +// Initialize sets up the global logger +func Initialize(cfg Config) { + defaultLogger = New(cfg) +} + +// Default returns the default logger instance +func Default() *Logger { + if defaultLogger == nil { + // Fallback to a basic logger if not initialized + defaultLogger = New(Config{Level: "info", Format: "text"}) + } + return defaultLogger +} diff --git a/internal/repository/query.go b/internal/repository/query.go index 2250d24..ffbb50c 100644 --- a/internal/repository/query.go +++ b/internal/repository/query.go @@ -4,29 +4,43 @@ import ( "context" "database/sql" "fmt" + "time" "golinks/internal/domain" + "golinks/internal/logger" ) // QueryRepository handles database operations for queries type QueryRepository struct { - db *sql.DB + db *sql.DB + logger *logger.Logger } // NewQueryRepository creates a new query repository -func NewQueryRepository(db *sql.DB) *QueryRepository { - return &QueryRepository{db: db} +func NewQueryRepository(db *sql.DB, log *logger.Logger) *QueryRepository { + log.Info("Query repository initialized") + return &QueryRepository{ + db: db, + logger: log, + } } // Create creates a new query log entry func (r *QueryRepository) Create(ctx context.Context, wordID int) error { + start := time.Now() + r.logger.Debug("Creating query log for word ID: %d", wordID) + query := `INSERT INTO queries (word_id, created_at) VALUES (?, CURRENT_TIMESTAMP)` _, err := r.db.ExecContext(ctx, query, wordID) + duration := time.Since(start) + if err != nil { + r.logger.Error("Database insert failed: %v (%v)", err, duration) return fmt.Errorf("failed to create query log: %w", err) } + r.logger.Debug("Query log created successfully (%v)", duration) return nil } @@ -34,6 +48,8 @@ func (r *QueryRepository) Create(ctx context.Context, wordID int) error { func (r *QueryRepository) GetRecentQueries( ctx context.Context, timeWindowDays, numResults int, ) ([]domain.PopularQuery, error) { + start := time.Now() + r.logger.Debug("Getting recent queries: %d days, max %d results", timeWindowDays, numResults) query := ` SELECT COUNT(q.word_id) as count, s.word, s.link @@ -47,6 +63,8 @@ func (r *QueryRepository) GetRecentQueries( rows, err := r.db.QueryContext(ctx, query, timeWindowDays, numResults) if err != nil { + duration := time.Since(start) + r.logger.Error("Database query failed: %v (%v)", err, duration) return nil, fmt.Errorf("failed to get recent queries: %w", err) } defer rows.Close() @@ -56,14 +74,20 @@ func (r *QueryRepository) GetRecentQueries( var pq domain.PopularQuery err := rows.Scan(&pq.Count, &pq.Word, &pq.Link) if err != nil { + duration := time.Since(start) + r.logger.Error("Failed to scan popular query row: %v (%v)", err, duration) return nil, fmt.Errorf("failed to scan popular query: %w", err) } queries = append(queries, pq) } if err := rows.Err(); err != nil { + duration := time.Since(start) + r.logger.Error("Error iterating recent query rows: %v (%v)", err, duration) return nil, fmt.Errorf("error iterating recent queries: %w", err) } + duration := time.Since(start) + r.logger.Debug("Recent queries retrieved successfully: %d queries (%v)", len(queries), duration) return queries, nil } diff --git a/internal/repository/query_test.go b/internal/repository/query_test.go index b9697e8..13739a9 100644 --- a/internal/repository/query_test.go +++ b/internal/repository/query_test.go @@ -5,6 +5,7 @@ import ( "testing" "golinks/internal/domain" + "golinks/internal/logger" ) func TestQueryRepository_Create(t *testing.T) { @@ -12,7 +13,8 @@ func TestQueryRepository_Create(t *testing.T) { defer db.Close() // First create a shortcut to reference - shortcutRepo := NewShortcutRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + shortcutRepo := NewShortcutRepository(db, mockLogger) shortcut := &domain.Shortcut{ Word: "test", Link: "https://test.com", @@ -23,7 +25,7 @@ func TestQueryRepository_Create(t *testing.T) { t.Fatalf("Failed to create test shortcut: %v", err) } - queryRepo := NewQueryRepository(db) + queryRepo := NewQueryRepository(db, mockLogger) tests := []struct { name string @@ -58,8 +60,9 @@ func TestQueryRepository_GetRecentQueries(t *testing.T) { defer db.Close() // Setup test data - shortcutRepo := NewShortcutRepository(db) - queryRepo := NewQueryRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + shortcutRepo := NewShortcutRepository(db, mockLogger) + queryRepo := NewQueryRepository(db, mockLogger) // Create shortcuts shortcuts := []*domain.Shortcut{ @@ -182,8 +185,9 @@ func TestQueryRepository_GetRecentQueries_TimeWindow(t *testing.T) { db := setupTestDB(t) defer db.Close() - shortcutRepo := NewShortcutRepository(db) - queryRepo := NewQueryRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + shortcutRepo := NewShortcutRepository(db, mockLogger) + queryRepo := NewQueryRepository(db, mockLogger) // Create a shortcut shortcut := &domain.Shortcut{ @@ -242,7 +246,8 @@ func TestQueryRepository_DatabaseError(t *testing.T) { db := setupTestDB(t) db.Close() // Close immediately to cause errors - repo := NewQueryRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewQueryRepository(db, mockLogger) // Test Create with closed DB err := repo.Create(context.Background(), 1) @@ -261,7 +266,8 @@ func TestQueryRepository_EmptyResults(t *testing.T) { db := setupTestDB(t) defer db.Close() - repo := NewQueryRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewQueryRepository(db, mockLogger) // Test GetRecentQueries with no data queries, err := repo.GetRecentQueries(context.Background(), 1, 10) diff --git a/internal/repository/shortcut.go b/internal/repository/shortcut.go index e61a076..4e7b789 100644 --- a/internal/repository/shortcut.go +++ b/internal/repository/shortcut.go @@ -4,22 +4,31 @@ import ( "context" "database/sql" "fmt" + "time" "golinks/internal/domain" + "golinks/internal/logger" ) // ShortcutRepository handles database operations for shortcuts type ShortcutRepository struct { - db *sql.DB + db *sql.DB + logger *logger.Logger } // NewShortcutRepository creates a new shortcut repository -func NewShortcutRepository(db *sql.DB) *ShortcutRepository { - return &ShortcutRepository{db: db} +func NewShortcutRepository(db *sql.DB, log *logger.Logger) *ShortcutRepository { + log.Info("Shortcut repository initialized") + return &ShortcutRepository{ + db: db, + logger: log, + } } // GetByWord retrieves the most recent shortcut by word func (r *ShortcutRepository) GetByWord(ctx context.Context, word string) (*domain.Shortcut, error) { + start := time.Now() + r.logger.Debug("Getting shortcut by word: %s", word) query := ` SELECT id, word, link, user, created_at @@ -38,18 +47,25 @@ func (r *ShortcutRepository) GetByWord(ctx context.Context, word string) (*domai &shortcut.CreatedAt, ) + duration := time.Since(start) + if err == sql.ErrNoRows { + r.logger.Debug("No shortcut found for word '%s' (%v)", word, duration) return nil, nil } if err != nil { + r.logger.Error("Database query failed for word '%s': %v (%v)", word, err, duration) return nil, fmt.Errorf("failed to get shortcut by word: %w", err) } + r.logger.Debug("Shortcut retrieved: id=%d user='%s' (%v)", shortcut.ID, shortcut.User, duration) return &shortcut, nil } // Create creates a new shortcut func (r *ShortcutRepository) Create(ctx context.Context, shortcut *domain.Shortcut) error { + start := time.Now() + r.logger.Debug("Creating shortcut: word='%s' link='%s' user='%s'", shortcut.Word, shortcut.Link, shortcut.User) query := ` INSERT INTO linktable (word, link, user, created_at) @@ -57,21 +73,28 @@ func (r *ShortcutRepository) Create(ctx context.Context, shortcut *domain.Shortc ` result, err := r.db.ExecContext(ctx, query, shortcut.Word, shortcut.Link, shortcut.User) + duration := time.Since(start) + if err != nil { + r.logger.Error("Database insert failed: %v (%v)", err, duration) return fmt.Errorf("failed to create shortcut: %w", err) } id, err := result.LastInsertId() if err != nil { + r.logger.Error("Failed to get last insert ID: %v (%v)", err, duration) return fmt.Errorf("failed to get last insert id: %w", err) } shortcut.ID = int(id) + r.logger.Info("Shortcut created successfully: id=%d (%v)", shortcut.ID, duration) return nil } // GetAllKeywords retrieves all keywords with their latest links func (r *ShortcutRepository) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, error) { + start := time.Now() + r.logger.Debug("Getting all keywords") query := ` SELECT word, link, created_at, MAX(id) as max_id @@ -82,6 +105,8 @@ func (r *ShortcutRepository) GetAllKeywords(ctx context.Context) ([]domain.Keywo rows, err := r.db.QueryContext(ctx, query) if err != nil { + duration := time.Since(start) + r.logger.Error("Database query failed: %v (%v)", err, duration) return nil, fmt.Errorf("failed to get all keywords: %w", err) } defer rows.Close() @@ -92,14 +117,20 @@ func (r *ShortcutRepository) GetAllKeywords(ctx context.Context) ([]domain.Keywo var maxID int err := rows.Scan(&keyword.Word, &keyword.Link, &keyword.CreatedAt, &maxID) if err != nil { + duration := time.Since(start) + r.logger.Error("Failed to scan keyword row: %v (%v)", err, duration) return nil, fmt.Errorf("failed to scan keyword: %w", err) } keywords = append(keywords, keyword) } if err := rows.Err(); err != nil { + duration := time.Since(start) + r.logger.Error("Error iterating keyword rows: %v (%v)", err, duration) return nil, fmt.Errorf("error iterating keywords: %w", err) } + duration := time.Since(start) + r.logger.Debug("All keywords retrieved successfully: %d keywords (%v)", len(keywords), duration) return keywords, nil } diff --git a/internal/repository/shortcut_test.go b/internal/repository/shortcut_test.go index 343637c..01c1f6b 100644 --- a/internal/repository/shortcut_test.go +++ b/internal/repository/shortcut_test.go @@ -7,6 +7,7 @@ import ( "time" "golinks/internal/domain" + "golinks/internal/logger" _ "github.com/mattn/go-sqlite3" ) @@ -49,7 +50,8 @@ func TestShortcutRepository_GetByWord(t *testing.T) { db := setupTestDB(t) defer db.Close() - repo := NewShortcutRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewShortcutRepository(db, mockLogger) // Insert test data testShortcut := &domain.Shortcut{ @@ -126,7 +128,8 @@ func TestShortcutRepository_Create(t *testing.T) { db := setupTestDB(t) defer db.Close() - repo := NewShortcutRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewShortcutRepository(db, mockLogger) tests := []struct { name string @@ -194,7 +197,8 @@ func TestShortcutRepository_GetAllKeywords(t *testing.T) { db := setupTestDB(t) defer db.Close() - repo := NewShortcutRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewShortcutRepository(db, mockLogger) // Insert test data testShortcuts := []*domain.Shortcut{ @@ -244,7 +248,8 @@ func TestShortcutRepository_GetByWord_MostRecent(t *testing.T) { db := setupTestDB(t) defer db.Close() - repo := NewShortcutRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewShortcutRepository(db, mockLogger) // Create multiple versions of the same word shortcuts := []*domain.Shortcut{ @@ -289,7 +294,8 @@ func TestShortcutRepository_DatabaseError(t *testing.T) { db := setupTestDB(t) db.Close() // Close immediately to cause errors - repo := NewShortcutRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewShortcutRepository(db, mockLogger) // Test GetByWord with closed DB _, err := repo.GetByWord(context.Background(), "test") diff --git a/internal/service/document.go b/internal/service/document.go index cbc9f12..8db272a 100644 --- a/internal/service/document.go +++ b/internal/service/document.go @@ -9,6 +9,8 @@ import ( "path/filepath" "strings" + "golinks/internal/logger" + "github.com/yuin/goldmark" meta "github.com/yuin/goldmark-meta" "github.com/yuin/goldmark/extension" @@ -20,6 +22,7 @@ import ( type DocumentService struct { docsPath string markdown goldmark.Markdown + logger *logger.Logger } // DocumentInfo contains metadata about a document @@ -38,7 +41,9 @@ type RenderResult struct { } // NewDocumentService creates a new document service -func NewDocumentService(docsPath string) *DocumentService { +func NewDocumentService(docsPath string, log *logger.Logger) *DocumentService { + log.Info("Initializing document service: %s", docsPath) + // Configure Goldmark with extensions md := goldmark.New( goldmark.WithExtensions( @@ -58,9 +63,12 @@ func NewDocumentService(docsPath string) *DocumentService { ), ) + log.Info("Document service initialized successfully") + return &DocumentService{ docsPath: docsPath, markdown: md, + logger: log, } } diff --git a/internal/service/link.go b/internal/service/link.go index 93bbbd1..789a181 100644 --- a/internal/service/link.go +++ b/internal/service/link.go @@ -8,6 +8,7 @@ import ( "time" "golinks/internal/domain" + "golinks/internal/logger" ) // ShortcutRepository interface for shortcut operations @@ -27,13 +28,16 @@ type QueryRepository interface { type LinkService struct { shortcutRepo ShortcutRepository queryRepo QueryRepository + logger *logger.Logger } // NewLinkService creates a new link service -func NewLinkService(shortcutRepo ShortcutRepository, queryRepo QueryRepository) *LinkService { +func NewLinkService(shortcutRepo ShortcutRepository, queryRepo QueryRepository, log *logger.Logger) *LinkService { + log.Info("Link service initialized") return &LinkService{ shortcutRepo: shortcutRepo, queryRepo: queryRepo, + logger: log, } } @@ -48,11 +52,12 @@ func (e InvalidQueryError) Error() string { // GetLink resolves a golink query to a URL func (s *LinkService) GetLink(ctx context.Context, word string, searchTerm string) (string, error) { - word = strings.TrimSpace(word) + s.logger.Debug("Processing golink query: '%s' (search: '%s')", word, searchTerm) shortcut, err := s.shortcutRepo.GetByWord(ctx, word) if err != nil { + s.logger.Error("Failed to get shortcut from repository: %v", err) return "", fmt.Errorf("failed to get shortcut: %w", err) } @@ -60,48 +65,59 @@ func (s *LinkService) GetLink(ctx context.Context, word string, searchTerm strin // Try splitting the word if it contains spaces if strings.Contains(word, " ") { newWord, newSearchTerm := moveLastWord(word, searchTerm) + s.logger.Debug("Splitting word '%s' -> '%s' and retrying", word, newWord) return s.GetLink(ctx, newWord, newSearchTerm) } + query := strings.Join([]string{word, searchTerm}, " ") + s.logger.Warn("No shortcut found for query: %s", query) return "", InvalidQueryError{ - Message: fmt.Sprintf("Unable to find link for query %s", strings.Join([]string{word, searchTerm}, " ")), + Message: fmt.Sprintf("Unable to find link for query %s", query), } } + s.logger.Info("Found shortcut: id=%d link='%s' user='%s'", shortcut.ID, shortcut.Link, shortcut.User) + // Log the query if err := s.queryRepo.Create(ctx, shortcut.ID); err != nil { - // Log error but don't fail the request - // In a production system, you might want to log this error - _ = err + s.logger.Error("Failed to log query usage for shortcut %d: %v", shortcut.ID, err) + // Don't fail the request for logging errors } // Handle different types of links if !isURL(shortcut.Link) { + s.logger.Debug("Link is an alias '%s', recursing", shortcut.Link) // This is an alias, recurse return s.GetLink(ctx, shortcut.Link, searchTerm) } // Process URL with search term substitution resultLink := processResultLink(shortcut.Link, searchTerm) + s.logger.Info("Link resolution successful: '%s' -> '%s'", word, resultLink) return resultLink, nil } // UpdateLink creates or updates a golink func (s *LinkService) UpdateLink(ctx context.Context, req domain.LinkRequest, userID string) error { + s.logger.Info("Processing link update: word='%s' link='%s' user='%s'", req.Word, req.Link, userID) // Validate the request if err := s.validateLinkRequest(ctx, req); err != nil { + s.logger.Warn("Link request validation failed: %v", err) return err } // If the link is not a URL, validate it's a valid alias if !isURL(req.Link) { + s.logger.Debug("Validating alias link: %s", req.Link) _, err := s.GetLink(ctx, req.Link, "") if err != nil { + s.logger.Error("Invalid alias link '%s': %v", req.Link, err) return InvalidQueryError{ Message: "The link target appears to neither be a URL, or a valid alias.", } } + s.logger.Debug("Alias validation successful: %s", req.Link) } shortcut := &domain.Shortcut{ @@ -112,24 +128,40 @@ func (s *LinkService) UpdateLink(ctx context.Context, req domain.LinkRequest, us } if err := s.shortcutRepo.Create(ctx, shortcut); err != nil { + s.logger.Error("Failed to create shortcut in repository: %v", err) return fmt.Errorf("failed to create shortcut: %w", err) } + s.logger.Info("Link update completed successfully: id=%d", shortcut.ID) return nil } // GetRecentQueries retrieves popular queries func (s *LinkService) GetRecentQueries(ctx context.Context) ([]domain.PopularQuery, error) { - return s.queryRepo.GetRecentQueries(ctx, 3, 20) + s.logger.Debug("Fetching recent queries (3 days, max 20 results)") + + queries, err := s.queryRepo.GetRecentQueries(ctx, 3, 20) + if err != nil { + s.logger.Error("Failed to get recent queries: %v", err) + return nil, err + } + + s.logger.Debug("Recent queries retrieved successfully: %d queries", len(queries)) + return queries, nil } // GetAllKeywords retrieves all keywords with aliases func (s *LinkService) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, error) { + s.logger.Debug("Fetching all keywords") + keywords, err := s.shortcutRepo.GetAllKeywords(ctx) if err != nil { + s.logger.Error("Failed to get all keywords: %v", err) return nil, err } + s.logger.Debug("Processing keywords for aliases: %d total keywords", len(keywords)) + // Process aliases (simplified version - not implementing full recursive alias resolution for now) for i := range keywords { if !isURL(keywords[i].Link) { @@ -145,6 +177,7 @@ func (s *LinkService) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, } } + s.logger.Debug("Keywords retrieved and processed successfully: %d total, %d URLs", len(keywords), len(result)) return result, nil } diff --git a/internal/service/link_test.go b/internal/service/link_test.go index d665d90..f4a76a9 100644 --- a/internal/service/link_test.go +++ b/internal/service/link_test.go @@ -6,6 +6,7 @@ import ( "time" "golinks/internal/domain" + "golinks/internal/logger" ) // Mock repositories for testing @@ -158,7 +159,8 @@ func TestLinkService_GetLink(t *testing.T) { t.Run(tt.name, func(t *testing.T) { shortcutRepo := &mockShortcutRepository{shortcuts: tt.shortcuts} queryRepo := &mockQueryRepository{} - service := NewLinkService(shortcutRepo, queryRepo) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + service := NewLinkService(shortcutRepo, queryRepo, mockLogger) got, err := service.GetLink(context.Background(), tt.word, tt.searchTerm) @@ -255,7 +257,8 @@ func TestLinkService_UpdateLink(t *testing.T) { t.Run(tt.name, func(t *testing.T) { shortcutRepo := &mockShortcutRepository{shortcuts: tt.shortcuts} queryRepo := &mockQueryRepository{} - service := NewLinkService(shortcutRepo, queryRepo) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + service := NewLinkService(shortcutRepo, queryRepo, mockLogger) err := service.UpdateLink(context.Background(), tt.request, tt.userID) @@ -269,7 +272,8 @@ func TestLinkService_UpdateLink(t *testing.T) { func TestLinkService_GetRecentQueries(t *testing.T) { shortcutRepo := &mockShortcutRepository{shortcuts: map[string]*domain.Shortcut{}} queryRepo := &mockQueryRepository{} - service := NewLinkService(shortcutRepo, queryRepo) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + service := NewLinkService(shortcutRepo, queryRepo, mockLogger) queries, err := service.GetRecentQueries(context.Background()) @@ -307,7 +311,8 @@ func TestLinkService_GetAllKeywords(t *testing.T) { shortcutRepo := &mockShortcutRepository{shortcuts: shortcuts} queryRepo := &mockQueryRepository{} - service := NewLinkService(shortcutRepo, queryRepo) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + service := NewLinkService(shortcutRepo, queryRepo, mockLogger) keywords, err := service.GetAllKeywords(context.Background()) diff --git a/web/static/styles.css b/web/static/styles.css index 84f6b35..b48bf19 100644 --- a/web/static/styles.css +++ b/web/static/styles.css @@ -75,6 +75,88 @@ body { padding: 0 var(--space-md); } +/* Navigation Bar */ +.navbar { + background: linear-gradient(135deg, var(--rams-black) 0%, var(--rams-charcoal) 100%); + box-shadow: var(--shadow-md); + position: sticky; + top: 0; + z-index: 1000; +} + +.navbar-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--space-md); + display: flex; + justify-content: space-between; + align-items: center; + min-height: 70px; +} + +.navbar-brand h1 { + font-family: var(--font-primary); + font-weight: 300; + font-size: 1.8rem; + letter-spacing: -0.02em; + color: var(--rams-white); + margin: 0; + padding: 0; + background: none; + text-align: left; + position: static; +} + +.navbar-brand h1::after { + display: none; +} + +.navbar-brand h1 .accent { + color: var(--rams-orange); + font-weight: 400; +} + +.navbar-nav { + display: flex; + gap: var(--space-lg); + align-items: center; +} + +.nav-link { + color: var(--rams-light-grey); + text-decoration: none; + font-size: 0.9rem; + font-weight: 500; + letter-spacing: 0.01em; + padding: var(--space-sm) var(--space-md); + border-radius: var(--radius-sm); + transition: all 0.2s ease; + position: relative; +} + +.nav-link:hover { + color: var(--rams-white); + background-color: rgba(255, 255, 255, 0.1); + text-decoration: none; +} + +.nav-link.active { + color: var(--rams-orange); + background-color: rgba(255, 107, 53, 0.1); +} + +.nav-link.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 50%; + transform: translateX(-50%); + width: 20px; + height: 2px; + background-color: var(--rams-orange); + border-radius: 1px; +} + /* Typography */ h1 { font-family: var(--font-primary); @@ -301,6 +383,24 @@ table tr:hover { /* Responsive design */ @media (max-width: 768px) { + .navbar-container { + padding: 0 var(--space-sm); + min-height: 60px; + } + + .navbar-brand h1 { + font-size: 1.5rem; + } + + .navbar-nav { + gap: var(--space-md); + } + + .nav-link { + font-size: 0.8rem; + padding: var(--space-xs) var(--space-sm); + } + .constrained-width { padding: 0 var(--space-sm); } @@ -374,3 +474,185 @@ table tr:hover { outline: 2px solid var(--rams-blue); outline-offset: 2px; } + + +/* 404 Error page styles */ +.error-page { + min-height: calc(100vh - 70px); + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-2xl) var(--space-md); +} + +.error-content { + text-align: center; + max-width: 600px; + margin: 0 auto; +} + +.error-icon { + margin-bottom: var(--space-xl); +} + +.error-icon span { + font-size: 6rem; + font-weight: 300; + color: var(--rams-light-grey); + font-family: var(--font-primary); + letter-spacing: -0.05em; + display: block; + line-height: 1; +} + +.error-content h2 { + font-size: 2rem; + font-weight: 500; + color: var(--rams-charcoal); + margin-bottom: var(--space-lg); + letter-spacing: -0.02em; +} + +.error-description { + color: var(--rams-dark-grey); + font-size: 1rem; + line-height: 1.6; + margin-bottom: var(--space-2xl); +} + +.error-description code { + background-color: var(--rams-light-grey); + padding: var(--space-xs) var(--space-sm); + border-radius: var(--radius-sm); + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--rams-charcoal); +} + +.error-actions { + display: flex; + gap: var(--space-md); + justify-content: center; + margin-bottom: var(--space-2xl); + flex-wrap: wrap; +} + +.primary-action, +.secondary-action { + display: inline-flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-md) var(--space-lg); + text-decoration: none; + font-size: 0.9rem; + font-weight: 500; + letter-spacing: 0.01em; + border-radius: var(--radius-md); + transition: all 0.2s ease; + min-width: 140px; + justify-content: center; +} + +.primary-action { + background: linear-gradient(135deg, var(--rams-blue) 0%, var(--rams-orange) 100%); + color: var(--rams-white); + box-shadow: var(--shadow-md); +} + +.primary-action:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); + text-decoration: none; +} + +.secondary-action { + background-color: var(--rams-white); + color: var(--rams-charcoal); + border: 1px solid var(--rams-medium-grey); + box-shadow: var(--shadow-sm); +} + +.secondary-action:hover { + background-color: var(--rams-off-white); + border-color: var(--rams-dark-grey); + transform: translateY(-1px); + text-decoration: none; +} + +.error-help { + background-color: var(--rams-white); + border: 1px solid var(--rams-light-grey); + border-radius: var(--radius-lg); + padding: var(--space-lg); + text-align: left; + box-shadow: var(--shadow-sm); +} + +.error-help p { + font-weight: 500; + color: var(--rams-charcoal); + margin-bottom: var(--space-md); +} + +.error-help ul { + list-style: none; + padding: 0; + margin: 0; +} + +.error-help li { + padding: var(--space-sm) 0; + color: var(--rams-dark-grey); + position: relative; + padding-left: var(--space-lg); +} + +.error-help li::before { + content: '•'; + color: var(--rams-orange); + font-weight: bold; + position: absolute; + left: 0; +} + +.error-help a { + color: var(--rams-blue); + text-decoration: none; + font-weight: 500; +} + +.error-help a:hover { + color: var(--rams-orange); + text-decoration: underline; +} + +/* Responsive design for 404 page */ +@media (max-width: 768px) { + .error-page { + min-height: calc(100vh - 60px); + padding: var(--space-xl) var(--space-sm); + } + + .error-icon span { + font-size: 4rem; + } + + .error-content h2 { + font-size: 1.5rem; + } + + .error-actions { + flex-direction: column; + align-items: center; + } + + .primary-action, + .secondary-action { + width: 100%; + max-width: 200px; + } + + .error-help { + padding: var(--space-md); + } +} diff --git a/web/templates/404.html b/web/templates/404.html new file mode 100644 index 0000000..d160dac --- /dev/null +++ b/web/templates/404.html @@ -0,0 +1,56 @@ + + + + + + 404 - Page Not Found | GoLinks + + + + + + +
+
+
+ 404 +
+

Page Not Found

+

+ The page you are looking for ({{.Path}}) does not exist. + It might have been moved, deleted, or you entered the wrong URL. +

+ + + +
+

Need help? Here are some suggestions:

+ +
+
+
+ + \ No newline at end of file diff --git a/web/templates/homepage.html b/web/templates/homepage.html index 46a0a53..b6c401f 100644 --- a/web/templates/homepage.html +++ b/web/templates/homepage.html @@ -5,11 +5,21 @@ golinks - + -

golinks

+ {{if .Missing}}
diff --git a/web/templates/setup.html b/web/templates/setup.html index ef284fe..443c20b 100644 --- a/web/templates/setup.html +++ b/web/templates/setup.html @@ -5,10 +5,20 @@ golinks - Setup - + -

golinks Setup

+

🔧 Browser Setup

From ef2f21d2322f424d83e0a738fceb310b04bb195a Mon Sep 17 00:00:00 2001 From: Alejandro Perez Gancedo Date: Thu, 25 Sep 2025 00:34:14 +0100 Subject: [PATCH 3/6] fix: remove alias and add validation --- README.md | 1 - TODO.md | 4 ++- internal/domain/models.go | 3 +- internal/handlers/handler.go | 6 +++- internal/service/link.go | 32 +++++------------- internal/service/link_test.go | 63 +++++++++++++++-------------------- web/static/styles.css | 17 ++++++++++ web/templates/homepage.html | 55 ++++++++++-------------------- 8 files changed, 80 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 4a30326..802aa0e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ A modern, minimalist URL shortener inspired by Google's internal golinks system. - **Simple URL Shortening**: Create memorable shortcuts for long URLs - **Variable Substitution**: Use `{*}` placeholders for dynamic content -- **Recursive Aliases**: Keywords can point to other keywords - **Usage Analytics**: Track popular queries and usage patterns - **Clean Architecture**: Modular, testable, and maintainable codebase - **Modern UI**: HTMX-powered interface with Dieter Rams-inspired design diff --git a/TODO.md b/TODO.md index 4eddbba..c32067a 100644 --- a/TODO.md +++ b/TODO.md @@ -7,4 +7,6 @@ - [ ] Consider the use of an ORM - [ ] Statistics of most accessed docs - [ ] Support to render Markdown docs and MDX docs -- [ ] Deployment in google app engine \ No newline at end of file +- [ ] Deployment in google app engine +- [ ] Add admin panel to manage golinks +- [ ] Add the ability to create a request to an admin for a keyword to be added \ No newline at end of file diff --git a/internal/domain/models.go b/internal/domain/models.go index dfd3519..5f88fa2 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -40,10 +40,9 @@ type PopularQuery struct { Link string `json:"link"` } -// KeywordInfo represents keyword information with aliases +// KeywordInfo represents keyword information type KeywordInfo struct { Word string `json:"word"` - Aliases string `json:"aliases"` Link string `json:"link"` CreatedAt time.Time `json:"created_at"` } diff --git a/internal/handlers/handler.go b/internal/handlers/handler.go index 401f480..e8307cc 100644 --- a/internal/handlers/handler.go +++ b/internal/handlers/handler.go @@ -144,7 +144,11 @@ func (h *Handler) UpdateLinkHandler(w http.ResponseWriter, r *http.Request) { // Return success message for HTMX w.Header().Set("Content-Type", "text/plain") - w.Write([]byte("Link added successfully!")) + if _, err := w.Write([]byte("Link added successfully!")); err != nil { + h.logger.Error("Failed to write response: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } } // HomepageHandler handles the homepage diff --git a/internal/service/link.go b/internal/service/link.go index 789a181..7295e68 100644 --- a/internal/service/link.go +++ b/internal/service/link.go @@ -86,8 +86,8 @@ func (s *LinkService) GetLink(ctx context.Context, word string, searchTerm strin // Handle different types of links if !isURL(shortcut.Link) { - s.logger.Debug("Link is an alias '%s', recursing", shortcut.Link) - // This is an alias, recurse + s.logger.Debug("Link is a keyword reference '%s', recursing", shortcut.Link) + // This is a keyword reference, recurse return s.GetLink(ctx, shortcut.Link, searchTerm) } @@ -107,17 +107,12 @@ func (s *LinkService) UpdateLink(ctx context.Context, req domain.LinkRequest, us return err } - // If the link is not a URL, validate it's a valid alias + // Validate that the link is a proper URL if !isURL(req.Link) { - s.logger.Debug("Validating alias link: %s", req.Link) - _, err := s.GetLink(ctx, req.Link, "") - if err != nil { - s.logger.Error("Invalid alias link '%s': %v", req.Link, err) - return InvalidQueryError{ - Message: "The link target appears to neither be a URL, or a valid alias.", - } + s.logger.Warn("Invalid URL format: %s", req.Link) + return InvalidQueryError{ + Message: "URL must start with http:// or https://", } - s.logger.Debug("Alias validation successful: %s", req.Link) } shortcut := &domain.Shortcut{ @@ -150,7 +145,7 @@ func (s *LinkService) GetRecentQueries(ctx context.Context) ([]domain.PopularQue return queries, nil } -// GetAllKeywords retrieves all keywords with aliases +// GetAllKeywords retrieves all keywords func (s *LinkService) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, error) { s.logger.Debug("Fetching all keywords") @@ -160,16 +155,7 @@ func (s *LinkService) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, return nil, err } - s.logger.Debug("Processing keywords for aliases: %d total keywords", len(keywords)) - - // Process aliases (simplified version - not implementing full recursive alias resolution for now) - for i := range keywords { - if !isURL(keywords[i].Link) { - keywords[i].Aliases = keywords[i].Link - } - } - - // Filter to only return URLs (not aliases) + // Filter to only return URLs var result []domain.KeywordInfo for _, keyword := range keywords { if isURL(keyword.Link) { @@ -177,7 +163,7 @@ func (s *LinkService) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, } } - s.logger.Debug("Keywords retrieved and processed successfully: %d total, %d URLs", len(keywords), len(result)) + s.logger.Debug("Keywords retrieved successfully: %d total, %d URLs", len(keywords), len(result)) return result, nil } diff --git a/internal/service/link_test.go b/internal/service/link_test.go index f4a76a9..4a8d034 100644 --- a/internal/service/link_test.go +++ b/internal/service/link_test.go @@ -34,13 +34,11 @@ func (m *mockShortcutRepository) Create(ctx context.Context, shortcut *domain.Sh func (m *mockShortcutRepository) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, error) { var keywords []domain.KeywordInfo for word, shortcut := range m.shortcuts { - if isURL(shortcut.Link) { - keywords = append(keywords, domain.KeywordInfo{ - Word: word, - Link: shortcut.Link, - CreatedAt: shortcut.CreatedAt, - }) - } + keywords = append(keywords, domain.KeywordInfo{ + Word: word, + Link: shortcut.Link, + CreatedAt: shortcut.CreatedAt, + }) } return keywords, nil } @@ -110,7 +108,7 @@ func TestLinkService_GetLink(t *testing.T) { wantErr: false, }, { - name: "alias redirect", + name: "keyword reference redirect", shortcuts: map[string]*domain.Shortcut{ "d": { ID: 1, @@ -194,23 +192,6 @@ func TestLinkService_UpdateLink(t *testing.T) { userID: "testuser", wantErr: false, }, - { - name: "valid alias", - shortcuts: map[string]*domain.Shortcut{ - "docs": { - ID: 1, - Word: "docs", - Link: "https://docs.example.com", - User: "testuser", - }, - }, - request: domain.LinkRequest{ - Word: "d", - Link: "docs", - }, - userID: "testuser", - wantErr: false, - }, { name: "empty word", shortcuts: map[string]*domain.Shortcut{}, @@ -242,11 +223,11 @@ func TestLinkService_UpdateLink(t *testing.T) { wantErr: true, }, { - name: "invalid alias target", + name: "invalid URL format", shortcuts: map[string]*domain.Shortcut{}, request: domain.LinkRequest{ - Word: "d", - Link: "nonexistent", + Word: "docs", + Link: "example.com", // Missing http:// or https:// }, userID: "testuser", wantErr: true, @@ -300,10 +281,10 @@ func TestLinkService_GetAllKeywords(t *testing.T) { User: "testuser", CreatedAt: time.Now(), }, - "d": { + "github": { ID: 2, - Word: "d", - Link: "docs", // This is an alias, should be filtered out + Word: "github", + Link: "https://github.com", User: "testuser", CreatedAt: time.Now(), }, @@ -320,13 +301,23 @@ func TestLinkService_GetAllKeywords(t *testing.T) { t.Errorf("LinkService.GetAllKeywords() error = %v", err) } - // Should only return URLs, not aliases - if len(keywords) != 1 { - t.Errorf("LinkService.GetAllKeywords() expected 1 keyword, got %d", len(keywords)) + // Should return only URLs + if len(keywords) != 2 { + t.Errorf("LinkService.GetAllKeywords() expected 2 keywords, got %d", len(keywords)) + } + + // Check that we have both keywords + keywordMap := make(map[string]bool) + for _, keyword := range keywords { + keywordMap[keyword.Word] = true + } + + if !keywordMap["docs"] { + t.Error("LinkService.GetAllKeywords() missing 'docs' keyword") } - if keywords[0].Word != "docs" { - t.Errorf("LinkService.GetAllKeywords() expected 'docs', got %s", keywords[0].Word) + if !keywordMap["github"] { + t.Error("LinkService.GetAllKeywords() missing 'github' keyword") } } diff --git a/web/static/styles.css b/web/static/styles.css index b48bf19..b82fe80 100644 --- a/web/static/styles.css +++ b/web/static/styles.css @@ -251,6 +251,7 @@ a:hover { text-decoration: underline; } + /* Forms - Dieter Rams functional design */ #formData { display: flex; @@ -338,6 +339,22 @@ a:hover { border: 1px solid rgba(220, 38, 38, 0.2); } +/* Form error messages */ +.error-message { + background-color: rgba(220, 38, 38, 0.1); + color: var(--rams-red); + border: 1px solid rgba(220, 38, 38, 0.2); + padding: var(--space-md); + margin: var(--space-md) 0; + border-radius: var(--radius-md); + font-size: 0.9rem; + font-weight: 500; + text-align: center; + position: relative; + z-index: 10; + clear: both; +} + /* Tables - minimal and functional */ table { width: 100%; diff --git a/web/templates/homepage.html b/web/templates/homepage.html index b6c401f..bff67e2 100644 --- a/web/templates/homepage.html +++ b/web/templates/homepage.html @@ -5,7 +5,7 @@ golinks - + @@ -62,27 +62,6 @@

➕ Add new keyword

- {{if .RecentQueries}} -

🔥 Popular queries

- - - - - - - - - - {{range .RecentQueries}} - - - - - - {{end}} - -
CountKeywordURL
{{.Count}}{{.Word}}{{urlify .Link}}
- {{end}} {{if .AllKeywords}}

🔎 Full keyword list

@@ -95,7 +74,6 @@

🔎 Full keyword list

Keyword - Aliases URL Created On @@ -104,7 +82,6 @@

🔎 Full keyword list

{{range .AllKeywords}} {{.Word}} - {{if .Aliases}}{{.Aliases}}{{else}}-{{end}} {{urlify .Link}} {{.CreatedAt.Format "2006-01-02"}} @@ -115,7 +92,7 @@

🔎 Full keyword list

- - - - -
-
-
{{.Type}}
-

{{.Title}}

- {{if .Description}}

{{.Description}}

{{end}} -
-
- {{.Content}} -
- -
- -` - - t, err := template.New("document").Parse(tmpl) - if err != nil { - http.Error(w, "Template error", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "text/html") - if err := t.Execute(w, data); err != nil { - http.Error(w, "Failed to render template", http.StatusInternalServerError) - return - } +func writeJSON(w http.ResponseWriter, status int, body interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(body) } diff --git a/internal/handlers/handler.go b/internal/handlers/handler.go index e8307cc..c037371 100644 --- a/internal/handlers/handler.go +++ b/internal/handlers/handler.go @@ -2,8 +2,8 @@ package handlers import ( "context" + "encoding/json" "fmt" - "html/template" "net/http" "strings" @@ -15,7 +15,7 @@ import ( "github.com/gorilla/mux" ) -// LinkService interface for link operations +// LinkService is the subset of the link service the HTTP layer depends on. type LinkService interface { GetLink(ctx context.Context, word string, searchTerm string) (string, error) UpdateLink(ctx context.Context, req domain.LinkRequest, userID string) error @@ -23,66 +23,45 @@ type LinkService interface { GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, error) } -// Handler holds the HTTP handlers +// Handler owns the redirect + JSON endpoints for links. +// The SPA fallback (serving index.html for unmatched routes) lives in the +// main package so it can reference the embedded frontend filesystem. type Handler struct { linkService LinkService config *config.Config - templates *template.Template logger *logger.Logger } -// NewHandler creates a new handler +// NewHandler builds a new Handler. No templates are loaded — the UI is a +// React SPA served from the embedded frontend filesystem. func NewHandler(linkService LinkService, cfg *config.Config, log *logger.Logger) *Handler { - log.Info("Loading HTML templates from web/templates/*.html") - - // Load templates - templates := template.Must(template.New("").Funcs(template.FuncMap{ - "urlify": func(url string) template.HTML { - if strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") { - return template.HTML(fmt.Sprintf(`%s`, url, url)) - } - return template.HTML(url) - }, - }).ParseGlob("web/templates/*.html")) - - log.Info("Handler initialized successfully") - + log.Info("Handler initialized (JSON + redirect only)") return &Handler{ linkService: linkService, config: cfg, - templates: templates, logger: log, } } -// RegisterRoutes registers all HTTP routes +// RegisterRoutes wires the redirect and JSON endpoints. Static-asset and SPA +// fallback handling is registered separately in cmd/server/main.go. func (h *Handler) RegisterRoutes(router *mux.Router) { - // Static files - router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("web/static/")))) - - // API routes + // The golink contract: server-side 302 so browser search-engine integrations work. router.HandleFunc("/query/{path:.*}", h.RedirectHandler).Methods("GET") - router.HandleFunc("/update/", h.UpdateLinkHandler).Methods("POST") - router.HandleFunc("/homepage/", h.HomepageHandler).Methods("GET") - router.HandleFunc("/setup/", h.SetupHandler).Methods("GET") - // Root redirect to homepage - router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/homepage/", http.StatusFound) - }).Methods("GET") + // JSON API. The SPA uses only these. + router.HandleFunc("/api/links", h.ListLinks).Methods("GET") + router.HandleFunc("/api/links", h.CreateLink).Methods("POST") - // 404 handler for all other routes - router.NotFoundHandler = http.HandlerFunc(h.NotFoundHandler) + // Back-compat aliases for the old template-era endpoints so browser + // engines configured against /update/ and /homepage/ keep working. + router.HandleFunc("/update/", h.UpdateLinkLegacy).Methods("POST") } -// RedirectHandler handles golink redirects +// RedirectHandler resolves a golink and issues a 302. func (h *Handler) RedirectHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - - vars := mux.Vars(r) - queryPath := vars["path"] - queryPath = strings.TrimSuffix(queryPath, "/") - + queryPath := strings.TrimSuffix(mux.Vars(r)["path"], "/") userID := h.getUserID(r) h.logger.Info("Processing golink redirect: %s (user: %s)", queryPath, userID) @@ -90,176 +69,91 @@ func (h *Handler) RedirectHandler(w http.ResponseWriter, r *http.Request) { targetURL, err := h.linkService.GetLink(ctx, queryPath, "") if err != nil { if _, ok := err.(service.InvalidQueryError); ok { - h.logger.Warn("Invalid query '%s' - redirecting to homepage: %v", queryPath, err) - // Redirect to homepage with missing query parameter - redirectURL := fmt.Sprintf("%s/homepage/?missing=%s", h.config.BaseURL, queryPath) - http.Redirect(w, r, redirectURL, http.StatusFound) + h.logger.Warn("Invalid query '%s' - redirecting to home: %v", queryPath, err) + http.Redirect(w, r, fmt.Sprintf("%s/?missing=%s", h.config.BaseURL, queryPath), http.StatusFound) return } - h.logger.Error("Failed to get link for query '%s': %v", queryPath, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - h.logger.Info("Redirecting '%s' to '%s' (user: %s)", queryPath, targetURL, userID) http.Redirect(w, r, targetURL, http.StatusFound) } -// UpdateLinkHandler handles link creation/updates -func (h *Handler) UpdateLinkHandler(w http.ResponseWriter, r *http.Request) { +// ListLinks returns everything the homepage needs in one call. +func (h *Handler) ListLinks(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - var req domain.LinkRequest - - // Parse form data - if err := r.ParseForm(); err != nil { - h.logger.Warn("Invalid form data in update request: %v", err) - http.Error(w, "Invalid form data", http.StatusBadRequest) - return - } - - req.Word = strings.TrimSpace(r.FormValue("word")) - req.Link = strings.TrimSpace(r.FormValue("link")) - - h.logger.Info("Parsed form data: word='%s' link='%s'", req.Word, req.Link) - - userID := h.getUserID(r) - - h.logger.Info("Processing link update: word='%s' link='%s' user='%s'", req.Word, req.Link, userID) - - if err := h.linkService.UpdateLink(ctx, req, userID); err != nil { - if _, ok := err.(service.InvalidQueryError); ok { - h.logger.Warn("Invalid link update request for word='%s': %v", req.Word, err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - h.logger.Error("Failed to update link word='%s': %v", req.Word, err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - h.logger.Info("Link updated successfully: word='%s' link='%s' user='%s'", req.Word, req.Link, userID) - - // Return success message for HTMX - w.Header().Set("Content-Type", "text/plain") - if _, err := w.Write([]byte("Link added successfully!")); err != nil { - h.logger.Error("Failed to write response: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } -} - -// HomepageHandler handles the homepage -func (h *Handler) HomepageHandler(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - userID := h.getUserID(r) - - // Get query parameters - success := r.URL.Query().Get("success") - failure := r.URL.Query().Get("failure") - reason := r.URL.Query().Get("reason") - missing := r.URL.Query().Get("missing") - - h.logger.Info("Rendering homepage for user '%s'", userID) - if missing != "" { - h.logger.Info("Homepage showing missing query: %s", missing) - } - - // Get recent queries and keywords - recentQueries, err := h.linkService.GetRecentQueries(ctx) + recent, err := h.linkService.GetRecentQueries(ctx) if err != nil { h.logger.Error("Failed to get recent queries: %v", err) - recentQueries = []domain.PopularQuery{} + recent = []domain.PopularQuery{} } - allKeywords, err := h.linkService.GetAllKeywords(ctx) + keywords, err := h.linkService.GetAllKeywords(ctx) if err != nil { h.logger.Error("Failed to get all keywords: %v", err) - allKeywords = []domain.KeywordInfo{} + keywords = []domain.KeywordInfo{} } - h.logger.Debug("Homepage data loaded: %d recent queries, %d keywords", len(recentQueries), len(allKeywords)) + writeJSON(w, http.StatusOK, map[string]interface{}{ + "keywords": keywords, + "recent_queries": recent, + "base_url": h.config.BaseURL, + }) +} - data := struct { - Success string - Failure string - Reason string - Missing string - RecentQueries []domain.PopularQuery - AllKeywords []domain.KeywordInfo - BaseURL string - }{ - Success: success, - Failure: failure, - Reason: reason, - Missing: missing, - RecentQueries: recentQueries, - AllKeywords: allKeywords, - BaseURL: h.config.BaseURL, +// CreateLink accepts a JSON {"word":"", "link":""} body. +func (h *Handler) CreateLink(w http.ResponseWriter, r *http.Request) { + var req domain.LinkRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON body", http.StatusBadRequest) + return } + req.Word = strings.TrimSpace(req.Word) + req.Link = strings.TrimSpace(req.Link) - w.Header().Set("Content-Type", "text/html") - if err := h.templates.ExecuteTemplate(w, "homepage.html", data); err != nil { - h.logger.Error("Failed to execute homepage template: %v", err) + if err := h.linkService.UpdateLink(r.Context(), req, h.getUserID(r)); err != nil { + if _, ok := err.(service.InvalidQueryError); ok { + h.logger.Warn("Invalid link request word='%s': %v", req.Word, err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + h.logger.Error("Failed to create link word='%s': %v", req.Word, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - h.logger.Debug("Homepage rendered successfully") + h.logger.Info("Link created: word='%s' link='%s'", req.Word, req.Link) + writeJSON(w, http.StatusOK, map[string]interface{}{"success": true}) } -// SetupHandler handles the setup page -func (h *Handler) SetupHandler(w http.ResponseWriter, r *http.Request) { - userID := h.getUserID(r) - - h.logger.Info("Rendering setup page for user '%s'", userID) - - data := struct { - BaseURL string - }{ - BaseURL: h.config.BaseURL, - } - - w.Header().Set("Content-Type", "text/html") - if err := h.templates.ExecuteTemplate(w, "setup.html", data); err != nil { - h.logger.Error("Failed to execute setup template: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) +// UpdateLinkLegacy preserves the old /update/ form endpoint so browser search +// engines that still POST form-encoded data keep working. +func (h *Handler) UpdateLinkLegacy(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "Invalid form data", http.StatusBadRequest) return } - - h.logger.Debug("Setup page rendered successfully") -} - -// NotFoundHandler handles 404 errors -func (h *Handler) NotFoundHandler(w http.ResponseWriter, r *http.Request) { - userID := h.getUserID(r) - - h.logger.Info("404 page requested for path '%s' by user '%s'", r.URL.Path, userID) - - data := struct { - BaseURL string - Path string - }{ - BaseURL: h.config.BaseURL, - Path: r.URL.Path, + req := domain.LinkRequest{ + Word: strings.TrimSpace(r.FormValue("word")), + Link: strings.TrimSpace(r.FormValue("link")), } - - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusNotFound) - if err := h.templates.ExecuteTemplate(w, "404.html", data); err != nil { - h.logger.Error("Failed to execute 404 template: %v", err) - http.Error(w, "Page not found", http.StatusNotFound) + if err := h.linkService.UpdateLink(r.Context(), req, h.getUserID(r)); err != nil { + if _, ok := err.(service.InvalidQueryError); ok { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + http.Error(w, "Internal server error", http.StatusInternalServerError) return } - - h.logger.Debug("404 page rendered successfully") + w.Header().Set("Content-Type", "text/plain") + _, _ = w.Write([]byte("Link added successfully!")) } -// getUserID extracts user ID from request (simplified - no OAuth2 for now) -func (h *Handler) getUserID(r *http.Request) string { - // For now, return a default user. In production, this would extract from OAuth2 cookie +// getUserID extracts the user ID from the request. Authentication is not +// implemented yet — see plan for the follow-up. +func (h *Handler) getUserID(_ *http.Request) string { return "DefaultUser" } diff --git a/internal/handlers/handler_test.go b/internal/handlers/handler_test.go index 4bf16d4..8609517 100644 --- a/internal/handlers/handler_test.go +++ b/internal/handlers/handler_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "html/template" "net/http" "net/http/httptest" "strings" @@ -12,12 +11,12 @@ import ( "golinks/internal/config" "golinks/internal/domain" + "golinks/internal/logger" "golinks/internal/service" "github.com/gorilla/mux" ) -// Mock LinkService for testing type mockLinkService struct { links map[string]string recentQueries []domain.PopularQuery @@ -26,7 +25,7 @@ type mockLinkService struct { getError error } -func (m *mockLinkService) GetLink(ctx context.Context, word string, searchTerm string) (string, error) { +func (m *mockLinkService) GetLink(_ context.Context, word string, _ string) (string, error) { if m.getError != nil { return "", m.getError } @@ -36,7 +35,7 @@ func (m *mockLinkService) GetLink(ctx context.Context, word string, searchTerm s return "", service.InvalidQueryError{Message: "not found"} } -func (m *mockLinkService) UpdateLink(ctx context.Context, req domain.LinkRequest, userID string) error { +func (m *mockLinkService) UpdateLink(_ context.Context, req domain.LinkRequest, _ string) error { if m.updateError != nil { return m.updateError } @@ -44,73 +43,34 @@ func (m *mockLinkService) UpdateLink(ctx context.Context, req domain.LinkRequest return nil } -func (m *mockLinkService) GetRecentQueries(ctx context.Context) ([]domain.PopularQuery, error) { +func (m *mockLinkService) GetRecentQueries(_ context.Context) ([]domain.PopularQuery, error) { return m.recentQueries, nil } -func (m *mockLinkService) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, error) { +func (m *mockLinkService) GetAllKeywords(_ context.Context) ([]domain.KeywordInfo, error) { return m.allKeywords, nil } func setupTestHandler() *Handler { - cfg := &config.Config{ - BaseURL: "http://localhost:8080", - } - - // Create simple templates for testing - templates := template.Must(template.New("").Funcs(template.FuncMap{ - "urlify": func(url string) template.HTML { - if strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") { - return template.HTML(`` + url + ``) - } - return template.HTML(url) - }, - }).Parse(` - {{define "homepage.html"}} - - -

GoLinks

- {{if .Missing}}
Missing: {{.Missing}}
{{end}} - {{if .Success}}
Success: {{.Success}}
{{end}} - {{if .Failure}}
Failure: {{.Failure}} - {{.Reason}}
{{end}} -
Recent Queries: {{len .RecentQueries}}
-
All Keywords: {{len .AllKeywords}}
- - - {{end}} - {{define "setup.html"}} - - -

Setup

-

Base URL: {{.BaseURL}}

- - - {{end}} - `)) - - mockService := &mockLinkService{ - links: map[string]string{ - "docs": "https://docs.example.com", - "github": "https://github.com", - }, - recentQueries: []domain.PopularQuery{ - {Count: 5, Word: "docs", Link: "https://docs.example.com"}, - }, - allKeywords: []domain.KeywordInfo{ - {Word: "docs", Link: "https://docs.example.com"}, + return &Handler{ + linkService: &mockLinkService{ + links: map[string]string{ + "docs": "https://docs.example.com", + "github": "https://github.com", + }, + recentQueries: []domain.PopularQuery{ + {Count: 5, Word: "docs", Link: "https://docs.example.com"}, + }, + allKeywords: []domain.KeywordInfo{ + {Word: "docs", Link: "https://docs.example.com"}, + }, }, + config: &config.Config{BaseURL: "http://localhost:8080"}, + logger: logger.Default(), } - - handler := &Handler{ - linkService: mockService, - config: cfg, - templates: templates, - } - - return handler } -func TestHandler_RedirectHandler(t *testing.T) { +func TestRedirectHandler(t *testing.T) { handler := setupTestHandler() tests := []struct { @@ -118,32 +78,11 @@ func TestHandler_RedirectHandler(t *testing.T) { path string expectedStatus int expectedHeader string - setupError error }{ - { - name: "successful redirect", - path: "/query/docs", - expectedStatus: http.StatusFound, - expectedHeader: "https://docs.example.com", - }, - { - name: "missing query redirect to homepage", - path: "/query/nonexistent", - expectedStatus: http.StatusFound, - expectedHeader: "http://localhost:8080/homepage/?missing=nonexistent", - }, - { - name: "empty path", - path: "/query/", - expectedStatus: http.StatusFound, - expectedHeader: "http://localhost:8080/homepage/?missing=", - }, - { - name: "path with trailing slash", - path: "/query/docs/", - expectedStatus: http.StatusFound, - expectedHeader: "https://docs.example.com", - }, + {"hit", "/query/docs", http.StatusFound, "https://docs.example.com"}, + {"miss", "/query/nonexistent", http.StatusFound, "http://localhost:8080/?missing=nonexistent"}, + {"empty", "/query/", http.StatusFound, "http://localhost:8080/?missing="}, + {"trailing slash", "/query/docs/", http.StatusFound, "https://docs.example.com"}, } for _, tt := range tests { @@ -151,246 +90,153 @@ func TestHandler_RedirectHandler(t *testing.T) { req := httptest.NewRequest("GET", tt.path, nil) w := httptest.NewRecorder() - // Setup router to extract path variable router := mux.NewRouter() router.HandleFunc("/query/{path:.*}", handler.RedirectHandler).Methods("GET") router.ServeHTTP(w, req) if w.Code != tt.expectedStatus { - t.Errorf("RedirectHandler() status = %v, want %v", w.Code, tt.expectedStatus) + t.Errorf("status = %v, want %v", w.Code, tt.expectedStatus) } - - if tt.expectedStatus == http.StatusFound { - location := w.Header().Get("Location") - if location != tt.expectedHeader { - t.Errorf("RedirectHandler() Location = %v, want %v", location, tt.expectedHeader) - } + if w.Header().Get("Location") != tt.expectedHeader { + t.Errorf("Location = %q, want %q", w.Header().Get("Location"), tt.expectedHeader) } }) } } -func TestHandler_UpdateLinkHandler(t *testing.T) { +func TestCreateLinkJSON(t *testing.T) { tests := []struct { name string - requestBody interface{} + body string expectedStatus int setupError error }{ { - name: "successful update", - requestBody: domain.LinkRequest{ - Word: "test", - Link: "https://test.com", - }, + name: "valid JSON body", + body: `{"word":"test","link":"https://test.com"}`, expectedStatus: http.StatusOK, }, { - name: "invalid JSON", - requestBody: "invalid json", + name: "malformed JSON", + body: "not json", expectedStatus: http.StatusBadRequest, }, { - name: "service error", - requestBody: domain.LinkRequest{ - Word: "error", - Link: "https://error.com", - }, + name: "service rejects input", + body: `{"word":"bad","link":"https://x.com"}`, expectedStatus: http.StatusBadRequest, - setupError: service.InvalidQueryError{Message: "test error"}, + setupError: service.InvalidQueryError{Message: "bad input"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler := setupTestHandler() - - // Setup error if needed if tt.setupError != nil { - mockService := handler.linkService.(*mockLinkService) - mockService.updateError = tt.setupError + handler.linkService.(*mockLinkService).updateError = tt.setupError } - var body []byte - var err error - - if str, ok := tt.requestBody.(string); ok { - body = []byte(str) - } else { - body, err = json.Marshal(tt.requestBody) - if err != nil { - t.Fatalf("Failed to marshal request body: %v", err) - } - } - - req := httptest.NewRequest("POST", "/update/", bytes.NewBuffer(body)) + req := httptest.NewRequest("POST", "/api/links", bytes.NewBufferString(tt.body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - handler.UpdateLinkHandler(w, req) + handler.CreateLink(w, req) if w.Code != tt.expectedStatus { - t.Errorf("UpdateLinkHandler() status = %v, want %v", w.Code, tt.expectedStatus) - } - - if tt.expectedStatus == http.StatusOK { - var response map[string]string - err := json.NewDecoder(w.Body).Decode(&response) - if err != nil { - t.Errorf("Failed to decode response: %v", err) - } - if response["status"] != "success" { - t.Errorf("Expected success response, got %v", response) - } + t.Errorf("status = %v, want %v, body=%q", w.Code, tt.expectedStatus, w.Body.String()) } }) } } -func TestHandler_HomepageHandler(t *testing.T) { +func TestListLinks(t *testing.T) { handler := setupTestHandler() - tests := []struct { - name string - queryParams string - expectedStatus int - expectedBody []string - }{ - { - name: "basic homepage", - queryParams: "", - expectedStatus: http.StatusOK, - expectedBody: []string{"

GoLinks

", "Recent Queries: 1", "All Keywords: 1"}, - }, - { - name: "homepage with success message", - queryParams: "?success=docs", - expectedStatus: http.StatusOK, - expectedBody: []string{"Success: docs"}, - }, - { - name: "homepage with failure message", - queryParams: "?failure=test&reason=invalid", - expectedStatus: http.StatusOK, - expectedBody: []string{"Failure: test - invalid"}, - }, - { - name: "homepage with missing query", - queryParams: "?missing=nonexistent", - expectedStatus: http.StatusOK, - expectedBody: []string{"Missing: nonexistent"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := httptest.NewRequest("GET", "/homepage/"+tt.queryParams, nil) - w := httptest.NewRecorder() - - handler.HomepageHandler(w, req) + req := httptest.NewRequest("GET", "/api/links", nil) + w := httptest.NewRecorder() + handler.ListLinks(w, req) - if w.Code != tt.expectedStatus { - t.Errorf("HomepageHandler() status = %v, want %v", w.Code, tt.expectedStatus) - } + if w.Code != http.StatusOK { + t.Fatalf("status = %v, want 200", w.Code) + } + if ct := w.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("content-type = %q, want application/json", ct) + } - body := w.Body.String() - for _, expected := range tt.expectedBody { - if !strings.Contains(body, expected) { - t.Errorf("HomepageHandler() body should contain %q, got %q", expected, body) - } - } - }) + var resp struct { + Keywords []domain.KeywordInfo `json:"keywords"` + RecentQueries []domain.PopularQuery `json:"recent_queries"` + BaseURL string `json:"base_url"` + } + if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if len(resp.Keywords) != 1 || resp.Keywords[0].Word != "docs" { + t.Errorf("unexpected keywords: %#v", resp.Keywords) + } + if len(resp.RecentQueries) != 1 { + t.Errorf("unexpected recent queries: %#v", resp.RecentQueries) + } + if resp.BaseURL != "http://localhost:8080" { + t.Errorf("base_url = %q", resp.BaseURL) } } -func TestHandler_SetupHandler(t *testing.T) { +func TestUpdateLinkLegacyForm(t *testing.T) { handler := setupTestHandler() - req := httptest.NewRequest("GET", "/setup/", nil) + form := strings.NewReader("word=leg&link=https://legacy.example.com") + req := httptest.NewRequest("POST", "/update/", form) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() - handler.SetupHandler(w, req) + handler.UpdateLinkLegacy(w, req) if w.Code != http.StatusOK { - t.Errorf("SetupHandler() status = %v, want %v", w.Code, http.StatusOK) + t.Fatalf("status = %v, want 200, body=%q", w.Code, w.Body.String()) } - - body := w.Body.String() - expectedContent := []string{ - "

Setup

", - "Base URL: http://localhost:8080", - } - - for _, expected := range expectedContent { - if !strings.Contains(body, expected) { - t.Errorf("SetupHandler() body should contain %q, got %q", expected, body) - } + if !strings.Contains(w.Body.String(), "Link added successfully") { + t.Errorf("unexpected body: %q", w.Body.String()) } } -func TestHandler_RegisterRoutes(t *testing.T) { +func TestRegisterRoutes(t *testing.T) { handler := setupTestHandler() router := mux.NewRouter() - - // This should not panic handler.RegisterRoutes(router) - // Test that routes are registered by making requests tests := []struct { method string path string + body string status int }{ - {"GET", "/", http.StatusFound}, // Root redirect - {"GET", "/homepage/", http.StatusOK}, // Homepage - {"GET", "/setup/", http.StatusOK}, // Setup - {"GET", "/query/docs", http.StatusFound}, // Query redirect - {"POST", "/update/", http.StatusBadRequest}, // Update (bad request due to no body) + {"GET", "/api/links", "", http.StatusOK}, + {"POST", "/api/links", `{"word":"x","link":"https://x.com"}`, http.StatusOK}, + {"GET", "/query/docs", "", http.StatusFound}, + {"POST", "/update/", "word=x&link=https://x.com", http.StatusOK}, } for _, tt := range tests { t.Run(tt.method+" "+tt.path, func(t *testing.T) { - var req *http.Request - if tt.method == "POST" { - req = httptest.NewRequest(tt.method, tt.path, strings.NewReader("")) - } else { - req = httptest.NewRequest(tt.method, tt.path, nil) + req := httptest.NewRequest(tt.method, tt.path, strings.NewReader(tt.body)) + if tt.method == "POST" && tt.path == "/api/links" { + req.Header.Set("Content-Type", "application/json") + } else if tt.method == "POST" { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } w := httptest.NewRecorder() - router.ServeHTTP(w, req) - if w.Code != tt.status { - t.Errorf("Route %s %s status = %v, want %v", tt.method, tt.path, w.Code, tt.status) + t.Errorf("status = %v, want %v, body=%q", w.Code, tt.status, w.Body.String()) } }) } } -func TestHandler_getUserID(t *testing.T) { - handler := setupTestHandler() - - req := httptest.NewRequest("GET", "/", nil) - userID := handler.getUserID(req) - - // Should return default user since we don't have OAuth2 implemented - if userID != "DefaultUser" { - t.Errorf("getUserID() = %v, want DefaultUser", userID) - } -} - -func TestHandler_MethodNotAllowed(t *testing.T) { +func TestGetUserID(t *testing.T) { handler := setupTestHandler() - router := mux.NewRouter() - handler.RegisterRoutes(router) - - // Test wrong method on homepage - req := httptest.NewRequest("POST", "/homepage/", nil) - w := httptest.NewRecorder() - - router.ServeHTTP(w, req) - - if w.Code != http.StatusMethodNotAllowed { - t.Errorf("Wrong method should return %v, got %v", http.StatusMethodNotAllowed, w.Code) + if uid := handler.getUserID(httptest.NewRequest("GET", "/", nil)); uid != "DefaultUser" { + t.Errorf("getUserID() = %v, want DefaultUser", uid) } } diff --git a/internal/service/document.go b/internal/service/document.go index 8db272a..3244da4 100644 --- a/internal/service/document.go +++ b/internal/service/document.go @@ -1,6 +1,7 @@ package service import ( + "bufio" "bytes" "context" "fmt" @@ -11,213 +12,117 @@ import ( "golinks/internal/logger" - "github.com/yuin/goldmark" - meta "github.com/yuin/goldmark-meta" - "github.com/yuin/goldmark/extension" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/renderer/html" + "gopkg.in/yaml.v2" ) -// DocumentService handles document rendering and management +// DocumentService stores and retrieves markdown/MDX documents on disk. +// +// Compared to the previous incarnation this service does NOT render the +// documents server-side: the Vite/React frontend handles MDX compilation at +// runtime via @mdx-js/mdx. The Go side is limited to file I/O plus lightweight +// frontmatter parsing so the client knows the title/description without +// having to parse the whole document twice. type DocumentService struct { docsPath string - markdown goldmark.Markdown logger *logger.Logger } -// DocumentInfo contains metadata about a document +// DocumentInfo contains metadata about a document. type DocumentInfo struct { Title string `json:"title"` - Description string `json:"description"` + Description string `json:"description,omitempty"` Type string `json:"type"` // "markdown" or "mdx" Path string `json:"path"` - Metadata map[string]interface{} `json:"metadata"` + Metadata map[string]interface{} `json:"metadata,omitempty"` } -// RenderResult contains the rendered document and metadata -type RenderResult struct { - HTML string `json:"html"` +// DocumentSource is the raw file contents plus its parsed metadata. +// The client receives this untouched and compiles MDX in the browser. +type DocumentSource struct { + Source string `json:"source"` + Type string `json:"type"` Metadata DocumentInfo `json:"metadata"` } -// NewDocumentService creates a new document service +// NewDocumentService creates a new document service rooted at docsPath. func NewDocumentService(docsPath string, log *logger.Logger) *DocumentService { log.Info("Initializing document service: %s", docsPath) - - // Configure Goldmark with extensions - md := goldmark.New( - goldmark.WithExtensions( - extension.GFM, // GitHub Flavored Markdown - extension.Table, // Tables - extension.Strikethrough, // ~~strikethrough~~ - extension.TaskList, // - [x] task lists - meta.Meta, // Frontmatter support - ), - goldmark.WithParserOptions( - parser.WithAutoHeadingID(), // Auto-generate heading IDs - ), - goldmark.WithRendererOptions( - html.WithHardWraps(), // Convert line breaks to
- html.WithXHTML(), // XHTML-compliant output - html.WithUnsafe(), // Allow raw HTML (be careful!) - ), - ) - - log.Info("Document service initialized successfully") - return &DocumentService{ docsPath: docsPath, - markdown: md, logger: log, } } -// GetDocument retrieves and renders a document by filename -func (s *DocumentService) GetDocument(ctx context.Context, filename string) (*RenderResult, error) { - // Sanitize filename to prevent directory traversal +// GetDocument reads a document by filename and returns its raw source plus metadata. +func (s *DocumentService) GetDocument(ctx context.Context, filename string) (*DocumentSource, error) { + _ = ctx filename = filepath.Base(filename) filePath := filepath.Join(s.docsPath, filename) - // Check if file exists if _, err := os.Stat(filePath); os.IsNotExist(err) { return nil, fmt.Errorf("document not found: %s", filename) } - // Read file content content, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("failed to read document: %w", err) } - // Determine document type docType := "markdown" if strings.HasSuffix(filename, ".mdx") { docType = "mdx" } - // Render based on type - switch docType { - case "mdx": - return s.renderMDX(ctx, filename, content) - default: - return s.renderMarkdown(ctx, filename, content) - } -} - -// renderMarkdown renders a markdown document -func (s *DocumentService) renderMarkdown(ctx context.Context, filename string, content []byte) (*RenderResult, error) { - var buf bytes.Buffer - context := parser.NewContext() - - // Parse and render - if err := s.markdown.Convert(content, &buf, parser.WithContext(context)); err != nil { - return nil, fmt.Errorf("failed to render markdown: %w", err) - } - - // Extract metadata from frontmatter - metaData := meta.Get(context) - if metaData == nil { - metaData = make(map[string]interface{}) - } - - // Create document info - docInfo := DocumentInfo{ - Title: getStringFromMeta(metaData, "title", strings.TrimSuffix(filename, filepath.Ext(filename))), - Description: getStringFromMeta(metaData, "description", ""), - Type: "markdown", - Path: filename, - Metadata: metaData, - } - - return &RenderResult{ - HTML: buf.String(), - Metadata: docInfo, - }, nil -} - -// renderMDX renders an MDX document (simplified for now) -func (s *DocumentService) renderMDX(ctx context.Context, filename string, content []byte) (*RenderResult, error) { - // For now, treat MDX as enhanced markdown - // In the future, we could add proper MDX compilation with esbuild - - // Remove JSX-like syntax for basic rendering - processedContent := s.preprocessMDX(content) - - var buf bytes.Buffer - context := parser.NewContext() - - // Parse and render as markdown - if err := s.markdown.Convert(processedContent, &buf, parser.WithContext(context)); err != nil { - return nil, fmt.Errorf("failed to render MDX: %w", err) - } - - // Extract metadata - metaData := meta.Get(context) - if metaData == nil { - metaData = make(map[string]interface{}) - } - - // Create document info - docInfo := DocumentInfo{ + metaData, body := splitFrontmatter(content) + info := DocumentInfo{ Title: getStringFromMeta(metaData, "title", strings.TrimSuffix(filename, filepath.Ext(filename))), Description: getStringFromMeta(metaData, "description", ""), - Type: "mdx", + Type: docType, Path: filename, Metadata: metaData, } - return &RenderResult{ - HTML: buf.String(), - Metadata: docInfo, + // Hand back the full source (including frontmatter) so the client can + // decide whether to strip it itself. remark/MDX pipelines tolerate both. + _ = body + return &DocumentSource{ + Source: string(content), + Type: docType, + Metadata: info, }, nil } -// preprocessMDX does basic preprocessing of MDX content -func (s *DocumentService) preprocessMDX(content []byte) []byte { - contentStr := string(content) - - // Convert simple JSX-like elements to HTML - // This is a very basic implementation - in production, use proper MDX compiler - contentStr = strings.ReplaceAll(contentStr, `
`, `
`) - contentStr = strings.ReplaceAll(contentStr, `className=`, `class=`) - - return []byte(contentStr) -} - -// SaveDocument saves a document to the filesystem +// SaveDocument writes a document to disk, creating or overwriting. func (s *DocumentService) SaveDocument(ctx context.Context, filename string, content io.Reader) error { - // Sanitize filename + _ = ctx filename = filepath.Base(filename) filePath := filepath.Join(s.docsPath, filename) - // Create file file, err := os.Create(filePath) if err != nil { return fmt.Errorf("failed to create document file: %w", err) } defer file.Close() - // Copy content if _, err := io.Copy(file, content); err != nil { return fmt.Errorf("failed to write document content: %w", err) } - return nil } -// ListDocuments returns a list of available documents +// ListDocuments returns metadata for every .md / .mdx file in the docs folder. func (s *DocumentService) ListDocuments(ctx context.Context) ([]DocumentInfo, error) { + _ = ctx entries, err := os.ReadDir(s.docsPath) if err != nil { return nil, fmt.Errorf("failed to read docs directory: %w", err) } - var docs []DocumentInfo + docs := make([]DocumentInfo, 0, len(entries)) for _, entry := range entries { if entry.IsDir() { continue } - name := entry.Name() if !strings.HasSuffix(name, ".md") && !strings.HasSuffix(name, ".mdx") { continue @@ -228,30 +133,85 @@ func (s *DocumentService) ListDocuments(ctx context.Context) ([]DocumentInfo, er docType = "mdx" } + // Cheap frontmatter peek for the list view: read only if the first + // line is `---`, otherwise fall back to the filename. + title := strings.TrimSuffix(name, filepath.Ext(name)) + description := "" + if meta := peekFrontmatter(filepath.Join(s.docsPath, name)); meta != nil { + title = getStringFromMeta(meta, "title", title) + description = getStringFromMeta(meta, "description", "") + } + docs = append(docs, DocumentInfo{ - Title: strings.TrimSuffix(name, filepath.Ext(name)), - Type: docType, - Path: name, + Title: title, + Description: description, + Type: docType, + Path: name, }) } return docs, nil } -// DeleteDocument removes a document from the filesystem +// DeleteDocument removes a document from disk. func (s *DocumentService) DeleteDocument(ctx context.Context, filename string) error { - // Sanitize filename + _ = ctx filename = filepath.Base(filename) filePath := filepath.Join(s.docsPath, filename) - if err := os.Remove(filePath); err != nil { return fmt.Errorf("failed to delete document: %w", err) } - return nil } -// Helper function to safely get string values from metadata +// splitFrontmatter separates a leading YAML `---` block from the body. +// Returns (nil, original) if no frontmatter is present. +func splitFrontmatter(content []byte) (map[string]interface{}, []byte) { + const delim = "---" + scanner := bufio.NewScanner(bytes.NewReader(content)) + scanner.Buffer(make([]byte, 1<<16), 1<<20) + + if !scanner.Scan() || strings.TrimSpace(scanner.Text()) != delim { + return nil, content + } + + var yamlBuf bytes.Buffer + closed := false + for scanner.Scan() { + line := scanner.Text() + if strings.TrimSpace(line) == delim { + closed = true + break + } + yamlBuf.WriteString(line) + yamlBuf.WriteByte('\n') + } + if !closed { + return nil, content + } + + var meta map[string]interface{} + if err := yaml.Unmarshal(yamlBuf.Bytes(), &meta); err != nil { + return nil, content + } + + var bodyBuf bytes.Buffer + for scanner.Scan() { + bodyBuf.WriteString(scanner.Text()) + bodyBuf.WriteByte('\n') + } + return meta, bodyBuf.Bytes() +} + +func peekFrontmatter(path string) map[string]interface{} { + data, err := os.ReadFile(path) + if err != nil { + return nil + } + meta, _ := splitFrontmatter(data) + return meta +} + func getStringFromMeta(meta map[string]interface{}, key, defaultValue string) string { if value, ok := meta[key]; ok { if str, ok := value.(string); ok { diff --git a/web/frontend/components.json b/web/frontend/components.json new file mode 100644 index 0000000..782a943 --- /dev/null +++ b/web/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/web/frontend/dist/index.html b/web/frontend/dist/index.html new file mode 100644 index 0000000..884cc99 --- /dev/null +++ b/web/frontend/dist/index.html @@ -0,0 +1,14 @@ + + + + + + + golinks + + + + +
+ + diff --git a/web/frontend/embed.go b/web/frontend/embed.go new file mode 100644 index 0000000..a2831dd --- /dev/null +++ b/web/frontend/embed.go @@ -0,0 +1,87 @@ +// Package frontend embeds the built Vite/React SPA and serves it. +// +// The go:embed directive below pulls every file under web/frontend/dist/ into +// the binary at compile time. For SPA routing to work we serve concrete files +// when they exist and fall back to index.html for every other GET — so a hard +// refresh on /docs/sample.md still lands on the React router. +// +// If the dist directory is empty (e.g. `go build` was run without `npm run +// build` first), Handler degrades to a helpful 503 rather than a crash so the +// caller can diagnose the missing build step. +package frontend + +import ( + "embed" + "fmt" + "io" + "io/fs" + "net/http" + "strings" +) + +//go:embed all:dist +var distFS embed.FS + +// Handler returns an http.Handler that serves the embedded SPA. +// Requests starting with any of `reservedPrefixes` are rejected with 404 so +// they can be handled by other routes; in practice we only call Handler on +// the catch-all route, so the reservation is belt-and-braces. +func Handler(reservedPrefixes ...string) http.Handler { + sub, err := fs.Sub(distFS, "dist") + if err != nil { + return brokenHandler(fmt.Errorf("failed to locate embedded frontend: %w", err)) + } + + if _, err := fs.Stat(sub, "index.html"); err != nil { + return brokenHandler(fmt.Errorf( + "embedded frontend is empty — run `npm run build` in web/frontend/ before `go build`: %w", + err, + )) + } + + fileServer := http.FileServer(http.FS(sub)) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + reqPath := strings.TrimPrefix(r.URL.Path, "/") + for _, p := range reservedPrefixes { + if strings.HasPrefix(reqPath, strings.TrimPrefix(p, "/")) { + http.NotFound(w, r) + return + } + } + + if reqPath != "" { + if f, err := sub.Open(reqPath); err == nil { + f.Close() + fileServer.ServeHTTP(w, r) + return + } + } + + serveIndex(w, sub) + }) +} + +func serveIndex(w http.ResponseWriter, sub fs.FS) { + f, err := sub.Open("index.html") + if err != nil { + http.Error(w, "index.html not found", http.StatusInternalServerError) + return + } + defer f.Close() + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache") + _, _ = io.Copy(w, f) +} + +func brokenHandler(err error) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + }) +} diff --git a/web/frontend/index.html b/web/frontend/index.html new file mode 100644 index 0000000..065dabf --- /dev/null +++ b/web/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + golinks + + +
+ + + diff --git a/web/frontend/package-lock.json b/web/frontend/package-lock.json new file mode 100644 index 0000000..ffc1113 --- /dev/null +++ b/web/frontend/package-lock.json @@ -0,0 +1,5683 @@ +{ + "name": "golinks-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "golinks-frontend", + "version": "0.1.0", + "dependencies": { + "@fontsource/inter": "^5.2.5", + "@fontsource/jetbrains-mono": "^5.2.5", + "@hookform/resolvers": "^4.1.3", + "@mdx-js/mdx": "^3.1.0", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", + "@tailwindcss/typography": "^0.5.16", + "@tanstack/react-query": "^5.66.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "highlight.js": "^11.11.1", + "lucide-react": "^0.475.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.54.2", + "react-router-dom": "^6.28.1", + "rehype-highlight": "^7.0.2", + "remark-gfm": "^4.0.1", + "sonner": "^2.0.1", + "tailwind-merge": "^3.0.2", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/mdx": "^2.0.13", + "@types/node": "^22.19.17", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.2", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vite": "^6.1.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", + "integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@hookform/resolvers": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz", + "integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.99.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.99.2.tgz", + "integrity": "sha512-1HunU0bXVsR1ZJMZbcOPE6VtaBJxsW809RE9xPe4Gz7MlB0GWwQvuTPhMoEmQ/hIzFKJ/DWAuttIe7BOaWx0tA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.99.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.99.2.tgz", + "integrity": "sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.99.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", + "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.343", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz", + "integrity": "sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.475.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz", + "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.73.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.73.1.tgz", + "integrity": "sha512-VAfVYOPcx3piiEVQy95vyFmBwbVUsP/AUIN+mpFG8h11yshDd444nn0VyfaGWSRnhOLVgiDu7HIuBtAIzxn9dA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-highlight": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", + "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-text": "^4.0.0", + "lowlight": "^3.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/web/frontend/package.json b/web/frontend/package.json new file mode 100644 index 0000000..c580306 --- /dev/null +++ b/web/frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "golinks-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "typecheck": "tsc -b --noEmit" + }, + "dependencies": { + "@fontsource/inter": "^5.2.5", + "@fontsource/jetbrains-mono": "^5.2.5", + "@hookform/resolvers": "^4.1.3", + "@mdx-js/mdx": "^3.1.0", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", + "@tailwindcss/typography": "^0.5.16", + "@tanstack/react-query": "^5.66.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "highlight.js": "^11.11.1", + "lucide-react": "^0.475.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.54.2", + "react-router-dom": "^6.28.1", + "rehype-highlight": "^7.0.2", + "remark-gfm": "^4.0.1", + "sonner": "^2.0.1", + "tailwind-merge": "^3.0.2", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/mdx": "^2.0.13", + "@types/node": "^22.19.17", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.2", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vite": "^6.1.0" + } +} diff --git a/web/frontend/postcss.config.js b/web/frontend/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/web/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/web/static/favicon.ico b/web/frontend/public/favicon.ico similarity index 100% rename from web/static/favicon.ico rename to web/frontend/public/favicon.ico diff --git a/web/frontend/src/App.tsx b/web/frontend/src/App.tsx new file mode 100644 index 0000000..a85c088 --- /dev/null +++ b/web/frontend/src/App.tsx @@ -0,0 +1,27 @@ +import { Route, Routes } from "react-router-dom"; + +import { Navbar } from "@/components/Navbar"; +import { HomePage } from "@/pages/Home"; +import { SetupPage } from "@/pages/Setup"; +import { DocsListPage } from "@/pages/DocsList"; +import { DocPage } from "@/pages/Doc"; +import { NotFoundPage } from "@/pages/NotFound"; + +export default function App() { + return ( +
+ +
+ + } /> + {/* Legacy route from the template era. */} + } /> + } /> + } /> + } /> + } /> + +
+
+ ); +} diff --git a/web/frontend/src/components/DocUploader.tsx b/web/frontend/src/components/DocUploader.tsx new file mode 100644 index 0000000..7299953 --- /dev/null +++ b/web/frontend/src/components/DocUploader.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { Upload } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { api, ApiError } from "@/lib/api"; + +export function DocUploader() { + const inputRef = React.useRef(null); + const qc = useQueryClient(); + + const mutation = useMutation({ + mutationFn: api.uploadDoc, + onSuccess: (data) => { + toast.success(`Uploaded ${data.filename}`); + qc.invalidateQueries({ queryKey: ["docs"] }); + }, + onError: (err: unknown) => { + const msg = err instanceof ApiError ? err.message : "Upload failed"; + toast.error(msg); + }, + }); + + return ( + <> + { + const file = e.target.files?.[0]; + if (file) mutation.mutate(file); + e.target.value = ""; + }} + /> + + + ); +} diff --git a/web/frontend/src/components/KeywordTable.tsx b/web/frontend/src/components/KeywordTable.tsx new file mode 100644 index 0000000..b005682 --- /dev/null +++ b/web/frontend/src/components/KeywordTable.tsx @@ -0,0 +1,65 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import type { KeywordInfo } from "@/lib/api"; + +type Props = { keywords: KeywordInfo[] }; + +function formatDate(iso: string) { + if (!iso) return ""; + try { + return new Date(iso).toISOString().slice(0, 10); + } catch { + return iso; + } +} + +export function KeywordTable({ keywords }: Props) { + if (keywords.length === 0) { + return ( +

+ No keywords yet — add your first one above. +

+ ); + } + + return ( + + + + Keyword + URL + Created + + + + {keywords.map((k) => ( + + + + {k.word} + + + + {k.link.startsWith("http://") || k.link.startsWith("https://") ? ( + + {k.link} + + ) : ( + k.link + )} + + + {formatDate(k.created_at)} + + + ))} + +
+ ); +} diff --git a/web/frontend/src/components/LinkForm.tsx b/web/frontend/src/components/LinkForm.tsx new file mode 100644 index 0000000..b1b061a --- /dev/null +++ b/web/frontend/src/components/LinkForm.tsx @@ -0,0 +1,95 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { api, ApiError } from "@/lib/api"; + +const schema = z.object({ + word: z + .string() + .trim() + .min(1, "Please enter a keyword.") + .refine((v) => !v.endsWith("/"), "Keywords ending in '/' are not supported."), + link: z.string().trim().min(1, "Please enter a URL."), +}); + +type FormValues = z.infer; + +export function LinkForm() { + const qc = useQueryClient(); + const { + register, + handleSubmit, + reset, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { word: "", link: "" }, + }); + + const mutation = useMutation({ + mutationFn: api.createLink, + onSuccess: (_, vars) => { + toast.success(`Added keyword "${vars.word}"`); + reset(); + qc.invalidateQueries({ queryKey: ["links"] }); + }, + onError: (err: unknown) => { + const msg = err instanceof ApiError ? err.message : "Failed to add link"; + toast.error(msg); + }, + }); + + return ( +
mutation.mutate(values))} + className="space-y-3" + noValidate + > +
+
+ + +
+
+ + +
+ +
+ {(errors.word || errors.link) && ( +

+ {errors.word?.message ?? errors.link?.message} +

+ )} +
+ ); +} diff --git a/web/frontend/src/components/MDXRenderer.tsx b/web/frontend/src/components/MDXRenderer.tsx new file mode 100644 index 0000000..562cc71 --- /dev/null +++ b/web/frontend/src/components/MDXRenderer.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; + +import { compileMDX } from "@/lib/mdx"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; + +type Props = { source: string }; + +export function MDXRenderer({ source }: Props) { + const [Content, setContent] = React.useState(null); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + let cancelled = false; + setContent(null); + setError(null); + + compileMDX(source) + .then((mod) => { + if (cancelled) return; + setContent(() => mod.default); + }) + .catch((err: unknown) => { + if (cancelled) return; + setError(err instanceof Error ? err.message : String(err)); + }); + + return () => { + cancelled = true; + }; + }, [source]); + + if (error) { + return ( + + Failed to render document + +
{error}
+
+
+ ); + } + + if (!Content) { + return ( +
+ + + + +
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/web/frontend/src/components/Navbar.tsx b/web/frontend/src/components/Navbar.tsx new file mode 100644 index 0000000..8434d62 --- /dev/null +++ b/web/frontend/src/components/Navbar.tsx @@ -0,0 +1,40 @@ +import { NavLink } from "react-router-dom"; + +import { cn } from "@/lib/utils"; + +const links = [ + { to: "/", label: "Home", end: true }, + { to: "/setup", label: "Setup" }, + { to: "/docs", label: "Docs" }, +]; + +export function Navbar() { + return ( + + ); +} diff --git a/web/frontend/src/components/RecentQueries.tsx b/web/frontend/src/components/RecentQueries.tsx new file mode 100644 index 0000000..deca34c --- /dev/null +++ b/web/frontend/src/components/RecentQueries.tsx @@ -0,0 +1,24 @@ +import type { PopularQuery } from "@/lib/api"; + +type Props = { queries: PopularQuery[] }; + +export function RecentQueries({ queries }: Props) { + if (queries.length === 0) { + return null; + } + + return ( +
+

Recent queries

+
    + {queries.map((q) => ( +
  • + ×{q.count} + {q.word} + → {q.link} +
  • + ))} +
+
+ ); +} diff --git a/web/frontend/src/components/ui/alert.tsx b/web/frontend/src/components/ui/alert.tsx new file mode 100644 index 0000000..f349aaa --- /dev/null +++ b/web/frontend/src/components/ui/alert.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const alertVariants = cva( + "relative w-full rounded-md border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "border-destructive/40 bg-destructive/10 text-destructive [&>svg]:text-destructive", + success: + "border-success/40 bg-success/10 text-success [&>svg]:text-success", + info: "border-accent/40 bg-accent/10 text-accent [&>svg]:text-accent", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/web/frontend/src/components/ui/button.tsx b/web/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..23da2da --- /dev/null +++ b/web/frontend/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-secondary hover:text-secondary-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-secondary hover:text-secondary-foreground", + link: "text-accent underline-offset-4 hover:underline hover:text-primary", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-6", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/web/frontend/src/components/ui/card.tsx b/web/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..4123a5b --- /dev/null +++ b/web/frontend/src/components/ui/card.tsx @@ -0,0 +1,62 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardFooter.displayName = "CardFooter"; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/web/frontend/src/components/ui/dialog.tsx b/web/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..58acf02 --- /dev/null +++ b/web/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,99 @@ +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; +const DialogTrigger = DialogPrimitive.Trigger; +const DialogPortal = DialogPrimitive.Portal; +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/web/frontend/src/components/ui/input.tsx b/web/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..36c4a33 --- /dev/null +++ b/web/frontend/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/web/frontend/src/components/ui/label.tsx b/web/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..530165c --- /dev/null +++ b/web/frontend/src/components/ui/label.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/web/frontend/src/components/ui/skeleton.tsx b/web/frontend/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..de994ae --- /dev/null +++ b/web/frontend/src/components/ui/skeleton.tsx @@ -0,0 +1,7 @@ +import { cn } from "@/lib/utils"; + +function Skeleton({ className, ...props }: React.HTMLAttributes) { + return
; +} + +export { Skeleton }; diff --git a/web/frontend/src/components/ui/sonner.tsx b/web/frontend/src/components/ui/sonner.tsx new file mode 100644 index 0000000..75b9e41 --- /dev/null +++ b/web/frontend/src/components/ui/sonner.tsx @@ -0,0 +1,22 @@ +import { Toaster as Sonner, type ToasterProps } from "sonner"; + +const Toaster = ({ ...props }: ToasterProps) => { + return ( + + ); +}; + +export { Toaster }; diff --git a/web/frontend/src/components/ui/table.tsx b/web/frontend/src/components/ui/table.tsx new file mode 100644 index 0000000..d6f7c46 --- /dev/null +++ b/web/frontend/src/components/ui/table.tsx @@ -0,0 +1,114 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Table = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ), +); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className, + )} + {...props} + /> +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = "TableCaption"; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/web/frontend/src/components/ui/tabs.tsx b/web/frontend/src/components/ui/tabs.tsx new file mode 100644 index 0000000..5e356fa --- /dev/null +++ b/web/frontend/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/web/frontend/src/index.css b/web/frontend/src/index.css new file mode 100644 index 0000000..a5ae1cd --- /dev/null +++ b/web/frontend/src/index.css @@ -0,0 +1,90 @@ +@import "@fontsource/inter/300.css"; +@import "@fontsource/inter/400.css"; +@import "@fontsource/inter/500.css"; +@import "@fontsource/inter/600.css"; +@import "@fontsource/jetbrains-mono/400.css"; +@import "@fontsource/jetbrains-mono/500.css"; +@import "highlight.js/styles/github.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + /* Rams-inspired palette ported to shadcn HSL tokens. + Originals kept as reference in the comments. */ + + --background: 0 0% 98%; /* rams-off-white #FAFAFA */ + --foreground: 0 0% 20%; /* rams-charcoal #333333 */ + + --card: 0 0% 100%; /* rams-white */ + --card-foreground: 0 0% 10%; /* rams-black */ + + --popover: 0 0% 100%; + --popover-foreground: 0 0% 10%; + + --primary: 14 100% 60%; /* rams-orange #FF6B35 — Braun accent */ + --primary-foreground: 0 0% 100%; + + --secondary: 0 0% 90%; /* rams-light-grey */ + --secondary-foreground: 0 0% 20%; + + --muted: 0 0% 96%; + --muted-foreground: 0 0% 40%; /* rams-dark-grey */ + + --accent: 221 83% 53%; /* rams-blue #2563EB — for links/focus */ + --accent-foreground: 0 0% 100%; + + --destructive: 0 72% 51%; /* rams-red #DC2626 */ + --destructive-foreground: 0 0% 100%; + + --success: 162 94% 30%; /* rams-green #059669 */ + --success-foreground: 0 0% 100%; + + --border: 0 0% 90%; /* rams-light-grey */ + --input: 0 0% 80%; /* rams-medium-grey */ + --ring: 14 100% 60%; /* rams-orange */ + + --radius: 0.25rem; /* 4px — rams-radius-md */ + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground font-sans antialiased; + font-feature-settings: "rlig" 1, "calt" 1; + } + + /* Rams-flavoured prose overrides for MDX-rendered docs. + We keep the accent orange for link hover and use the mono font for code. */ + .prose :where(a):not(:where([class~="not-prose"] *)) { + @apply text-accent no-underline font-medium; + } + .prose :where(a):not(:where([class~="not-prose"] *)):hover { + @apply text-primary underline; + } + .prose :where(code):not(:where([class~="not-prose"] *)) { + @apply font-mono text-sm bg-secondary border border-input rounded-sm px-1 py-0.5; + } + .prose :where(code):not(:where([class~="not-prose"] *))::before, + .prose :where(code):not(:where([class~="not-prose"] *))::after { + content: none; + } + .prose :where(pre):not(:where([class~="not-prose"] *)) { + @apply font-mono bg-card border border-border rounded-md; + } + .prose :where(pre code):not(:where([class~="not-prose"] *)) { + @apply bg-transparent border-0 p-0 text-sm; + } + .prose :where(h1, h2, h3, h4):not(:where([class~="not-prose"] *)) { + @apply font-medium tracking-tight text-foreground; + } + .prose :where(h1):not(:where([class~="not-prose"] *)) { + @apply font-light; + } +} diff --git a/web/frontend/src/lib/api.ts b/web/frontend/src/lib/api.ts new file mode 100644 index 0000000..584a3aa --- /dev/null +++ b/web/frontend/src/lib/api.ts @@ -0,0 +1,96 @@ +export type KeywordInfo = { + word: string; + link: string; + created_at: string; +}; + +export type PopularQuery = { + count: number; + word: string; + link: string; +}; + +export type LinksResponse = { + keywords: KeywordInfo[]; + recent_queries: PopularQuery[]; + base_url: string; +}; + +export type DocumentInfo = { + title: string; + description?: string; + type: "markdown" | "mdx"; + path: string; +}; + +export type DocumentSource = { + source: string; + type: "markdown" | "mdx"; + metadata: DocumentInfo; +}; + +export type LinkInput = { + word: string; + link: string; +}; + +class ApiError extends Error { + constructor( + message: string, + public status: number, + ) { + super(message); + this.name = "ApiError"; + } +} + +async function request(path: string, init?: RequestInit): Promise { + const res = await fetch(path, { + headers: { + "Content-Type": "application/json", + ...(init?.headers ?? {}), + }, + ...init, + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new ApiError(text || res.statusText, res.status); + } + + // DELETE can return empty + const ct = res.headers.get("content-type") ?? ""; + if (!ct.includes("application/json")) { + return undefined as T; + } + return (await res.json()) as T; +} + +export const api = { + listLinks: () => request("/api/links"), + createLink: (input: LinkInput) => + request<{ success: true }>("/api/links", { + method: "POST", + body: JSON.stringify(input), + }), + listDocs: () => request<{ documents: DocumentInfo[] }>("/api/docs"), + getDoc: (filename: string) => + request(`/api/docs/${encodeURIComponent(filename)}`), + uploadDoc: async (file: File) => { + const form = new FormData(); + form.append("file", file); + const res = await fetch("/api/docs", { method: "POST", body: form }); + if (!res.ok) throw new ApiError(await res.text(), res.status); + return (await res.json()) as { + success: true; + filename: string; + url: string; + }; + }, + deleteDoc: (filename: string) => + request<{ success: true }>(`/api/docs/${encodeURIComponent(filename)}`, { + method: "DELETE", + }), +}; + +export { ApiError }; diff --git a/web/frontend/src/lib/mdx.tsx b/web/frontend/src/lib/mdx.tsx new file mode 100644 index 0000000..d2f944b --- /dev/null +++ b/web/frontend/src/lib/mdx.tsx @@ -0,0 +1,55 @@ +import * as React from "react"; +import * as runtime from "react/jsx-runtime"; +import { evaluate } from "@mdx-js/mdx"; + +type MDXModule = { default: React.ComponentType }; +import remarkGfm from "remark-gfm"; +import rehypeHighlight from "rehype-highlight"; + +import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { + Table, + TableHeader, + TableBody, + TableRow, + TableHead, + TableCell, +} from "@/components/ui/table"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; + +// Components exposed to MDX. Authors can write ... +// directly inside their .mdx files and it will render as a shadcn Alert. +const mdxComponents = { + Alert, + AlertTitle, + AlertDescription, + Button, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Tabs, + TabsList, + TabsTrigger, + TabsContent, + // Wire shadcn Table primitives to plain markdown tables so GFM tables pick up the styling. + table: (props: React.HTMLAttributes) => , + thead: (props: React.HTMLAttributes) => , + tbody: (props: React.HTMLAttributes) => , + tr: (props: React.HTMLAttributes) => , + th: (props: React.ThHTMLAttributes) => , + td: (props: React.TdHTMLAttributes) => , +}; + +export async function compileMDX(source: string): Promise { + return evaluate(source, { + ...(runtime as typeof runtime & { Fragment: React.ComponentType }), + baseUrl: import.meta.url, + remarkPlugins: [remarkGfm], + rehypePlugins: [rehypeHighlight], + useMDXComponents: () => mdxComponents, + }); +} diff --git a/web/frontend/src/lib/utils.ts b/web/frontend/src/lib/utils.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/web/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/web/frontend/src/main.tsx b/web/frontend/src/main.tsx new file mode 100644 index 0000000..516dca1 --- /dev/null +++ b/web/frontend/src/main.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "@/components/ui/sonner"; + +import App from "./App"; +import "./index.css"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { staleTime: 30_000, refetchOnWindowFocus: false }, + }, +}); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + + + , +); diff --git a/web/frontend/src/pages/Doc.tsx b/web/frontend/src/pages/Doc.tsx new file mode 100644 index 0000000..99792fd --- /dev/null +++ b/web/frontend/src/pages/Doc.tsx @@ -0,0 +1,61 @@ +import { Link, useParams } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { ArrowLeft } from "lucide-react"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { MDXRenderer } from "@/components/MDXRenderer"; +import { api } from "@/lib/api"; + +export function DocPage() { + const { filename = "" } = useParams(); + const { data, isLoading, error } = useQuery({ + queryKey: ["doc", filename], + queryFn: () => api.getDoc(filename), + enabled: Boolean(filename), + }); + + return ( +
+ + + {error && ( + + Document not found + + {error instanceof Error ? error.message : String(error)} + + + )} + + {isLoading && ( +
+ + + +
+ )} + + {data && ( + <> +
+
+ {data.metadata.type} +
+

{data.metadata.title}

+ {data.metadata.description && ( +

{data.metadata.description}

+ )} +
+ + + )} +
+ ); +} diff --git a/web/frontend/src/pages/DocsList.tsx b/web/frontend/src/pages/DocsList.tsx new file mode 100644 index 0000000..39bc315 --- /dev/null +++ b/web/frontend/src/pages/DocsList.tsx @@ -0,0 +1,104 @@ +import { Link } from "react-router-dom"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { FileText, Trash2 } from "lucide-react"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { DocUploader } from "@/components/DocUploader"; +import { api, ApiError } from "@/lib/api"; + +export function DocsListPage() { + const qc = useQueryClient(); + const { data, isLoading, error } = useQuery({ + queryKey: ["docs"], + queryFn: api.listDocs, + }); + + const deleteMutation = useMutation({ + mutationFn: api.deleteDoc, + onSuccess: () => { + toast.success("Document deleted"); + qc.invalidateQueries({ queryKey: ["docs"] }); + }, + onError: (err: unknown) => { + toast.error(err instanceof ApiError ? err.message : "Delete failed"); + }, + }); + + return ( +
+
+
+

Docs

+

+ Markdown and MDX documents rendered with shadcn primitives. +

+
+ +
+ + {error && ( + + Couldn't load documents + + {error instanceof Error ? error.message : String(error)} + + + )} + + {isLoading ? ( +
+ + +
+ ) : (data?.documents?.length ?? 0) === 0 ? ( + + + + No documents yet. Upload a .md or{" "} + .mdx file to get started. + + + ) : ( +
    + {data!.documents.map((doc) => ( +
  • + + + + +
    + {doc.title} + {doc.path} +
    + + {doc.type} + + + +
    +
    +
  • + ))} +
+ )} +
+ ); +} diff --git a/web/frontend/src/pages/Home.tsx b/web/frontend/src/pages/Home.tsx new file mode 100644 index 0000000..d71a71e --- /dev/null +++ b/web/frontend/src/pages/Home.tsx @@ -0,0 +1,93 @@ +import { useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { toast } from "sonner"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Skeleton } from "@/components/ui/skeleton"; +import { KeywordTable } from "@/components/KeywordTable"; +import { LinkForm } from "@/components/LinkForm"; +import { RecentQueries } from "@/components/RecentQueries"; +import { api } from "@/lib/api"; + +export function HomePage() { + const [params, setParams] = useSearchParams(); + const missing = params.get("missing"); + + const { data, isLoading, error } = useQuery({ + queryKey: ["links"], + queryFn: api.listLinks, + }); + + // One-shot toast when the golink resolver bounces the user back here. + useEffect(() => { + if (missing) { + toast.error(`No shortcut found for "${missing}"`); + params.delete("missing"); + setParams(params, { replace: true }); + } + }, [missing, params, setParams]); + + return ( +
+
+

+ golinks +

+

+ Memorable shortcuts for long URLs. Type go <keyword> in your browser address bar once + you've finished the setup. +

+ {data?.base_url && ( +

+ Your search engine should read:{" "} + + {data.base_url}/query/%s + +

+ )} +
+ +
+

+ + Add a new keyword +

+ +

+ Use {"{*}"} in the URL + for a variable. Example: https://github.com/search?q={"{*}"}, then go github claude. +

+
+ + {error && ( + + Couldn't load keywords + {error instanceof Error ? error.message : String(error)} + + )} + +
+

+ + All keywords +

+ {isLoading ? ( +
+ + + +
+ ) : ( + + )} +
+ + {data?.recent_queries && data.recent_queries.length > 0 && ( +
+ +
+ )} +
+ ); +} diff --git a/web/frontend/src/pages/NotFound.tsx b/web/frontend/src/pages/NotFound.tsx new file mode 100644 index 0000000..d1c7e9e --- /dev/null +++ b/web/frontend/src/pages/NotFound.tsx @@ -0,0 +1,49 @@ +import { Link, useLocation } from "react-router-dom"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; + +export function NotFoundPage() { + const { pathname } = useLocation(); + + return ( +
+
+
404
+

Page not found

+

+ Couldn't find{" "} + + {pathname} + + . +

+
+ + +
+ + +

If you were trying to use a golink:

+
    +
  • + Check the keyword is typed correctly — try the{" "} + + keyword list + + . +
  • +
  • + Add it on the home page if it doesn't exist yet. +
  • +
+
+
+
+
+ ); +} diff --git a/web/frontend/src/pages/Setup.tsx b/web/frontend/src/pages/Setup.tsx new file mode 100644 index 0000000..c3009da --- /dev/null +++ b/web/frontend/src/pages/Setup.tsx @@ -0,0 +1,164 @@ +import { useQuery } from "@tanstack/react-query"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { api } from "@/lib/api"; + +function BaseUrlCode() { + const { data } = useQuery({ queryKey: ["links"], queryFn: api.listLinks }); + return ( + + {(data?.base_url ?? "") + "/query/%s"} + + ); +} + +export function SetupPage() { + return ( +
+
+

Browser setup

+

+ Configure your browser to use GoLinks as a search engine so typing{" "} + go <keyword> in the + address bar redirects to the matching URL. +

+
+ + + + Chrome / Edge + Firefox + Safari + + +
    +
  1. Open Chrome/Edge settings.
  2. +
  3. + Go to Search engine →{" "} + Manage search engines and site search. +
  4. +
  5. + Click Add next to "Site search". +
  6. +
  7. + Fill in: +
      +
    • + Name: GoLinks +
    • +
    • + Shortcut:{" "} + go +
    • +
    • + URL: +
    • +
    +
  8. +
  9. + Click Add. +
  10. +
+
+ +
    +
  1. Open Bookmarks → Manage Bookmarks.
  2. +
  3. + Create a new bookmark with: +
      +
    • + Name: GoLinks +
    • +
    • + Location: +
    • +
    • + Keyword:{" "} + go +
    • +
    +
  4. +
  5. Save.
  6. +
+
+ +

+ Safari doesn't support custom search engines natively. Options: +

+
    +
  • + Bookmark / for quick + access. +
  • +
  • + Use an extension like Keyword Search. +
  • +
+
+
+ + + Pro tip + + Once configured, just type go keyword in your address bar. + + + +
+

+ + Usage examples +

+
+ + + Command + Description + Notes + + + + + + go docs + + Navigate to a fixed URL. + + If docs points to{" "} + https://docs.example.com. + + + + + go jira 123 + + Navigate with a parameter. + + Works if jira contains{" "} + {"{*}"}. + + + + + go github myrepo + + Dynamic search. + + Same {"{*}"} substitution. + + + +
+ + + ); +} diff --git a/web/frontend/tailwind.config.ts b/web/frontend/tailwind.config.ts new file mode 100644 index 0000000..0b6e761 --- /dev/null +++ b/web/frontend/tailwind.config.ts @@ -0,0 +1,89 @@ +import type { Config } from "tailwindcss"; +import typography from "@tailwindcss/typography"; +import animate from "tailwindcss-animate"; + +const config: Config = { + darkMode: ["class"], + content: ["./index.html", "./src/**/*.{ts,tsx}"], + theme: { + container: { + center: true, + padding: "1rem", + screens: { + "2xl": "1200px", + }, + }, + extend: { + fontFamily: { + sans: ["Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "sans-serif"], + mono: ["JetBrains Mono", "SF Mono", "Monaco", "Cascadia Code", "monospace"], + }, + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + success: { + DEFAULT: "hsl(var(--success))", + foreground: "hsl(var(--success-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "fade-in": { + from: { opacity: "0", transform: "translateY(6px)" }, + to: { opacity: "1", transform: "translateY(0)" }, + }, + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "fade-in": "fade-in 0.3s ease-out", + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [typography, animate], +}; + +export default config; diff --git a/web/frontend/tsconfig.app.json b/web/frontend/tsconfig.app.json new file mode 100644 index 0000000..b6d27ad --- /dev/null +++ b/web/frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/web/frontend/tsconfig.app.tsbuildinfo b/web/frontend/tsconfig.app.tsbuildinfo new file mode 100644 index 0000000..a9ab351 --- /dev/null +++ b/web/frontend/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/main.tsx","./src/components/docuploader.tsx","./src/components/keywordtable.tsx","./src/components/linkform.tsx","./src/components/mdxrenderer.tsx","./src/components/navbar.tsx","./src/components/recentqueries.tsx","./src/components/ui/alert.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/lib/api.ts","./src/lib/mdx.tsx","./src/lib/utils.ts","./src/pages/doc.tsx","./src/pages/docslist.tsx","./src/pages/home.tsx","./src/pages/notfound.tsx","./src/pages/setup.tsx"],"version":"5.9.3"} \ No newline at end of file diff --git a/web/frontend/tsconfig.json b/web/frontend/tsconfig.json new file mode 100644 index 0000000..fec8c8e --- /dev/null +++ b/web/frontend/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/web/frontend/tsconfig.node.json b/web/frontend/tsconfig.node.json new file mode 100644 index 0000000..ecd65f5 --- /dev/null +++ b/web/frontend/tsconfig.node.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/frontend/tsconfig.node.tsbuildinfo b/web/frontend/tsconfig.node.tsbuildinfo new file mode 100644 index 0000000..62c7bf9 --- /dev/null +++ b/web/frontend/tsconfig.node.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/web/frontend/vite.config.ts b/web/frontend/vite.config.ts new file mode 100644 index 0000000..4af253d --- /dev/null +++ b/web/frontend/vite.config.ts @@ -0,0 +1,25 @@ +import path from "node:path"; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// Dev server proxies API + redirect + legacy static to the Go backend on :8080. +// In production the Go binary serves the built assets directly via embed.FS. +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + server: { + port: 5173, + proxy: { + "/api": "http://localhost:8080", + "/query": "http://localhost:8080", + }, + }, + build: { + outDir: "dist", + emptyOutDir: true, + }, +}); diff --git a/web/static/styles.css b/web/static/styles.css deleted file mode 100644 index b82fe80..0000000 --- a/web/static/styles.css +++ /dev/null @@ -1,675 +0,0 @@ -/* Dieter Rams-inspired GoLinks Theme */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap'); - -:root { - /* Dieter Rams color palette - inspired by Braun design */ - --rams-white: #FFFFFF; - --rams-off-white: #FAFAFA; - --rams-light-grey: #E5E5E5; - --rams-medium-grey: #CCCCCC; - --rams-dark-grey: #666666; - --rams-charcoal: #333333; - --rams-black: #1A1A1A; - - /* Accent colors - minimal and functional */ - --rams-orange: #FF6B35; /* Braun orange */ - --rams-blue: #2563EB; /* Functional blue */ - --rams-green: #059669; /* Success green */ - --rams-red: #DC2626; /* Error red */ - --rams-yellow: #F59E0B; /* Warning yellow */ - - /* Typography */ - --font-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - --font-mono: 'JetBrains Mono', 'SF Mono', Monaco, 'Cascadia Code', monospace; - - /* Spacing - based on 8px grid system */ - --space-xs: 0.25rem; /* 4px */ - --space-sm: 0.5rem; /* 8px */ - --space-md: 1rem; /* 16px */ - --space-lg: 1.5rem; /* 24px */ - --space-xl: 2rem; /* 32px */ - --space-2xl: 3rem; /* 48px */ - - /* Border radius - minimal, functional */ - --radius-sm: 2px; - --radius-md: 4px; - --radius-lg: 6px; - - /* Shadows - subtle and functional */ - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); -} - -/* Reset and base styles */ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - font-size: 16px; - line-height: 1.5; -} - -body { - font-family: var(--font-primary); - font-weight: 400; - color: var(--rams-charcoal); - background-color: var(--rams-off-white); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -/* Layout */ -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 var(--space-md); -} - -.constrained-width { - max-width: 800px; - margin: var(--space-md) auto; - padding: 0 var(--space-md); -} - -/* Navigation Bar */ -.navbar { - background: linear-gradient(135deg, var(--rams-black) 0%, var(--rams-charcoal) 100%); - box-shadow: var(--shadow-md); - position: sticky; - top: 0; - z-index: 1000; -} - -.navbar-container { - max-width: 1200px; - margin: 0 auto; - padding: 0 var(--space-md); - display: flex; - justify-content: space-between; - align-items: center; - min-height: 70px; -} - -.navbar-brand h1 { - font-family: var(--font-primary); - font-weight: 300; - font-size: 1.8rem; - letter-spacing: -0.02em; - color: var(--rams-white); - margin: 0; - padding: 0; - background: none; - text-align: left; - position: static; -} - -.navbar-brand h1::after { - display: none; -} - -.navbar-brand h1 .accent { - color: var(--rams-orange); - font-weight: 400; -} - -.navbar-nav { - display: flex; - gap: var(--space-lg); - align-items: center; -} - -.nav-link { - color: var(--rams-light-grey); - text-decoration: none; - font-size: 0.9rem; - font-weight: 500; - letter-spacing: 0.01em; - padding: var(--space-sm) var(--space-md); - border-radius: var(--radius-sm); - transition: all 0.2s ease; - position: relative; -} - -.nav-link:hover { - color: var(--rams-white); - background-color: rgba(255, 255, 255, 0.1); - text-decoration: none; -} - -.nav-link.active { - color: var(--rams-orange); - background-color: rgba(255, 107, 53, 0.1); -} - -.nav-link.active::after { - content: ''; - position: absolute; - bottom: -2px; - left: 50%; - transform: translateX(-50%); - width: 20px; - height: 2px; - background-color: var(--rams-orange); - border-radius: 1px; -} - -/* Typography */ -h1 { - font-family: var(--font-primary); - font-weight: 300; - font-size: 3rem; - letter-spacing: -0.02em; - color: var(--rams-white); - background: linear-gradient(135deg, var(--rams-black) 0%, var(--rams-charcoal) 100%); - text-align: center; - padding: var(--space-2xl) 0; - margin: 0 0 var(--space-2xl) 0; - position: relative; -} - -h1::after { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 60px; - height: 2px; - background-color: var(--rams-orange); -} - -h1 .accent { - color: var(--rams-orange); - font-weight: 400; -} - -h2 { - font-family: var(--font-primary); - font-weight: 500; - font-size: 1.5rem; - color: var(--rams-charcoal); - margin: var(--space-2xl) 0 var(--space-lg) 0; - display: flex; - align-items: center; - gap: var(--space-sm); -} - -h2::before { - content: ''; - width: 4px; - height: 1.5rem; - background-color: var(--rams-orange); - border-radius: var(--radius-sm); -} - -h3 { - font-family: var(--font-primary); - font-weight: 500; - font-size: 1.25rem; - color: var(--rams-charcoal); - margin: var(--space-xl) 0 var(--space-md) 0; -} - -p { - margin-bottom: var(--space-md); - line-height: 1.6; - color: var(--rams-dark-grey); -} - -/* Code and monospace */ -code { - font-family: var(--font-mono); - font-size: 0.875rem; - font-weight: 500; - background-color: var(--rams-light-grey); - color: var(--rams-charcoal); - padding: var(--space-xs) var(--space-sm); - border-radius: var(--radius-sm); - border: 1px solid var(--rams-medium-grey); -} - -.url { - font-family: var(--font-mono); - font-size: 0.875rem; - word-break: break-all; -} - -/* Links */ -a { - color: var(--rams-blue); - text-decoration: none; - font-weight: 500; - transition: color 0.15s ease; -} - -a:hover { - color: var(--rams-orange); - text-decoration: underline; -} - - -/* Forms - Dieter Rams functional design */ -#formData { - display: flex; - gap: 0; - margin: var(--space-lg) 0; - box-shadow: var(--shadow-md); - border-radius: var(--radius-lg); - overflow: hidden; - background: var(--rams-white); -} - -#formData input { - font-family: var(--font-primary); - font-size: 1rem; - padding: var(--space-md); - border: 1px solid var(--rams-medium-grey); - background: var(--rams-white); - color: var(--rams-charcoal); - transition: all 0.15s ease; - outline: none; -} - -#formData input:not(:last-child) { - border-right: none; -} - -#formData input:focus { - border-color: var(--rams-blue); - box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1); -} - -#formData input[type="text"] { - flex: 1; - min-width: 0; -} - -#formData input::placeholder { - color: var(--rams-dark-grey); - font-weight: 400; -} - -#formData input[type="submit"] { - background: linear-gradient(135deg, var(--rams-blue) 0%, var(--rams-orange) 100%); - color: var(--rams-white); - font-weight: 500; - border: none; - cursor: pointer; - padding: var(--space-md) var(--space-xl); - transition: all 0.15s ease; - position: relative; - overflow: hidden; -} - -#formData input[type="submit"]:hover { - transform: translateY(-1px); - box-shadow: var(--shadow-lg); -} - -#formData input[type="submit"]:active { - transform: translateY(0); -} - -/* Status messages - clean and functional */ -.status-message { - text-align: center; - padding: var(--space-md); - margin: calc(-1 * var(--space-2xl)) 0 var(--space-xl) 0; - border-radius: var(--radius-md); - font-weight: 500; - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-sm); -} - -#success { - background-color: rgba(5, 150, 105, 0.1); - color: var(--rams-green); - border: 1px solid rgba(5, 150, 105, 0.2); -} - -#failure { - background-color: rgba(220, 38, 38, 0.1); - color: var(--rams-red); - border: 1px solid rgba(220, 38, 38, 0.2); -} - -/* Form error messages */ -.error-message { - background-color: rgba(220, 38, 38, 0.1); - color: var(--rams-red); - border: 1px solid rgba(220, 38, 38, 0.2); - padding: var(--space-md); - margin: var(--space-md) 0; - border-radius: var(--radius-md); - font-size: 0.9rem; - font-weight: 500; - text-align: center; - position: relative; - z-index: 10; - clear: both; -} - -/* Tables - minimal and functional */ -table { - width: 100%; - border-collapse: separate; - border-spacing: 0; - background: var(--rams-white); - border-radius: var(--radius-lg); - overflow: hidden; - box-shadow: var(--shadow-md); - margin: var(--space-lg) 0; -} - -table th { - background: linear-gradient(135deg, var(--rams-charcoal) 0%, var(--rams-black) 100%); - color: var(--rams-white); - font-family: var(--font-primary); - font-weight: 500; - font-size: 0.875rem; - text-transform: uppercase; - letter-spacing: 0.05em; - padding: var(--space-md); - text-align: left; - border-bottom: 2px solid var(--rams-orange); -} - -table td { - padding: var(--space-md); - border-bottom: 1px solid var(--rams-light-grey); - vertical-align: top; -} - -table tr:last-child td { - border-bottom: none; -} - -table tr:nth-child(even) { - background-color: rgba(229, 229, 229, 0.3); -} - -table tr:hover { - background-color: rgba(37, 99, 235, 0.05); -} - -/* Responsive design */ -@media (max-width: 768px) { - .navbar-container { - padding: 0 var(--space-sm); - min-height: 60px; - } - - .navbar-brand h1 { - font-size: 1.5rem; - } - - .navbar-nav { - gap: var(--space-md); - } - - .nav-link { - font-size: 0.8rem; - padding: var(--space-xs) var(--space-sm); - } - - .constrained-width { - padding: 0 var(--space-sm); - } - - h1 { - font-size: 2rem; - padding: var(--space-xl) 0; - } - - #formData { - flex-direction: column; - border-radius: var(--radius-md); - } - - #formData input { - border-right: 1px solid var(--rams-medium-grey); - border-radius: 0; - } - - #formData input:first-child { - border-top-left-radius: var(--radius-md); - border-top-right-radius: var(--radius-md); - } - - #formData input:last-child { - border-bottom-left-radius: var(--radius-md); - border-bottom-right-radius: var(--radius-md); - } - - table { - font-size: 0.875rem; - } - - table th, - table td { - padding: var(--space-sm); - } -} - -/* Utility classes */ -.text-center { - text-align: center; -} - -.text-muted { - color: var(--rams-dark-grey); -} - -.mb-0 { margin-bottom: 0; } -.mb-1 { margin-bottom: var(--space-sm); } -.mb-2 { margin-bottom: var(--space-md); } -.mb-3 { margin-bottom: var(--space-lg); } - -.mt-0 { margin-top: 0; } -.mt-1 { margin-top: var(--space-sm); } -.mt-2 { margin-top: var(--space-md); } -.mt-3 { margin-top: var(--space-lg); } - -/* Loading states and animations */ -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} - -.fade-in { - animation: fadeIn 0.3s ease-out; -} - -/* Focus management for accessibility */ -:focus-visible { - outline: 2px solid var(--rams-blue); - outline-offset: 2px; -} - - -/* 404 Error page styles */ -.error-page { - min-height: calc(100vh - 70px); - display: flex; - align-items: center; - justify-content: center; - padding: var(--space-2xl) var(--space-md); -} - -.error-content { - text-align: center; - max-width: 600px; - margin: 0 auto; -} - -.error-icon { - margin-bottom: var(--space-xl); -} - -.error-icon span { - font-size: 6rem; - font-weight: 300; - color: var(--rams-light-grey); - font-family: var(--font-primary); - letter-spacing: -0.05em; - display: block; - line-height: 1; -} - -.error-content h2 { - font-size: 2rem; - font-weight: 500; - color: var(--rams-charcoal); - margin-bottom: var(--space-lg); - letter-spacing: -0.02em; -} - -.error-description { - color: var(--rams-dark-grey); - font-size: 1rem; - line-height: 1.6; - margin-bottom: var(--space-2xl); -} - -.error-description code { - background-color: var(--rams-light-grey); - padding: var(--space-xs) var(--space-sm); - border-radius: var(--radius-sm); - font-family: var(--font-mono); - font-size: 0.9rem; - color: var(--rams-charcoal); -} - -.error-actions { - display: flex; - gap: var(--space-md); - justify-content: center; - margin-bottom: var(--space-2xl); - flex-wrap: wrap; -} - -.primary-action, -.secondary-action { - display: inline-flex; - align-items: center; - gap: var(--space-sm); - padding: var(--space-md) var(--space-lg); - text-decoration: none; - font-size: 0.9rem; - font-weight: 500; - letter-spacing: 0.01em; - border-radius: var(--radius-md); - transition: all 0.2s ease; - min-width: 140px; - justify-content: center; -} - -.primary-action { - background: linear-gradient(135deg, var(--rams-blue) 0%, var(--rams-orange) 100%); - color: var(--rams-white); - box-shadow: var(--shadow-md); -} - -.primary-action:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); - text-decoration: none; -} - -.secondary-action { - background-color: var(--rams-white); - color: var(--rams-charcoal); - border: 1px solid var(--rams-medium-grey); - box-shadow: var(--shadow-sm); -} - -.secondary-action:hover { - background-color: var(--rams-off-white); - border-color: var(--rams-dark-grey); - transform: translateY(-1px); - text-decoration: none; -} - -.error-help { - background-color: var(--rams-white); - border: 1px solid var(--rams-light-grey); - border-radius: var(--radius-lg); - padding: var(--space-lg); - text-align: left; - box-shadow: var(--shadow-sm); -} - -.error-help p { - font-weight: 500; - color: var(--rams-charcoal); - margin-bottom: var(--space-md); -} - -.error-help ul { - list-style: none; - padding: 0; - margin: 0; -} - -.error-help li { - padding: var(--space-sm) 0; - color: var(--rams-dark-grey); - position: relative; - padding-left: var(--space-lg); -} - -.error-help li::before { - content: '•'; - color: var(--rams-orange); - font-weight: bold; - position: absolute; - left: 0; -} - -.error-help a { - color: var(--rams-blue); - text-decoration: none; - font-weight: 500; -} - -.error-help a:hover { - color: var(--rams-orange); - text-decoration: underline; -} - -/* Responsive design for 404 page */ -@media (max-width: 768px) { - .error-page { - min-height: calc(100vh - 60px); - padding: var(--space-xl) var(--space-sm); - } - - .error-icon span { - font-size: 4rem; - } - - .error-content h2 { - font-size: 1.5rem; - } - - .error-actions { - flex-direction: column; - align-items: center; - } - - .primary-action, - .secondary-action { - width: 100%; - max-width: 200px; - } - - .error-help { - padding: var(--space-md); - } -} diff --git a/web/templates/404.html b/web/templates/404.html deleted file mode 100644 index d160dac..0000000 --- a/web/templates/404.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - 404 - Page Not Found | GoLinks - - - - - - -
-
-
- 404 -
-

Page Not Found

-

- The page you are looking for ({{.Path}}) does not exist. - It might have been moved, deleted, or you entered the wrong URL. -

- - - -
-

Need help? Here are some suggestions:

- -
-
-
- - \ No newline at end of file diff --git a/web/templates/homepage.html b/web/templates/homepage.html deleted file mode 100644 index bff67e2..0000000 --- a/web/templates/homepage.html +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - golinks - - - - - - - - {{if .Missing}} -
- ⚠️ -
Unable to find a shortcut for the query {{.Missing}}
-
- {{else if .Success}} -
- -
Added keyword {{.Success}}
-
- {{else if .Failure}} -
- -
Adding keyword {{.Failure}} failed: {{.Reason}}
-
- {{end}} - -
-

- If you're not yet setup with GoLinks then you should follow the guide - here. -

-

- Your search engine should now read: {{.BaseURL}}/query/%s -

- -

➕ Add new keyword

-
-
- - - -
-
- -
- - - {{if .AllKeywords}} -

🔎 Full keyword list

-

- If you're needing inspiration, here are the current listed keywords. - Use {*} in a URL for variable links and space separated queries, - like go google cats. -

- - - - - - - - - - {{range .AllKeywords}} - - - - - - {{end}} - -
KeywordURLCreated On
{{.Word}}{{urlify .Link}}{{.CreatedAt.Format "2006-01-02"}}
- {{end}} -
- - - - diff --git a/web/templates/setup.html b/web/templates/setup.html deleted file mode 100644 index 443c20b..0000000 --- a/web/templates/setup.html +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - golinks - Setup - - - - - - -
-

🔧 Browser Setup

-

- To use GoLinks, you need to configure your browser to use our service as a search engine. - This allows you to type go keyword in your address bar and be redirected to the corresponding URL. -

- -

Chrome / Edge Setup

-
    -
  1. Open Chrome/Edge Settings
  2. -
  3. Go to Search engineManage search engines and site search
  4. -
  5. Click Add next to "Site search"
  6. -
  7. Fill in the form: -
      -
    • Search engine: GoLinks
    • -
    • Shortcut: go
    • -
    • URL with %s in place of query: {{.BaseURL}}/query/%s
    • -
    -
  8. -
  9. Click Add
  10. -
- -

Firefox Setup

-
    -
  1. Right-click in the address bar and select Add a Keyword for this Search
  2. -
  3. Or go to Bookmarks → Manage Bookmarks
  4. -
  5. Right-click and select New Bookmark
  6. -
  7. Fill in: -
      -
    • Name: GoLinks
    • -
    • Location: {{.BaseURL}}/query/%s
    • -
    • Keyword: go
    • -
    -
  8. -
  9. Click Save
  10. -
- -

Safari Setup

-
    -
  1. Safari doesn't support custom search engines directly
  2. -
  3. You can bookmark {{.BaseURL}}/homepage/ for easy access
  4. -
  5. Or use a browser extension like Keyword Search
  6. -
- -
- 💡 -
- Pro Tip: After setup, you can type go keyword in your address bar - to quickly navigate to any configured link! -
-
- -

🚀 Usage Examples

- - - - - - - - - - - - - - - - - - - - - - - - - -
CommandDescriptionExample
go docsNavigate to documentationIf "docs" points to https://docs.company.com
go jira 123Navigate with parametersIf "jira" contains {*} placeholder
go github myrepoSearch within a serviceDynamic search using {*}
- -

📝 Creating Links

-

- Once setup is complete, visit the homepage to: -

-
    -
  • Create new keyword shortcuts
  • -
  • View popular and recent queries
  • -
  • Browse all available keywords
  • -
  • Use {*} in URLs for dynamic content
  • -
- - -
- - - - From 819bfdc409bafca8c7cfd3b9287194624d94d16a Mon Sep 17 00:00:00 2001 From: Alejandro Perez Date: Tue, 28 Apr 2026 00:03:57 +0100 Subject: [PATCH 5/6] chore: update TODO --- TODO.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 12 deletions(-) diff --git a/TODO.md b/TODO.md index c32067a..cd73c25 100644 --- a/TODO.md +++ b/TODO.md @@ -1,12 +1,63 @@ -- [ ] Support Postgres as a DB not just SQLite -- [ ] Add authentication and login, whether propietary or integration with 3rd parties -- [ ] Add authorization, different users can have different roles -- [ ] Support for theme customization -- [ ] Support for user administration -- [ ] Add golang migrations -- [ ] Consider the use of an ORM -- [ ] Statistics of most accessed docs -- [ ] Support to render Markdown docs and MDX docs -- [ ] Deployment in google app engine -- [ ] Add admin panel to manage golinks -- [ ] Add the ability to create a request to an admin for a keyword to be added \ No newline at end of file +# TODO + +Roadmap for GoLinks. Items are grouped by theme and ordered roughly by dependency — top sections unblock lower ones. Each item has a short scope line so Claude (or future-you) can pick one up without re-deriving context. See `ARCHITECTURE.md` for the underlying design and `CLAUDE.md` for conventions. + +## Suggested order + +1. **MCP server (Phase 1)** — independent, ships immediate value, can run before broader auth via a shared bearer token. +2. **Authentication** — unblocks the MDX upload security gap and is required for everything below. +3. **Authorization & roles** — depends on auth. +4. **Admin features** (edit/delete UI, request/approval flow) — depend on roles. +5. **Tooling** (migrations library) and **independent features** (dark mode, doc analytics) — do whenever. + +--- + +## MCP server — Phase 1 (GoLinks-native) + +The aim: let agents resolve, search, read, and create golinks via MCP. **Phase 1 only exposes GoLinks' own data (`linktable` + `docs/`).** Federated search across third-party sources is intentionally a Phase 2 decision, gated on real usage gaps observed after Phase 1 ships — see *Open questions* below. + +- [ ] **Add `internal/mcp/` server package.** Use `github.com/mark3labs/mcp-go` for protocol plumbing (HTTP-streamable transport, since this is team-shared). Reuse the existing `LinkService` and `DocumentService` — no business logic in the MCP layer. +- [ ] **`MCP_TOKEN` bearer middleware.** Read from env, validate `Authorization: Bearer ` on every MCP request. Reject with 401 on miss. Document in `env.example`. +- [ ] **Mount at `/mcp`** in `cmd/server/main.go` after `/api/*` and before the SPA catch-all. Add `mcp` to the `frontend.Handler` reserved-prefix list. +- [ ] **FTS5 index over `linktable`.** New migration: virtual table mirroring `word + link`, with INSERT/UPDATE/DELETE triggers to keep it in sync. Required for `search_golinks`. +- [ ] **FTS5 index over `docs/`.** Built on startup, updated on upload and delete in `DocumentService`. Strip frontmatter before indexing. Required for `search_docs`. +- [ ] **Tool: `resolve_golink(word: string)`** — wraps `LinkService.GetLink`. Returns `{url}` on hit, error on miss. +- [ ] **Tool: `search_golinks(query: string, limit?: int = 10)`** — FTS5 query over `linktable`. Returns `[{word, link, score}]`. +- [ ] **Tool: `list_golinks(limit?: int = 100, offset?: int = 0)`** — wraps `LinkService.GetAllKeywords` with pagination. Returns `[{word, link, created_at}]`. +- [ ] **Tool: `search_docs(query: string, limit?: int = 10)`** — FTS5 query over the docs index. Returns `[{filename, title, snippet, score}]`. +- [ ] **Tool: `fetch_doc(filename: string)`** — wraps `DocumentService.GetDocument`. Returns `{source, type, metadata}`. +- [ ] **Tool: `create_golink(word: string, url: string)`** — wraps `LinkService.UpdateLink`. Returns `{success}` or validation error. +- [ ] **Smoke tests in `internal/mcp/server_test.go`** — table-driven, with the same mock pattern as `handler_test.go`. Cover: token rejection, each tool happy path, search with no results, validation errors. +- [ ] **Update `ARCHITECTURE.md`** with the `/mcp` endpoint, tool catalog, and the Phase 1/Phase 2 scope decision. Update `CLAUDE.md`'s endpoint list and add a short "MCP conventions" section. +- [ ] **Update `README.md`** with a "Connecting an agent" section: example MCP client config (Claude Desktop / Claude Code) using `MCP_TOKEN`. + +## Authentication & authorization + +- [ ] **Authentication.** Pick an approach and implement it: (a) proprietary email+password with bcrypt, (b) OAuth via GitHub/Google, (c) shared session token. Today `getUserID` in `internal/handlers/handler.go` returns `"DefaultUser"` unconditionally — replace it with a real identity lookup. Blocks the runtime MDX upload risk flagged in `ARCHITECTURE.md` and `CLAUDE.md`. +- [ ] **Authorization with roles.** Two roles to start: `admin` (full CRUD on golinks and docs) and `user` (read, search, propose). Gate `POST /api/links`, `POST /api/docs`, `DELETE /api/docs/*`, and `create_golink` MCP tool on `admin`. _Depends on authentication._ + +## Tooling + +- [ ] **Switch to `goose` (or `golang-migrate`).** Migrations today are an inline string slice in `internal/database/sqlite.go:Migrate`. As the schema grows (auth tables, FTS5, doc analytics), versioned migration files become important for safe rollback and review. + +## Features + +- [ ] **Dark mode.** shadcn theming is already token-driven — add a `.dark` block in `web/frontend/src/index.css` overriding the HSL variables. Toggle via a `