(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

more backend + frontend changes

scanash00 8f498b1f c3dd1754

+2905 -885
+3
backend/internal/api/apikey.go
··· 8 8 "encoding/json" 9 9 "encoding/pem" 10 10 "fmt" 11 + "log" 11 12 "net/http" 12 13 "strings" 13 14 "time" ··· 72 73 return createErr 73 74 }) 74 75 if err != nil { 76 + log.Printf("[ERROR] Failed to create API key record on PDS: %v", err) 75 77 http.Error(w, "Failed to create key record: "+err.Error(), http.StatusInternalServerError) 76 78 return 77 79 } ··· 90 92 } 91 93 92 94 if err := h.db.CreateAPIKey(apiKey); err != nil { 95 + log.Printf("[ERROR] Failed to insert API key into DB: %v", err) 93 96 http.Error(w, "Failed to create key", http.StatusInternalServerError) 94 97 return 95 98 }
+49 -10
backend/internal/api/handler.go
··· 271 271 annotations, _ = h.db.GetMarginAnnotations(fetchLimit, 0) 272 272 case "semble": 273 273 annotations, _ = h.db.GetSembleAnnotations(fetchLimit, 0) 274 + case "popular": 275 + annotations, _ = h.db.GetPopularAnnotations(fetchLimit, 0) 276 + case "shelved": 277 + annotations, _ = h.db.GetShelvedAnnotations(fetchLimit, 0) 274 278 default: 275 279 annotations, _ = h.db.GetRecentAnnotations(fetchLimit, 0) 276 280 } ··· 281 285 highlights, _ = h.db.GetMarginHighlights(fetchLimit, 0) 282 286 case "semble": 283 287 highlights, _ = h.db.GetSembleHighlights(fetchLimit, 0) 288 + case "popular": 289 + highlights, _ = h.db.GetPopularHighlights(fetchLimit, 0) 290 + case "shelved": 291 + highlights, _ = h.db.GetShelvedHighlights(fetchLimit, 0) 284 292 default: 285 293 highlights, _ = h.db.GetRecentHighlights(fetchLimit, 0) 286 294 } ··· 291 299 bookmarks, _ = h.db.GetMarginBookmarks(fetchLimit, 0) 292 300 case "semble": 293 301 bookmarks, _ = h.db.GetSembleBookmarks(fetchLimit, 0) 302 + case "popular": 303 + bookmarks, _ = h.db.GetPopularBookmarks(fetchLimit, 0) 304 + case "shelved": 305 + bookmarks, _ = h.db.GetShelvedBookmarks(fetchLimit, 0) 294 306 default: 295 307 bookmarks, _ = h.db.GetRecentBookmarks(fetchLimit, 0) 296 308 } 297 309 } 298 310 if motivation == "" { 299 - collectionItems, err = h.db.GetRecentCollectionItems(fetchLimit, 0) 311 + switch feedType { 312 + case "popular": 313 + collectionItems, err = h.db.GetPopularCollectionItems(fetchLimit, 0) 314 + case "shelved": 315 + collectionItems, err = h.db.GetShelvedCollectionItems(fetchLimit, 0) 316 + default: 317 + collectionItems, err = h.db.GetRecentCollectionItems(fetchLimit, 0) 318 + } 300 319 if err != nil { 301 320 log.Printf("Error fetching collection items: %v\n", err) 302 321 } ··· 374 393 case APIBookmark: 375 394 uri = v.ID 376 395 case APICollectionItem: 377 - uri = v.ID 396 + if v.Annotation != nil { 397 + uri = v.Annotation.ID 398 + } else if v.Highlight != nil { 399 + uri = v.Highlight.ID 400 + } else if v.Bookmark != nil { 401 + uri = v.Bookmark.ID 402 + } else { 403 + uri = v.ID 404 + } 378 405 } 379 406 if strings.Contains(uri, "network.cosmik") { 380 407 isSemble = true ··· 389 416 if !isSemble { 390 417 filtered = append(filtered, item) 391 418 } 392 - case "popular": 419 + case "popular", "shelved": 393 420 filtered = append(filtered, item) 394 - case "shelved": 395 - createdAt := getCreatedAt(item) 396 - popularity := getPopularity(item) 397 - if time.Since(createdAt) > 24*time.Hour && popularity == 0 { 398 - filtered = append(filtered, item) 399 - } 400 421 } 401 422 } 402 423 feed = filtered 403 424 } 404 425 426 + // ... 405 427 switch feedType { 406 428 case "popular": 407 429 sortFeedByPopularity(feed) ··· 409 431 sortFeed(feed) 410 432 } 411 433 434 + log.Printf("[DEBUG] FeedType: %s, Total Items before slice: %d", feedType, len(feed)) 435 + if len(feed) > 0 { 436 + first := feed[0] 437 + switch v := first.(type) { 438 + case APIAnnotation: 439 + log.Printf("[DEBUG] First Item (Annotation): %s, Likes: %d, Replies: %d", v.ID, v.LikeCount, v.ReplyCount) 440 + case APIHighlight: 441 + log.Printf("[DEBUG] First Item (Highlight): %s, Likes: %d, Replies: %d", v.ID, v.LikeCount, v.ReplyCount) 442 + } 443 + } 444 + 412 445 if offset < len(feed) { 413 446 feed = feed[offset:] 414 447 } else { 415 448 feed = []interface{}{} 416 449 } 450 + // ... 417 451 418 452 if len(feed) > limit { 419 453 feed = feed[:limit] ··· 619 653 sort.Slice(feed, func(i, j int) bool { 620 654 p1 := getPopularity(feed[i]) 621 655 p2 := getPopularity(feed[j]) 622 - return p1 > p2 656 + if p1 != p2 { 657 + return p1 > p2 658 + } 659 + t1 := getCreatedAt(feed[i]) 660 + t2 := getCreatedAt(feed[j]) 661 + return t1.After(t2) 623 662 }) 624 663 } 625 664
+10 -3
backend/internal/api/hydration.go
··· 655 655 } 656 656 } 657 657 658 - result := make([]APICollectionItem, len(items)) 659 - for i, item := range items { 658 + var result []APICollectionItem 659 + for _, item := range items { 660 660 apiItem := APICollectionItem{ 661 661 ID: item.URI, 662 662 Type: "CollectionItem", ··· 670 670 apiItem.Collection = &coll 671 671 } 672 672 673 + isValid := false 673 674 if val, ok := annotationsMap[item.AnnotationURI]; ok { 674 675 apiItem.Annotation = &val 676 + isValid = true 675 677 } else if val, ok := highlightsMap[item.AnnotationURI]; ok { 676 678 apiItem.Highlight = &val 679 + isValid = true 677 680 } else if val, ok := bookmarksMap[item.AnnotationURI]; ok { 678 681 apiItem.Bookmark = &val 682 + isValid = true 679 683 } else if strings.Contains(item.AnnotationURI, "network.cosmik.card") { 680 684 apiItem.Annotation = &APIAnnotation{ 681 685 ID: item.AnnotationURI, ··· 687 691 CreatedAt: item.CreatedAt, 688 692 Author: profiles[item.AuthorDID], 689 693 } 694 + isValid = true 690 695 } 691 696 692 - result[i] = apiItem 697 + if isValid && apiItem.Collection != nil { 698 + result = append(result, apiItem) 699 + } 693 700 } 694 701 return result, nil 695 702 }
+6 -6
backend/internal/api/pds.go
··· 89 89 90 90 targetSource := record.Target.Source 91 91 92 - targetHash := record.Target.SourceHash 93 - if targetHash == "" && targetSource != "" { 92 + var targetHash string 93 + if targetSource != "" { 94 94 targetHash = db.HashURL(targetSource) 95 95 } 96 96 ··· 154 154 createdAt = time.Now() 155 155 } 156 156 157 - targetHash := record.Target.SourceHash 158 - if targetHash == "" && record.Target.Source != "" { 157 + var targetHash string 158 + if record.Target.Source != "" { 159 159 targetHash = db.HashURL(record.Target.Source) 160 160 } 161 161 ··· 219 219 220 220 createdAt, _ := time.Parse(time.RFC3339, record.CreatedAt) 221 221 222 - sourceHash := record.SourceHash 223 - if sourceHash == "" && record.Source != "" { 222 + var sourceHash string 223 + if record.Source != "" { 224 224 sourceHash = db.HashURL(record.Source) 225 225 } 226 226
+5 -2
backend/internal/db/queries.go
··· 42 42 43 43 func HashURL(rawURL string) string { 44 44 parsed, err := url.Parse(rawURL) 45 - if err != nil { 45 + if err != nil || parsed.Host == "" { 46 46 return hashString(rawURL) 47 47 } 48 48 49 - normalized := strings.ToLower(parsed.Host) + parsed.Path 49 + host := strings.ToLower(parsed.Host) 50 + host = strings.TrimPrefix(host, "www.") 51 + 52 + normalized := host + parsed.Path 50 53 if parsed.RawQuery != "" { 51 54 normalized += "?" + parsed.RawQuery 52 55 }
+46
backend/internal/db/queries_annotations.go
··· 13 13 body_value = excluded.body_value, 14 14 body_format = excluded.body_format, 15 15 body_uri = excluded.body_uri, 16 + target_source = excluded.target_source, 17 + target_hash = excluded.target_hash, 16 18 target_title = excluded.target_title, 17 19 selector_json = excluded.selector_json, 18 20 tags_json = excluded.tags_json, ··· 122 124 ORDER BY created_at DESC 123 125 LIMIT ? OFFSET ? 124 126 `), limit, offset) 127 + if err != nil { 128 + return nil, err 129 + } 130 + defer rows.Close() 131 + 132 + return scanAnnotations(rows) 133 + } 134 + 135 + func (db *DB) GetPopularAnnotations(limit, offset int) ([]Annotation, error) { 136 + since := time.Now().AddDate(0, 0, -14) 137 + rows, err := db.Query(db.Rebind(` 138 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 139 + FROM annotations 140 + WHERE created_at > ? AND ( 141 + (SELECT COUNT(*) FROM likes WHERE subject_uri = annotations.uri) + 142 + (SELECT COUNT(*) FROM replies WHERE root_uri = annotations.uri) 143 + ) > 0 144 + ORDER BY ( 145 + (SELECT COUNT(*) FROM likes WHERE subject_uri = annotations.uri) + 146 + (SELECT COUNT(*) FROM replies WHERE root_uri = annotations.uri) 147 + ) DESC, created_at DESC 148 + LIMIT ? OFFSET ? 149 + `), since, limit, offset) 150 + if err != nil { 151 + return nil, err 152 + } 153 + defer rows.Close() 154 + 155 + return scanAnnotations(rows) 156 + } 157 + 158 + func (db *DB) GetShelvedAnnotations(limit, offset int) ([]Annotation, error) { 159 + olderThan := time.Now().AddDate(0, 0, -1) 160 + since := time.Now().AddDate(0, 0, -14) 161 + rows, err := db.Query(db.Rebind(` 162 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 163 + FROM annotations 164 + WHERE created_at < ? AND created_at > ? AND ( 165 + (SELECT COUNT(*) FROM likes WHERE subject_uri = annotations.uri) + 166 + (SELECT COUNT(*) FROM replies WHERE root_uri = annotations.uri) 167 + ) = 0 168 + ORDER BY RANDOM() 169 + LIMIT ? OFFSET ? 170 + `), olderThan, since, limit, offset) 125 171 if err != nil { 126 172 return nil, err 127 173 }
+62
backend/internal/db/queries_bookmarks.go
··· 9 9 INSERT INTO bookmarks (uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid) 10 10 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 11 11 ON CONFLICT(uri) DO UPDATE SET 12 + source = excluded.source, 13 + source_hash = excluded.source_hash, 12 14 title = excluded.title, 13 15 description = excluded.description, 14 16 tags_json = excluded.tags_json, ··· 38 40 ORDER BY created_at DESC 39 41 LIMIT ? OFFSET ? 40 42 `), limit, offset) 43 + if err != nil { 44 + return nil, err 45 + } 46 + defer rows.Close() 47 + 48 + var bookmarks []Bookmark 49 + for rows.Next() { 50 + var b Bookmark 51 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 52 + return nil, err 53 + } 54 + bookmarks = append(bookmarks, b) 55 + } 56 + return bookmarks, nil 57 + } 58 + 59 + func (db *DB) GetPopularBookmarks(limit, offset int) ([]Bookmark, error) { 60 + since := time.Now().AddDate(0, 0, -14) 61 + rows, err := db.Query(db.Rebind(` 62 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 63 + FROM bookmarks 64 + WHERE created_at > ? AND ( 65 + (SELECT COUNT(*) FROM likes WHERE subject_uri = bookmarks.uri) + 66 + (SELECT COUNT(*) FROM replies WHERE root_uri = bookmarks.uri) 67 + ) > 0 68 + ORDER BY ( 69 + (SELECT COUNT(*) FROM likes WHERE subject_uri = bookmarks.uri) + 70 + (SELECT COUNT(*) FROM replies WHERE root_uri = bookmarks.uri) 71 + ) DESC, created_at DESC 72 + LIMIT ? OFFSET ? 73 + `), since, limit, offset) 74 + if err != nil { 75 + return nil, err 76 + } 77 + defer rows.Close() 78 + 79 + var bookmarks []Bookmark 80 + for rows.Next() { 81 + var b Bookmark 82 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 83 + return nil, err 84 + } 85 + bookmarks = append(bookmarks, b) 86 + } 87 + return bookmarks, nil 88 + } 89 + 90 + func (db *DB) GetShelvedBookmarks(limit, offset int) ([]Bookmark, error) { 91 + olderThan := time.Now().AddDate(0, 0, -1) 92 + since := time.Now().AddDate(0, 0, -14) 93 + rows, err := db.Query(db.Rebind(` 94 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 95 + FROM bookmarks 96 + WHERE created_at < ? AND created_at > ? AND ( 97 + (SELECT COUNT(*) FROM likes WHERE subject_uri = bookmarks.uri) + 98 + (SELECT COUNT(*) FROM replies WHERE root_uri = bookmarks.uri) 99 + ) = 0 100 + ORDER BY RANDOM() 101 + LIMIT ? OFFSET ? 102 + `), olderThan, since, limit, offset) 41 103 if err != nil { 42 104 return nil, err 43 105 }
+62
backend/internal/db/queries_collections.go
··· 1 1 package db 2 2 3 + import "time" 4 + 3 5 func (db *DB) CreateCollection(c *Collection) error { 4 6 _, err := db.Exec(db.Rebind(` 5 7 INSERT INTO collections (uri, author_did, name, description, icon, created_at, indexed_at) ··· 102 104 ORDER BY created_at DESC 103 105 LIMIT ? OFFSET ? 104 106 `), limit, offset) 107 + if err != nil { 108 + return nil, err 109 + } 110 + defer rows.Close() 111 + 112 + var items []CollectionItem 113 + for rows.Next() { 114 + var item CollectionItem 115 + if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil { 116 + return nil, err 117 + } 118 + items = append(items, item) 119 + } 120 + return items, nil 121 + } 122 + 123 + func (db *DB) GetPopularCollectionItems(limit, offset int) ([]CollectionItem, error) { 124 + since := time.Now().AddDate(0, 0, -14) 125 + rows, err := db.Query(db.Rebind(` 126 + SELECT uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at 127 + FROM collection_items 128 + WHERE created_at > ? AND ( 129 + (SELECT COUNT(*) FROM likes WHERE subject_uri = collection_items.annotation_uri) + 130 + (SELECT COUNT(*) FROM replies WHERE root_uri = collection_items.annotation_uri) 131 + ) > 0 132 + ORDER BY ( 133 + (SELECT COUNT(*) FROM likes WHERE subject_uri = collection_items.annotation_uri) + 134 + (SELECT COUNT(*) FROM replies WHERE root_uri = collection_items.annotation_uri) 135 + ) DESC, created_at DESC 136 + LIMIT ? OFFSET ? 137 + `), since, limit, offset) 138 + if err != nil { 139 + return nil, err 140 + } 141 + defer rows.Close() 142 + 143 + var items []CollectionItem 144 + for rows.Next() { 145 + var item CollectionItem 146 + if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil { 147 + return nil, err 148 + } 149 + items = append(items, item) 150 + } 151 + return items, nil 152 + } 153 + 154 + func (db *DB) GetShelvedCollectionItems(limit, offset int) ([]CollectionItem, error) { 155 + olderThan := time.Now().AddDate(0, 0, -1) 156 + since := time.Now().AddDate(0, 0, -14) 157 + rows, err := db.Query(db.Rebind(` 158 + SELECT uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at 159 + FROM collection_items 160 + WHERE created_at < ? AND created_at > ? AND ( 161 + (SELECT COUNT(*) FROM likes WHERE subject_uri = collection_items.annotation_uri) + 162 + (SELECT COUNT(*) FROM replies WHERE root_uri = collection_items.annotation_uri) 163 + ) = 0 164 + ORDER BY RANDOM() 165 + LIMIT ? OFFSET ? 166 + `), olderThan, since, limit, offset) 105 167 if err != nil { 106 168 return nil, err 107 169 }
+62
backend/internal/db/queries_highlights.go
··· 9 9 INSERT INTO highlights (uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid) 10 10 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 11 11 ON CONFLICT(uri) DO UPDATE SET 12 + target_source = excluded.target_source, 13 + target_hash = excluded.target_hash, 12 14 target_title = excluded.target_title, 13 15 selector_json = excluded.selector_json, 14 16 color = excluded.color, ··· 39 41 ORDER BY created_at DESC 40 42 LIMIT ? OFFSET ? 41 43 `), limit, offset) 44 + if err != nil { 45 + return nil, err 46 + } 47 + defer rows.Close() 48 + 49 + var highlights []Highlight 50 + for rows.Next() { 51 + var h Highlight 52 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 53 + return nil, err 54 + } 55 + highlights = append(highlights, h) 56 + } 57 + return highlights, nil 58 + } 59 + 60 + func (db *DB) GetPopularHighlights(limit, offset int) ([]Highlight, error) { 61 + since := time.Now().AddDate(0, 0, -14) 62 + rows, err := db.Query(db.Rebind(` 63 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 64 + FROM highlights 65 + WHERE created_at > ? AND ( 66 + (SELECT COUNT(*) FROM likes WHERE subject_uri = highlights.uri) + 67 + (SELECT COUNT(*) FROM replies WHERE root_uri = highlights.uri) 68 + ) > 0 69 + ORDER BY ( 70 + (SELECT COUNT(*) FROM likes WHERE subject_uri = highlights.uri) + 71 + (SELECT COUNT(*) FROM replies WHERE root_uri = highlights.uri) 72 + ) DESC, created_at DESC 73 + LIMIT ? OFFSET ? 74 + `), since, limit, offset) 75 + if err != nil { 76 + return nil, err 77 + } 78 + defer rows.Close() 79 + 80 + var highlights []Highlight 81 + for rows.Next() { 82 + var h Highlight 83 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 84 + return nil, err 85 + } 86 + highlights = append(highlights, h) 87 + } 88 + return highlights, nil 89 + } 90 + 91 + func (db *DB) GetShelvedHighlights(limit, offset int) ([]Highlight, error) { 92 + olderThan := time.Now().AddDate(0, 0, -1) 93 + since := time.Now().AddDate(0, 0, -14) 94 + rows, err := db.Query(db.Rebind(` 95 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 96 + FROM highlights 97 + WHERE created_at < ? AND created_at > ? AND ( 98 + (SELECT COUNT(*) FROM likes WHERE subject_uri = highlights.uri) + 99 + (SELECT COUNT(*) FROM replies WHERE root_uri = highlights.uri) 100 + ) = 0 101 + ORDER BY RANDOM() 102 + LIMIT ? OFFSET ? 103 + `), olderThan, since, limit, offset) 42 104 if err != nil { 43 105 return nil, err 44 106 }
+3 -3
backend/internal/db/queries_keys.go
··· 6 6 7 7 func (db *DB) CreateAPIKey(key *APIKey) error { 8 8 _, err := db.Exec(db.Rebind(` 9 - INSERT INTO api_keys (id, owner_did, name, key_hash, created_at, uri, cid, indexed_at) 10 - VALUES (?, ?, ?, ?, ?, ?, ?, ?) 11 - `), key.ID, key.OwnerDID, key.Name, key.KeyHash, key.CreatedAt, key.URI, key.CID, key.IndexedAt) 9 + INSERT INTO api_keys (id, owner_did, name, key_hash, created_at, uri, cid) 10 + VALUES (?, ?, ?, ?, ?, ?, ?) 11 + `), key.ID, key.OwnerDID, key.Name, key.KeyHash, key.CreatedAt, key.URI, key.CID) 12 12 return err 13 13 } 14 14
+6 -9
backend/internal/firehose/ingester.go
··· 342 342 targetSource = record.URL 343 343 } 344 344 345 - targetHash := record.Target.SourceHash 346 - if targetHash == "" { 347 - targetHash = record.URLHash 348 - } 349 - if targetHash == "" && targetSource != "" { 345 + var targetHash string 346 + if targetSource != "" { 350 347 targetHash = db.HashURL(targetSource) 351 348 } 352 349 ··· 501 498 createdAt = time.Now() 502 499 } 503 500 504 - targetHash := record.Target.SourceHash 505 - if targetHash == "" && record.Target.Source != "" { 501 + var targetHash string 502 + if record.Target.Source != "" { 506 503 targetHash = db.HashURL(record.Target.Source) 507 504 } 508 505 ··· 564 561 createdAt = time.Now() 565 562 } 566 563 567 - sourceHash := record.SourceHash 568 - if sourceHash == "" && record.Source != "" { 564 + var sourceHash string 565 + if record.Source != "" { 569 566 sourceHash = db.HashURL(record.Source) 570 567 } 571 568
+14
backend/internal/oauth/handler.go
··· 362 362 func (h *Handler) HandleCallback(w http.ResponseWriter, r *http.Request) { 363 363 client := h.getDynamicClient(r) 364 364 365 + if oauthErr := r.URL.Query().Get("error"); oauthErr != "" { 366 + errDesc := r.URL.Query().Get("error_description") 367 + log.Printf("OAuth callback error: %s - %s", oauthErr, errDesc) 368 + 369 + if state := r.URL.Query().Get("state"); state != "" { 370 + h.pendingMu.Lock() 371 + delete(h.pending, state) 372 + h.pendingMu.Unlock() 373 + } 374 + 375 + http.Redirect(w, r, "/login?error="+url.QueryEscape(errDesc), http.StatusFound) 376 + return 377 + } 378 + 365 379 state := r.URL.Query().Get("state") 366 380 code := r.URL.Query().Get("code") 367 381 iss := r.URL.Query().Get("iss")
+6 -6
backend/internal/sync/service.go
··· 275 275 276 276 } 277 277 278 - targetHash := record.Target.SourceHash 279 - if targetHash == "" && targetSource != "" { 278 + var targetHash string 279 + if targetSource != "" { 280 280 targetHash = db.HashURL(targetSource) 281 281 } 282 282 ··· 338 338 createdAt = time.Now() 339 339 } 340 340 341 - targetHash := record.Target.SourceHash 342 - if targetHash == "" && record.Target.Source != "" { 341 + var targetHash string 342 + if record.Target.Source != "" { 343 343 targetHash = db.HashURL(record.Target.Source) 344 344 } 345 345 ··· 384 384 385 385 createdAt, _ := time.Parse(time.RFC3339, record.CreatedAt) 386 386 387 - sourceHash := record.SourceHash 388 - if sourceHash == "" && record.Source != "" { 387 + var sourceHash string 388 + if record.Source != "" { 389 389 sourceHash = db.HashURL(record.Source) 390 390 } 391 391
+19 -7
backend/internal/xrpc/records.go
··· 71 71 return nil 72 72 } 73 73 74 + type Generator struct { 75 + ID string `json:"id,omitempty"` 76 + Name string `json:"name,omitempty"` 77 + Homepage string `json:"homepage,omitempty"` 78 + } 79 + 74 80 type AnnotationRecord struct { 75 81 Type string `json:"$type"` 76 82 Motivation string `json:"motivation,omitempty"` ··· 78 84 Target AnnotationTarget `json:"target"` 79 85 Tags []string `json:"tags,omitempty"` 80 86 Facets []Facet `json:"facets,omitempty"` 87 + Generator *Generator `json:"generator,omitempty"` 88 + Rights string `json:"rights,omitempty"` 81 89 CreatedAt string `json:"createdAt"` 82 90 } 83 91 ··· 193 201 Target AnnotationTarget `json:"target"` 194 202 Color string `json:"color,omitempty"` 195 203 Tags []string `json:"tags,omitempty"` 204 + Generator *Generator `json:"generator,omitempty"` 205 + Rights string `json:"rights,omitempty"` 196 206 CreatedAt string `json:"createdAt"` 197 207 } 198 208 ··· 297 307 } 298 308 299 309 type BookmarkRecord struct { 300 - Type string `json:"$type"` 301 - Source string `json:"source"` 302 - SourceHash string `json:"sourceHash"` 303 - Title string `json:"title,omitempty"` 304 - Description string `json:"description,omitempty"` 305 - Tags []string `json:"tags,omitempty"` 306 - CreatedAt string `json:"createdAt"` 310 + Type string `json:"$type"` 311 + Source string `json:"source"` 312 + SourceHash string `json:"sourceHash"` 313 + Title string `json:"title,omitempty"` 314 + Description string `json:"description,omitempty"` 315 + Tags []string `json:"tags,omitempty"` 316 + Generator *Generator `json:"generator,omitempty"` 317 + Rights string `json:"rights,omitempty"` 318 + CreatedAt string `json:"createdAt"` 307 319 } 308 320 309 321 func (r *BookmarkRecord) Validate() error {
+4 -1
extension/eslint.config.js
··· 12 12 { 13 13 rules: { 14 14 '@typescript-eslint/no-explicit-any': 'off', 15 - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], 15 + '@typescript-eslint/no-unused-vars': [ 16 + 'warn', 17 + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, 18 + ], 16 19 }, 17 20 } 18 21 );
+28 -27
extension/src/assets/styles.css
··· 1 - @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap'); 1 + @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;600;700&display=swap'); 2 2 3 3 @tailwind base; 4 4 @tailwind components; 5 5 @tailwind utilities; 6 6 7 7 :root { 8 - --bg-primary: #0a0a0d; 9 - --bg-secondary: #121216; 10 - --bg-tertiary: #1a1a1f; 11 - --bg-card: #0f0f13; 12 - --bg-elevated: #18181d; 13 - --bg-hover: #1e1e24; 14 - --text-primary: #eaeaee; 15 - --text-secondary: #b7b6c5; 16 - --text-tertiary: #6e6d7a; 17 - --border: rgba(183, 182, 197, 0.12); 18 - --border-strong: rgba(183, 182, 197, 0.2); 19 - --accent: #957a86; 20 - --accent-hover: #a98d98; 21 - --accent-subtle: rgba(149, 122, 134, 0.15); 8 + --bg-primary: #020617; 9 + --bg-secondary: #0f172a; 10 + --bg-tertiary: #1e293b; 11 + --bg-card: #0f172a; 12 + --bg-elevated: #1e293b; 13 + --bg-hover: #334155; 14 + --text-primary: #f8fafc; 15 + --text-secondary: #94a3b8; 16 + --text-tertiary: #64748b; 17 + --border: rgba(148, 163, 184, 0.12); 18 + --border-strong: rgba(148, 163, 184, 0.2); 19 + --accent: #8b5cf6; 20 + --accent-hover: #a78bfa; 21 + --accent-subtle: rgba(139, 92, 246, 0.15); 22 22 --success: #34d399; 23 23 --warning: #fbbf24; 24 24 } 25 25 26 26 .light { 27 - --bg-primary: #f8f8fa; 27 + --bg-primary: #f8fafc; 28 28 --bg-secondary: #ffffff; 29 - --bg-tertiary: #f0f0f4; 29 + --bg-tertiary: #f1f5f9; 30 30 --bg-card: #ffffff; 31 31 --bg-elevated: #ffffff; 32 - --bg-hover: #eeeef2; 33 - --text-primary: #18171c; 34 - --text-secondary: #5c495a; 35 - --text-tertiary: #8a8494; 36 - --border: rgba(92, 73, 90, 0.12); 37 - --border-strong: rgba(92, 73, 90, 0.2); 38 - --accent: #7a5f6d; 39 - --accent-hover: #664e5b; 40 - --accent-subtle: rgba(149, 122, 134, 0.12); 32 + --bg-hover: #e2e8f0; 33 + --text-primary: #0f172a; 34 + --text-secondary: #64748b; 35 + --text-tertiary: #94a3b8; 36 + --border: rgba(100, 116, 139, 0.15); 37 + --border-strong: rgba(100, 116, 139, 0.25); 38 + --accent: #7c3aed; 39 + --accent-hover: #6d28d9; 40 + --accent-subtle: rgba(124, 58, 237, 0.12); 41 41 } 42 42 43 43 body { 44 44 background: var(--bg-primary); 45 45 color: var(--text-primary); 46 46 font-family: 47 - 'IBM Plex Sans', 47 + 'Inter', 48 + system-ui, 48 49 -apple-system, 49 50 BlinkMacSystemFont, 50 51 sans-serif;
+1 -1
extension/src/entrypoints/popup/index.html
··· 1 - <!DOCTYPE html> 1 + <!doctype html> 2 2 <html lang="en" class="popup"> 3 3 <head> 4 4 <meta charset="UTF-8" />
+5 -2
extension/src/entrypoints/sidepanel/index.html
··· 1 - <!DOCTYPE html> 1 + <!doctype html> 2 2 <html lang="en"> 3 3 <head> 4 4 <meta charset="UTF-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 6 <title>Margin</title> 7 - <meta name="manifest.default_icon" content="{ '16': '/icons/icon-16.png', '32': '/icons/icon-32.png', '48': '/icons/icon-48.png' }" /> 7 + <meta 8 + name="manifest.default_icon" 9 + content="{ '16': '/icons/icon-16.png', '32': '/icons/icon-32.png', '48': '/icons/icon-48.png' }" 10 + /> 8 11 </head> 9 12 <body> 10 13 <div id="root"></div>
+27 -27
extension/src/utils/overlay-styles.ts
··· 1 1 export const overlayStyles = /* css */ ` 2 2 :host { 3 3 all: initial; 4 - --bg-primary: #0a0a0d; 5 - --bg-secondary: #121216; 6 - --bg-tertiary: #1a1a1f; 7 - --bg-card: #0f0f13; 8 - --bg-elevated: #18181d; 9 - --bg-hover: #1e1e24; 4 + --bg-primary: #020617; 5 + --bg-secondary: #0f172a; 6 + --bg-tertiary: #1e293b; 7 + --bg-card: #0f172a; 8 + --bg-elevated: #1e293b; 9 + --bg-hover: #334155; 10 10 11 - --text-primary: #eaeaee; 12 - --text-secondary: #b7b6c5; 13 - --text-tertiary: #6e6d7a; 14 - --border: rgba(183, 182, 197, 0.12); 11 + --text-primary: #f8fafc; 12 + --text-secondary: #94a3b8; 13 + --text-tertiary: #64748b; 14 + --border: rgba(148, 163, 184, 0.12); 15 15 16 - --accent: #957a86; 17 - --accent-hover: #a98d98; 18 - --accent-subtle: rgba(149, 122, 134, 0.15); 16 + --accent: #8b5cf6; 17 + --accent-hover: #a78bfa; 18 + --accent-subtle: rgba(139, 92, 246, 0.15); 19 19 20 20 --highlight-yellow: #fbbf24; 21 21 --highlight-green: #34d399; ··· 25 25 } 26 26 27 27 :host(.light) { 28 - --bg-primary: #f8f8fa; 28 + --bg-primary: #f8fafc; 29 29 --bg-secondary: #ffffff; 30 - --bg-tertiary: #f0f0f4; 30 + --bg-tertiary: #f1f5f9; 31 31 --bg-card: #ffffff; 32 32 --bg-elevated: #ffffff; 33 - --bg-hover: #eeeef2; 33 + --bg-hover: #e2e8f0; 34 34 35 - --text-primary: #18171c; 36 - --text-secondary: #5c495a; 37 - --text-tertiary: #8a8494; 38 - --border: rgba(92, 73, 90, 0.12); 35 + --text-primary: #0f172a; 36 + --text-secondary: #64748b; 37 + --text-tertiary: #94a3b8; 38 + --border: rgba(100, 116, 139, 0.15); 39 39 40 - --accent: #7a5f6d; 41 - --accent-hover: #664e5b; 42 - --accent-subtle: rgba(149, 122, 134, 0.12); 40 + --accent: #7c3aed; 41 + --accent-hover: #6d28d9; 42 + --accent-subtle: rgba(124, 58, 237, 0.12); 43 43 } 44 44 45 45 .margin-overlay { ··· 63 63 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255,255,255,0.05); 64 64 z-index: 2147483647; 65 65 pointer-events: auto; 66 - font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 66 + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; 67 67 opacity: 0; 68 68 transform: translateY(8px) scale(0.95); 69 69 animation: toolbar-in 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; ··· 161 161 flex-direction: column; 162 162 pointer-events: auto; 163 163 z-index: 2147483647; 164 - font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 164 + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; 165 165 color: var(--text-primary); 166 166 opacity: 0; 167 167 transform: translateY(-8px) scale(0.96); ··· 369 369 box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255,255,255,0.05); 370 370 z-index: 2147483647; 371 371 pointer-events: auto; 372 - font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 372 + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; 373 373 color: var(--text-primary); 374 374 animation: modal-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; 375 375 overflow: hidden; ··· 539 539 border: 1px solid var(--border); 540 540 border-radius: 10px; 541 541 box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3); 542 - font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 542 + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; 543 543 font-size: 13px; 544 544 font-weight: 500; 545 545 color: var(--text-primary);
+49 -1
extension/tailwind.config.js
··· 4 4 theme: { 5 5 extend: { 6 6 fontFamily: { 7 - sans: ['IBM Plex Sans', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'], 7 + sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'], 8 + display: ['Outfit', 'system-ui', 'sans-serif'], 9 + }, 10 + colors: { 11 + primary: { 12 + 50: '#f5f3ff', 13 + 100: '#ede9fe', 14 + 200: '#ddd6fe', 15 + 300: '#c4b5fd', 16 + 400: '#a78bfa', 17 + 500: '#8b5cf6', 18 + 600: '#7c3aed', 19 + 700: '#6d28d9', 20 + 800: '#5b21b6', 21 + 900: '#4c1d95', 22 + 950: '#2e1065', 23 + }, 24 + surface: { 25 + 50: '#f8fafc', 26 + 100: '#f1f5f9', 27 + 200: '#e2e8f0', 28 + 300: '#cbd5e1', 29 + 400: '#94a3b8', 30 + 500: '#64748b', 31 + 600: '#475569', 32 + 700: '#334155', 33 + 800: '#1e293b', 34 + 900: '#0f172a', 35 + 950: '#020617', 36 + }, 37 + }, 38 + animation: { 39 + 'fade-in': 'fadeIn 0.3s ease-out', 40 + 'slide-up': 'slideUp 0.3s ease-out', 41 + 'scale-in': 'scaleIn 0.2s ease-out', 42 + }, 43 + keyframes: { 44 + fadeIn: { 45 + '0%': { opacity: '0' }, 46 + '100%': { opacity: '1' }, 47 + }, 48 + slideUp: { 49 + '0%': { opacity: '0', transform: 'translateY(8px)' }, 50 + '100%': { opacity: '1', transform: 'translateY(0)' }, 51 + }, 52 + scaleIn: { 53 + '0%': { opacity: '0', transform: 'scale(0.95)' }, 54 + '100%': { opacity: '1', transform: 'scale(1)' }, 55 + }, 8 56 }, 9 57 }, 10 58 },
+22 -19
extension/wxt.config.ts
··· 23 23 24 24 return { 25 25 name: 'Margin', 26 - description: 'Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.', 26 + description: 27 + 'Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.', 27 28 permissions: browser === 'firefox' ? basePermissions : chromePermissions, 28 29 host_permissions: ['https://margin.at/*', '*://*/*'], 29 30 icons: { ··· 72 73 128: '/icons/icon-128.png', 73 74 }, 74 75 }, 75 - ...(browser === 'chrome' ? { 76 - side_panel: { 77 - default_path: 'sidepanel.html', 78 - }, 79 - } : { 80 - sidebar_action: { 81 - default_title: 'Margin', 82 - default_panel: 'sidepanel.html', 83 - }, 84 - browser_specific_settings: { 85 - gecko: { 86 - id: 'hello@margin.at', 87 - strict_min_version: '140.0', 88 - data_collection_permissions: { 89 - required: ['none'], 76 + ...(browser === 'chrome' 77 + ? { 78 + side_panel: { 79 + default_path: 'sidepanel.html', 90 80 }, 91 - }, 92 - }, 93 - }), 81 + } 82 + : { 83 + sidebar_action: { 84 + default_title: 'Margin', 85 + default_panel: 'sidepanel.html', 86 + }, 87 + browser_specific_settings: { 88 + gecko: { 89 + id: 'hello@margin.at', 90 + strict_min_version: '140.0', 91 + data_collection_permissions: { 92 + required: ['none'], 93 + }, 94 + }, 95 + }, 96 + }), 94 97 }; 95 98 }, 96 99 });
+22
lexicons/at/margin/annotation.json
··· 48 48 }, 49 49 "maxLength": 10 50 50 }, 51 + "generator": { 52 + "type": "object", 53 + "description": "The client/agent that created this record", 54 + "properties": { 55 + "id": { 56 + "type": "string", 57 + "format": "uri" 58 + }, 59 + "name": { 60 + "type": "string" 61 + }, 62 + "homepage": { 63 + "type": "string", 64 + "format": "uri" 65 + } 66 + } 67 + }, 68 + "rights": { 69 + "type": "string", 70 + "format": "uri", 71 + "description": "License URI (e.g., https://creativecommons.org/licenses/by/4.0/)" 72 + }, 51 73 "createdAt": { 52 74 "type": "string", 53 75 "format": "datetime"
+3 -1
lexicons/at/margin/authFull.json
··· 21 21 "at.margin.like", 22 22 "at.margin.collection", 23 23 "at.margin.collectionItem", 24 - "at.margin.profile" 24 + "at.margin.profile", 25 + "at.margin.apikey", 26 + "at.margin.preferences" 25 27 ] 26 28 } 27 29 ]
+22
lexicons/at/margin/bookmark.json
··· 41 41 }, 42 42 "maxLength": 10 43 43 }, 44 + "generator": { 45 + "type": "object", 46 + "description": "The client/agent that created this record", 47 + "properties": { 48 + "id": { 49 + "type": "string", 50 + "format": "uri" 51 + }, 52 + "name": { 53 + "type": "string" 54 + }, 55 + "homepage": { 56 + "type": "string", 57 + "format": "uri" 58 + } 59 + } 60 + }, 61 + "rights": { 62 + "type": "string", 63 + "format": "uri", 64 + "description": "License URI (e.g., https://creativecommons.org/licenses/by/4.0/)" 65 + }, 44 66 "createdAt": { 45 67 "type": "string", 46 68 "format": "datetime"
+22
lexicons/at/margin/highlight.json
··· 31 31 }, 32 32 "maxLength": 10 33 33 }, 34 + "generator": { 35 + "type": "object", 36 + "description": "The client/agent that created this record", 37 + "properties": { 38 + "id": { 39 + "type": "string", 40 + "format": "uri" 41 + }, 42 + "name": { 43 + "type": "string" 44 + }, 45 + "homepage": { 46 + "type": "string", 47 + "format": "uri" 48 + } 49 + } 50 + }, 51 + "rights": { 52 + "type": "string", 53 + "format": "uri", 54 + "description": "License URI (e.g., https://creativecommons.org/licenses/by/4.0/)" 55 + }, 34 56 "createdAt": { 35 57 "type": "string", 36 58 "format": "datetime"
+426 -6
web/bun.lock
··· 23 23 "tailwindcss": "^3.4.19", 24 24 }, 25 25 "devDependencies": { 26 + "@eslint/js": "^10.0.1", 26 27 "@types/react": "^19.2.11", 27 28 "@types/react-dom": "^19.2.3", 29 + "@typescript-eslint/eslint-plugin": "^8.54.0", 30 + "@typescript-eslint/parser": "^8.54.0", 31 + "eslint": "^10.0.0", 32 + "eslint-config-prettier": "^10.1.8", 33 + "eslint-plugin-prettier": "^5.5.5", 34 + "eslint-plugin-react": "^7.37.5", 35 + "eslint-plugin-react-hooks": "^7.0.1", 36 + "eslint-plugin-react-refresh": "^0.5.0", 37 + "globals": "^17.3.0", 38 + "prettier": "^3.8.1", 28 39 "react-icons": "^5.5.0", 29 40 "typescript": "^5.9.3", 41 + "typescript-eslint": "^8.54.0", 30 42 }, 31 43 }, 32 44 }, ··· 143 155 144 156 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 145 157 158 + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], 159 + 160 + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], 161 + 162 + "@eslint/config-array": ["@eslint/config-array@0.23.1", "", { "dependencies": { "@eslint/object-schema": "^3.0.1", "debug": "^4.3.1", "minimatch": "^10.1.1" } }, "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA=="], 163 + 164 + "@eslint/config-helpers": ["@eslint/config-helpers@0.5.2", "", { "dependencies": { "@eslint/core": "^1.1.0" } }, "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ=="], 165 + 166 + "@eslint/core": ["@eslint/core@1.1.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw=="], 167 + 168 + "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], 169 + 170 + "@eslint/object-schema": ["@eslint/object-schema@3.0.1", "", {}, "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg=="], 171 + 172 + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.0", "", { "dependencies": { "@eslint/core": "^1.1.0", "levn": "^0.4.1" } }, "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ=="], 173 + 174 + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], 175 + 176 + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], 177 + 178 + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], 179 + 180 + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], 181 + 146 182 "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], 147 183 148 184 "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], ··· 193 229 194 230 "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], 195 231 232 + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], 233 + 234 + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="], 235 + 196 236 "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], 197 237 198 238 "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], ··· 213 253 214 254 "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], 215 255 256 + "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], 257 + 216 258 "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], 217 259 218 260 "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], ··· 321 363 322 364 "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], 323 365 366 + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], 367 + 324 368 "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 325 369 326 370 "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], 371 + 372 + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], 327 373 328 374 "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], 329 375 ··· 337 383 338 384 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], 339 385 386 + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="], 387 + 388 + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.54.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA=="], 389 + 390 + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.54.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.54.0", "@typescript-eslint/types": "^8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g=="], 391 + 392 + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0" } }, "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg=="], 393 + 394 + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.54.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw=="], 395 + 396 + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA=="], 397 + 398 + "@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="], 399 + 400 + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.54.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.54.0", "@typescript-eslint/tsconfig-utils": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA=="], 401 + 402 + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.54.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA=="], 403 + 404 + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="], 405 + 340 406 "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], 341 407 342 408 "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "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" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], 343 409 344 410 "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], 345 411 412 + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], 413 + 414 + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], 415 + 346 416 "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], 347 417 348 418 "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], ··· 359 429 360 430 "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], 361 431 432 + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], 433 + 434 + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], 435 + 362 436 "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], 363 437 438 + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], 439 + 440 + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], 441 + 442 + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], 443 + 444 + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], 445 + 446 + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], 447 + 364 448 "astro": ["astro@5.17.1", "", { "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.5", "@astrojs/markdown-remark": "6.3.10", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^4.0.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.3.1", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.1.1", "cssesc": "^3.0.0", "debug": "^4.4.3", "deterministic-object-hash": "^2.0.2", "devalue": "^5.6.2", "diff": "^8.0.3", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.4.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.1", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.1", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.3", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "svgo": "^4.0.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.3", "unist-util-visit": "^5.0.0", "unstorage": "^1.17.4", "vfile": "^6.0.3", "vite": "^6.4.1", "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.1", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "astro.js" } }, "sha512-oD3tlxTaVWGq/Wfbqk6gxzVRz98xa/rYlpe+gU2jXJMSD01k6sEDL01ZlT8mVSYB/rMgnvIOfiQQ3BbLdN237A=="], 365 449 450 + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], 451 + 366 452 "autoprefixer": ["autoprefixer@10.4.24", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw=="], 453 + 454 + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], 367 455 368 456 "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], 369 457 370 458 "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], 371 459 460 + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 461 + 372 462 "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], 373 463 374 464 "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], ··· 378 468 "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], 379 469 380 470 "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], 471 + 472 + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], 381 473 382 474 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 383 475 384 476 "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], 385 477 478 + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], 479 + 480 + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], 481 + 482 + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], 483 + 386 484 "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], 387 485 388 486 "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], ··· 413 511 414 512 "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], 415 513 514 + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], 515 + 416 516 "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], 417 517 418 518 "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], 419 519 420 520 "cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], 521 + 522 + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 421 523 422 524 "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], 423 525 ··· 432 534 "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], 433 535 434 536 "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], 537 + 538 + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], 539 + 540 + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], 541 + 542 + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], 435 543 436 544 "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], 437 545 ··· 439 547 440 548 "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], 441 549 550 + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 551 + 552 + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], 553 + 554 + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], 555 + 442 556 "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], 443 557 444 558 "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], ··· 461 575 462 576 "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], 463 577 578 + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], 579 + 464 580 "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], 465 581 466 582 "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], ··· 471 587 472 588 "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], 473 589 590 + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], 591 + 474 592 "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], 475 593 476 594 "electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="], ··· 483 601 484 602 "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], 485 603 604 + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], 605 + 606 + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], 607 + 608 + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], 609 + 610 + "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="], 611 + 486 612 "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], 487 613 614 + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], 615 + 616 + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], 617 + 618 + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], 619 + 620 + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], 621 + 488 622 "esbuild": ["esbuild@0.25.12", "", { "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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 489 623 490 624 "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 491 625 492 626 "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], 493 627 494 - "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], 628 + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 629 + 630 + "eslint": ["eslint@10.0.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.0", "@eslint/config-helpers": "^0.5.2", "@eslint/core": "^1.1.0", "@eslint/plugin-kit": "^0.6.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.0", "eslint-visitor-keys": "^5.0.0", "espree": "^11.1.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.1.1", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg=="], 631 + 632 + "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], 633 + 634 + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.5", "", { "dependencies": { "prettier-linter-helpers": "^1.0.1", "synckit": "^0.11.12" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw=="], 635 + 636 + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], 637 + 638 + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], 639 + 640 + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.0", "", { "peerDependencies": { "eslint": ">=9" } }, "sha512-ZYvmh7VfVgqR/7wR71I3Zl6hK/C5CcxdWYKZSpHawS5JCNgE4efhQWg/+/WPpgGAp9Ngp/rRZYyaIwmPQBq/lA=="], 641 + 642 + "eslint-scope": ["eslint-scope@9.1.0", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ=="], 643 + 644 + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.0", "", {}, "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q=="], 645 + 646 + "espree": ["espree@11.1.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw=="], 647 + 648 + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], 649 + 650 + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], 651 + 652 + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], 495 653 496 654 "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], 655 + 656 + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 497 657 498 658 "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 499 659 ··· 501 661 502 662 "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], 503 663 664 + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 665 + 666 + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], 667 + 504 668 "fast-glob": ["fast-glob@3.3.3", "", { "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" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], 505 669 670 + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], 671 + 672 + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], 673 + 506 674 "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], 507 675 508 676 "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 509 677 678 + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], 679 + 510 680 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 511 681 682 + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], 683 + 684 + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], 685 + 686 + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], 687 + 512 688 "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], 513 689 514 690 "fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], 515 691 516 692 "fontkitten": ["fontkitten@1.0.2", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-piJxbLnkD9Xcyi7dWJRnqszEURixe7CrF/efBfbffe2DPyabmuIuqraruY8cXTs19QoM8VJzx47BDRVNXETM7Q=="], 693 + 694 + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], 517 695 518 696 "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], 519 697 ··· 523 701 524 702 "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], 525 703 704 + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], 705 + 706 + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], 707 + 708 + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], 709 + 526 710 "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], 527 711 528 712 "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], 529 713 714 + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], 715 + 716 + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], 717 + 718 + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], 719 + 530 720 "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], 531 721 532 722 "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], 533 723 724 + "globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="], 725 + 726 + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], 727 + 728 + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], 729 + 534 730 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 535 731 536 732 "h3": ["h3@1.15.5", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="], 537 733 734 + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], 735 + 736 + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], 737 + 738 + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], 739 + 740 + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 741 + 742 + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], 743 + 538 744 "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 539 745 540 746 "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], ··· 557 763 558 764 "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], 559 765 766 + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], 767 + 768 + "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], 769 + 560 770 "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], 561 771 562 772 "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], ··· 565 775 566 776 "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], 567 777 778 + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], 779 + 568 780 "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], 781 + 782 + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], 569 783 570 784 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 571 785 786 + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], 787 + 572 788 "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], 573 789 790 + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], 791 + 792 + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], 793 + 794 + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], 795 + 574 796 "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], 575 797 798 + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], 799 + 800 + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], 801 + 576 802 "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], 577 803 804 + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], 805 + 806 + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], 807 + 578 808 "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], 579 809 580 810 "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], 811 + 812 + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], 581 813 582 814 "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], 583 815 816 + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], 817 + 584 818 "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], 585 819 586 820 "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], 587 821 822 + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], 823 + 824 + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], 825 + 588 826 "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], 589 827 828 + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], 829 + 590 830 "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], 591 831 832 + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], 833 + 834 + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], 835 + 836 + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], 837 + 838 + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], 839 + 840 + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], 841 + 842 + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], 843 + 844 + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], 845 + 846 + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], 847 + 848 + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], 849 + 592 850 "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], 593 851 852 + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], 853 + 854 + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 855 + 856 + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], 857 + 594 858 "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], 595 859 596 860 "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], ··· 599 863 600 864 "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], 601 865 866 + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], 867 + 868 + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], 869 + 870 + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], 871 + 602 872 "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], 603 873 874 + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], 875 + 876 + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], 877 + 604 878 "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], 879 + 880 + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], 605 881 606 882 "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], 607 883 ··· 631 907 632 908 "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], 633 909 910 + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], 911 + 634 912 "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], 913 + 914 + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], 635 915 636 916 "lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], 637 917 ··· 642 922 "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], 643 923 644 924 "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], 925 + 926 + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], 645 927 646 928 "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="], 647 929 ··· 735 1017 736 1018 "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], 737 1019 1020 + "minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="], 1021 + 738 1022 "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], 739 1023 740 1024 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], ··· 744 1028 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 745 1029 746 1030 "nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="], 1031 + 1032 + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], 747 1033 748 1034 "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], 749 1035 ··· 763 1049 764 1050 "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], 765 1051 1052 + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], 1053 + 1054 + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], 1055 + 1056 + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], 1057 + 1058 + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], 1059 + 1060 + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], 1061 + 1062 + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], 1063 + 766 1064 "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], 767 1065 768 1066 "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], ··· 772 1070 "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], 773 1071 774 1072 "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], 1073 + 1074 + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], 1075 + 1076 + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], 775 1077 776 1078 "p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], 777 1079 1080 + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], 1081 + 778 1082 "p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="], 779 1083 780 1084 "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], ··· 785 1089 786 1090 "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], 787 1091 1092 + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], 1093 + 1094 + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 1095 + 788 1096 "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], 789 1097 790 1098 "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], ··· 797 1105 798 1106 "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], 799 1107 1108 + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], 1109 + 800 1110 "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], 801 1111 802 1112 "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], ··· 810 1120 "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], 811 1121 812 1122 "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], 1123 + 1124 + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 1125 + 1126 + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], 1127 + 1128 + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], 813 1129 814 1130 "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], 815 1131 816 1132 "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], 817 1133 1134 + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], 1135 + 818 1136 "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], 1137 + 1138 + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 819 1139 820 1140 "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], 821 1141 ··· 828 1148 "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], 829 1149 830 1150 "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="], 1151 + 1152 + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], 831 1153 832 1154 "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], 833 1155 ··· 839 1161 840 1162 "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], 841 1163 1164 + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], 1165 + 842 1166 "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], 843 1167 844 1168 "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], 845 1169 846 1170 "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], 1171 + 1172 + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], 847 1173 848 1174 "rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="], 849 1175 ··· 863 1189 864 1190 "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], 865 1191 866 - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], 1192 + "resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], 867 1193 868 1194 "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], 869 1195 ··· 879 1205 880 1206 "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], 881 1207 1208 + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], 1209 + 1210 + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], 1211 + 1212 + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], 1213 + 882 1214 "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], 883 1215 884 1216 "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], 885 1217 886 - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 1218 + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 887 1219 888 1220 "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], 889 1221 ··· 891 1223 892 1224 "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], 893 1225 1226 + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], 1227 + 1228 + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], 1229 + 1230 + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], 1231 + 894 1232 "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], 895 1233 896 1234 "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], 897 1235 1236 + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 1237 + 1238 + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 1239 + 898 1240 "shiki": ["shiki@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/engine-javascript": "3.22.0", "@shikijs/engine-oniguruma": "3.22.0", "@shikijs/langs": "3.22.0", "@shikijs/themes": "3.22.0", "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g=="], 899 1241 1242 + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], 1243 + 1244 + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], 1245 + 1246 + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], 1247 + 1248 + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], 1249 + 900 1250 "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], 901 1251 902 1252 "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="], ··· 906 1256 "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], 907 1257 908 1258 "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], 1259 + 1260 + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], 909 1261 910 1262 "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], 911 1263 1264 + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], 1265 + 1266 + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], 1267 + 1268 + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], 1269 + 1270 + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], 1271 + 1272 + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], 1273 + 912 1274 "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], 913 1275 914 1276 "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], ··· 918 1280 "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], 919 1281 920 1282 "svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="], 1283 + 1284 + "synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="], 921 1285 922 1286 "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], 923 1287 ··· 942 1306 "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], 943 1307 944 1308 "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], 1309 + 1310 + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], 945 1311 946 1312 "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], 947 1313 ··· 949 1315 950 1316 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 951 1317 1318 + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 1319 + 952 1320 "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], 953 1321 1322 + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], 1323 + 1324 + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], 1325 + 1326 + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], 1327 + 1328 + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], 1329 + 954 1330 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 955 1331 1332 + "typescript-eslint": ["typescript-eslint@8.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="], 1333 + 956 1334 "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], 957 1335 958 1336 "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="], 959 1337 1338 + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], 1339 + 960 1340 "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], 961 1341 962 1342 "unified": ["unified@11.0.5", "", { "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" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], ··· 985 1365 986 1366 "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], 987 1367 1368 + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], 1369 + 988 1370 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 989 1371 990 1372 "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], ··· 999 1381 1000 1382 "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], 1001 1383 1384 + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 1385 + 1386 + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], 1387 + 1388 + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], 1389 + 1390 + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], 1391 + 1002 1392 "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], 1003 1393 1394 + "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], 1395 + 1004 1396 "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], 1397 + 1398 + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], 1005 1399 1006 1400 "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], 1007 1401 ··· 1025 1419 1026 1420 "zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="], 1027 1421 1422 + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], 1423 + 1028 1424 "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], 1029 1425 1030 - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 1031 - 1032 1426 "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], 1033 1427 1034 - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 1428 + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 1035 1429 1036 1430 "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], 1037 1431 ··· 1052 1446 "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 1053 1447 1054 1448 "@tailwindcss/vite/tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], 1449 + 1450 + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 1451 + 1452 + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 1453 + 1454 + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], 1055 1455 1056 1456 "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 1057 1457 1058 1458 "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 1059 1459 1460 + "astro/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 1461 + 1060 1462 "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 1061 1463 1062 1464 "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], 1063 1465 1064 1466 "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 1065 1467 1468 + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 1469 + 1470 + "eslint-plugin-react/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], 1471 + 1066 1472 "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 1067 1473 1474 + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], 1475 + 1068 1476 "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 1069 1477 1478 + "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], 1479 + 1480 + "postcss-import/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], 1481 + 1070 1482 "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 1483 + 1484 + "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 1071 1485 1072 1486 "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], 1073 1487 1488 + "tailwindcss/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], 1489 + 1074 1490 "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], 1075 1491 1492 + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 1493 + 1076 1494 "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 1077 1495 1078 1496 "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 1079 1497 1080 1498 "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], 1499 + 1500 + "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 1081 1501 1082 1502 "unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], 1083 1503
+51
web/eslint.config.js
··· 1 + import js from "@eslint/js"; 2 + import globals from "globals"; 3 + import reactHooks from "eslint-plugin-react-hooks"; 4 + import reactRefresh from "eslint-plugin-react-refresh"; 5 + import tseslint from "typescript-eslint"; 6 + import prettierPlugin from "eslint-plugin-prettier"; 7 + import prettierConfig from "eslint-config-prettier"; 8 + import reactPlugin from "eslint-plugin-react"; 9 + 10 + export default tseslint.config( 11 + { ignores: ["dist", ".astro", "node_modules"] }, 12 + { 13 + extends: [js.configs.recommended, ...tseslint.configs.recommended], 14 + files: ["src/**/*.{ts,tsx}"], 15 + languageOptions: { 16 + ecmaVersion: 2020, 17 + globals: globals.browser, 18 + parserOptions: { 19 + ecmaFeatures: { 20 + jsx: true, 21 + }, 22 + }, 23 + }, 24 + plugins: { 25 + "react-hooks": reactHooks, 26 + "react-refresh": reactRefresh, 27 + react: reactPlugin, 28 + prettier: prettierPlugin, 29 + }, 30 + rules: { 31 + ...reactHooks.configs.recommended.rules, 32 + "react-refresh/only-export-components": [ 33 + "warn", 34 + { allowConstantExport: true }, 35 + ], 36 + "react/react-in-jsx-scope": "off", 37 + "@typescript-eslint/explicit-module-boundary-types": "off", 38 + "@typescript-eslint/no-unused-vars": [ 39 + "warn", 40 + { argsIgnorePattern: "^_" }, 41 + ], 42 + "prettier/prettier": "error", 43 + }, 44 + settings: { 45 + react: { 46 + version: "detect", 47 + }, 48 + }, 49 + }, 50 + prettierConfig, 51 + );
+15 -2
web/package.json
··· 6 6 "dev": "astro dev", 7 7 "build": "astro build", 8 8 "preview": "astro preview", 9 - "astro": "astro" 9 + "astro": "astro", 10 + "lint": "eslint 'src/**/*.{ts,tsx,js,jsx}' --fix" 10 11 }, 11 12 "dependencies": { 12 13 "@astrojs/node": "^9.5.2", ··· 28 29 "tailwindcss": "^3.4.19" 29 30 }, 30 31 "devDependencies": { 32 + "@eslint/js": "^10.0.1", 31 33 "@types/react": "^19.2.11", 32 34 "@types/react-dom": "^19.2.3", 35 + "@typescript-eslint/eslint-plugin": "^8.54.0", 36 + "@typescript-eslint/parser": "^8.54.0", 37 + "eslint": "^10.0.0", 38 + "eslint-config-prettier": "^10.1.8", 39 + "eslint-plugin-prettier": "^5.5.5", 40 + "eslint-plugin-react": "^7.37.5", 41 + "eslint-plugin-react-hooks": "^7.0.1", 42 + "eslint-plugin-react-refresh": "^0.5.0", 43 + "globals": "^17.3.0", 44 + "prettier": "^3.8.1", 33 45 "react-icons": "^5.5.0", 34 - "typescript": "^5.9.3" 46 + "typescript": "^5.9.3", 47 + "typescript-eslint": "^8.54.0" 35 48 } 36 49 }
+2 -10
web/src/App.tsx
··· 19 19 AnnotationDetailWrapper, 20 20 UserUrlWrapper, 21 21 } from "./routes/wrappers"; 22 - 23 - function PageHeader({ title }: { title: string }) { 24 - return ( 25 - <div className="max-w-2xl mx-auto mb-6 text-center lg:text-left"> 26 - <h1 className="text-3xl font-display font-bold text-surface-900 dark:text-white"> 27 - {title} 28 - </h1> 29 - </div> 30 - ); 31 - } 22 + import About from "./views/About"; 32 23 33 24 export default function App() { 34 25 React.useEffect(() => { ··· 40 31 <BrowserRouter> 41 32 <Routes> 42 33 <Route path="/login" element={<Login />} /> 34 + <Route path="/about" element={<About />} /> 43 35 <Route path="/auth/*" element={<div>Redirecting...</div>} /> 44 36 45 37 <Route
+162 -55
web/src/api/client.ts
··· 4 4 FeedResponse, 5 5 AnnotationItem, 6 6 Collection, 7 + NotificationItem, 8 + Target, 9 + Selector, 7 10 } from "../types"; 8 11 export type { Collection } from "../types"; 9 12 ··· 47 50 } 48 51 49 52 try { 50 - const marginProfile = await getProfile(data.did); 51 - if (marginProfile) { 52 - if (marginProfile.description) 53 - baseProfile.description = marginProfile.description; 54 - if (marginProfile.followersCount) 55 - baseProfile.followersCount = marginProfile.followersCount; 56 - if (marginProfile.followsCount) 57 - baseProfile.followsCount = marginProfile.followsCount; 58 - if (marginProfile.postsCount) 59 - baseProfile.postsCount = marginProfile.postsCount; 60 - if (marginProfile.website) 61 - baseProfile.website = marginProfile.website; 62 - if (marginProfile.links) baseProfile.links = marginProfile.links; 53 + const res = await fetch(`/api/profile/${data.did}`); 54 + if (res.ok) { 55 + const marginProfile = await res.json(); 56 + if (marginProfile) { 57 + if (marginProfile.description) 58 + baseProfile.description = marginProfile.description; 59 + if (marginProfile.followersCount) 60 + baseProfile.followersCount = marginProfile.followersCount; 61 + if (marginProfile.followsCount) 62 + baseProfile.followsCount = marginProfile.followsCount; 63 + if (marginProfile.postsCount) 64 + baseProfile.postsCount = marginProfile.postsCount; 65 + if (marginProfile.website) 66 + baseProfile.website = marginProfile.website; 67 + if (marginProfile.links) baseProfile.links = marginProfile.links; 68 + } 63 69 } 64 - } catch (e) {} 70 + } catch (e) { 71 + console.debug("Failed to fetch Margin profile:", e); 72 + } 65 73 66 74 sessionAtom.set(baseProfile); 67 75 return baseProfile; ··· 70 78 sessionAtom.set(null); 71 79 return null; 72 80 } catch (e) { 81 + console.error("Session check failed:", e); 73 82 sessionAtom.set(null); 74 83 return null; 75 84 } ··· 77 86 78 87 async function apiRequest( 79 88 path: string, 80 - options: RequestInit = {}, 89 + options: RequestInit & { skipAuthRedirect?: boolean } = {}, 81 90 ): Promise<Response> { 91 + const { skipAuthRedirect, ...fetchOptions } = options; 82 92 const headers = { 83 93 "Content-Type": "application/json", 84 - ...(options.headers || {}), 94 + ...(fetchOptions.headers || {}), 85 95 }; 86 96 87 97 const apiPath = 88 98 path.startsWith("/api") || path.startsWith("/auth") ? path : `/api${path}`; 89 99 90 100 const response = await fetch(apiPath, { 91 - ...options, 101 + ...fetchOptions, 92 102 headers, 93 103 }); 94 104 95 - if (response.status === 401) { 105 + if (response.status === 401 && !skipAuthRedirect) { 96 106 sessionAtom.set(null); 97 - window.location.href = "/login"; 107 + if (window.location.pathname !== "/login") { 108 + window.location.href = "/login"; 109 + } 98 110 } 99 111 100 112 return response; ··· 110 122 creator?: string; 111 123 } 112 124 113 - function normalizeItem(raw: any): AnnotationItem { 125 + interface RawItem { 126 + type?: string; 127 + collectionUri?: string; 128 + annotation?: RawItem; 129 + highlight?: RawItem; 130 + bookmark?: RawItem; 131 + uri?: string; 132 + id?: string; 133 + cid?: string; 134 + author?: UserProfile; 135 + creator?: UserProfile; 136 + collection?: { 137 + uri: string; 138 + name: string; 139 + icon?: string; 140 + }; 141 + created?: string; 142 + createdAt?: string; 143 + target?: string | { source?: string; title?: string; selector?: Selector }; 144 + url?: string; 145 + targetUrl?: string; 146 + title?: string; 147 + selector?: Selector; 148 + viewer?: { like?: string; [key: string]: unknown }; 149 + viewerHasLiked?: boolean; 150 + motivation?: string; 151 + [key: string]: unknown; 152 + } 153 + 154 + function normalizeItem(raw: RawItem): AnnotationItem { 114 155 if (raw.type === "CollectionItem" || raw.collectionUri) { 115 156 const inner = raw.annotation || raw.highlight || raw.bookmark || {}; 116 157 const normalizedInner = normalizeItem(inner); 117 158 118 159 return { 119 160 ...normalizedInner, 120 - uri: normalizedInner.uri || raw.uri, 121 - author: normalizedInner.author || raw.author, 161 + uri: normalizedInner.uri || raw.uri || "", 162 + cid: raw.cid || "", 163 + author: (normalizedInner.author || 164 + raw.author || 165 + raw.creator) as UserProfile, 122 166 collection: raw.collection 123 167 ? { 124 168 uri: raw.collection.uri, ··· 127 171 } 128 172 : undefined, 129 173 addedBy: raw.creator || raw.author, 130 - createdAt: raw.created || raw.createdAt, 174 + createdAt: raw.created || raw.createdAt || new Date().toISOString(), 131 175 collectionItemUri: raw.uri, 132 176 }; 133 177 } 134 178 135 - let target = raw.target; 179 + let target: Target | undefined; 180 + 181 + if (raw.target) { 182 + if (typeof raw.target === "string") { 183 + target = { source: raw.target, title: raw.title, selector: raw.selector }; 184 + } else { 185 + target = { 186 + source: raw.target.source || "", 187 + title: raw.target.title || raw.title, 188 + selector: raw.target.selector || raw.selector, 189 + }; 190 + } 191 + } 192 + 136 193 if (!target || !target.source) { 137 194 const url = 138 195 raw.url || ··· 141 198 if (url) { 142 199 target = { 143 200 source: url, 144 - title: raw.title || raw.target?.title, 145 - selector: raw.selector || raw.target?.selector, 201 + title: 202 + raw.title || 203 + (typeof raw.target !== "string" ? raw.target?.title : undefined), 204 + selector: 205 + raw.selector || 206 + (typeof raw.target !== "string" ? raw.target?.selector : undefined), 146 207 }; 147 208 } 148 209 } 149 210 150 211 return { 151 212 ...raw, 152 - uri: raw.id || raw.uri, 153 - author: raw.creator || raw.author, 154 - createdAt: raw.created || raw.createdAt, 155 - target: target || raw.target, 213 + uri: raw.id || raw.uri || "", 214 + cid: raw.cid || "", 215 + author: (raw.creator || raw.author) as UserProfile, 216 + createdAt: raw.created || raw.createdAt || new Date().toISOString(), 217 + target: target, 156 218 viewer: raw.viewer || { like: raw.viewerHasLiked ? "true" : undefined }, 219 + motivation: raw.motivation || "highlighting", 157 220 }; 158 221 } 159 222 ··· 178 241 const endpoint = source ? "/api/targets" : "/api/annotations/feed"; 179 242 180 243 try { 181 - const res = await apiRequest(`${endpoint}?${params.toString()}`); 244 + const res = await apiRequest(`${endpoint}?${params.toString()}`, { 245 + skipAuthRedirect: true, 246 + }); 182 247 if (!res.ok) throw new Error("Failed to fetch feed"); 183 248 const data = await res.json(); 184 249 return { ··· 214 279 if (!res.ok) throw new Error(await res.text()); 215 280 const raw = await res.json(); 216 281 return normalizeItem(raw); 217 - } catch (e: any) { 282 + } catch (e) { 218 283 console.error(e); 219 - return { error: e.message }; 284 + return { error: e instanceof Error ? e.message : "Unknown error" }; 220 285 } 221 286 } 222 287 ··· 243 308 if (!res.ok) throw new Error(await res.text()); 244 309 const raw = await res.json(); 245 310 return normalizeItem(raw); 246 - } catch (e: any) { 311 + } catch (e) { 247 312 console.error(e); 248 - return { error: e.message }; 313 + return { error: e instanceof Error ? e.message : "Unknown error" }; 249 314 } 250 315 } 251 316 ··· 266 331 if (!res.ok) throw new Error(await res.text()); 267 332 const raw = await res.json(); 268 333 return normalizeItem(raw); 269 - } catch (e: any) { 334 + } catch (e) { 270 335 console.error(e); 271 - return { error: e.message }; 336 + return { error: e instanceof Error ? e.message : "Unknown error" }; 272 337 } 273 338 } 274 339 275 - export async function uploadAvatar(file: File): Promise<{ blob: any }> { 340 + export async function uploadAvatar( 341 + file: File, 342 + ): Promise<{ blob: Blob | string }> { 276 343 const formData = new FormData(); 277 344 formData.append("file", file); 278 345 const res = await fetch("/api/upload/avatar", { ··· 289 356 export async function updateProfile(updates: { 290 357 displayName?: string; 291 358 description?: string; 292 - avatar?: any; 359 + avatar?: Blob | string | null; 293 360 website?: string; 294 361 links?: string[]; 295 362 }): Promise<boolean> { ··· 313 380 }); 314 381 return res.ok; 315 382 } catch (e) { 383 + console.error("Failed to like item:", e); 316 384 return false; 317 385 } 318 386 } ··· 327 395 ); 328 396 return res.ok; 329 397 } catch (e) { 398 + console.error("Failed to unlike item:", e); 330 399 return false; 331 400 } 332 401 } 333 402 334 403 export async function deleteItem( 335 404 uri: string, 336 - type: string = "annotation", 405 + _type: string = "annotation", 337 406 ): Promise<boolean> { 338 407 const rkey = (uri || "").split("/").pop(); 339 408 let endpoint = "/api/annotations"; ··· 346 415 }); 347 416 return res.ok; 348 417 } catch (e) { 418 + console.error("Failed to delete item:", e); 349 419 return false; 350 420 } 351 421 } ··· 365 435 ); 366 436 return res.ok; 367 437 } catch (e) { 438 + console.error("Failed to update annotation:", e); 368 439 return false; 369 440 } 370 441 } ··· 384 455 ); 385 456 return res.ok; 386 457 } catch (e) { 458 + console.error("Failed to update highlight:", e); 387 459 return false; 388 460 } 389 461 } ··· 404 476 ); 405 477 return res.ok; 406 478 } catch (e) { 479 + console.error("Failed to save bookmark:", e); 407 480 return false; 408 481 } 409 482 } ··· 418 491 if (!res.ok) return []; 419 492 return await res.json(); 420 493 } catch (e) { 494 + console.error("Failed to fetch containing collections:", e); 421 495 return []; 422 496 } 423 497 } 424 498 425 - export async function getEditHistory(uri: string): Promise<any[]> { 499 + import type { EditHistoryItem } from "../types"; 500 + 501 + export async function getEditHistory(uri: string): Promise<EditHistoryItem[]> { 426 502 try { 427 503 const res = await apiRequest( 428 504 `/api/annotations/history?uri=${encodeURIComponent(uri)}`, ··· 430 506 if (!res.ok) return []; 431 507 return await res.json(); 432 508 } catch (e) { 509 + console.error("Failed to fetch edit history:", e); 433 510 return []; 434 511 } 435 512 } ··· 440 517 if (!res.ok) return null; 441 518 return await res.json(); 442 519 } catch (e) { 520 + console.error("Failed to fetch profile:", e); 443 521 return null; 444 522 } 445 523 } ··· 472 550 if (!res.ok) throw new Error("Search failed"); 473 551 return await res.json(); 474 552 } catch (e) { 553 + console.error("Failed to search actors:", e); 475 554 return { actors: [] }; 476 555 } 477 556 } ··· 485 564 const data = await res.json(); 486 565 return data.did; 487 566 } catch (e) { 567 + console.error("Failed to resolve handle:", e); 488 568 return null; 489 569 } 490 570 } ··· 511 591 return await res.json(); 512 592 } 513 593 514 - export async function getNotifications(limit = 50, offset = 0): Promise<any[]> { 594 + export async function getNotifications( 595 + limit = 50, 596 + offset = 0, 597 + ): Promise<NotificationItem[]> { 515 598 try { 516 599 const res = await apiRequest( 517 600 `/api/notifications?limit=${limit}&offset=${offset}`, 518 601 ); 519 602 if (!res.ok) throw new Error("Failed to fetch notifications"); 520 603 const data = await res.json(); 521 - return (data.items || []).map((n: any) => ({ 604 + return (data.items || []).map((n: NotificationItem) => ({ 522 605 ...n, 523 - subject: n.subject ? normalizeItem(n.subject) : undefined, 606 + subject: n.subject ? normalizeItem(n.subject as RawItem) : undefined, 524 607 })); 525 608 } catch (e) { 609 + console.error("Failed to fetch notifications:", e); 526 610 return []; 527 611 } 528 612 } 529 613 530 614 export async function getUnreadNotificationCount(): Promise<number> { 531 615 try { 532 - const res = await apiRequest("/api/notifications/count"); 616 + const res = await apiRequest("/api/notifications/count", { 617 + skipAuthRedirect: true, 618 + }); 533 619 if (!res.ok) return 0; 534 620 const data = await res.json(); 535 621 return data.count || 0; 536 622 } catch (e) { 623 + console.error("Failed to fetch unread notification count:", e); 537 624 return 0; 538 625 } 539 626 } ··· 543 630 const res = await apiRequest("/api/notifications/read", { method: "POST" }); 544 631 return res.ok; 545 632 } catch (e) { 633 + console.error("Failed to mark notifications as read:", e); 546 634 return false; 547 635 } 548 636 } 549 637 550 638 export interface APIKey { 551 639 id: string; 552 - alias: string; 640 + name: string; 553 641 key?: string; 554 642 createdAt: string; 555 643 } ··· 561 649 const data = await res.json(); 562 650 return Array.isArray(data) ? data : data.keys || []; 563 651 } catch (e) { 652 + console.error("Failed to fetch API keys:", e); 564 653 return []; 565 654 } 566 655 } 567 656 568 - export async function createAPIKey(alias: string): Promise<APIKey | null> { 657 + export async function createAPIKey(name: string): Promise<APIKey | null> { 569 658 try { 570 659 const res = await apiRequest("/api/keys", { 571 660 method: "POST", 572 - body: JSON.stringify({ alias }), 661 + body: JSON.stringify({ name }), 573 662 }); 574 663 if (!res.ok) return null; 575 664 return await res.json(); 576 665 } catch (e) { 666 + console.error("Failed to create API key:", e); 577 667 return null; 578 668 } 579 669 } ··· 583 673 const res = await apiRequest(`/api/keys/${id}`, { method: "DELETE" }); 584 674 return res.ok; 585 675 } catch (e) { 676 + console.error("Failed to delete API key:", e); 586 677 return false; 587 678 } 588 679 } ··· 594 685 595 686 export async function getTrendingTags(limit = 10): Promise<Tag[]> { 596 687 try { 597 - const res = await apiRequest(`/api/tags/trending?limit=${limit}`); 688 + const res = await apiRequest(`/api/tags/trending?limit=${limit}`, { 689 + skipAuthRedirect: true, 690 + }); 598 691 if (!res.ok) return []; 599 692 const data = await res.json(); 600 693 return Array.isArray(data) ? data : data.tags || []; 601 694 } catch (e) { 695 + console.error("Failed to fetch trending tags:", e); 602 696 return []; 603 697 } 604 698 } ··· 609 703 const res = await apiRequest(`/api/collections${query}`); 610 704 if (!res.ok) throw new Error("Failed to fetch collections"); 611 705 const data = await res.json(); 612 - return Array.isArray(data) ? data : data.items || []; 706 + let items = Array.isArray(data) 707 + ? data 708 + : data.items || data.collections || []; 709 + 710 + items = items.map((item: Record<string, unknown>) => { 711 + if (!item.id && item.uri) { 712 + item.id = (item.uri as string).split("/").pop(); 713 + } 714 + return item; 715 + }); 716 + 717 + return items; 613 718 } catch (e) { 614 719 console.error(e); 615 720 return []; ··· 778 883 ); 779 884 if (!res.ok) return null; 780 885 return normalizeItem(await res.json()); 781 - } catch (e) { 886 + } catch { 782 887 return null; 783 888 } 784 889 } ··· 791 896 if (!res.ok) return { items: [] }; 792 897 const data = await res.json(); 793 898 return { items: (data.items || []).map(normalizeItem) }; 794 - } catch (e) { 899 + } catch { 795 900 return { items: [] }; 796 901 } 797 902 } ··· 811 916 annotations: (data.annotations || []).map(normalizeItem), 812 917 highlights: (data.highlights || []).map(normalizeItem), 813 918 }; 814 - } catch (e) { 919 + } catch { 815 920 return { annotations: [], highlights: [] }; 816 921 } 817 922 } ··· 832 937 annotations: (data.annotations || []).map(normalizeItem), 833 938 highlights: (data.highlights || []).map(normalizeItem), 834 939 }; 835 - } catch (e) { 940 + } catch { 836 941 return { annotations: [], highlights: [] }; 837 942 } 838 943 } ··· 840 945 externalLinkSkippedHostnames?: string[]; 841 946 }> { 842 947 try { 843 - const res = await apiRequest("/api/preferences"); 948 + const res = await apiRequest("/api/preferences", { 949 + skipAuthRedirect: true, 950 + }); 844 951 if (!res.ok) return {}; 845 952 return await res.json(); 846 953 } catch (e) {
+27 -9
web/src/components/common/Card.tsx
··· 39 39 const [showExternalLinkModal, setShowExternalLinkModal] = useState(false); 40 40 const [externalLinkUrl, setExternalLinkUrl] = useState<string | null>(null); 41 41 42 + React.useEffect(() => { 43 + setLiked(!!item.viewer?.like); 44 + setLikes(item.likeCount || 0); 45 + }, [item.viewer?.like, item.likeCount]); 46 + 42 47 const type = 43 48 item.motivation === "highlighting" 44 49 ? "highlight" ··· 76 81 e.stopPropagation(); 77 82 78 83 try { 79 - const hostname = new URL(url).hostname; 80 - const skipped = $preferences.get().externalLinkSkippedHostnames; 81 - if (skipped.includes(hostname)) { 82 - window.open(url, "_blank", "noopener,noreferrer"); 83 - return; 84 + const hostname = safeUrlHostname(url); 85 + if (hostname) { 86 + const skipped = $preferences.get().externalLinkSkippedHostnames; 87 + if (skipped.includes(hostname)) { 88 + window.open(url, "_blank", "noopener,noreferrer"); 89 + return; 90 + } 84 91 } 85 - } catch { 86 - // ignore 92 + } catch (err) { 93 + if (err instanceof Error && err.name !== "TypeError") { 94 + console.debug("Failed to check skipped hostname:", err); 95 + } 87 96 } 88 97 89 98 setExternalLinkUrl(url); ··· 103 112 104 113 const detailUrl = `/${item.author?.handle || item.author?.did}/${type}/${(item.uri || "").split("/").pop()}`; 105 114 115 + const safeUrlHostname = (url: string | null | undefined) => { 116 + if (!url) return null; 117 + try { 118 + return new URL(url).hostname; 119 + } catch { 120 + return null; 121 + } 122 + }; 123 + 106 124 const pageUrl = item.target?.source || item.source; 107 125 const pageTitle = 108 126 item.target?.title || 109 127 item.title || 110 - (pageUrl ? new URL(pageUrl).hostname : null); 128 + (pageUrl ? safeUrlHostname(pageUrl) : null); 111 129 const pageHostname = pageUrl 112 - ? new URL(pageUrl).hostname.replace("www.", "") 130 + ? safeUrlHostname(pageUrl)?.replace("www.", "") 113 131 : null; 114 132 const isBookmark = type === "bookmark"; 115 133
+2 -85
web/src/components/common/CollectionIcon.tsx
··· 1 1 import React from "react"; 2 - import { 3 - Folder, 4 - Star, 5 - Heart, 6 - Bookmark, 7 - Lightbulb, 8 - Zap, 9 - Coffee, 10 - Music, 11 - Camera, 12 - Code, 13 - Globe, 14 - Flag, 15 - Tag, 16 - Box, 17 - Archive, 18 - FileText, 19 - Image, 20 - Video, 21 - Mail, 22 - MapPin, 23 - Calendar, 24 - Clock, 25 - Search, 26 - Settings, 27 - User, 28 - Users, 29 - Home, 30 - Briefcase, 31 - Gift, 32 - Award, 33 - Target, 34 - TrendingUp, 35 - Activity, 36 - Cpu, 37 - Database, 38 - Cloud, 39 - Sun, 40 - Moon, 41 - Flame, 42 - Leaf, 43 - } from "lucide-react"; 44 - 45 - export const ICON_MAP: Record<string, React.ElementType> = { 46 - folder: Folder, 47 - star: Star, 48 - heart: Heart, 49 - bookmark: Bookmark, 50 - lightbulb: Lightbulb, 51 - zap: Zap, 52 - coffee: Coffee, 53 - music: Music, 54 - camera: Camera, 55 - code: Code, 56 - globe: Globe, 57 - flag: Flag, 58 - tag: Tag, 59 - box: Box, 60 - archive: Archive, 61 - file: FileText, 62 - image: Image, 63 - video: Video, 64 - mail: Mail, 65 - pin: MapPin, 66 - calendar: Calendar, 67 - clock: Clock, 68 - search: Search, 69 - settings: Settings, 70 - user: User, 71 - users: Users, 72 - home: Home, 73 - briefcase: Briefcase, 74 - gift: Gift, 75 - award: Award, 76 - target: Target, 77 - trending: TrendingUp, 78 - activity: Activity, 79 - cpu: Cpu, 80 - database: Database, 81 - cloud: Cloud, 82 - sun: Sun, 83 - moon: Moon, 84 - flame: Flame, 85 - leaf: Leaf, 86 - }; 2 + import { Folder } from "lucide-react"; 3 + import { ICON_MAP } from "./iconMap"; 87 4 88 5 interface CollectionIconProps { 89 6 icon?: string;
+14
web/src/components/common/Icons.tsx
··· 554 554 ); 555 555 } 556 556 557 + export function AppleIcon({ size = 18 }: IconProps) { 558 + return ( 559 + <svg 560 + width={size} 561 + height={size} 562 + viewBox="0 0 24 24" 563 + fill="currentColor" 564 + xmlns="http://www.w3.org/2000/svg" 565 + > 566 + <path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" /> 567 + </svg> 568 + ); 569 + } 570 + 557 571 export function DeerIcon({ size = 18 }: IconProps) { 558 572 return ( 559 573 <svg fill="none" viewBox="0 0 512 512" width={size} height={size}>
+85
web/src/components/common/iconMap.ts
··· 1 + import { 2 + Folder, 3 + Star, 4 + Heart, 5 + Bookmark, 6 + Lightbulb, 7 + Zap, 8 + Coffee, 9 + Music, 10 + Camera, 11 + Code, 12 + Globe, 13 + Flag, 14 + Tag, 15 + Box, 16 + Archive, 17 + FileText, 18 + Image, 19 + Video, 20 + Mail, 21 + MapPin, 22 + Calendar, 23 + Clock, 24 + Search, 25 + Settings, 26 + User, 27 + Users, 28 + Home, 29 + Briefcase, 30 + Gift, 31 + Award, 32 + Target, 33 + TrendingUp, 34 + Activity, 35 + Cpu, 36 + Database, 37 + Cloud, 38 + Sun, 39 + Moon, 40 + Flame, 41 + Leaf, 42 + } from "lucide-react"; 43 + 44 + export const ICON_MAP: Record<string, React.ElementType> = { 45 + folder: Folder, 46 + star: Star, 47 + heart: Heart, 48 + bookmark: Bookmark, 49 + lightbulb: Lightbulb, 50 + zap: Zap, 51 + coffee: Coffee, 52 + music: Music, 53 + camera: Camera, 54 + code: Code, 55 + globe: Globe, 56 + flag: Flag, 57 + tag: Tag, 58 + box: Box, 59 + archive: Archive, 60 + file: FileText, 61 + image: Image, 62 + video: Video, 63 + mail: Mail, 64 + pin: MapPin, 65 + calendar: Calendar, 66 + clock: Clock, 67 + search: Search, 68 + settings: Settings, 69 + user: User, 70 + users: Users, 71 + home: Home, 72 + briefcase: Briefcase, 73 + gift: Gift, 74 + award: Award, 75 + target: Target, 76 + trending: TrendingUp, 77 + activity: Activity, 78 + cpu: Cpu, 79 + database: Database, 80 + cloud: Cloud, 81 + sun: Sun, 82 + moon: Moon, 83 + flame: Flame, 84 + leaf: Leaf, 85 + };
+13 -4
web/src/components/feed/Composer.tsx
··· 1 1 import React, { useState } from "react"; 2 2 import { createAnnotation, createHighlight } from "../../api/client"; 3 + import type { Selector } from "../../types"; 3 4 import { X } from "lucide-react"; 4 5 5 6 interface ComposerProps { 6 7 url: string; 7 - selector?: any; 8 + selector?: Selector | null; 8 9 onSuccess?: () => void; 9 10 onCancel?: () => void; 10 11 } ··· 48 49 .filter(Boolean); 49 50 50 51 if (!text.trim()) { 52 + if (!finalSelector) throw new Error("No text selected"); 51 53 await createHighlight({ 52 54 url, 53 - selector: finalSelector, 55 + selector: finalSelector as { 56 + exact: string; 57 + prefix?: string; 58 + suffix?: string; 59 + }, 54 60 color: "yellow", 55 61 tags: tagList, 56 62 }); ··· 67 73 setQuoteText(""); 68 74 setSelector(null); 69 75 if (onSuccess) onSuccess(); 70 - } catch (err: any) { 71 - setError(err.message || "Failed to post"); 76 + } catch (err) { 77 + setError( 78 + (err instanceof Error ? err.message : "Unknown error") || 79 + "Failed to post", 80 + ); 72 81 } finally { 73 82 setLoading(false); 74 83 }
+134 -63
web/src/components/feed/MasonryFeed.tsx
··· 3 3 import Card from "../common/Card"; 4 4 import { Loader2 } from "lucide-react"; 5 5 import { useStore } from "@nanostores/react"; 6 - import { $user, initAuth } from "../../store/auth"; 6 + import { $user } from "../../store/auth"; 7 7 import type { AnnotationItem } from "../../types"; 8 8 import { Tabs, EmptyState } from "../ui"; 9 + import LayoutToggle from "../ui/LayoutToggle"; 10 + import { useStore as useNanoStore } from "@nanostores/react"; 11 + import { $feedLayout } from "../../store/feedLayout"; 9 12 10 13 interface MasonryFeedProps { 11 14 motivation?: string; ··· 14 17 title?: string; 15 18 } 16 19 17 - export default function MasonryFeed({ 20 + function MasonryContent({ 21 + tab, 18 22 motivation, 19 - emptyMessage = "No items found.", 20 - showTabs = false, 21 - title, 22 - }: MasonryFeedProps) { 23 - const user = useStore($user); 23 + emptyMessage, 24 + userDid, 25 + layout, 26 + }: { 27 + tab: string; 28 + motivation?: string; 29 + emptyMessage: string; 30 + userDid?: string; 31 + layout: "list" | "mosaic"; 32 + }) { 24 33 const [items, setItems] = useState<AnnotationItem[]>([]); 25 34 const [loading, setLoading] = useState(true); 26 - const [activeTab, setActiveTab] = useState("my"); 27 35 28 36 useEffect(() => { 29 - initAuth(); 30 - }, []); 37 + let cancelled = false; 31 38 32 - useEffect(() => { 33 - const fetchFeed = async () => { 34 - setLoading(true); 35 - try { 36 - const params: { type?: string; motivation?: string; creator?: string } = 37 - { 38 - motivation, 39 - }; 39 + const params: { type?: string; motivation?: string; creator?: string } = { 40 + motivation, 41 + }; 40 42 41 - if (activeTab === "my" && user?.did) { 42 - params.creator = user.did; 43 - params.type = "my-feed"; 44 - } else { 45 - params.type = "all"; 46 - } 43 + if (tab === "my" && userDid) { 44 + params.creator = userDid; 45 + params.type = "my-feed"; 46 + } else { 47 + params.type = "all"; 48 + } 47 49 48 - const data = await getFeed(params); 50 + getFeed(params) 51 + .then((data) => { 52 + if (cancelled) return; 49 53 setItems(data?.items || []); 50 - } catch (e) { 54 + setLoading(false); 55 + }) 56 + .catch((e) => { 57 + if (cancelled) return; 51 58 console.error(e); 52 - } finally { 59 + setItems([]); 53 60 setLoading(false); 54 - } 61 + }); 62 + 63 + return () => { 64 + cancelled = true; 55 65 }; 56 - fetchFeed(); 57 - }, [motivation, activeTab, user?.did]); 66 + }, [tab, motivation, userDid]); 58 67 59 68 const handleDelete = (uri: string) => { 60 69 setItems((prev) => prev.filter((i) => i.uri !== uri)); 61 70 }; 62 71 63 - const tabs = [ 64 - { id: "my", label: "My" }, 65 - { id: "global", label: "Global" }, 66 - ]; 72 + if (loading) { 73 + return ( 74 + <div className="flex justify-center py-20"> 75 + <Loader2 76 + className="animate-spin text-primary-600 dark:text-primary-400" 77 + size={32} 78 + /> 79 + </div> 80 + ); 81 + } 82 + 83 + if (items.length === 0) { 84 + return ( 85 + <EmptyState 86 + message={ 87 + tab === "my" 88 + ? emptyMessage 89 + : `No ${motivation === "bookmarking" ? "bookmarks" : "highlights"} from the community yet.` 90 + } 91 + /> 92 + ); 93 + } 94 + 95 + if (layout === "list") { 96 + return ( 97 + <div className="space-y-3 animate-fade-in"> 98 + {items.map((item) => ( 99 + <Card 100 + key={item.uri || item.cid} 101 + item={item} 102 + onDelete={handleDelete} 103 + /> 104 + ))} 105 + </div> 106 + ); 107 + } 108 + 109 + return ( 110 + <div className="columns-1 sm:columns-2 gap-4 animate-fade-in"> 111 + {items.map((item) => ( 112 + <div key={item.uri || item.cid} className="break-inside-avoid mb-4"> 113 + <Card item={item} onDelete={handleDelete} /> 114 + </div> 115 + ))} 116 + </div> 117 + ); 118 + } 119 + 120 + export default function MasonryFeed({ 121 + motivation, 122 + emptyMessage = "No items found.", 123 + showTabs = false, 124 + title, 125 + }: MasonryFeedProps) { 126 + const user = useStore($user); 127 + const layout = useNanoStore($feedLayout); 128 + const [activeTab, setActiveTab] = useState(user ? "my" : "global"); 129 + 130 + const handleTabChange = (id: string) => { 131 + if (id === activeTab) return; 132 + setActiveTab(id); 133 + window.scrollTo({ top: 0, behavior: "smooth" }); 134 + }; 135 + 136 + const tabs = user 137 + ? [ 138 + { id: "my", label: "My" }, 139 + { id: "global", label: "Global" }, 140 + ] 141 + : [{ id: "global", label: "Global" }]; 67 142 68 143 return ( 69 - <div className="max-w-2xl mx-auto animate-slide-up"> 144 + <div className="max-w-2xl mx-auto"> 70 145 {title && ( 71 146 <h1 className="text-3xl font-display font-bold text-surface-900 dark:text-white mb-6 text-center lg:text-left"> 72 147 {title} ··· 74 149 )} 75 150 76 151 {showTabs && ( 77 - <Tabs 78 - tabs={tabs} 79 - activeTab={activeTab} 80 - onChange={setActiveTab} 81 - className="mb-6" 82 - /> 152 + <div className="sticky top-0 z-10 bg-surface-50/95 dark:bg-surface-950/95 backdrop-blur-sm pb-4 mb-2 -mx-1 px-1 pt-1"> 153 + <div className="flex items-center gap-3"> 154 + <div className="flex-1"> 155 + <Tabs 156 + tabs={tabs} 157 + activeTab={activeTab} 158 + onChange={handleTabChange} 159 + /> 160 + </div> 161 + <LayoutToggle /> 162 + </div> 163 + </div> 83 164 )} 84 165 85 - {loading ? ( 86 - <div className="flex justify-center py-20"> 87 - <Loader2 88 - className="animate-spin text-primary-600 dark:text-primary-400" 89 - size={32} 90 - /> 91 - </div> 92 - ) : items.length === 0 ? ( 93 - <EmptyState 94 - message={ 95 - activeTab === "my" 96 - ? emptyMessage 97 - : `No ${motivation === "bookmarking" ? "bookmarks" : "highlights"} from the community yet.` 98 - } 99 - /> 100 - ) : ( 101 - <div className="columns-1 xl:columns-2 gap-4 animate-fade-in"> 102 - {items.map((item) => ( 103 - <div key={item.uri || item.cid} className="break-inside-avoid mb-4"> 104 - <Card item={item} onDelete={handleDelete} /> 105 - </div> 106 - ))} 166 + {!showTabs && ( 167 + <div className="flex justify-end mb-4"> 168 + <LayoutToggle /> 107 169 </div> 108 170 )} 171 + 172 + <MasonryContent 173 + key={activeTab} 174 + tab={activeTab} 175 + motivation={motivation} 176 + emptyMessage={emptyMessage} 177 + userDid={user?.did} 178 + layout={layout} 179 + /> 109 180 </div> 110 181 ); 111 182 }
+6 -3
web/src/components/feed/ReplyList.tsx
··· 207 207 } 208 208 209 209 const buildReplyTree = () => { 210 - const replyMap: Record<string, any> = {}; 211 - const rootReplies: any[] = []; 210 + const replyMap: Record< 211 + string, 212 + AnnotationItem & { children: AnnotationItem[] } 213 + > = {}; 214 + const rootReplies: (AnnotationItem & { children: AnnotationItem[] })[] = []; 212 215 213 216 replies.forEach((r) => { 214 217 replyMap[r.uri || r.id || ""] = { ...r, children: [] }; 215 218 }); 216 219 217 220 replies.forEach((r) => { 218 - const parentUri = (r as any).reply?.parent?.uri || (r as any).parentUri; 221 + const parentUri = r.reply?.parent?.uri || r.parentUri; 219 222 if (parentUri === rootUri || !parentUri || !replyMap[parentUri]) { 220 223 rootReplies.push(replyMap[r.uri || r.id || ""]); 221 224 } else {
+2 -1
web/src/components/modals/AddToCollectionModal.tsx
··· 7 7 ChevronRight, 8 8 FolderPlus, 9 9 } from "lucide-react"; 10 - import CollectionIcon, { ICON_MAP } from "../common/CollectionIcon"; 10 + import CollectionIcon from "../common/CollectionIcon"; 11 + import { ICON_MAP } from "../common/iconMap"; 11 12 import { useStore } from "@nanostores/react"; 12 13 import { $user } from "../../store/auth"; 13 14 import {
+10 -5
web/src/components/modals/EditProfileModal.tsx
··· 20 20 const [links, setLinks] = useState<string[]>(profile.links || []); 21 21 const [newLink, setNewLink] = useState(""); 22 22 23 - const [avatarBlob, setAvatarBlob] = useState<Blob | null>(null); 23 + const [avatarBlob, setAvatarBlob] = useState<Blob | string | null>(null); 24 24 const [avatarPreview, setAvatarPreview] = useState<string | null>(null); 25 25 const [uploading, setUploading] = useState(false); 26 26 ··· 49 49 try { 50 50 const result = await uploadAvatar(file); 51 51 setAvatarBlob(result.blob); 52 - } catch (err: any) { 53 - setError("Failed to upload: " + err.message); 52 + setAvatarBlob(result.blob); 53 + } catch (err) { 54 + setError( 55 + "Failed to upload: " + 56 + (err instanceof Error ? err.message : "Unknown error"), 57 + ); 54 58 setAvatarPreview(null); 55 59 } finally { 56 60 setUploading(false); ··· 91 95 avatar: avatarPreview || profile.avatar, 92 96 }); 93 97 onClose(); 94 - } catch (err: any) { 95 - setError(err.message); 98 + onClose(); 99 + } catch (err) { 100 + setError(err instanceof Error ? err.message : "Unknown error"); 96 101 } finally { 97 102 setSaving(false); 98 103 }
+1 -1
web/src/components/modals/ExternalLinkModal.tsx
··· 1 1 import React, { useState } from "react"; 2 2 import { Button } from "../ui"; 3 - import { ExternalLink, AlertTriangle, X } from "lucide-react"; 3 + import { ExternalLink, AlertTriangle } from "lucide-react"; 4 4 import { addSkippedHostname } from "../../store/preferences"; 5 5 6 6 interface ExternalLinkModalProps {
-7
web/src/components/modals/ShareMenu.tsx
··· 21 21 22 22 const BLUESKY_COLOR = "#1185fe"; 23 23 24 - interface ShareOption { 25 - name: string; 26 - icon: React.ReactNode; 27 - action: () => void; 28 - highlight?: boolean; 29 - } 30 - 31 24 interface ShareMenuProps { 32 25 uri: string; 33 26 text?: string;
+134 -96
web/src/components/modals/SignUpModal.tsx
··· 1 - import React, { useState, useEffect } from "react"; 1 + import React, { useState, useEffect, useMemo } from "react"; 2 2 import { X, ChevronRight, Loader2, AlertCircle } from "lucide-react"; 3 3 import { 4 4 BlackskyIcon, ··· 13 13 id: string; 14 14 name: string; 15 15 service: string; 16 - Icon: any; 16 + Icon: React.ComponentType<{ size?: number }> | null; 17 17 description: string; 18 18 custom?: boolean; 19 19 wide?: boolean; 20 20 } 21 21 22 - const RECOMMENDED_PROVIDER: Provider = { 22 + const MARGIN_PROVIDER: Provider = { 23 23 id: "margin", 24 24 name: "Margin", 25 25 service: "https://margin.cafe", ··· 80 80 }, 81 81 ]; 82 82 83 + function shuffleArray<T>(arr: T[]): T[] { 84 + const shuffled = [...arr]; 85 + for (let i = shuffled.length - 1; i > 0; i--) { 86 + const j = Math.floor(Math.random() * (i + 1)); 87 + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; 88 + } 89 + return shuffled; 90 + } 91 + 92 + const inviteStatusPromise: Promise<Record<string, boolean>> = (async () => { 93 + const results: Record<string, boolean> = {}; 94 + await Promise.allSettled( 95 + [MARGIN_PROVIDER, ...OTHER_PROVIDERS] 96 + .filter((p) => p.service && !p.custom) 97 + .map(async (p) => { 98 + try { 99 + const res = await fetch( 100 + `${p.service}/xrpc/com.atproto.server.describeServer`, 101 + ); 102 + if (res.ok) { 103 + const data = await res.json(); 104 + results[p.id] = !!data.inviteCodeRequired; 105 + } 106 + } catch { 107 + // ignore unreachable providers 108 + } 109 + }), 110 + ); 111 + return results; 112 + })(); 113 + 83 114 interface SignUpModalProps { 84 115 onClose: () => void; 85 116 } 86 117 87 118 export default function SignUpModal({ onClose }: SignUpModalProps) { 88 - const [showOtherProviders, setShowOtherProviders] = useState(false); 89 119 const [showCustomInput, setShowCustomInput] = useState(false); 90 120 const [customService, setCustomService] = useState(""); 91 121 const [loading, setLoading] = useState(false); 92 122 const [error, setError] = useState<string | null>(null); 123 + const [inviteStatus, setInviteStatus] = useState<Record<string, boolean>>({}); 124 + const [statusLoaded, setStatusLoaded] = useState(false); 125 + 126 + useEffect(() => { 127 + inviteStatusPromise.then((status) => { 128 + setInviteStatus(status); 129 + setStatusLoaded(true); 130 + }); 131 + }, []); 132 + 133 + const providers = useMemo(() => { 134 + const nonCustom = OTHER_PROVIDERS.filter((p) => !p.custom); 135 + const custom = OTHER_PROVIDERS.find((p) => p.custom); 136 + 137 + if (!statusLoaded) { 138 + return [ 139 + MARGIN_PROVIDER, 140 + ...shuffleArray(nonCustom), 141 + ...(custom ? [custom] : []), 142 + ]; 143 + } 144 + 145 + const open = nonCustom.filter((p) => !inviteStatus[p.id]); 146 + const inviteOnly = nonCustom.filter((p) => inviteStatus[p.id]); 147 + return [ 148 + MARGIN_PROVIDER, 149 + ...shuffleArray(open), 150 + ...shuffleArray(inviteOnly), 151 + ...(custom ? [custom] : []), 152 + ]; 153 + }, [statusLoaded, inviteStatus]); 93 154 94 155 useEffect(() => { 95 156 document.body.style.overflow = "hidden"; ··· 110 171 try { 111 172 const result = await startSignup(provider.service); 112 173 if (result.authorizationUrl) { 113 - window.location.href = result.authorizationUrl; 174 + window.location.assign(result.authorizationUrl); 114 175 } 115 176 } catch (err) { 116 177 console.error(err); ··· 144 205 }; 145 206 146 207 return ( 147 - <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in"> 148 - <div className="w-full max-w-md bg-white rounded-3xl shadow-2xl overflow-hidden animate-slide-up"> 149 - <div className="p-4 flex justify-end"> 208 + <div className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-4 bg-black/60 backdrop-blur-sm animate-fade-in"> 209 + <div className="w-full sm:max-w-md bg-white dark:bg-surface-900 rounded-t-3xl sm:rounded-3xl shadow-2xl overflow-hidden animate-slide-up max-h-[90vh] sm:max-h-[85vh] flex flex-col"> 210 + <div className="p-3 sm:p-4 flex justify-end flex-shrink-0"> 150 211 <button 151 212 onClick={onClose} 152 - className="p-2 text-surface-400 hover:text-surface-900 hover:bg-surface-50 rounded-full transition-colors" 213 + className="p-2 text-surface-400 dark:text-surface-500 hover:text-surface-900 dark:hover:text-white hover:bg-surface-50 dark:hover:bg-surface-800 rounded-full transition-colors" 153 214 > 154 215 <X size={20} /> 155 216 </button> 156 217 </div> 157 218 158 - <div className="px-8 pb-10"> 219 + <div className="px-5 sm:px-8 pb-8 sm:pb-10 overflow-y-auto"> 159 220 {loading ? ( 160 221 <div className="text-center py-10"> 161 222 <Loader2 162 223 size={40} 163 - className="animate-spin text-primary-600 mx-auto mb-4" 224 + className="animate-spin text-primary-600 dark:text-primary-400 mx-auto mb-4" 164 225 /> 165 - <p className="text-surface-600 font-medium"> 226 + <p className="text-surface-600 dark:text-surface-400 font-medium"> 166 227 Connecting to provider... 167 228 </p> 168 229 </div> 169 230 ) : showCustomInput ? ( 170 231 <div> 171 - <h2 className="text-2xl font-display font-bold text-surface-900 mb-6"> 232 + <h2 className="text-2xl font-display font-bold text-surface-900 dark:text-white mb-6"> 172 233 Custom Provider 173 234 </h2> 174 235 <form onSubmit={handleCustomSubmit} className="space-y-4"> 175 236 <div> 176 - <label className="block text-sm font-medium text-surface-700 mb-1"> 237 + <label className="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1"> 177 238 PDS address (e.g. pds.example.com) 178 239 </label> 179 240 <input 180 241 type="text" 181 - className="w-full px-4 py-3 bg-surface-50 border border-surface-200 rounded-xl focus:border-primary-500 focus:ring-4 focus:ring-primary-500/10 outline-none transition-all" 242 + className="w-full px-4 py-3 bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-xl text-surface-900 dark:text-white placeholder:text-surface-400 dark:placeholder:text-surface-500 focus:border-primary-500 dark:focus:border-primary-400 focus:ring-4 focus:ring-primary-500/10 dark:focus:ring-primary-400/10 outline-none transition-all" 182 243 value={customService} 183 244 onChange={(e) => setCustomService(e.target.value)} 184 245 placeholder="pds.example.com" ··· 187 248 </div> 188 249 189 250 {error && ( 190 - <div className="p-3 bg-red-50 text-red-600 text-sm rounded-lg flex items-center gap-2"> 251 + <div className="p-3 bg-red-50 dark:bg-red-950/40 text-red-600 dark:text-red-400 text-sm rounded-lg flex items-center gap-2 border border-red-100 dark:border-red-900/40"> 191 252 <AlertCircle size={16} /> 192 253 {error} 193 254 </div> ··· 196 257 <div className="flex gap-3 pt-4"> 197 258 <button 198 259 type="button" 199 - className="flex-1 py-3 bg-white border border-surface-200 text-surface-700 font-semibold rounded-xl hover:bg-surface-50 transition-colors" 260 + className="flex-1 py-3 bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700 text-surface-700 dark:text-surface-300 font-semibold rounded-xl hover:bg-surface-50 dark:hover:bg-surface-700 transition-colors" 200 261 onClick={() => { 201 262 setShowCustomInput(false); 202 263 setError(null); ··· 206 267 </button> 207 268 <button 208 269 type="submit" 209 - className="flex-1 py-3 bg-primary-600 text-white font-semibold rounded-xl hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" 270 + className="flex-1 py-3 bg-primary-600 dark:bg-primary-500 text-white font-semibold rounded-xl hover:bg-primary-700 dark:hover:bg-primary-400 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" 210 271 disabled={!customService.trim()} 211 272 > 212 273 Continue ··· 216 277 </div> 217 278 ) : ( 218 279 <div> 219 - <h2 className="text-2xl font-display font-bold text-surface-900 mb-2"> 280 + <h2 className="text-2xl font-display font-bold text-surface-900 dark:text-white mb-2"> 220 281 Create your account 221 282 </h2> 222 - <p className="text-surface-500 mb-6"> 223 - Margin adheres to the AT Protocol. Choose a provider to host 224 - your account. 283 + <p className="text-surface-500 dark:text-surface-400 mb-6"> 284 + Margin adheres to the{" "} 285 + <a 286 + href="https://atproto.com" 287 + target="_blank" 288 + rel="noopener noreferrer" 289 + className="text-primary-600 dark:text-primary-400 hover:underline" 290 + > 291 + AT Protocol 292 + </a> 293 + . Choose a provider to host your account. 225 294 </p> 226 295 227 296 {error && ( 228 - <div className="mb-4 p-3 bg-red-50 text-red-600 text-sm rounded-lg flex items-center gap-2"> 297 + <div className="mb-4 p-3 bg-red-50 dark:bg-red-950/40 text-red-600 dark:text-red-400 text-sm rounded-lg flex items-center gap-2 border border-red-100 dark:border-red-900/40"> 229 298 <AlertCircle size={16} /> 230 299 {error} 231 300 </div> 232 301 )} 233 302 234 - <div className="mb-6"> 235 - <div className="inline-block px-2 py-0.5 bg-primary-50 text-primary-700 text-xs font-bold uppercase tracking-wider rounded-md mb-2"> 236 - Recommended 237 - </div> 238 - <button 239 - className="w-full flex items-center gap-4 p-4 bg-white border-2 border-primary-100 hover:border-primary-300 rounded-2xl shadow-sm hover:shadow-md transition-all group text-left" 240 - onClick={() => handleProviderSelect(RECOMMENDED_PROVIDER)} 241 - > 242 - <div className="w-12 h-12 bg-primary-50 rounded-full flex items-center justify-center text-primary-600 flex-shrink-0"> 243 - {RECOMMENDED_PROVIDER.Icon && ( 244 - <RECOMMENDED_PROVIDER.Icon size={24} /> 303 + <div className="space-y-2"> 304 + {providers.map((p) => ( 305 + <button 306 + key={p.id} 307 + className={`w-full flex items-center gap-3 p-3 rounded-xl transition-all text-left group ${ 308 + p.id === "margin" 309 + ? "bg-primary-50/80 dark:bg-primary-900/20 border border-primary-200/60 dark:border-primary-800/40 hover:border-primary-300 dark:hover:border-primary-700" 310 + : "bg-surface-50 dark:bg-surface-800/60 hover:bg-surface-100 dark:hover:bg-surface-800 border border-transparent" 311 + }`} 312 + onClick={() => handleProviderSelect(p)} 313 + > 314 + <div 315 + className={`w-9 h-9 flex items-center justify-center rounded-full flex-shrink-0 ${ 316 + p.id === "margin" 317 + ? "bg-primary-100 dark:bg-primary-900/40 text-primary-600 dark:text-primary-400" 318 + : "bg-white dark:bg-surface-700 shadow-sm dark:shadow-none text-surface-600 dark:text-surface-300" 319 + }`} 320 + > 321 + {p.Icon ? ( 322 + <p.Icon size={18} /> 323 + ) : ( 324 + <span className="font-bold text-xs">{p.name[0]}</span> 325 + )} 326 + </div> 327 + <div className="flex-1 min-w-0"> 328 + <h3 className="text-sm font-bold text-surface-900 dark:text-white"> 329 + {p.name} 330 + </h3> 331 + <p className="text-xs text-surface-500 dark:text-surface-400 line-clamp-1"> 332 + {p.description} 333 + </p> 334 + </div> 335 + {inviteStatus[p.id] && ( 336 + <span className="text-[10px] font-medium text-surface-400 dark:text-surface-500 bg-surface-100 dark:bg-surface-800 px-1.5 py-0.5 rounded-md flex-shrink-0"> 337 + Invite 338 + </span> 245 339 )} 246 - </div> 247 - <div className="flex-1 min-w-0"> 248 - <h3 className="font-bold text-surface-900 group-hover:text-primary-700 transition-colors"> 249 - {RECOMMENDED_PROVIDER.name} 250 - </h3> 251 - <span className="text-sm text-surface-500 line-clamp-1"> 252 - {RECOMMENDED_PROVIDER.description} 253 - </span> 254 - </div> 255 - <ChevronRight 256 - size={20} 257 - className="text-surface-300 group-hover:text-primary-500" 258 - /> 259 - </button> 260 - </div> 261 - 262 - <div className="border-t border-surface-100 pt-4"> 263 - <button 264 - type="button" 265 - className="flex items-center gap-2 text-sm font-medium text-surface-500 hover:text-surface-900 transition-colors mb-4" 266 - onClick={() => setShowOtherProviders(!showOtherProviders)} 267 - > 268 - {showOtherProviders ? "Hide other options" : "More options"} 269 - <ChevronRight 270 - size={14} 271 - className={`transition-transform duration-200 ${showOtherProviders ? "rotate-90" : ""}`} 272 - /> 273 - </button> 274 - 275 - {showOtherProviders && ( 276 - <div className="space-y-2 animate-fade-in"> 277 - {OTHER_PROVIDERS.map((p) => ( 278 - <button 279 - key={p.id} 280 - className="w-full flex items-center gap-3 p-3 bg-surface-50 hover:bg-surface-100 rounded-xl transition-colors text-left group" 281 - onClick={() => handleProviderSelect(p)} 282 - > 283 - <div className="w-8 h-8 flex items-center justify-center bg-white rounded-full shadow-sm text-surface-600"> 284 - {p.Icon ? ( 285 - <p.Icon size={18} /> 286 - ) : ( 287 - <span className="font-bold text-xs"> 288 - {p.name[0]} 289 - </span> 290 - )} 291 - </div> 292 - <div className="flex-1 min-w-0"> 293 - <h3 className="text-sm font-bold text-surface-900"> 294 - {p.name} 295 - </h3> 296 - <p className="text-xs text-surface-500 line-clamp-1"> 297 - {p.description} 298 - </p> 299 - </div> 300 - <ChevronRight 301 - size={16} 302 - className="text-surface-300 group-hover:text-surface-600" 303 - /> 304 - </button> 305 - ))} 306 - </div> 307 - )} 340 + <ChevronRight 341 + size={16} 342 + className="text-surface-300 dark:text-surface-600 group-hover:text-surface-600 dark:group-hover:text-surface-400" 343 + /> 344 + </button> 345 + ))} 308 346 </div> 309 347 </div> 310 348 )}
+3 -3
web/src/components/navigation/MobileNav.tsx
··· 45 45 <> 46 46 {isMenuOpen && ( 47 47 <div 48 - className="fixed inset-0 bg-black/50 z-40 lg:hidden" 48 + className="fixed inset-0 bg-black/50 z-40 md:hidden" 49 49 onClick={closeMenu} 50 50 /> 51 51 )} 52 52 53 53 {isMenuOpen && ( 54 - <div className="fixed bottom-16 left-0 right-0 bg-white dark:bg-surface-900 rounded-t-2xl shadow-2xl z-50 lg:hidden animate-slide-up"> 54 + <div className="fixed bottom-16 left-0 right-0 bg-white dark:bg-surface-900 rounded-t-2xl shadow-2xl z-50 md:hidden animate-slide-up"> 55 55 <div className="p-4 space-y-1"> 56 56 {isAuthenticated && user ? ( 57 57 <> ··· 164 164 </div> 165 165 )} 166 166 167 - <nav className="fixed bottom-0 left-0 right-0 h-14 bg-white dark:bg-surface-900 border-t border-surface-200 dark:border-surface-700 flex items-center justify-around px-2 z-50 lg:hidden safe-area-bottom"> 167 + <nav className="fixed bottom-0 left-0 right-0 h-14 bg-white dark:bg-surface-900 border-t border-surface-200 dark:border-surface-700 flex items-center justify-around px-2 z-50 md:hidden safe-area-bottom"> 168 168 <Link 169 169 to="/home" 170 170 className={`flex flex-col items-center justify-center w-14 h-14 rounded-xl transition-colors ${
+38 -44
web/src/components/navigation/RightSidebar.tsx
··· 1 1 import React, { useEffect, useState } from "react"; 2 2 import { useNavigate } from "react-router-dom"; 3 - import { 4 - ArrowRight, 5 - Github, 6 - Twitter, 7 - ExternalLink, 8 - Loader2, 9 - Search, 10 - } from "lucide-react"; 3 + import { Search } from "lucide-react"; 11 4 import { getTrendingTags, type Tag } from "../../api/client"; 12 5 13 6 export default function RightSidebar() { 14 7 const navigate = useNavigate(); 15 8 const [tags, setTags] = useState<Tag[]>([]); 16 - const [browser, setBrowser] = useState<"chrome" | "firefox" | "other">( 17 - "other", 18 - ); 9 + const [browser] = useState<"chrome" | "firefox" | "edge" | "other">(() => { 10 + if (typeof navigator === "undefined") return "other"; 11 + const ua = navigator.userAgent; 12 + if (/Edg\//i.test(ua)) return "edge"; 13 + if (/Firefox/i.test(ua)) return "firefox"; 14 + if (/Chrome/i.test(ua)) return "chrome"; 15 + return "other"; 16 + }); 19 17 const [searchQuery, setSearchQuery] = useState(""); 20 18 21 19 const handleSearch = (e: React.KeyboardEvent) => { ··· 25 23 }; 26 24 27 25 useEffect(() => { 28 - const ua = navigator.userAgent.toLowerCase(); 29 - if (ua.includes("firefox")) setBrowser("firefox"); 30 - else if (ua.includes("chrome")) setBrowser("chrome"); 31 26 getTrendingTags().then(setTags); 32 27 }, []); 33 28 34 29 const extensionLink = 35 30 browser === "firefox" 36 31 ? "https://addons.mozilla.org/en-US/firefox/addon/margin/" 37 - : "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa"; 32 + : browser === "edge" 33 + ? "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn" 34 + : "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa"; 38 35 39 36 return ( 40 - <aside className="hidden lg:block w-[280px] shrink-0 sticky top-0 h-screen overflow-y-auto px-4 py-4 border-l border-surface-100/50 dark:border-surface-800/50"> 41 - <div className="space-y-6"> 37 + <aside className="hidden xl:block w-[280px] shrink-0 sticky top-0 h-screen overflow-y-auto px-5 py-6 border-l border-surface-200/60 dark:border-surface-800/60"> 38 + <div className="space-y-5"> 42 39 <div className="relative"> 43 40 <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> 44 41 <Search 45 42 className="text-surface-400 dark:text-surface-500" 46 - size={16} 43 + size={15} 47 44 /> 48 45 </div> 49 46 <input ··· 51 48 value={searchQuery} 52 49 onChange={(e) => setSearchQuery(e.target.value)} 53 50 onKeyDown={handleSearch} 54 - placeholder="Search Margin..." 55 - className="w-full bg-surface-100 dark:bg-surface-800 rounded-full pl-10 pr-5 py-2.5 text-sm font-medium text-surface-900 dark:text-white placeholder:text-surface-500 dark:placeholder:text-surface-400 focus:outline-none focus:ring-2 focus:ring-primary-500/20 transition-all border-none" 51 + placeholder="Search..." 52 + className="w-full bg-surface-100 dark:bg-surface-800/80 rounded-lg pl-9 pr-4 py-2 text-sm text-surface-900 dark:text-white placeholder:text-surface-400 dark:placeholder:text-surface-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:bg-white dark:focus:bg-surface-800 transition-all border border-surface-200/60 dark:border-surface-700/60" 56 53 /> 57 54 </div> 58 55 59 - <div className="bg-surface-50 dark:bg-surface-900 rounded-2xl p-4 border border-surface-100 dark:border-surface-800"> 60 - <h3 className="font-bold text-base mb-1 text-surface-900 dark:text-white"> 56 + <div className="rounded-xl p-4 bg-gradient-to-br from-primary-50 to-primary-100/50 dark:from-primary-950/30 dark:to-primary-900/10 border border-primary-200/40 dark:border-primary-800/30"> 57 + <h3 className="font-semibold text-sm mb-1 text-surface-900 dark:text-white"> 61 58 Get the Extension 62 59 </h3> 63 - <p className="text-surface-500 dark:text-surface-400 text-sm mb-4 leading-snug"> 64 - Save anything, annotate anywhere. 60 + <p className="text-surface-500 dark:text-surface-400 text-xs mb-3 leading-relaxed"> 61 + Highlight, annotate, and bookmark from any page. 65 62 </p> 66 63 <a 67 64 href={extensionLink} 68 65 target="_blank" 69 66 rel="noopener noreferrer" 70 - className="flex items-center justify-center w-full px-4 py-2 bg-surface-900 dark:bg-white text-white dark:text-surface-900 rounded-full hover:bg-black dark:hover:bg-surface-100 transition-all text-sm font-semibold" 67 + className="flex items-center justify-center w-full px-4 py-2 bg-primary-600 hover:bg-primary-700 dark:bg-primary-500 dark:hover:bg-primary-400 text-white dark:text-white rounded-lg transition-colors text-sm font-medium" 71 68 > 72 - Download for {browser === "firefox" ? "Firefox" : "Chrome"} 69 + Download for{" "} 70 + {browser === "firefox" 71 + ? "Firefox" 72 + : browser === "edge" 73 + ? "Edge" 74 + : "Chrome"} 73 75 </a> 74 76 </div> 75 77 76 - <div className="py-2"> 77 - <h3 className="font-bold text-xl px-2 mb-4 text-surface-900 dark:text-white"> 78 + <div> 79 + <h3 className="font-semibold text-sm px-1 mb-3 text-surface-900 dark:text-white tracking-tight"> 78 80 Trending 79 81 </h3> 80 82 {tags.length > 0 ? ( ··· 83 85 <a 84 86 key={t.tag} 85 87 href={`/search?q=${t.tag}`} 86 - className="px-2 py-3 hover:bg-surface-50 dark:hover:bg-surface-800 rounded-xl transition-colors group" 88 + className="px-2 py-2.5 hover:bg-surface-100 dark:hover:bg-surface-800/60 rounded-lg transition-colors group" 87 89 > 88 - <div className="flex justify-between items-center mb-0.5"> 89 - <span className="text-xs text-surface-500 dark:text-surface-400 font-medium"> 90 - Trending 91 - </span> 92 - <span className="text-xs text-surface-400 dark:text-surface-500 opacity-0 group-hover:opacity-100 transition-opacity"> 93 - ... 94 - </span> 95 - </div> 96 - <div className="font-bold text-surface-900 dark:text-white"> 90 + <div className="font-semibold text-sm text-surface-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors"> 97 91 #{t.tag} 98 92 </div> 99 - <div className="text-xs text-surface-500 dark:text-surface-400 mt-0.5"> 100 - {t.count} posts 93 + <div className="text-xs text-surface-400 dark:text-surface-500 mt-0.5"> 94 + {t.count} {t.count === 1 ? "post" : "posts"} 101 95 </div> 102 96 </a> 103 97 ))} 104 98 </div> 105 99 ) : ( 106 100 <div className="px-2"> 107 - <p className="text-sm text-surface-500 dark:text-surface-400"> 101 + <p className="text-sm text-surface-400 dark:text-surface-500"> 108 102 Nothing trending right now. 109 103 </p> 110 104 </div> 111 105 )} 112 106 </div> 113 107 114 - <div className="px-2 pt-2"> 115 - <div className="flex flex-wrap gap-x-3 gap-y-1 text-[13px] text-surface-400 dark:text-surface-500 leading-relaxed"> 108 + <div className="px-1 pt-2"> 109 + <div className="flex flex-wrap gap-x-3 gap-y-1 text-[12px] text-surface-400 dark:text-surface-500 leading-relaxed"> 116 110 <a 117 - href="#" 111 + href="/about" 118 112 className="hover:underline hover:text-surface-600 dark:hover:text-surface-300" 119 113 > 120 114 About
+106 -63
web/src/components/navigation/Sidebar.tsx
··· 10 10 Moon, 11 11 Monitor, 12 12 Folder, 13 + LogIn, 14 + PenSquare, 13 15 } from "lucide-react"; 14 16 import { useStore } from "@nanostores/react"; 15 17 import { $user, logout } from "../../store/auth"; ··· 38 40 return () => clearInterval(interval); 39 41 }, [user]); 40 42 41 - const navItems = [ 43 + const publicNavItems = [ 44 + { icon: Home, label: "Feed", href: "/home" }, 45 + { icon: Bookmark, label: "Bookmarks", href: "/bookmarks" }, 46 + { icon: PenTool, label: "Highlights", href: "/highlights" }, 47 + ]; 48 + 49 + const authNavItems = [ 42 50 { icon: Home, label: "Feed", href: "/home" }, 43 51 { 44 52 icon: Bell, ··· 51 59 { icon: Folder, label: "Collections", href: "/collections" }, 52 60 ]; 53 61 54 - if (!user) return null; 62 + const navItems = user ? authNavItems : publicNavItems; 55 63 56 64 return ( 57 - <aside className="sticky top-0 h-screen w-[240px] hidden md:flex flex-col justify-between py-5 px-4 z-50"> 58 - <div className="flex flex-col gap-8"> 65 + <aside className="sticky top-0 h-screen hidden md:flex flex-col justify-between py-6 px-2 lg:px-3 z-50 border-r border-surface-200/60 dark:border-surface-800/60 w-[68px] lg:w-[220px] transition-all duration-200"> 66 + <div className="flex flex-col gap-6"> 59 67 <Link 60 68 to="/home" 61 - className="px-3 hover:opacity-80 transition-opacity w-fit" 69 + className="px-3 hover:opacity-80 transition-opacity w-fit flex items-center gap-2.5" 62 70 > 63 - <img src="/logo.svg" alt="Margin" className="w-9 h-9" /> 71 + <img src="/logo.svg" alt="Margin" className="w-8 h-8" /> 72 + <span className="font-display font-bold text-lg text-surface-900 dark:text-white tracking-tight hidden lg:inline"> 73 + Margin 74 + </span> 64 75 </Link> 65 76 66 - <nav className="flex flex-col gap-1"> 77 + <nav className="flex flex-col gap-0.5"> 67 78 {navItems.map((item) => { 68 79 const isActive = 69 80 currentPath === item.href || ··· 72 83 <Link 73 84 key={item.href} 74 85 to={item.href} 75 - className={`flex items-center gap-4 px-4 py-3 rounded-xl transition-all duration-200 text-[15px] group ${ 86 + title={item.label} 87 + className={`flex items-center justify-center lg:justify-start gap-3 px-0 lg:px-3 py-2.5 rounded-lg transition-all duration-150 text-[14px] group ${ 76 88 isActive 77 - ? "font-bold text-surface-900 dark:text-white bg-surface-100 dark:bg-surface-800" 78 - : "font-medium text-surface-600 dark:text-surface-400 hover:bg-surface-50 dark:hover:bg-surface-800/50 hover:text-surface-900 dark:hover:text-white" 89 + ? "font-semibold text-primary-700 dark:text-primary-300 bg-primary-50 dark:bg-primary-950/40" 90 + : "font-medium text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-800/60 hover:text-surface-900 dark:hover:text-white" 79 91 }`} 80 92 > 81 93 <item.icon 82 - size={22} 94 + size={20} 83 95 className={`transition-colors ${isActive ? "text-primary-600 dark:text-primary-400" : ""}`} 84 - strokeWidth={isActive ? 2.5 : 2} 96 + strokeWidth={isActive ? 2.25 : 1.75} 85 97 /> 86 - <span className="flex-1">{item.label}</span> 98 + <span className="flex-1 hidden lg:inline">{item.label}</span> 87 99 {(item.badge ?? 0) > 0 && ( 88 100 <CountBadge count={item.badge ?? 0} /> 89 101 )} 90 102 </Link> 91 103 ); 92 104 })} 105 + 106 + {user && ( 107 + <Link 108 + to="/new" 109 + title="New annotation" 110 + className="flex items-center justify-center lg:justify-start gap-3 px-0 lg:px-3 py-2.5 mt-2 rounded-lg bg-primary-600 dark:bg-primary-500 text-white hover:bg-primary-700 dark:hover:bg-primary-400 transition-colors text-[14px] font-semibold" 111 + > 112 + <PenSquare size={20} strokeWidth={1.75} /> 113 + <span className="hidden lg:inline">New</span> 114 + </Link> 115 + )} 93 116 </nav> 94 117 </div> 95 118 96 - <div className="relative group"> 97 - <Link 98 - to={`/profile/${user.did}`} 99 - className="flex items-center gap-3 p-3 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors w-full" 119 + <div className="space-y-1"> 120 + <button 121 + onClick={cycleTheme} 122 + title={ 123 + theme === "light" ? "Light" : theme === "dark" ? "Dark" : "System" 124 + } 125 + className="flex items-center justify-center lg:justify-start gap-3 px-0 lg:px-3 py-2.5 rounded-lg hover:bg-surface-100 dark:hover:bg-surface-800/60 text-[13px] font-medium text-surface-500 dark:text-surface-400 w-full transition-colors" 100 126 > 101 - <Avatar did={user.did} avatar={user.avatar} size="md" /> 102 - <div className="flex-1 min-w-0"> 103 - <p className="font-semibold text-surface-900 dark:text-white truncate text-sm"> 104 - {user.displayName || user.handle} 105 - </p> 106 - <p className="text-xs text-surface-500 dark:text-surface-400 truncate"> 107 - @{user.handle} 108 - </p> 109 - </div> 110 - </Link> 127 + {theme === "light" ? ( 128 + <Sun size={18} /> 129 + ) : theme === "dark" ? ( 130 + <Moon size={18} /> 131 + ) : ( 132 + <Monitor size={18} /> 133 + )} 134 + <span className="hidden lg:inline"> 135 + {theme === "light" ? "Light" : theme === "dark" ? "Dark" : "System"} 136 + </span> 137 + </button> 138 + 139 + {user ? ( 140 + <> 141 + <Link 142 + to="/settings" 143 + title="Settings" 144 + className="flex items-center justify-center lg:justify-start gap-3 px-0 lg:px-3 py-2.5 rounded-lg hover:bg-surface-100 dark:hover:bg-surface-800/60 text-[13px] font-medium text-surface-500 dark:text-surface-400 transition-colors" 145 + > 146 + <Settings size={18} /> 147 + <span className="hidden lg:inline">Settings</span> 148 + </Link> 149 + 150 + <div className="h-px bg-surface-200/60 dark:bg-surface-800/60 my-2" /> 151 + 152 + <Link 153 + to={`/profile/${user.did}`} 154 + title={user.displayName || user.handle} 155 + className="flex items-center justify-center lg:justify-start gap-2.5 p-2 rounded-lg hover:bg-surface-100 dark:hover:bg-surface-800/60 transition-colors w-full" 156 + > 157 + <Avatar did={user.did} avatar={user.avatar} size="sm" /> 158 + <div className="flex-1 min-w-0 hidden lg:block"> 159 + <p className="font-medium text-surface-900 dark:text-white truncate text-[13px]"> 160 + {user.displayName || user.handle} 161 + </p> 162 + <p className="text-[11px] text-surface-500 dark:text-surface-400 truncate"> 163 + @{user.handle} 164 + </p> 165 + </div> 166 + </Link> 111 167 112 - <div className="absolute bottom-full left-0 w-full mb-2 bg-white dark:bg-surface-900 rounded-xl shadow-xl border border-surface-100 dark:border-surface-800 p-2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 transform origin-bottom scale-95 group-hover:scale-100"> 113 - <button 114 - onClick={cycleTheme} 115 - className="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-surface-50 dark:hover:bg-surface-800 text-sm font-medium text-surface-700 dark:text-surface-300 w-full transition-colors" 116 - > 117 - {theme === "light" ? ( 118 - <Sun size={18} /> 119 - ) : theme === "dark" ? ( 120 - <Moon size={18} /> 121 - ) : ( 122 - <Monitor size={18} /> 123 - )} 124 - <span className="flex-1 text-left"> 125 - {theme === "light" 126 - ? "Light" 127 - : theme === "dark" 128 - ? "Dark" 129 - : "System"} 130 - </span> 131 - </button> 132 - <Link 133 - to="/settings" 134 - className="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-surface-50 dark:hover:bg-surface-800 text-sm font-medium text-surface-700 dark:text-surface-300 transition-colors" 135 - > 136 - <Settings size={18} /> 137 - <span>Settings</span> 138 - </Link> 139 - <div className="h-px bg-surface-100 dark:bg-surface-800 my-1" /> 140 - <button 141 - onClick={logout} 142 - className="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 text-sm font-medium text-red-600 dark:text-red-400 w-full text-left transition-colors" 143 - > 144 - <LogOut size={18} /> 145 - <span>Log out</span> 146 - </button> 147 - </div> 168 + <button 169 + onClick={logout} 170 + title="Log out" 171 + className="flex items-center justify-center lg:justify-start gap-3 px-0 lg:px-3 py-2 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 text-[13px] font-medium text-surface-400 dark:text-surface-500 hover:text-red-600 dark:hover:text-red-400 w-full text-left transition-colors" 172 + > 173 + <LogOut size={16} /> 174 + <span className="hidden lg:inline">Log out</span> 175 + </button> 176 + </> 177 + ) : ( 178 + <> 179 + <div className="h-px bg-surface-200/60 dark:bg-surface-800/60 my-2" /> 180 + 181 + <Link 182 + to="/login" 183 + title="Sign in" 184 + className="flex items-center justify-center lg:justify-start gap-3 px-0 lg:px-3 py-2.5 rounded-lg bg-primary-50 dark:bg-primary-950/40 text-primary-700 dark:text-primary-300 hover:bg-primary-100 dark:hover:bg-primary-950/60 text-[13px] font-semibold transition-colors" 185 + > 186 + <LogIn size={18} /> 187 + <span className="hidden lg:inline">Sign in</span> 188 + </Link> 189 + </> 190 + )} 148 191 </div> 149 192 </aside> 150 193 );
+38
web/src/components/ui/LayoutToggle.tsx
··· 1 + import React from "react"; 2 + import { List, LayoutGrid } from "lucide-react"; 3 + import { useStore } from "@nanostores/react"; 4 + import { 5 + $feedLayout, 6 + setFeedLayout, 7 + type FeedLayout, 8 + } from "../../store/feedLayout"; 9 + import { clsx } from "clsx"; 10 + 11 + export default function LayoutToggle() { 12 + const layout = useStore($feedLayout); 13 + 14 + const options: { id: FeedLayout; icon: typeof List; label: string }[] = [ 15 + { id: "list", icon: List, label: "List" }, 16 + { id: "mosaic", icon: LayoutGrid, label: "Mosaic" }, 17 + ]; 18 + 19 + return ( 20 + <div className="inline-flex items-center rounded-lg border border-surface-200 dark:border-surface-700 p-0.5 bg-surface-100 dark:bg-surface-800/60"> 21 + {options.map((opt) => ( 22 + <button 23 + key={opt.id} 24 + onClick={() => setFeedLayout(opt.id)} 25 + title={opt.label} 26 + className={clsx( 27 + "flex items-center justify-center w-7 h-7 rounded-md transition-all", 28 + layout === opt.id 29 + ? "bg-white dark:bg-surface-700 text-surface-900 dark:text-white shadow-sm" 30 + : "text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300", 31 + )} 32 + > 33 + <opt.icon size={14} strokeWidth={2} /> 34 + </button> 35 + ))} 36 + </div> 37 + ); 38 + }
+1
web/src/components/ui/index.ts
··· 5 5 export { default as Skeleton, SkeletonCard } from "./Skeleton"; 6 6 export { default as EmptyState } from "./EmptyState"; 7 7 export { default as Badge, CountBadge } from "./Badge"; 8 + export { default as LayoutToggle } from "./LayoutToggle";
+3 -3
web/src/layouts/AppLayout.tsx
··· 16 16 <div className="min-h-screen bg-surface-50 dark:bg-surface-950 flex"> 17 17 <Sidebar /> 18 18 19 - <div className="flex-1 min-w-0 transition-all duration-300"> 20 - <div className="flex w-full max-w-[1100px] mx-auto"> 21 - <main className="flex-1 w-full min-w-0 py-6 px-3 md:px-6 lg:px-8 pb-20 lg:pb-6"> 19 + <div className="flex-1 min-w-0 transition-all duration-200"> 20 + <div className="flex w-full max-w-[1200px] mx-auto"> 21 + <main className="flex-1 w-full min-w-0 py-6 px-3 md:px-5 lg:px-8 pb-20 md:pb-6"> 22 22 {children} 23 23 </main> 24 24
+19
web/src/store/feedLayout.ts
··· 1 + import { atom } from "nanostores"; 2 + 3 + export type FeedLayout = "list" | "mosaic"; 4 + 5 + const STORAGE_KEY = "margin:feed-layout"; 6 + 7 + function getInitial(): FeedLayout { 8 + if (typeof window === "undefined") return "list"; 9 + const stored = localStorage.getItem(STORAGE_KEY); 10 + if (stored === "mosaic") return "mosaic"; 11 + return "list"; 12 + } 13 + 14 + export const $feedLayout = atom<FeedLayout>(getInitial()); 15 + 16 + export function setFeedLayout(layout: FeedLayout) { 17 + $feedLayout.set(layout); 18 + localStorage.setItem(STORAGE_KEY, layout); 19 + }
+5 -5
web/src/styles/global.css
··· 10 10 } 11 11 12 12 html { 13 - background-color: #fafafa; 14 - color: #18181b; 13 + background-color: #f8fafc; 14 + color: #0f172a; 15 15 } 16 16 17 17 html[data-theme="dark"] { 18 - background-color: #09090b; 19 - color: #fafafa; 18 + background-color: #020617; 19 + color: #f8fafc; 20 20 } 21 21 22 22 h1, ··· 52 52 } 53 53 54 54 .card { 55 - @apply bg-white dark:bg-surface-900 rounded-lg shadow-sm ring-1 ring-black/5 dark:ring-white/5; 55 + @apply bg-white dark:bg-surface-900 rounded-xl border border-surface-200/80 dark:border-surface-800 shadow-sm; 56 56 } 57 57 58 58 .transition-default {
+21 -1
web/src/types.ts
··· 13 13 } 14 14 15 15 export interface Selector { 16 + type?: string; 16 17 exact: string; 17 18 prefix?: string; 18 19 suffix?: string; ··· 68 69 }; 69 70 addedBy?: UserProfile; 70 71 collectionItemUri?: string; 72 + reply?: { 73 + parent?: { 74 + uri: string; 75 + cid: string; 76 + }; 77 + root?: { 78 + uri: string; 79 + cid: string; 80 + }; 81 + }; 82 + parentUri?: string; 71 83 } 72 84 73 85 export type ActorSearchItem = UserProfile; ··· 90 102 | "like" 91 103 | "follow"; 92 104 subjectUri: string; 93 - subject?: any; 105 + subject?: AnnotationItem | unknown; 94 106 createdAt: string; 95 107 readAt?: string; 96 108 } ··· 114 126 createdAt: string; 115 127 annotation?: AnnotationItem; 116 128 } 129 + 130 + export interface EditHistoryItem { 131 + uri: string; 132 + cid: string; 133 + author: UserProfile; 134 + text: string; 135 + createdAt: string; 136 + }
+588
web/src/views/About.tsx
··· 1 + import React from "react"; 2 + import { useStore } from "@nanostores/react"; 3 + import { Link } from "react-router-dom"; 4 + import { $theme } from "../store/theme"; 5 + import { 6 + MessageSquareText, 7 + Highlighter, 8 + Bookmark, 9 + FolderOpen, 10 + Keyboard, 11 + PanelRight, 12 + MousePointerClick, 13 + Shield, 14 + Users, 15 + Sparkles, 16 + Chrome, 17 + ArrowRight, 18 + Github, 19 + ExternalLink, 20 + Hash, 21 + Heart, 22 + Eye, 23 + } from "lucide-react"; 24 + import { AppleIcon } from "../components/common/Icons"; 25 + import { FaFirefox, FaEdge } from "react-icons/fa"; 26 + 27 + function FeatureCard({ 28 + icon: Icon, 29 + title, 30 + description, 31 + accent = false, 32 + }: { 33 + icon: React.ElementType; 34 + title: string; 35 + description: string; 36 + accent?: boolean; 37 + }) { 38 + return ( 39 + <div 40 + className={`group p-6 rounded-2xl border transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md ${ 41 + accent 42 + ? "bg-primary-50 dark:bg-primary-950/30 border-primary-200/50 dark:border-primary-800/40" 43 + : "bg-white dark:bg-surface-900 border-surface-200/80 dark:border-surface-800" 44 + }`} 45 + > 46 + <div 47 + className={`w-11 h-11 rounded-xl flex items-center justify-center mb-4 transition-colors ${ 48 + accent 49 + ? "bg-primary-600 text-white" 50 + : "bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 group-hover:bg-primary-600 group-hover:text-white dark:group-hover:bg-primary-500" 51 + }`} 52 + > 53 + <Icon size={20} /> 54 + </div> 55 + <h3 className="font-display font-semibold text-base mb-2 text-surface-900 dark:text-white"> 56 + {title} 57 + </h3> 58 + <p className="text-sm text-surface-500 dark:text-surface-400 leading-relaxed"> 59 + {description} 60 + </p> 61 + </div> 62 + ); 63 + } 64 + 65 + function ExtensionFeature({ 66 + icon: Icon, 67 + title, 68 + description, 69 + }: { 70 + icon: React.ElementType; 71 + title: string; 72 + description: string; 73 + }) { 74 + return ( 75 + <div className="flex gap-4 items-start"> 76 + <div className="w-9 h-9 rounded-lg bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center flex-shrink-0 text-primary-600 dark:text-primary-400"> 77 + <Icon size={18} /> 78 + </div> 79 + <div> 80 + <h4 className="font-semibold text-sm text-surface-900 dark:text-white mb-1"> 81 + {title} 82 + </h4> 83 + <p className="text-sm text-surface-500 dark:text-surface-400 leading-relaxed"> 84 + {description} 85 + </p> 86 + </div> 87 + </div> 88 + ); 89 + } 90 + 91 + export default function About() { 92 + useStore($theme); // ensure theme is applied on this page 93 + 94 + const [browser] = React.useState< 95 + "chrome" | "firefox" | "edge" | "safari" | "other" 96 + >(() => { 97 + if (typeof navigator === "undefined") return "other"; 98 + const ua = navigator.userAgent; 99 + if (/Edg\//i.test(ua)) return "edge"; 100 + if (/Firefox/i.test(ua)) return "firefox"; 101 + if (/^((?!chrome|android).)*safari/i.test(ua)) return "safari"; 102 + if (/Chrome/i.test(ua)) return "chrome"; 103 + return "other"; 104 + }); 105 + 106 + const extensionLink = 107 + browser === "firefox" 108 + ? "https://addons.mozilla.org/en-US/firefox/addon/margin/" 109 + : browser === "edge" 110 + ? "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn" 111 + : "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa"; 112 + 113 + const ExtensionIcon = 114 + browser === "firefox" ? FaFirefox : browser === "edge" ? FaEdge : Chrome; 115 + const extensionLabel = 116 + browser === "firefox" ? "Firefox" : browser === "edge" ? "Edge" : "Chrome"; 117 + 118 + return ( 119 + <div className="min-h-screen bg-surface-50 dark:bg-surface-950"> 120 + <nav className="sticky top-0 z-50 bg-surface-50/80 dark:bg-surface-950/80 backdrop-blur-xl border-b border-surface-200/60 dark:border-surface-800/60"> 121 + <div className="max-w-5xl mx-auto px-6 h-14 flex items-center justify-between"> 122 + <Link to="/home" className="flex items-center gap-2.5 group"> 123 + <img src="/logo.svg" alt="Margin" className="w-7 h-7" /> 124 + <span className="font-display font-bold text-lg tracking-tight text-surface-900 dark:text-white"> 125 + Margin 126 + </span> 127 + </Link> 128 + <div className="flex items-center gap-3"> 129 + <Link 130 + to="/home" 131 + className="text-sm text-surface-500 dark:text-surface-400 hover:text-surface-900 dark:hover:text-white transition-colors px-3 py-1.5" 132 + > 133 + Feed 134 + </Link> 135 + <a 136 + href={extensionLink} 137 + target="_blank" 138 + rel="noopener noreferrer" 139 + className="text-sm font-medium px-4 py-2 bg-primary-600 hover:bg-primary-700 dark:bg-primary-500 dark:hover:bg-primary-400 text-white rounded-lg transition-colors" 140 + > 141 + Get Extension 142 + </a> 143 + </div> 144 + </div> 145 + </nav> 146 + 147 + <section> 148 + <div className="max-w-5xl mx-auto px-6 pt-24 pb-20 md:pt-32 md:pb-28 text-center"> 149 + <div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-400 text-xs font-medium mb-6"> 150 + <Sparkles size={13} /> 151 + Built on the AT Protocol 152 + </div> 153 + 154 + <h1 className="font-display text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight text-surface-900 dark:text-white leading-[1.1] mb-6"> 155 + Write on the margins 156 + <br /> 157 + <span className="text-primary-600 dark:text-primary-400"> 158 + of the internet 159 + </span> 160 + </h1> 161 + 162 + <p className="text-lg md:text-xl text-surface-500 dark:text-surface-400 max-w-2xl mx-auto leading-relaxed mb-10"> 163 + Margin is an open annotation layer for the internet. Highlight text, 164 + leave notes, and bookmark pages, all stored on your decentralized 165 + identity with the{" "} 166 + <a 167 + href="https://atproto.com" 168 + target="_blank" 169 + rel="noreferrer" 170 + className="text-primary-600 dark:text-primary-400 hover:underline" 171 + > 172 + AT Protocol 173 + </a> 174 + . 175 + </p> 176 + 177 + <div className="flex flex-col sm:flex-row items-center justify-center gap-3"> 178 + <Link 179 + to="/login" 180 + className="inline-flex items-center gap-2 px-7 py-3 bg-primary-600 hover:bg-primary-700 dark:bg-primary-500 dark:hover:bg-primary-400 text-white rounded-xl font-semibold transition-colors" 181 + > 182 + Get Started 183 + <ArrowRight size={16} /> 184 + </Link> 185 + <a 186 + href={extensionLink} 187 + target="_blank" 188 + rel="noopener noreferrer" 189 + className="inline-flex items-center gap-2 px-7 py-3 bg-surface-100 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 text-surface-700 dark:text-surface-200 hover:text-surface-900 dark:hover:text-white rounded-xl font-semibold transition-colors" 190 + > 191 + <ExtensionIcon size={16} /> 192 + Install for {extensionLabel} 193 + </a> 194 + </div> 195 + </div> 196 + </section> 197 + 198 + <section className="max-w-5xl mx-auto px-6 py-20 md:py-24"> 199 + <div className="text-center mb-12"> 200 + <h2 className="font-display text-2xl md:text-3xl font-bold tracking-tight text-surface-900 dark:text-white mb-3"> 201 + Everything you need to engage with the web 202 + </h2> 203 + <p className="text-surface-500 dark:text-surface-400 max-w-xl mx-auto"> 204 + More than bookmarks. A full toolkit for reading, thinking, and 205 + sharing on the open web. 206 + </p> 207 + </div> 208 + 209 + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5"> 210 + <FeatureCard 211 + icon={MessageSquareText} 212 + title="Annotations" 213 + description="Leave notes on any web page. Start discussions, share insights, or just jot down your thoughts for later." 214 + accent 215 + /> 216 + <FeatureCard 217 + icon={Highlighter} 218 + title="Highlights" 219 + description="Select and highlight text on any page with customizable colors. Your highlights are rendered inline with the CSS Highlights API." 220 + /> 221 + <FeatureCard 222 + icon={Bookmark} 223 + title="Bookmarks" 224 + description="Save pages with one click or a keyboard shortcut. All your bookmarks are synced to your AT Protocol identity." 225 + /> 226 + <FeatureCard 227 + icon={FolderOpen} 228 + title="Collections" 229 + description="Organize your annotations, highlights, and bookmarks into themed collections. Share them publicly or keep them private." 230 + /> 231 + <FeatureCard 232 + icon={Users} 233 + title="Social Discovery" 234 + description="See what others are saying about the pages you visit. Discover annotations, trending tags, and connect with other readers." 235 + /> 236 + <FeatureCard 237 + icon={Hash} 238 + title="Tags & Search" 239 + description="Tag your annotations for easy retrieval. Search by URL, tag, or content to find exactly what you're looking for." 240 + /> 241 + </div> 242 + </section> 243 + 244 + <section className="bg-surface-100/50 dark:bg-surface-900/50 border-y border-surface-200/60 dark:border-surface-800/60"> 245 + <div className="max-w-5xl mx-auto px-6 py-20 md:py-24"> 246 + <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center"> 247 + <div> 248 + <div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-surface-200/60 dark:bg-surface-800 text-surface-600 dark:text-surface-400 text-xs font-medium mb-5"> 249 + <ExtensionIcon size={13} /> 250 + Browser Extension 251 + </div> 252 + <h2 className="font-display text-2xl md:text-3xl font-bold tracking-tight text-surface-900 dark:text-white mb-4"> 253 + Your annotation toolkit, 254 + <br /> 255 + right in the browser 256 + </h2> 257 + <p className="text-surface-500 dark:text-surface-400 leading-relaxed mb-8"> 258 + The Margin extension brings the full annotation experience 259 + directly into every page you visit. Just select, annotate, and 260 + go. 261 + </p> 262 + 263 + <div className="space-y-5"> 264 + <ExtensionFeature 265 + icon={Eye} 266 + title="Inline Overlay" 267 + description="See annotations and highlights rendered directly on the page. Uses the CSS Highlights API for beautiful, native-feeling text underlines." 268 + /> 269 + <ExtensionFeature 270 + icon={MousePointerClick} 271 + title="Context Menu & Selection" 272 + description="Right-click any selected text to annotate, highlight, or quote it. Or just right-click the page to bookmark it instantly." 273 + /> 274 + <ExtensionFeature 275 + icon={Keyboard} 276 + title="Keyboard Shortcuts" 277 + description="Toggle the overlay, bookmark the current page, or annotate selected text without reaching for the mouse." 278 + /> 279 + <ExtensionFeature 280 + icon={PanelRight} 281 + title="Side Panel" 282 + description="Open the Margin side panel to browse annotations, bookmarks, and collections without leaving the page you're reading." 283 + /> 284 + </div> 285 + 286 + <div className="flex flex-col sm:flex-row gap-3 mt-8 flex-wrap"> 287 + <a 288 + href="https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa" 289 + target="_blank" 290 + rel="noopener noreferrer" 291 + className="inline-flex items-center justify-center gap-2 px-5 py-2.5 bg-surface-900 dark:bg-white text-white dark:text-surface-900 rounded-lg font-medium text-sm transition-all hover:opacity-90" 292 + > 293 + <Chrome size={15} /> 294 + Chrome Web Store 295 + <ExternalLink size={12} /> 296 + </a> 297 + <a 298 + href="https://addons.mozilla.org/en-US/firefox/addon/margin/" 299 + target="_blank" 300 + rel="noopener noreferrer" 301 + className="inline-flex items-center justify-center gap-2 px-5 py-2.5 bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-200 rounded-lg font-medium text-sm transition-all hover:bg-surface-200 dark:hover:bg-surface-700 border border-surface-200/80 dark:border-surface-700/80" 302 + > 303 + <FaFirefox size={15} /> 304 + Firefox Add-ons 305 + <ExternalLink size={12} /> 306 + </a> 307 + <a 308 + href="https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn" 309 + target="_blank" 310 + rel="noopener noreferrer" 311 + className="inline-flex items-center justify-center gap-2 px-5 py-2.5 bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-200 rounded-lg font-medium text-sm transition-all hover:bg-surface-200 dark:hover:bg-surface-700 border border-surface-200/80 dark:border-surface-700/80" 312 + > 313 + <FaEdge size={15} /> 314 + Edge Add-ons 315 + <ExternalLink size={12} /> 316 + </a> 317 + <a 318 + href="https://www.icloud.com/shortcuts/21c87edf29b046db892c9e57dac6d1fd" 319 + target="_blank" 320 + rel="noopener noreferrer" 321 + className="inline-flex items-center justify-center gap-2 px-5 py-2.5 bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-200 rounded-lg font-medium text-sm transition-all hover:bg-surface-200 dark:hover:bg-surface-700 border border-surface-200/80 dark:border-surface-700/80" 322 + > 323 + <AppleIcon size={15} /> 324 + iOS Shortcut 325 + <ExternalLink size={12} /> 326 + </a> 327 + </div> 328 + </div> 329 + 330 + <div className="relative hidden lg:block"> 331 + <div className="relative rounded-2xl border border-surface-200 dark:border-surface-700 bg-surface-50 dark:bg-surface-900 p-6 shadow-xl"> 332 + <div className="flex items-center gap-2 mb-4"> 333 + <div className="flex gap-1.5"> 334 + <div className="w-3 h-3 rounded-full bg-red-400/60" /> 335 + <div className="w-3 h-3 rounded-full bg-yellow-400/60" /> 336 + <div className="w-3 h-3 rounded-full bg-green-400/60" /> 337 + </div> 338 + <div className="flex-1 mx-3 bg-surface-200 dark:bg-surface-800 rounded-md h-6 flex items-center px-3"> 339 + <span className="text-[10px] text-surface-400 truncate"> 340 + example.com/article/how-to-think-clearly 341 + </span> 342 + </div> 343 + </div> 344 + 345 + <div className="space-y-3 text-sm text-surface-600 dark:text-surface-300 leading-relaxed"> 346 + <div className="h-3 bg-surface-200 dark:bg-surface-800 rounded w-3/4" /> 347 + <div className="h-3 bg-surface-200 dark:bg-surface-800 rounded w-full" /> 348 + <div className="flex gap-0.5 flex-wrap"> 349 + <div className="h-3 bg-surface-200 dark:bg-surface-800 rounded w-1/4" /> 350 + <span className="px-1 py-0.5 bg-yellow-200/70 dark:bg-yellow-500/30 rounded text-xs text-surface-700 dark:text-yellow-200 font-medium leading-none"> 351 + The point here is that Margin is indeed 352 + </span> 353 + <div className="h-3 bg-surface-200 dark:bg-surface-800 rounded w-1/5" /> 354 + </div> 355 + <div className="h-3 bg-surface-200 dark:bg-surface-800 rounded w-5/6" /> 356 + <div className="flex gap-0.5 flex-wrap"> 357 + <div className="h-3 bg-surface-200 dark:bg-surface-800 rounded w-2/5" /> 358 + <span className="px-1 py-0.5 bg-primary-200/70 dark:bg-primary-500/30 rounded text-xs text-primary-700 dark:text-primary-200 font-medium leading-none"> 359 + the best thing ever 360 + </span> 361 + <div className="h-3 bg-surface-200 dark:bg-surface-800 rounded w-1/4" /> 362 + </div> 363 + <div className="h-3 bg-surface-200 dark:bg-surface-800 rounded w-2/3" /> 364 + </div> 365 + 366 + <div className="absolute -right-4 top-1/3 w-56 bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 shadow-lg p-3.5"> 367 + <div className="flex items-center gap-2 mb-2"> 368 + <div className="w-6 h-6 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white text-[10px] font-bold"> 369 + S 370 + </div> 371 + <span className="text-xs font-semibold text-surface-900 dark:text-white"> 372 + @scan.margin.cafe 373 + </span> 374 + </div> 375 + <p className="text-xs text-surface-600 dark:text-surface-300 leading-relaxed"> 376 + I agree, Margin is just so good, like the other day I was 377 + drinking some of that Margin for breakfast 378 + </p> 379 + <div className="flex items-center gap-3 mt-2.5 pt-2 border-t border-surface-100 dark:border-surface-700"> 380 + <span className="text-[10px] text-surface-400 flex items-center gap-1"> 381 + <Heart size={10} /> 3 382 + </span> 383 + <span className="text-[10px] text-surface-400 flex items-center gap-1"> 384 + <MessageSquareText size={10} /> 1 385 + </span> 386 + </div> 387 + </div> 388 + </div> 389 + </div> 390 + </div> 391 + </div> 392 + </section> 393 + 394 + <section className="max-w-5xl mx-auto px-6 py-20 md:py-24"> 395 + <div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center"> 396 + <div> 397 + <div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-400 text-xs font-medium mb-5"> 398 + <Shield size={13} /> 399 + Decentralized 400 + </div> 401 + <h2 className="font-display text-2xl md:text-3xl font-bold tracking-tight text-surface-900 dark:text-white mb-4"> 402 + Your data, your identity 403 + </h2> 404 + <p className="text-surface-500 dark:text-surface-400 leading-relaxed mb-6"> 405 + Margin is built on the{" "} 406 + <a 407 + href="https://atproto.com" 408 + target="_blank" 409 + rel="noreferrer" 410 + className="text-primary-600 dark:text-primary-400 hover:underline font-medium" 411 + > 412 + AT Protocol 413 + </a> 414 + , the open protocol that powers apps like Bluesky. Your 415 + annotations, highlights, and bookmarks are stored in your personal 416 + data repository, not locked in a silo. 417 + </p> 418 + <ul className="space-y-3 text-sm text-surface-600 dark:text-surface-300"> 419 + <li className="flex items-start gap-3"> 420 + <div className="w-5 h-5 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center flex-shrink-0 mt-0.5"> 421 + <div className="w-1.5 h-1.5 rounded-full bg-primary-600 dark:bg-primary-400" /> 422 + </div> 423 + Sign in with your AT Protocol handle, no new account needed 424 + </li> 425 + <li className="flex items-start gap-3"> 426 + <div className="w-5 h-5 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center flex-shrink-0 mt-0.5"> 427 + <div className="w-1.5 h-1.5 rounded-full bg-primary-600 dark:bg-primary-400" /> 428 + </div> 429 + Your data lives in your PDS, portable and under your control 430 + </li> 431 + <li className="flex items-start gap-3"> 432 + <div className="w-5 h-5 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center flex-shrink-0 mt-0.5"> 433 + <div className="w-1.5 h-1.5 rounded-full bg-primary-600 dark:bg-primary-400" /> 434 + </div> 435 + Custom Lexicon schemas for annotations, highlights, collections 436 + & more 437 + </li> 438 + <li className="flex items-start gap-3"> 439 + <div className="w-5 h-5 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center flex-shrink-0 mt-0.5"> 440 + <div className="w-1.5 h-1.5 rounded-full bg-primary-600 dark:bg-primary-400" /> 441 + </div> 442 + Fully open source, check out the code and contribute 443 + </li> 444 + </ul> 445 + </div> 446 + 447 + <div className="rounded-2xl bg-surface-900 dark:bg-surface-800 p-5 text-sm font-mono shadow-xl border border-surface-800 dark:border-surface-700"> 448 + <div className="flex items-center gap-2 mb-4"> 449 + <div className="text-xs text-surface-500">lexicon</div> 450 + <div className="text-xs text-primary-400 px-2 py-0.5 rounded bg-primary-400/10"> 451 + at.margin.annotation 452 + </div> 453 + </div> 454 + <div className="space-y-1 text-[13px] leading-relaxed"> 455 + <span className="text-surface-500">{"{"}</span> 456 + <div className="pl-4"> 457 + <span className="text-green-400">"type"</span> 458 + <span className="text-surface-400">: </span> 459 + <span className="text-amber-400">"record"</span> 460 + <span className="text-surface-400">,</span> 461 + </div> 462 + <div className="pl-4"> 463 + <span className="text-green-400">"record"</span> 464 + <span className="text-surface-400">: {"{"}</span> 465 + </div> 466 + <div className="pl-8"> 467 + <span className="text-green-400">"body"</span> 468 + <span className="text-surface-400">: </span> 469 + <span className="text-amber-400">"Great insight..."</span> 470 + <span className="text-surface-400">,</span> 471 + </div> 472 + <div className="pl-8"> 473 + <span className="text-green-400">"target"</span> 474 + <span className="text-surface-400">: {"{"}</span> 475 + </div> 476 + <div className="pl-12"> 477 + <span className="text-green-400">"source"</span> 478 + <span className="text-surface-400">: </span> 479 + <span className="text-sky-400">"https://..."</span> 480 + <span className="text-surface-400">,</span> 481 + </div> 482 + <div className="pl-12"> 483 + <span className="text-green-400">"selector"</span> 484 + <span className="text-surface-400">: {"{"}</span> 485 + </div> 486 + <div className="pl-16"> 487 + <span className="text-green-400">"exact"</span> 488 + <span className="text-surface-400">: </span> 489 + <span className="text-amber-400">"selected text"</span> 490 + </div> 491 + <div className="pl-12"> 492 + <span className="text-surface-400">{"}"}</span> 493 + </div> 494 + <div className="pl-8"> 495 + <span className="text-surface-400">{"}"}</span> 496 + </div> 497 + <div className="pl-4"> 498 + <span className="text-surface-400">{"}"}</span> 499 + </div> 500 + <span className="text-surface-500">{"}"}</span> 501 + </div> 502 + </div> 503 + </div> 504 + </section> 505 + 506 + <section className="border-t border-surface-200/60 dark:border-surface-800/60"> 507 + <div className="max-w-5xl mx-auto px-6 py-20 md:py-24 text-center"> 508 + <h2 className="font-display text-2xl md:text-3xl font-bold tracking-tight text-surface-900 dark:text-white mb-4"> 509 + Start writing on the margins 510 + </h2> 511 + <p className="text-surface-500 dark:text-surface-400 max-w-lg mx-auto mb-8"> 512 + Join the open annotation layer. Sign in with your AT Protocol 513 + identity and install the extension to get started. 514 + </p> 515 + <div className="flex flex-col sm:flex-row items-center justify-center gap-3"> 516 + <Link 517 + to="/login" 518 + className="inline-flex items-center gap-2 px-7 py-3 bg-primary-600 hover:bg-primary-700 dark:bg-primary-500 dark:hover:bg-primary-400 text-white rounded-xl font-semibold transition-colors" 519 + > 520 + Sign in 521 + <ArrowRight size={16} /> 522 + </Link> 523 + <a 524 + href="https://github.com/margin-at" 525 + target="_blank" 526 + rel="noreferrer" 527 + className="inline-flex items-center gap-2 px-7 py-3 text-surface-600 dark:text-surface-300 hover:text-surface-900 dark:hover:text-white transition-colors font-medium" 528 + > 529 + <Github size={16} /> 530 + View on GitHub 531 + </a> 532 + </div> 533 + </div> 534 + </section> 535 + 536 + <footer className="border-t border-surface-200/60 dark:border-surface-800/60"> 537 + <div className="max-w-5xl mx-auto px-6 py-8"> 538 + <div className="flex flex-col sm:flex-row items-center justify-between gap-4"> 539 + <div className="flex items-center gap-2.5"> 540 + <img 541 + src="/logo.svg" 542 + alt="Margin" 543 + className="w-5 h-5 opacity-60" 544 + /> 545 + <span className="text-sm text-surface-400 dark:text-surface-500"> 546 + © 2026 Margin 547 + </span> 548 + </div> 549 + <div className="flex items-center gap-5 text-sm text-surface-400 dark:text-surface-500"> 550 + <Link 551 + to="/home" 552 + className="hover:text-surface-600 dark:hover:text-surface-300 transition-colors" 553 + > 554 + Feed 555 + </Link> 556 + <a 557 + href="/privacy" 558 + className="hover:text-surface-600 dark:hover:text-surface-300 transition-colors" 559 + > 560 + Privacy 561 + </a> 562 + <a 563 + href="/terms" 564 + className="hover:text-surface-600 dark:hover:text-surface-300 transition-colors" 565 + > 566 + Terms 567 + </a> 568 + <a 569 + href="https://github.com/margin-at" 570 + target="_blank" 571 + rel="noreferrer" 572 + className="hover:text-surface-600 dark:hover:text-surface-300 transition-colors" 573 + > 574 + GitHub 575 + </a> 576 + <a 577 + href="mailto:hello@margin.at" 578 + className="hover:text-surface-600 dark:hover:text-surface-300 transition-colors" 579 + > 580 + Contact 581 + </a> 582 + </div> 583 + </div> 584 + </div> 585 + </footer> 586 + </div> 587 + ); 588 + }
+30 -10
web/src/views/auth/Login.tsx
··· 1 1 import React, { useState, useEffect, useRef } from "react"; 2 - import { Link } from "react-router-dom"; 3 - import { Loader2, AtSign } from "lucide-react"; 2 + import { Link, useSearchParams, Navigate } from "react-router-dom"; 3 + import { AtSign } from "lucide-react"; 4 4 import { BlueskyIcon, MarginIcon } from "../../components/common/Icons"; 5 5 import SignUpModal from "../../components/modals/SignUpModal"; 6 6 import { ··· 9 9 type ActorSearchItem, 10 10 } from "../../api/client"; 11 11 import { Avatar } from "../../components/ui"; 12 + import { useStore } from "@nanostores/react"; 13 + import { $theme } from "../../store/theme"; 14 + import { $user } from "../../store/auth"; 12 15 13 16 export default function Login() { 17 + useStore($theme); // ensure theme is applied on this page 18 + const user = useStore($user); 19 + const [searchParams] = useSearchParams(); 14 20 const [handle, setHandle] = useState(""); 15 21 const [suggestions, setSuggestions] = useState<ActorSearchItem[]>([]); 16 22 const [showSuggestions, setShowSuggestions] = useState(false); 17 23 const [loading, setLoading] = useState(false); 18 - const [error, setError] = useState<string | null>(null); 24 + const [error, setError] = useState<string | null>( 25 + searchParams.get("error") || null, 26 + ); 19 27 const [selectedIndex, setSelectedIndex] = useState(-1); 20 28 const [showSignUp, setShowSignUp] = useState(false); 21 29 ··· 70 78 } 71 79 }, 300); 72 80 return () => clearTimeout(timer); 73 - } else { 74 - setSuggestions([]); 75 - setShowSuggestions(false); 76 81 } 77 82 }, [handle]); 78 83 ··· 128 133 if (result.authorizationUrl) { 129 134 window.location.href = result.authorizationUrl; 130 135 } 131 - } catch (err: any) { 132 - setError(err.message || "Failed to initiate login. Please try again."); 136 + if (result.authorizationUrl) { 137 + window.location.href = result.authorizationUrl; 138 + } 139 + } catch (err) { 140 + const message = err instanceof Error ? err.message : "Unknown error"; 141 + setError(message || "Failed to initiate login. Please try again."); 133 142 setLoading(false); 134 143 } 135 144 }; 145 + 146 + if (user) { 147 + return <Navigate to="/home" replace />; 148 + } 136 149 137 150 return ( 138 151 <div className="min-h-screen flex items-center justify-center bg-surface-50 dark:bg-surface-950 p-4"> ··· 166 179 ref={inputRef} 167 180 type="text" 168 181 value={handle} 169 - onChange={(e) => setHandle(e.target.value)} 182 + onChange={(e) => { 183 + const val = e.target.value; 184 + setHandle(val); 185 + if (val.length < 3) { 186 + setSuggestions([]); 187 + setShowSuggestions(false); 188 + } 189 + }} 170 190 onKeyDown={handleKeyDown} 171 191 onFocus={() => 172 192 handle.length >= 3 && ··· 219 239 <button 220 240 type="submit" 221 241 disabled={loading || !handle} 222 - className="w-full py-3.5 bg-surface-900 dark:bg-white hover:bg-surface-800 dark:hover:bg-surface-100 text-white dark:text-surface-900 rounded-xl font-bold text-lg shadow-lg shadow-surface-900/10 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2" 242 + className="w-full py-3.5 bg-primary-600 dark:bg-primary-500 hover:bg-primary-700 dark:hover:bg-primary-400 text-white rounded-xl font-bold text-lg shadow-lg shadow-primary-600/20 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2" 223 243 > 224 244 {loading ? "Connecting..." : "Continue"} 225 245 </button>
+31 -31
web/src/views/collections/CollectionDetail.tsx
··· 32 32 const [error, setError] = useState<string | null>(null); 33 33 34 34 useEffect(() => { 35 - loadData(); 36 - }, [handle, rkey, uri]); 35 + const loadData = async () => { 36 + setLoading(true); 37 + try { 38 + let targetUri = uri; 39 + if (!targetUri && handle && rkey) { 40 + if (handle.startsWith("did:")) { 41 + targetUri = `at://${handle}/at.margin.collection/${rkey}`; 42 + } else { 43 + const did = await resolveHandle(handle); 44 + if (did) { 45 + targetUri = `at://${did}/at.margin.collection/${rkey}`; 46 + } else { 47 + setError("Collection not found"); 48 + setLoading(false); 49 + return; 50 + } 51 + } 52 + } 37 53 38 - const loadData = async () => { 39 - setLoading(true); 40 - try { 41 - let targetUri = uri; 42 - if (!targetUri && handle && rkey) { 43 - if (handle.startsWith("did:")) { 44 - targetUri = `at://${handle}/at.margin.collection/${rkey}`; 45 - } else { 46 - const did = await resolveHandle(handle); 47 - if (did) { 48 - targetUri = `at://${did}/at.margin.collection/${rkey}`; 54 + if (targetUri) { 55 + const col = await getCollection(targetUri); 56 + if (col) { 57 + setCollection(col); 58 + const colItems = await getCollectionItems(col.uri); 59 + setItems(colItems.filter((i) => i && i.uri)); 49 60 } else { 50 61 setError("Collection not found"); 51 - setLoading(false); 52 - return; 53 62 } 54 63 } 64 + } catch { 65 + setError("Failed to load collection"); 66 + } finally { 67 + setLoading(false); 55 68 } 69 + }; 56 70 57 - if (targetUri) { 58 - const col = await getCollection(targetUri); 59 - if (col) { 60 - setCollection(col); 61 - const colItems = await getCollectionItems(col.uri); 62 - setItems(colItems.filter((i) => i && i.uri)); 63 - } else { 64 - setError("Collection not found"); 65 - } 66 - } 67 - } catch (e) { 68 - setError("Failed to load collection"); 69 - } finally { 70 - setLoading(false); 71 - } 72 - }; 71 + loadData(); 72 + }, [handle, rkey, uri]); 73 73 74 74 const handleDelete = async () => { 75 75 if (!collection) return;
+16 -12
web/src/views/collections/Collections.tsx
··· 5 5 deleteCollection, 6 6 } from "../../api/client"; 7 7 import { Plus, Folder, Trash2, X } from "lucide-react"; 8 - import CollectionIcon, { 9 - ICON_MAP, 10 - } from "../../components/common/CollectionIcon"; 8 + import CollectionIcon from "../../components/common/CollectionIcon"; 9 + import { ICON_MAP } from "../../components/common/iconMap"; 11 10 import { useStore } from "@nanostores/react"; 12 11 import { $user } from "../../store/auth"; 13 12 import type { Collection } from "../../types"; ··· 25 24 const [newItemIcon, setNewItemIcon] = useState("folder"); 26 25 const [creating, setCreating] = useState(false); 27 26 27 + const fetchCollections = async () => { 28 + try { 29 + setLoading(true); 30 + const data = await getCollections(); 31 + setCollections(data); 32 + } catch (error) { 33 + console.error("Failed to load collections:", error); 34 + } finally { 35 + setLoading(false); 36 + } 37 + }; 38 + 28 39 useEffect(() => { 29 - loadCollections(); 40 + fetchCollections(); 30 41 }, []); 31 - 32 - const loadCollections = async () => { 33 - setLoading(true); 34 - const data = await getCollections(); 35 - setCollections(data); 36 - setLoading(false); 37 - }; 38 42 39 43 const handleCreate = async (e: React.FormEvent) => { 40 44 e.preventDefault(); ··· 48 52 setNewItemName(""); 49 53 setNewItemDesc(""); 50 54 setNewItemIcon("folder"); 51 - loadCollections(); 55 + fetchCollections(); 52 56 } 53 57 setCreating(false); 54 58 };
+17 -9
web/src/views/content/AnnotationDetail.tsx
··· 19 19 X, 20 20 AlertTriangle, 21 21 } from "lucide-react"; 22 - import { clsx } from "clsx"; 23 22 import { getAvatarUrl } from "../../api/client"; 24 23 25 24 export default function AnnotationDetail() { ··· 60 59 } else { 61 60 throw new Error("Could not resolve handle"); 62 61 } 63 - } catch (e: any) { 64 - setError("Failed to resolve handle: " + e.message); 62 + } catch (e) { 63 + setError( 64 + "Failed to resolve handle: " + 65 + (e instanceof Error ? e.message : "Unknown error"), 66 + ); 65 67 setLoading(false); 66 68 } 67 69 } else if (did && rkey) { ··· 108 110 setAnnotation(annData); 109 111 setReplies(repliesData.items || []); 110 112 } 111 - } catch (err: any) { 112 - setError(err.message); 113 + } catch (err) { 114 + setError(err instanceof Error ? err.message : "Unknown error"); 113 115 } finally { 114 116 setLoading(false); 115 117 } ··· 142 144 setReplyText(""); 143 145 setReplyingTo(null); 144 146 await refreshReplies(); 145 - } catch (err: any) { 146 - alert("Failed to post reply: " + err.message); 147 + } catch (err) { 148 + alert( 149 + "Failed to post reply: " + 150 + (err instanceof Error ? err.message : "Unknown error"), 151 + ); 147 152 } finally { 148 153 setPosting(false); 149 154 } ··· 154 159 try { 155 160 await deleteReply(reply.uri || reply.id!); 156 161 await refreshReplies(); 157 - } catch (err: any) { 158 - alert("Failed to delete: " + err.message); 162 + } catch (err) { 163 + alert( 164 + "Failed to delete: " + 165 + (err instanceof Error ? err.message : "Unknown error"), 166 + ); 159 167 } 160 168 }; 161 169
+63 -57
web/src/views/content/Url.tsx
··· 1 - import React, { useState, useEffect } from "react"; 1 + import React, { useState, useEffect, useCallback } from "react"; 2 2 import { useNavigate, useSearchParams } from "react-router-dom"; 3 3 import { useStore } from "@nanostores/react"; 4 4 import { $user } from "../../store/auth"; ··· 16 16 Clock, 17 17 Globe, 18 18 } from "lucide-react"; 19 - import { clsx } from "clsx"; 19 + 20 20 import { EmptyState, Tabs } from "../../components/ui"; 21 21 22 22 export default function UrlPage() { ··· 41 41 if (stored) { 42 42 try { 43 43 setRecentSearches(JSON.parse(stored).slice(0, 5)); 44 - } catch {} 44 + } catch (e) { 45 + console.warn("Failed to parse recent searches", e); 46 + } 45 47 } 46 48 }, []); 47 49 48 - const saveRecentSearch = (q: string) => { 49 - const updated = [q, ...recentSearches.filter((s) => s !== q)].slice(0, 5); 50 - setRecentSearches(updated); 51 - localStorage.setItem("margin-recent-searches", JSON.stringify(updated)); 52 - }; 50 + const saveRecentSearch = useCallback((q: string) => { 51 + setRecentSearches((prev) => { 52 + const updated = [q, ...prev.filter((s) => s !== q)].slice(0, 5); 53 + localStorage.setItem("margin-recent-searches", JSON.stringify(updated)); 54 + return updated; 55 + }); 56 + }, []); 53 57 54 58 useEffect(() => { 55 - if (query) { 56 - performSearch(query); 57 - } else { 58 - setSearched(false); 59 + const performSearch = async (urlOrHandle: string) => { 60 + if (!urlOrHandle.trim()) return; 61 + 62 + setLoading(true); 63 + setError(null); 64 + setSearched(true); 59 65 setAnnotations([]); 60 66 setHighlights([]); 61 - setLoading(false); 62 - } 63 - }, [query]); 64 67 65 - const performSearch = async (urlOrHandle: string) => { 66 - if (!urlOrHandle.trim()) return; 68 + const isProtocol = 69 + urlOrHandle.startsWith("http://") || urlOrHandle.startsWith("https://"); 67 70 68 - setLoading(true); 69 - setError(null); 70 - setSearched(true); 71 - setAnnotations([]); 72 - setHighlights([]); 73 - 74 - const isProtocol = 75 - urlOrHandle.startsWith("http://") || urlOrHandle.startsWith("https://"); 76 - 77 - if (isProtocol) { 78 - try { 79 - const data = await getByTarget(urlOrHandle); 80 - setAnnotations(data.annotations || []); 81 - setHighlights(data.highlights || []); 82 - saveRecentSearch(urlOrHandle); 83 - } catch (err: any) { 84 - setError(err.message); 85 - } finally { 86 - setLoading(false); 87 - } 88 - } else { 89 - try { 90 - const actorRes = await searchActors(urlOrHandle); 91 - if (actorRes?.actors?.length > 0) { 92 - const match = actorRes.actors[0]; 93 - navigate(`/profile/${encodeURIComponent(match.handle)}`, { 94 - replace: true, 95 - }); 96 - return; 97 - } else { 98 - setError( 99 - "User not found. To search for a URL, please include 'http://' or 'https://'.", 100 - ); 71 + if (isProtocol) { 72 + try { 73 + const data = await getByTarget(urlOrHandle); 74 + setAnnotations(data.annotations || []); 75 + setHighlights(data.highlights || []); 76 + saveRecentSearch(urlOrHandle); 77 + } catch (err) { 78 + setError(err instanceof Error ? err.message : "Search failed"); 79 + } finally { 101 80 setLoading(false); 102 81 } 103 - } catch (err: any) { 104 - setError("Failed to search user."); 105 - setLoading(false); 82 + } else { 83 + try { 84 + const actorRes = await searchActors(urlOrHandle); 85 + if (actorRes?.actors?.length > 0) { 86 + const match = actorRes.actors[0]; 87 + navigate(`/profile/${encodeURIComponent(match.handle)}`, { 88 + replace: true, 89 + }); 90 + return; 91 + } else { 92 + setError( 93 + "User not found. To search for a URL, please include 'http://' or 'https://'.", 94 + ); 95 + setLoading(false); 96 + } 97 + } catch { 98 + setError("Failed to search user."); 99 + setLoading(false); 100 + } 106 101 } 102 + }; 103 + 104 + if (query) { 105 + performSearch(query); 106 + } else { 107 + setSearched(false); 108 + setAnnotations([]); 109 + setHighlights([]); 110 + setLoading(false); 107 111 } 108 - }; 112 + }, [query, navigate, saveRecentSearch]); 109 113 110 114 const myAnnotations = user 111 115 ? annotations.filter((a) => (a.author?.did || a.creator?.did) === user.did) ··· 127 131 await navigator.clipboard.writeText(shareUrl); 128 132 setCopied(true); 129 133 setTimeout(() => setCopied(false), 2000); 130 - } catch { 131 - prompt("Copy this link:", shareUrl); 134 + } catch (err) { 135 + console.error("Failed to copy link:", err); 132 136 } 133 137 }; 134 138 ··· 271 275 }, 272 276 ]} 273 277 activeTab={activeTab} 274 - onChange={(id) => setActiveTab(id as any)} 278 + onChange={(id: string) => 279 + setActiveTab(id as "all" | "annotations" | "highlights") 280 + } 275 281 /> 276 282 </div> 277 283
+3 -3
web/src/views/content/UserUrl.tsx
··· 1 1 import React, { useState, useEffect } from "react"; 2 - import { useParams, Link } from "react-router-dom"; 2 + import { useParams } from "react-router-dom"; 3 3 import { getUserTargetItems } from "../../api/client"; 4 4 import type { AnnotationItem, UserProfile } from "../../types"; 5 5 import Card from "../../components/common/Card"; ··· 55 55 const data = await getUserTargetItems(did, decodedUrl); 56 56 setAnnotations(data.annotations || []); 57 57 setHighlights(data.highlights || []); 58 - } catch (err: any) { 59 - setError(err.message); 58 + } catch (err) { 59 + setError(err instanceof Error ? err.message : "Unknown error"); 60 60 } finally { 61 61 setLoading(false); 62 62 }
+210 -116
web/src/views/core/Feed.tsx
··· 1 - import React, { useEffect, useState } from "react"; 1 + import React, { useEffect, useState, useCallback } from "react"; 2 2 import { getFeed } from "../../api/client"; 3 3 import Card from "../../components/common/Card"; 4 - import { Loader2, Sparkles, Clock, Bookmark, Users } from "lucide-react"; 4 + import { 5 + Loader2, 6 + Clock, 7 + Bookmark, 8 + MessageSquare, 9 + Highlighter, 10 + } from "lucide-react"; 5 11 import { useStore } from "@nanostores/react"; 6 - import { $user, initAuth } from "../../store/auth"; 12 + import { $user } from "../../store/auth"; 7 13 import type { AnnotationItem } from "../../types"; 8 14 import { Tabs, EmptyState, Button } from "../../components/ui"; 15 + import LayoutToggle from "../../components/ui/LayoutToggle"; 16 + import { $feedLayout } from "../../store/feedLayout"; 17 + import { clsx } from "clsx"; 9 18 10 19 interface FeedProps { 11 20 initialType?: string; ··· 14 23 emptyMessage?: string; 15 24 } 16 25 17 - export default function Feed({ 18 - initialType = "all", 26 + function FeedContent({ 27 + type, 19 28 motivation, 20 - showTabs = true, 21 - emptyMessage = "No items found.", 22 - }: FeedProps) { 23 - const user = useStore($user); 29 + emptyMessage, 30 + layout, 31 + }: { 32 + type: string; 33 + motivation?: string; 34 + emptyMessage: string; 35 + layout: "list" | "mosaic"; 36 + }) { 24 37 const [items, setItems] = useState<AnnotationItem[]>([]); 25 38 const [loading, setLoading] = useState(true); 26 - const [activeTab, setActiveTab] = useState(initialType); 39 + const [loadingMore, setLoadingMore] = useState(false); 40 + const [hasMore, setHasMore] = useState(false); 41 + const [offset, setOffset] = useState(0); 27 42 28 - useEffect(() => { 29 - initAuth(); 30 - }, []); 43 + const LIMIT = 50; 31 44 32 45 useEffect(() => { 33 - const fetchFeed = async () => { 34 - setLoading(true); 35 - try { 36 - const type = activeTab; 37 - const data = await getFeed({ type, motivation }); 38 - setItems(data?.items || []); 39 - } catch (e) { 46 + let cancelled = false; 47 + 48 + getFeed({ type, motivation, limit: LIMIT, offset: 0 }) 49 + .then((data) => { 50 + if (cancelled) return; 51 + const fetched = data?.items || []; 52 + setItems(fetched); 53 + setHasMore(fetched.length >= LIMIT); 54 + setOffset(fetched.length); 55 + setLoading(false); 56 + }) 57 + .catch((e) => { 58 + if (cancelled) return; 40 59 console.error(e); 41 - } finally { 60 + setItems([]); 61 + setHasMore(false); 42 62 setLoading(false); 43 - } 63 + }); 64 + 65 + return () => { 66 + cancelled = true; 44 67 }; 45 - fetchFeed(); 46 - }, [activeTab, motivation]); 68 + }, [type, motivation]); 69 + 70 + const loadMore = useCallback(async () => { 71 + setLoadingMore(true); 72 + try { 73 + const data = await getFeed({ type, motivation, limit: LIMIT, offset }); 74 + const fetched = data?.items || []; 75 + setItems((prev) => [...prev, ...fetched]); 76 + setHasMore(fetched.length >= LIMIT); 77 + setOffset((prev) => prev + fetched.length); 78 + } catch (e) { 79 + console.error(e); 80 + } finally { 81 + setLoadingMore(false); 82 + } 83 + }, [type, motivation, offset]); 47 84 48 85 const handleDelete = (uri: string) => { 49 86 setItems((prev) => prev.filter((i) => i.uri !== uri)); 50 87 }; 51 88 89 + if (loading) { 90 + return ( 91 + <div className="flex flex-col items-center justify-center py-20 gap-3"> 92 + <Loader2 93 + className="animate-spin text-primary-600 dark:text-primary-400" 94 + size={32} 95 + /> 96 + <p className="text-sm text-surface-400 dark:text-surface-500"> 97 + Loading feed... 98 + </p> 99 + </div> 100 + ); 101 + } 102 + 103 + if (items.length === 0) { 104 + return ( 105 + <EmptyState 106 + icon={<Clock size={48} />} 107 + title="Nothing here yet" 108 + message={emptyMessage} 109 + /> 110 + ); 111 + } 112 + 113 + const loadMoreButton = hasMore && ( 114 + <div className="flex justify-center py-6"> 115 + <button 116 + onClick={loadMore} 117 + disabled={loadingMore} 118 + className="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-xl bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors disabled:opacity-50" 119 + > 120 + {loadingMore ? ( 121 + <> 122 + <Loader2 size={16} className="animate-spin" /> 123 + Loading... 124 + </> 125 + ) : ( 126 + "Load more" 127 + )} 128 + </button> 129 + </div> 130 + ); 131 + 132 + if (layout === "mosaic") { 133 + return ( 134 + <> 135 + <div className="columns-1 sm:columns-2 gap-4 animate-fade-in"> 136 + {items.map((item) => ( 137 + <div key={item.uri || item.cid} className="break-inside-avoid mb-4"> 138 + <Card item={item} onDelete={handleDelete} /> 139 + </div> 140 + ))} 141 + </div> 142 + {loadMoreButton} 143 + </> 144 + ); 145 + } 146 + 147 + return ( 148 + <> 149 + <div className="space-y-3 animate-fade-in"> 150 + {items.map((item) => ( 151 + <Card 152 + key={item.uri || item.cid} 153 + item={item} 154 + onDelete={handleDelete} 155 + /> 156 + ))} 157 + </div> 158 + {loadMoreButton} 159 + </> 160 + ); 161 + } 162 + 163 + export default function Feed({ 164 + initialType = "all", 165 + motivation, 166 + showTabs = true, 167 + emptyMessage = "No items found.", 168 + }: FeedProps) { 169 + const user = useStore($user); 170 + const layout = useStore($feedLayout); 171 + const [activeTab, setActiveTab] = useState(initialType); 172 + const [activeFilter, setActiveFilter] = useState<string | undefined>( 173 + motivation, 174 + ); 175 + 176 + const handleTabChange = (id: string) => { 177 + if (id === activeTab) return; 178 + setActiveTab(id); 179 + window.scrollTo({ top: 0, behavior: "smooth" }); 180 + }; 181 + 182 + const handleFilterChange = (id: string) => { 183 + const next = id === "all" ? undefined : id; 184 + if (next === activeFilter) return; 185 + setActiveFilter(next); 186 + window.scrollTo({ top: 0, behavior: "smooth" }); 187 + }; 188 + 52 189 const tabs = [ 53 190 { id: "all", label: "Recent" }, 54 191 { id: "popular", label: "Popular" }, ··· 57 194 { id: "semble", label: "Semble" }, 58 195 ]; 59 196 60 - if (!user && !loading) { 61 - return ( 62 - <div className="max-w-2xl mx-auto animate-fade-in"> 63 - <div className="text-center py-20 px-6"> 64 - <div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-primary-500 to-primary-600 mb-6"> 65 - <Sparkles className="text-white" size={28} /> 66 - </div> 67 - <h1 className="text-3xl font-display font-bold mb-3 tracking-tight text-surface-900 dark:text-white"> 197 + const filters = [ 198 + { id: "all", label: "All", icon: null }, 199 + { id: "commenting", label: "Annotations", icon: MessageSquare }, 200 + { id: "highlighting", label: "Highlights", icon: Highlighter }, 201 + { id: "bookmarking", label: "Bookmarks", icon: Bookmark }, 202 + ]; 203 + 204 + return ( 205 + <div className="max-w-2xl mx-auto"> 206 + {!user && ( 207 + <div className="text-center py-10 px-6 mb-4 animate-fade-in"> 208 + <h1 className="text-2xl font-display font-bold mb-2 tracking-tight text-surface-900 dark:text-white"> 68 209 Welcome to Margin 69 210 </h1> 70 - <p className="text-surface-500 dark:text-surface-400 mb-8 text-lg max-w-md mx-auto"> 71 - Annotate, highlight, and bookmark anything on the web. Your curated 72 - corner of the internet. 211 + <p className="text-surface-500 dark:text-surface-400 mb-4 max-w-md mx-auto"> 212 + Annotate, highlight, and bookmark anything on the web. 73 213 </p> 74 - <div className="flex flex-col sm:flex-row gap-3 justify-center"> 75 - <Button size="lg" onClick={() => (window.location.href = "/login")}> 214 + <div className="flex gap-3 justify-center"> 215 + <Button onClick={() => (window.location.href = "/login")}> 76 216 Get Started 77 217 </Button> 78 218 <Button 79 219 variant="secondary" 80 - size="lg" 81 220 onClick={() => 82 221 window.open("https://github.com/margin-at", "_blank") 83 222 } ··· 86 225 </Button> 87 226 </div> 88 227 </div> 228 + )} 89 229 90 - <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-8"> 91 - <div className="p-5 rounded-xl bg-surface-50 dark:bg-surface-900 border border-surface-100 dark:border-surface-800"> 92 - <div className="w-10 h-10 rounded-lg bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center mb-3"> 93 - <Sparkles 94 - size={20} 95 - className="text-yellow-600 dark:text-yellow-400" 96 - /> 230 + {showTabs && ( 231 + <div className="sticky top-0 z-10 bg-surface-50/95 dark:bg-surface-950/95 backdrop-blur-sm pb-3 mb-2 -mx-1 px-1 pt-1 space-y-2"> 232 + <Tabs tabs={tabs} activeTab={activeTab} onChange={handleTabChange} /> 233 + <div className="flex items-center gap-1.5 flex-wrap"> 234 + {filters.map((f) => { 235 + const isActive = 236 + f.id === "all" ? !activeFilter : activeFilter === f.id; 237 + return ( 238 + <button 239 + key={f.id} 240 + onClick={() => handleFilterChange(f.id)} 241 + className={clsx( 242 + "inline-flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded-full border transition-all", 243 + isActive 244 + ? "bg-primary-600 dark:bg-primary-500 text-white border-transparent shadow-sm" 245 + : "bg-white dark:bg-surface-900 text-surface-500 dark:text-surface-400 border-surface-200 dark:border-surface-700 hover:border-primary-300 dark:hover:border-primary-700 hover:text-primary-600 dark:hover:text-primary-400", 246 + )} 247 + > 248 + {f.icon && <f.icon size={12} />} 249 + {f.label} 250 + </button> 251 + ); 252 + })} 253 + <div className="ml-auto"> 254 + <LayoutToggle /> 97 255 </div> 98 - <h3 className="font-semibold text-surface-900 dark:text-white mb-1"> 99 - Highlight 100 - </h3> 101 - <p className="text-sm text-surface-500 dark:text-surface-400"> 102 - Save key passages from any page 103 - </p> 104 - </div> 105 - <div className="p-5 rounded-xl bg-surface-50 dark:bg-surface-900 border border-surface-100 dark:border-surface-800"> 106 - <div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center mb-3"> 107 - <Bookmark 108 - size={20} 109 - className="text-blue-600 dark:text-blue-400" 110 - /> 111 - </div> 112 - <h3 className="font-semibold text-surface-900 dark:text-white mb-1"> 113 - Bookmark 114 - </h3> 115 - <p className="text-sm text-surface-500 dark:text-surface-400"> 116 - Keep pages for later reading 117 - </p> 118 - </div> 119 - <div className="p-5 rounded-xl bg-surface-50 dark:bg-surface-900 border border-surface-100 dark:border-surface-800"> 120 - <div className="w-10 h-10 rounded-lg bg-green-100 dark:bg-green-900/30 flex items-center justify-center mb-3"> 121 - <Users size={20} className="text-green-600 dark:text-green-400" /> 122 - </div> 123 - <h3 className="font-semibold text-surface-900 dark:text-white mb-1"> 124 - Share 125 - </h3> 126 - <p className="text-sm text-surface-500 dark:text-surface-400"> 127 - Discover what others are reading 128 - </p> 129 256 </div> 130 257 </div> 131 - </div> 132 - ); 133 - } 134 - 135 - return ( 136 - <div className="max-w-2xl mx-auto animate-slide-up"> 137 - {showTabs && ( 138 - <Tabs 139 - tabs={tabs} 140 - activeTab={activeTab} 141 - onChange={setActiveTab} 142 - className="mb-6" 143 - /> 144 258 )} 145 259 146 - {loading ? ( 147 - <div className="flex flex-col items-center justify-center py-20 gap-3"> 148 - <Loader2 149 - className="animate-spin text-primary-600 dark:text-primary-400" 150 - size={32} 151 - /> 152 - <p className="text-sm text-surface-400 dark:text-surface-500"> 153 - Loading feed... 154 - </p> 155 - </div> 156 - ) : items.length > 0 ? ( 157 - <div className="space-y-3"> 158 - {items.map((item) => ( 159 - <Card 160 - key={item.uri || item.cid} 161 - item={item} 162 - onDelete={handleDelete} 163 - /> 164 - ))} 165 - </div> 166 - ) : ( 167 - <EmptyState 168 - icon={<Clock size={48} />} 169 - title="Nothing here yet" 170 - message={emptyMessage} 171 - /> 172 - )} 260 + <FeedContent 261 + key={`${activeTab}-${activeFilter || "all"}`} 262 + type={activeTab} 263 + motivation={activeFilter} 264 + emptyMessage={emptyMessage} 265 + layout={layout} 266 + /> 173 267 </div> 174 268 ); 175 269 }
+2 -1
web/src/views/core/New.tsx
··· 3 3 import { useStore } from "@nanostores/react"; 4 4 import { $user } from "../../store/auth"; 5 5 import Composer from "../../components/feed/Composer"; 6 + import type { Selector } from "../../types"; 6 7 7 8 export default function NewAnnotationPage() { 8 9 const user = useStore($user); ··· 11 12 12 13 const initialUrl = searchParams.get("url") || ""; 13 14 14 - let initialSelector: any = null; 15 + let initialSelector: Selector | null = null; 15 16 const selectorParam = searchParams.get("selector"); 16 17 if (selectorParam) { 17 18 try {
+15 -15
web/src/views/core/Notifications.tsx
··· 1 1 import React, { useEffect, useState } from "react"; 2 2 import { getNotifications, markNotificationsRead } from "../../api/client"; 3 - import type { NotificationItem } from "../../types"; 4 - import { Heart, MessageCircle, Bell, PenTool, Loader2 } from "lucide-react"; 3 + import type { NotificationItem, AnnotationItem } from "../../types"; 4 + import { Heart, MessageCircle, Bell, PenTool } from "lucide-react"; 5 5 import Card from "../../components/common/Card"; 6 6 import { formatDistanceToNow } from "date-fns"; 7 7 import { clsx } from "clsx"; ··· 42 42 const [loading, setLoading] = useState(true); 43 43 44 44 useEffect(() => { 45 + const loadNotifications = async () => { 46 + setLoading(true); 47 + const data = await getNotifications(); 48 + setNotifications(data); 49 + setLoading(false); 50 + markNotificationsRead(); 51 + }; 45 52 loadNotifications(); 46 53 }, []); 47 - 48 - const loadNotifications = async () => { 49 - setLoading(true); 50 - const data = await getNotifications(); 51 - setNotifications(data); 52 - setLoading(false); 53 - markNotificationsRead(); 54 - }; 55 54 56 55 if (loading) { 57 56 return ( ··· 127 126 </span> 128 127 </div> 129 128 130 - {n.subject && ( 129 + {!!n.subject && ( 131 130 <div className="mt-3 pl-3 border-l-2 border-surface-200 dark:border-surface-700"> 132 - {n.type === "reply" && n.subject.text ? ( 131 + {n.type === "reply" && 132 + (n.subject as AnnotationItem).text ? ( 133 133 <p className="text-surface-600 dark:text-surface-300 text-sm"> 134 - {n.subject.text} 134 + {(n.subject as AnnotationItem).text} 135 135 </p> 136 - ) : n.subject.uri ? ( 137 - <Card item={n.subject} hideShare /> 136 + ) : (n.subject as AnnotationItem).uri ? ( 137 + <Card item={n.subject as AnnotationItem} hideShare /> 138 138 ) : null} 139 139 </div> 140 140 )}
+27 -9
web/src/views/core/Settings.tsx
··· 27 27 Skeleton, 28 28 EmptyState, 29 29 } from "../../components/ui"; 30 + import { AppleIcon } from "../../components/common/Icons"; 30 31 31 32 export default function Settings() { 32 33 const user = useStore($user); ··· 39 40 const [creating, setCreating] = useState(false); 40 41 41 42 useEffect(() => { 43 + const loadKeys = async () => { 44 + setLoading(true); 45 + const data = await getAPIKeys(); 46 + setKeys(data); 47 + setLoading(false); 48 + }; 42 49 loadKeys(); 43 50 }, []); 44 - 45 - const loadKeys = async () => { 46 - setLoading(true); 47 - const data = await getAPIKeys(); 48 - setKeys(data); 49 - setLoading(false); 50 - }; 51 51 52 52 const handleCreate = async (e: React.FormEvent) => { 53 53 e.preventDefault(); ··· 219 219 {keys.map((key) => ( 220 220 <div 221 221 key={key.id} 222 - className="flex items-center justify-between p-4 bg-surface-50 dark:bg-surface-800 rounded-xl group transition-all hover:bg-surface-100 dark:hover:bg-surface-750" 222 + className="flex items-center justify-between p-4 bg-surface-50 dark:bg-surface-800 rounded-xl group transition-all hover:bg-surface-100 dark:hover:bg-surface-700" 223 223 > 224 224 <div className="flex items-center gap-3"> 225 225 <div className="p-2 bg-surface-200 dark:bg-surface-700 rounded-lg"> ··· 230 230 </div> 231 231 <div> 232 232 <p className="font-medium text-surface-900 dark:text-white"> 233 - {key.alias} 233 + {key.name} 234 234 </p> 235 235 <p className="text-xs text-surface-500 dark:text-surface-400"> 236 236 Created {new Date(key.createdAt).toLocaleDateString()} ··· 247 247 ))} 248 248 </div> 249 249 )} 250 + </section> 251 + 252 + <section className="card p-5"> 253 + <h2 className="text-xs font-semibold text-surface-500 dark:text-surface-400 uppercase tracking-wider mb-1"> 254 + iOS Shortcut 255 + </h2> 256 + <p className="text-sm text-surface-400 dark:text-surface-500 mb-4"> 257 + Save pages to Margin from Safari on iPhone and iPad 258 + </p> 259 + <a 260 + href="https://www.icloud.com/shortcuts/21c87edf29b046db892c9e57dac6d1fd" 261 + target="_blank" 262 + rel="noopener noreferrer" 263 + className="inline-flex items-center gap-2.5 px-4 py-2.5 bg-surface-900 dark:bg-white text-white dark:text-surface-900 rounded-xl font-medium text-sm transition-all hover:opacity-90" 264 + > 265 + <AppleIcon size={16} /> 266 + Get iOS Shortcut 267 + </a> 250 268 </section> 251 269 252 270 <section className="card p-5">
+5 -9
web/src/views/profile/Profile.tsx
··· 2 2 import { getProfile, getFeed, getCollections } from "../../api/client"; 3 3 import Card from "../../components/common/Card"; 4 4 import { 5 - Loader2, 6 5 Edit2, 7 - Bookmark, 8 - PenTool, 9 - MessageSquare, 10 - Folder, 11 - Share2, 12 - MoreHorizontal, 13 - Plus, 14 - ArrowRight, 15 6 Github, 16 7 Linkedin, 8 + Loader2, 9 + Folder, 10 + MessageSquare, 11 + PenTool, 12 + Bookmark, 17 13 Link2, 18 14 } from "lucide-react"; 19 15 import { TangledIcon } from "../../components/common/Icons";
+22 -22
web/tailwind.config.mjs
··· 12 12 }, 13 13 colors: { 14 14 primary: { 15 - 50: "#f0f7ff", 16 - 100: "#e0effe", 17 - 200: "#bae0fd", 18 - 300: "#7cc2fc", 19 - 400: "#36a2fa", 20 - 500: "#0083f5", 21 - 600: "#0066d6", 22 - 700: "#0051ab", 23 - 800: "#00458d", 24 - 900: "#063a70", 25 - 950: "#04254d", 15 + 50: "#f5f3ff", 16 + 100: "#ede9fe", 17 + 200: "#ddd6fe", 18 + 300: "#c4b5fd", 19 + 400: "#a78bfa", 20 + 500: "#8b5cf6", 21 + 600: "#7c3aed", 22 + 700: "#6d28d9", 23 + 800: "#5b21b6", 24 + 900: "#4c1d95", 25 + 950: "#2e1065", 26 26 }, 27 27 surface: { 28 - 50: "#fafafa", 29 - 100: "#f4f4f5", 30 - 200: "#e4e4e7", 31 - 300: "#d4d4d8", 32 - 400: "#a1a1aa", 33 - 500: "#71717a", 34 - 600: "#52525b", 35 - 700: "#3f3f46", 36 - 800: "#27272a", 37 - 900: "#18181b", 38 - 950: "#09090b", 28 + 50: "#f8fafc", 29 + 100: "#f1f5f9", 30 + 200: "#e2e8f0", 31 + 300: "#cbd5e1", 32 + 400: "#94a3b8", 33 + 500: "#64748b", 34 + 600: "#475569", 35 + 700: "#334155", 36 + 800: "#1e293b", 37 + 900: "#0f172a", 38 + 950: "#020617", 39 39 }, 40 40 }, 41 41 animation: {