Write on the margins of the internet. Powered by the AT Protocol.
0
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: {