Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

Compare changes

Choose any two refs to compare.

+1 -1
.github/workflows/docker-publish.yml
··· 2 2 3 3 on: 4 4 push: 5 - branches: [ "main" ] 5 + branches: ["main"] 6 6 workflow_dispatch: 7 7 8 8 env:
+13 -3
.github/workflows/release-extension.yml
··· 3 3 on: 4 4 push: 5 5 tags: 6 - - 'v*' 6 + - "v*" 7 7 8 8 jobs: 9 9 release: ··· 19 19 run: | 20 20 VERSION=${GITHUB_REF_NAME#v} 21 21 echo "Updating manifests to version $VERSION" 22 - 22 + 23 23 cd extension 24 24 for manifest in manifest.json manifest.chrome.json manifest.firefox.json; do 25 25 if [ -f "$manifest" ]; then ··· 36 36 cp manifest.chrome.json manifest.json 37 37 zip -r ../margin-extension-chrome.zip . -x "*.DS_Store" -x "*.git*" -x "manifest.*.json" 38 38 cd .. 39 - 39 + 40 40 - name: Build Extension (Firefox) 41 41 run: | 42 42 cd extension ··· 76 76 npx web-ext sign --channel=listed --api-key=$AMO_JWT_ISSUER --api-secret=$AMO_JWT_SECRET --source-dir=. --artifacts-dir=../web-ext-artifacts --approval-timeout=300000 --amo-metadata=amo-metadata.json || echo "Web-ext sign timed out (expected), continuing..." 77 77 rm amo-metadata.json 78 78 cd .. 79 + 80 + - name: Prepare signed Firefox XPI 81 + run: | 82 + if ls web-ext-artifacts/*.xpi 1> /dev/null 2>&1; then 83 + SIGNED_XPI=$(ls web-ext-artifacts/*.xpi | head -1) 84 + echo "Found signed XPI: $SIGNED_XPI" 85 + cp "$SIGNED_XPI" margin-extension-firefox.xpi 86 + else 87 + echo "No signed XPI found, using unsigned build" 88 + fi 79 89 80 90 - name: Create Release 81 91 uses: softprops/action-gh-release@v1
+1 -1
README.md
··· 1 1 # Margin 2 2 3 - *Write in the margins of the web* 3 + _Write in the margins of the web_ 4 4 5 5 A web comments layer built on [AT Protocol](https://atproto.com) that lets you annotate any URL on the internet. 6 6
+2
backend/cmd/server/main.go
··· 101 101 r.Get("/{handle}/highlight/{rkey}", ogHandler.HandleAnnotationPage) 102 102 r.Get("/{handle}/bookmark/{rkey}", ogHandler.HandleAnnotationPage) 103 103 104 + r.Get("/api/tags/trending", handler.HandleGetTrendingTags) 105 + 104 106 r.Get("/collection/{uri}", ogHandler.HandleCollectionPage) 105 107 r.Get("/{handle}/collection/{rkey}", ogHandler.HandleCollectionPage) 106 108
+16 -1
backend/go.mod
··· 3 3 go 1.24.0 4 4 5 5 require ( 6 + github.com/fxamacker/cbor/v2 v2.9.0 6 7 github.com/go-chi/chi/v5 v5.1.0 7 8 github.com/go-chi/cors v1.2.1 8 9 github.com/go-jose/go-jose/v4 v4.0.4 10 + github.com/gorilla/websocket v1.5.3 11 + github.com/ipfs/go-cid v0.6.0 9 12 github.com/joho/godotenv v1.5.1 10 13 github.com/lib/pq v1.10.9 11 14 github.com/mattn/go-sqlite3 v1.14.22 ··· 14 17 15 18 require ( 16 19 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 20 + github.com/klauspost/cpuid/v2 v2.0.9 // indirect 21 + github.com/minio/sha256-simd v1.0.0 // indirect 22 + github.com/mr-tron/base58 v1.2.0 // indirect 23 + github.com/multiformats/go-base32 v0.0.3 // indirect 24 + github.com/multiformats/go-base36 v0.1.0 // indirect 25 + github.com/multiformats/go-multibase v0.2.0 // indirect 26 + github.com/multiformats/go-multihash v0.2.3 // indirect 27 + github.com/multiformats/go-varint v0.1.0 // indirect 17 28 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 29 + github.com/spaolacci/murmur3 v1.1.0 // indirect 18 30 github.com/stretchr/testify v1.10.0 // indirect 19 - golang.org/x/crypto v0.31.0 // indirect 31 + github.com/x448/float16 v0.8.4 // indirect 32 + golang.org/x/crypto v0.35.0 // indirect 33 + golang.org/x/sys v0.30.0 // indirect 20 34 golang.org/x/text v0.32.0 // indirect 35 + lukechampine.com/blake3 v1.1.6 // indirect 21 36 )
+33 -2
backend/go.sum
··· 1 1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 2 2 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 + github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 4 + github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 3 5 github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= 4 6 github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 5 7 github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= ··· 8 10 github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= 9 11 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 12 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 13 + github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 14 + github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 15 + github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30= 16 + github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ= 11 17 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 12 18 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 19 + github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 20 + github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= 21 + github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 13 22 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 14 23 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 15 24 github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 16 25 github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 26 + github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= 27 + github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= 28 + github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 29 + github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 30 + github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= 31 + github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= 32 + github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= 33 + github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= 34 + github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 35 + github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 36 + github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 37 + github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 38 + github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= 39 + github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= 17 40 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 18 41 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 + github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 43 + github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 19 44 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 20 45 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 21 - golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 22 - golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 46 + github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 47 + github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 48 + golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 49 + golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 23 50 golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8= 24 51 golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU= 52 + golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 53 + golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 25 54 golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= 26 55 golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 27 56 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 57 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 58 + lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= 59 + lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
+4 -1
backend/internal/api/annotations.go
··· 480 480 481 481 func resolveDIDToPDS(did string) (string, error) { 482 482 if strings.HasPrefix(did, "did:plc:") { 483 - resp, err := http.Get("https://plc.directory/" + did) 483 + client := &http.Client{ 484 + Timeout: 10 * time.Second, 485 + } 486 + resp, err := client.Get("https://plc.directory/" + did) 484 487 if err != nil { 485 488 return "", err 486 489 }
+435
backend/internal/api/apikey.go
··· 1 + package api 2 + 3 + import ( 4 + "crypto/rand" 5 + "crypto/sha256" 6 + "crypto/x509" 7 + "encoding/hex" 8 + "encoding/json" 9 + "encoding/pem" 10 + "fmt" 11 + "net/http" 12 + "strings" 13 + "time" 14 + 15 + "github.com/go-chi/chi/v5" 16 + 17 + "margin.at/internal/db" 18 + "margin.at/internal/xrpc" 19 + ) 20 + 21 + type APIKeyHandler struct { 22 + db *db.DB 23 + refresher *TokenRefresher 24 + } 25 + 26 + func NewAPIKeyHandler(database *db.DB, refresher *TokenRefresher) *APIKeyHandler { 27 + return &APIKeyHandler{db: database, refresher: refresher} 28 + } 29 + 30 + type CreateKeyRequest struct { 31 + Name string `json:"name"` 32 + } 33 + 34 + type CreateKeyResponse struct { 35 + ID string `json:"id"` 36 + Name string `json:"name"` 37 + Key string `json:"key"` 38 + CreatedAt time.Time `json:"createdAt"` 39 + } 40 + 41 + func (h *APIKeyHandler) CreateKey(w http.ResponseWriter, r *http.Request) { 42 + session, err := h.refresher.GetSessionWithAutoRefresh(r) 43 + if err != nil { 44 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 45 + return 46 + } 47 + 48 + var req CreateKeyRequest 49 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 50 + http.Error(w, "Invalid request body", http.StatusBadRequest) 51 + return 52 + } 53 + 54 + if req.Name == "" { 55 + req.Name = "API Key" 56 + } 57 + 58 + rawKey := generateAPIKey() 59 + keyHash := hashAPIKey(rawKey) 60 + keyID := generateKeyID() 61 + 62 + apiKey := &db.APIKey{ 63 + ID: keyID, 64 + OwnerDID: session.DID, 65 + Name: req.Name, 66 + KeyHash: keyHash, 67 + CreatedAt: time.Now(), 68 + } 69 + 70 + if err := h.db.CreateAPIKey(apiKey); err != nil { 71 + http.Error(w, "Failed to create key", http.StatusInternalServerError) 72 + return 73 + } 74 + 75 + w.Header().Set("Content-Type", "application/json") 76 + json.NewEncoder(w).Encode(CreateKeyResponse{ 77 + ID: keyID, 78 + Name: req.Name, 79 + Key: rawKey, 80 + CreatedAt: apiKey.CreatedAt, 81 + }) 82 + } 83 + 84 + func (h *APIKeyHandler) ListKeys(w http.ResponseWriter, r *http.Request) { 85 + session, err := h.refresher.GetSessionWithAutoRefresh(r) 86 + if err != nil { 87 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 88 + return 89 + } 90 + 91 + keys, err := h.db.GetAPIKeysByOwner(session.DID) 92 + if err != nil { 93 + http.Error(w, "Failed to get keys", http.StatusInternalServerError) 94 + return 95 + } 96 + 97 + if keys == nil { 98 + keys = []db.APIKey{} 99 + } 100 + 101 + w.Header().Set("Content-Type", "application/json") 102 + json.NewEncoder(w).Encode(map[string]interface{}{"keys": keys}) 103 + } 104 + 105 + func (h *APIKeyHandler) DeleteKey(w http.ResponseWriter, r *http.Request) { 106 + session, err := h.refresher.GetSessionWithAutoRefresh(r) 107 + if err != nil { 108 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 109 + return 110 + } 111 + 112 + keyID := chi.URLParam(r, "id") 113 + if keyID == "" { 114 + http.Error(w, "Key ID required", http.StatusBadRequest) 115 + return 116 + } 117 + 118 + if err := h.db.DeleteAPIKey(keyID, session.DID); err != nil { 119 + http.Error(w, "Failed to delete key", http.StatusInternalServerError) 120 + return 121 + } 122 + 123 + w.Header().Set("Content-Type", "application/json") 124 + json.NewEncoder(w).Encode(map[string]bool{"success": true}) 125 + } 126 + 127 + type QuickBookmarkRequest struct { 128 + URL string `json:"url"` 129 + Title string `json:"title,omitempty"` 130 + Description string `json:"description,omitempty"` 131 + } 132 + 133 + func (h *APIKeyHandler) QuickBookmark(w http.ResponseWriter, r *http.Request) { 134 + apiKey, err := h.authenticateAPIKey(r) 135 + if err != nil { 136 + http.Error(w, err.Error(), http.StatusUnauthorized) 137 + return 138 + } 139 + 140 + var req QuickBookmarkRequest 141 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 142 + http.Error(w, "Invalid request body", http.StatusBadRequest) 143 + return 144 + } 145 + 146 + if req.URL == "" { 147 + http.Error(w, "URL is required", http.StatusBadRequest) 148 + return 149 + } 150 + 151 + session, err := h.getSessionByDID(apiKey.OwnerDID) 152 + if err != nil { 153 + http.Error(w, "User session not found. Please log in to margin.at first.", http.StatusUnauthorized) 154 + return 155 + } 156 + 157 + urlHash := db.HashURL(req.URL) 158 + record := xrpc.NewBookmarkRecord(req.URL, urlHash, req.Title, req.Description) 159 + 160 + var result *xrpc.CreateRecordOutput 161 + err = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error { 162 + var createErr error 163 + result, createErr = client.CreateRecord(r.Context(), did, xrpc.CollectionBookmark, record) 164 + return createErr 165 + }) 166 + if err != nil { 167 + http.Error(w, "Failed to create bookmark: "+err.Error(), http.StatusInternalServerError) 168 + return 169 + } 170 + 171 + h.db.UpdateAPIKeyLastUsed(apiKey.ID) 172 + 173 + var titlePtr, descPtr *string 174 + if req.Title != "" { 175 + titlePtr = &req.Title 176 + } 177 + if req.Description != "" { 178 + descPtr = &req.Description 179 + } 180 + 181 + cid := result.CID 182 + bookmark := &db.Bookmark{ 183 + URI: result.URI, 184 + AuthorDID: apiKey.OwnerDID, 185 + Source: req.URL, 186 + SourceHash: urlHash, 187 + Title: titlePtr, 188 + Description: descPtr, 189 + CreatedAt: time.Now(), 190 + IndexedAt: time.Now(), 191 + CID: &cid, 192 + } 193 + h.db.CreateBookmark(bookmark) 194 + 195 + w.Header().Set("Content-Type", "application/json") 196 + json.NewEncoder(w).Encode(map[string]string{ 197 + "uri": result.URI, 198 + "cid": result.CID, 199 + "message": "Bookmark created successfully", 200 + }) 201 + } 202 + 203 + type QuickAnnotationRequest struct { 204 + URL string `json:"url"` 205 + Text string `json:"text"` 206 + } 207 + 208 + func (h *APIKeyHandler) QuickAnnotation(w http.ResponseWriter, r *http.Request) { 209 + apiKey, err := h.authenticateAPIKey(r) 210 + if err != nil { 211 + http.Error(w, err.Error(), http.StatusUnauthorized) 212 + return 213 + } 214 + 215 + var req QuickAnnotationRequest 216 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 217 + http.Error(w, "Invalid request body", http.StatusBadRequest) 218 + return 219 + } 220 + 221 + if req.URL == "" || req.Text == "" { 222 + http.Error(w, "URL and text are required", http.StatusBadRequest) 223 + return 224 + } 225 + 226 + session, err := h.getSessionByDID(apiKey.OwnerDID) 227 + if err != nil { 228 + http.Error(w, "User session not found. Please log in to margin.at first.", http.StatusUnauthorized) 229 + return 230 + } 231 + 232 + urlHash := db.HashURL(req.URL) 233 + record := xrpc.NewAnnotationRecord(req.URL, urlHash, req.Text, nil, "") 234 + 235 + var result *xrpc.CreateRecordOutput 236 + err = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error { 237 + var createErr error 238 + result, createErr = client.CreateRecord(r.Context(), did, xrpc.CollectionAnnotation, record) 239 + return createErr 240 + }) 241 + if err != nil { 242 + http.Error(w, "Failed to create annotation: "+err.Error(), http.StatusInternalServerError) 243 + return 244 + } 245 + 246 + h.db.UpdateAPIKeyLastUsed(apiKey.ID) 247 + 248 + bodyValue := req.Text 249 + annotation := &db.Annotation{ 250 + URI: result.URI, 251 + AuthorDID: apiKey.OwnerDID, 252 + Motivation: "commenting", 253 + BodyValue: &bodyValue, 254 + TargetSource: req.URL, 255 + TargetHash: urlHash, 256 + CreatedAt: time.Now(), 257 + IndexedAt: time.Now(), 258 + CID: &result.CID, 259 + } 260 + h.db.CreateAnnotation(annotation) 261 + 262 + w.Header().Set("Content-Type", "application/json") 263 + json.NewEncoder(w).Encode(map[string]string{ 264 + "uri": result.URI, 265 + "cid": result.CID, 266 + "message": "Annotation created successfully", 267 + }) 268 + } 269 + 270 + type QuickHighlightRequest struct { 271 + URL string `json:"url"` 272 + Selector interface{} `json:"selector"` 273 + Color string `json:"color,omitempty"` 274 + } 275 + 276 + func (h *APIKeyHandler) QuickHighlight(w http.ResponseWriter, r *http.Request) { 277 + apiKey, err := h.authenticateAPIKey(r) 278 + if err != nil { 279 + http.Error(w, err.Error(), http.StatusUnauthorized) 280 + return 281 + } 282 + 283 + var req QuickHighlightRequest 284 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 285 + http.Error(w, "Invalid request body", http.StatusBadRequest) 286 + return 287 + } 288 + 289 + if req.URL == "" || req.Selector == nil { 290 + http.Error(w, "URL and selector are required", http.StatusBadRequest) 291 + return 292 + } 293 + 294 + session, err := h.getSessionByDID(apiKey.OwnerDID) 295 + if err != nil { 296 + http.Error(w, "User session not found. Please log in to margin.at first.", http.StatusUnauthorized) 297 + return 298 + } 299 + 300 + urlHash := db.HashURL(req.URL) 301 + color := req.Color 302 + if color == "" { 303 + color = "yellow" 304 + } 305 + 306 + record := xrpc.NewHighlightRecord(req.URL, urlHash, req.Selector, color, nil) 307 + 308 + var result *xrpc.CreateRecordOutput 309 + err = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error { 310 + var createErr error 311 + result, createErr = client.CreateRecord(r.Context(), did, xrpc.CollectionHighlight, record) 312 + return createErr 313 + }) 314 + if err != nil { 315 + http.Error(w, "Failed to create highlight: "+err.Error(), http.StatusInternalServerError) 316 + return 317 + } 318 + 319 + h.db.UpdateAPIKeyLastUsed(apiKey.ID) 320 + 321 + selectorJSON, _ := json.Marshal(req.Selector) 322 + selectorStr := string(selectorJSON) 323 + colorPtr := &color 324 + 325 + highlight := &db.Highlight{ 326 + URI: result.URI, 327 + AuthorDID: apiKey.OwnerDID, 328 + TargetSource: req.URL, 329 + TargetHash: urlHash, 330 + SelectorJSON: &selectorStr, 331 + Color: colorPtr, 332 + CreatedAt: time.Now(), 333 + IndexedAt: time.Now(), 334 + CID: &result.CID, 335 + } 336 + if err := h.db.CreateHighlight(highlight); err != nil { 337 + fmt.Printf("Warning: failed to index highlight in local DB: %v\n", err) 338 + } 339 + 340 + w.Header().Set("Content-Type", "application/json") 341 + json.NewEncoder(w).Encode(map[string]string{ 342 + "uri": result.URI, 343 + "cid": result.CID, 344 + "message": "Highlight created successfully", 345 + }) 346 + } 347 + 348 + func (h *APIKeyHandler) authenticateAPIKey(r *http.Request) (*db.APIKey, error) { 349 + auth := r.Header.Get("Authorization") 350 + if auth == "" { 351 + return nil, fmt.Errorf("missing Authorization header") 352 + } 353 + 354 + if !strings.HasPrefix(auth, "Bearer ") { 355 + return nil, fmt.Errorf("invalid Authorization format, expected 'Bearer <key>'") 356 + } 357 + 358 + rawKey := strings.TrimPrefix(auth, "Bearer ") 359 + keyHash := hashAPIKey(rawKey) 360 + 361 + apiKey, err := h.db.GetAPIKeyByHash(keyHash) 362 + if err != nil { 363 + return nil, fmt.Errorf("invalid API key") 364 + } 365 + 366 + return apiKey, nil 367 + } 368 + 369 + func (h *APIKeyHandler) getSessionByDID(did string) (*SessionData, error) { 370 + rows, err := h.db.Query(h.db.Rebind(` 371 + SELECT id, did, handle, access_token, refresh_token, COALESCE(dpop_key, '') 372 + FROM sessions 373 + WHERE did = ? AND expires_at > ? 374 + ORDER BY created_at DESC 375 + LIMIT 1 376 + `), did, time.Now()) 377 + if err != nil { 378 + return nil, err 379 + } 380 + defer rows.Close() 381 + 382 + if !rows.Next() { 383 + return nil, fmt.Errorf("no active session") 384 + } 385 + 386 + var sessionID, sessDID, handle, accessToken, refreshToken, dpopKeyStr string 387 + if err := rows.Scan(&sessionID, &sessDID, &handle, &accessToken, &refreshToken, &dpopKeyStr); err != nil { 388 + return nil, err 389 + } 390 + 391 + block, _ := pem.Decode([]byte(dpopKeyStr)) 392 + if block == nil { 393 + return nil, fmt.Errorf("invalid session DPoP key") 394 + } 395 + dpopKey, err := x509.ParseECPrivateKey(block.Bytes) 396 + if err != nil { 397 + return nil, fmt.Errorf("invalid session DPoP key: %w", err) 398 + } 399 + 400 + pds, err := resolveDIDToPDS(sessDID) 401 + if err != nil { 402 + return nil, fmt.Errorf("failed to resolve PDS: %w", err) 403 + } 404 + if pds == "" { 405 + return nil, fmt.Errorf("PDS not found for DID: %s", sessDID) 406 + } 407 + 408 + return &SessionData{ 409 + ID: sessionID, 410 + DID: sessDID, 411 + Handle: handle, 412 + AccessToken: accessToken, 413 + RefreshToken: refreshToken, 414 + DPoPKey: dpopKey, 415 + PDS: pds, 416 + }, nil 417 + } 418 + 419 + func generateAPIKey() string { 420 + b := make([]byte, 32) 421 + rand.Read(b) 422 + return "mk_" + hex.EncodeToString(b) 423 + } 424 + 425 + func generateKeyID() string { 426 + b := make([]byte, 16) 427 + rand.Read(b) 428 + return hex.EncodeToString(b) 429 + } 430 + 431 + func hashAPIKey(key string) string { 432 + h := sha256.New() 433 + h.Write([]byte(key)) 434 + return hex.EncodeToString(h.Sum(nil)) 435 + }
+117
backend/internal/api/avatar.go
··· 1 + package api 2 + 3 + import ( 4 + "encoding/json" 5 + "io" 6 + "net/http" 7 + "net/url" 8 + "os" 9 + "sync" 10 + "time" 11 + 12 + "github.com/go-chi/chi/v5" 13 + ) 14 + 15 + type avatarCache struct { 16 + url string 17 + fetchedAt time.Time 18 + } 19 + 20 + var ( 21 + avatarCacheMu sync.RWMutex 22 + avatarCacheMap = make(map[string]avatarCache) 23 + avatarCacheTTL = 5 * time.Minute 24 + ) 25 + 26 + func (h *Handler) HandleAvatarProxy(w http.ResponseWriter, r *http.Request) { 27 + did := chi.URLParam(r, "did") 28 + if did == "" { 29 + http.Error(w, "DID required", http.StatusBadRequest) 30 + return 31 + } 32 + 33 + if decoded, err := url.QueryUnescape(did); err == nil { 34 + did = decoded 35 + } 36 + 37 + avatarURL := getAvatarURL(did) 38 + if avatarURL == "" { 39 + http.Error(w, "Avatar not found", http.StatusNotFound) 40 + return 41 + } 42 + 43 + client := &http.Client{Timeout: 10 * time.Second} 44 + resp, err := client.Get(avatarURL) 45 + if err != nil { 46 + http.Error(w, "Failed to fetch avatar", http.StatusBadGateway) 47 + return 48 + } 49 + defer resp.Body.Close() 50 + 51 + if resp.StatusCode != http.StatusOK { 52 + http.Error(w, "Avatar not available", http.StatusNotFound) 53 + return 54 + } 55 + 56 + contentType := resp.Header.Get("Content-Type") 57 + if contentType == "" { 58 + contentType = "image/jpeg" 59 + } 60 + 61 + w.Header().Set("Content-Type", contentType) 62 + w.Header().Set("Cache-Control", "public, max-age=3600") 63 + w.Header().Set("Access-Control-Allow-Origin", "*") 64 + 65 + io.Copy(w, resp.Body) 66 + } 67 + 68 + func getAvatarURL(did string) string { 69 + avatarCacheMu.RLock() 70 + if cached, ok := avatarCacheMap[did]; ok && time.Since(cached.fetchedAt) < avatarCacheTTL { 71 + avatarCacheMu.RUnlock() 72 + return cached.url 73 + } 74 + avatarCacheMu.RUnlock() 75 + 76 + q := url.Values{} 77 + q.Add("actor", did) 78 + 79 + resp, err := http.Get("https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?" + q.Encode()) 80 + if err != nil { 81 + return "" 82 + } 83 + defer resp.Body.Close() 84 + 85 + if resp.StatusCode != 200 { 86 + return "" 87 + } 88 + 89 + var profile struct { 90 + Avatar string `json:"avatar"` 91 + } 92 + if err := json.NewDecoder(resp.Body).Decode(&profile); err != nil { 93 + return "" 94 + } 95 + 96 + avatarCacheMu.Lock() 97 + avatarCacheMap[did] = avatarCache{ 98 + url: profile.Avatar, 99 + fetchedAt: time.Now(), 100 + } 101 + avatarCacheMu.Unlock() 102 + 103 + return profile.Avatar 104 + } 105 + 106 + func getProxiedAvatarURL(did, originalURL string) string { 107 + if originalURL == "" { 108 + return "" 109 + } 110 + 111 + baseURL := os.Getenv("BASE_URL") 112 + if baseURL == "" { 113 + return originalURL 114 + } 115 + 116 + return baseURL + "/api/avatar/" + url.PathEscape(did) 117 + }
+16 -1
backend/internal/api/handler.go
··· 19 19 db *db.DB 20 20 annotationService *AnnotationService 21 21 refresher *TokenRefresher 22 + apiKeys *APIKeyHandler 22 23 } 23 24 24 25 func NewHandler(database *db.DB, annotationService *AnnotationService, refresher *TokenRefresher) *Handler { 25 - return &Handler{db: database, annotationService: annotationService, refresher: refresher} 26 + return &Handler{ 27 + db: database, 28 + annotationService: annotationService, 29 + refresher: refresher, 30 + apiKeys: NewAPIKeyHandler(database, refresher), 31 + } 26 32 } 27 33 28 34 func (h *Handler) RegisterRoutes(r chi.Router) { ··· 64 70 r.Get("/notifications", h.GetNotifications) 65 71 r.Get("/notifications/count", h.GetUnreadNotificationCount) 66 72 r.Post("/notifications/read", h.MarkNotificationsRead) 73 + r.Get("/avatar/{did}", h.HandleAvatarProxy) 74 + 75 + r.Post("/keys", h.apiKeys.CreateKey) 76 + r.Get("/keys", h.apiKeys.ListKeys) 77 + r.Delete("/keys/{id}", h.apiKeys.DeleteKey) 78 + 79 + r.Post("/quick/bookmark", h.apiKeys.QuickBookmark) 80 + r.Post("/quick/annotation", h.apiKeys.QuickAnnotation) 81 + r.Post("/quick/highlight", h.apiKeys.QuickHighlight) 67 82 }) 68 83 } 69 84
+1 -1
backend/internal/api/hydration.go
··· 470 470 DID: p.DID, 471 471 Handle: p.Handle, 472 472 DisplayName: p.DisplayName, 473 - Avatar: p.Avatar, 473 + Avatar: getProxiedAvatarURL(p.DID, p.Avatar), 474 474 } 475 475 } 476 476
+25
backend/internal/api/tags.go
··· 1 + package api 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + "strconv" 7 + ) 8 + 9 + func (h *Handler) HandleGetTrendingTags(w http.ResponseWriter, r *http.Request) { 10 + limit := 10 11 + if l := r.URL.Query().Get("limit"); l != "" { 12 + if val, err := strconv.Atoi(l); err == nil && val > 0 && val <= 50 { 13 + limit = val 14 + } 15 + } 16 + 17 + tags, err := h.db.GetTrendingTags(limit) 18 + if err != nil { 19 + http.Error(w, `{"error": "Failed to fetch trending tags: `+err.Error()+`"}`, http.StatusInternalServerError) 20 + return 21 + } 22 + 23 + w.Header().Set("Content-Type", "application/json") 24 + json.NewEncoder(w).Encode(tags) 25 + }
+10 -4
backend/internal/api/token_refresh.go
··· 52 52 } 53 53 54 54 type SessionData struct { 55 + ID string 55 56 DID string 56 57 Handle string 57 58 AccessToken string ··· 94 95 } 95 96 96 97 return &SessionData{ 98 + ID: sessionID, 97 99 DID: did, 98 100 Handle: handle, 99 101 AccessToken: accessToken, ··· 104 106 } 105 107 106 108 func (tr *TokenRefresher) RefreshSessionToken(r *http.Request, session *SessionData) (*SessionData, error) { 107 - cookie, err := r.Cookie("margin_session") 108 - if err != nil { 109 - return nil, fmt.Errorf("not authenticated") 109 + if session.ID == "" { 110 + return nil, fmt.Errorf("invalid session ID") 110 111 } 111 112 112 113 oauthClient := tr.getOAuthClient(r) ··· 138 139 139 140 expiresAt := time.Now().Add(7 * 24 * time.Hour) 140 141 if err := tr.db.SaveSession( 141 - cookie.Value, 142 + session.ID, 142 143 session.DID, 143 144 session.Handle, 144 145 tokenResp.AccessToken, ··· 152 153 log.Printf("Successfully refreshed token for user %s", session.Handle) 153 154 154 155 return &SessionData{ 156 + ID: session.ID, 155 157 DID: session.DID, 156 158 Handle: session.Handle, 157 159 AccessToken: tokenResp.AccessToken, ··· 196 198 client = xrpc.NewClient(newSession.PDS, newSession.AccessToken, newSession.DPoPKey) 197 199 return fn(client, newSession.DID) 198 200 } 201 + 202 + func (tr *TokenRefresher) CreateClientFromSession(session *SessionData) *xrpc.Client { 203 + return xrpc.NewClient(session.PDS, session.AccessToken, session.DPoPKey) 204 + }
+25 -1
backend/internal/db/db.go
··· 120 120 ReadAt *time.Time `json:"readAt,omitempty"` 121 121 } 122 122 123 + type APIKey struct { 124 + ID string `json:"id"` 125 + OwnerDID string `json:"ownerDid"` 126 + Name string `json:"name"` 127 + KeyHash string `json:"-"` 128 + CreatedAt time.Time `json:"createdAt"` 129 + LastUsedAt *time.Time `json:"lastUsedAt,omitempty"` 130 + } 131 + 123 132 func New(dsn string) (*DB, error) { 124 133 driver := "sqlite3" 125 134 if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { ··· 296 305 db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON notifications(recipient_did)`) 297 306 db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at DESC)`) 298 307 308 + db.Exec(`CREATE TABLE IF NOT EXISTS api_keys ( 309 + id TEXT PRIMARY KEY, 310 + owner_did TEXT NOT NULL, 311 + name TEXT NOT NULL, 312 + key_hash TEXT NOT NULL, 313 + created_at ` + dateType + ` NOT NULL, 314 + last_used_at ` + dateType + ` 315 + )`) 316 + db.Exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_owner ON api_keys(owner_did)`) 317 + db.Exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)`) 318 + 299 319 db.runMigrations() 300 320 301 321 db.Exec(`CREATE TABLE IF NOT EXISTS cursors ( 302 322 id TEXT PRIMARY KEY, 303 - last_cursor INTEGER NOT NULL, 323 + last_cursor BIGINT NOT NULL, 304 324 updated_at ` + dateType + ` NOT NULL 305 325 )`) 306 326 ··· 353 373 db.Exec(`UPDATE annotations SET body_value = text WHERE body_value IS NULL AND text IS NOT NULL`) 354 374 db.Exec(`UPDATE annotations SET target_title = title WHERE target_title IS NULL AND title IS NOT NULL`) 355 375 db.Exec(`UPDATE annotations SET motivation = 'commenting' WHERE motivation IS NULL`) 376 + 377 + if db.driver == "postgres" { 378 + db.Exec(`ALTER TABLE cursors ALTER COLUMN last_cursor TYPE BIGINT`) 379 + } 356 380 } 357 381 358 382 func (db *DB) Close() error {
+57
backend/internal/db/queries.go
··· 825 825 } 826 826 827 827 normalized := strings.ToLower(parsed.Host) + parsed.Path 828 + if parsed.RawQuery != "" { 829 + normalized += "?" + parsed.RawQuery 830 + } 828 831 normalized = strings.TrimSuffix(normalized, "/") 829 832 830 833 return hashString(normalized) ··· 907 910 908 911 return "", fmt.Errorf("uri not found or no author") 909 912 } 913 + 914 + func (db *DB) CreateAPIKey(key *APIKey) error { 915 + _, err := db.Exec(db.Rebind(` 916 + INSERT INTO api_keys (id, owner_did, name, key_hash, created_at) 917 + VALUES (?, ?, ?, ?, ?) 918 + `), key.ID, key.OwnerDID, key.Name, key.KeyHash, key.CreatedAt) 919 + return err 920 + } 921 + 922 + func (db *DB) GetAPIKeysByOwner(ownerDID string) ([]APIKey, error) { 923 + rows, err := db.Query(db.Rebind(` 924 + SELECT id, owner_did, name, key_hash, created_at, last_used_at 925 + FROM api_keys 926 + WHERE owner_did = ? 927 + ORDER BY created_at DESC 928 + `), ownerDID) 929 + if err != nil { 930 + return nil, err 931 + } 932 + defer rows.Close() 933 + 934 + var keys []APIKey 935 + for rows.Next() { 936 + var k APIKey 937 + if err := rows.Scan(&k.ID, &k.OwnerDID, &k.Name, &k.KeyHash, &k.CreatedAt, &k.LastUsedAt); err != nil { 938 + return nil, err 939 + } 940 + keys = append(keys, k) 941 + } 942 + return keys, nil 943 + } 944 + 945 + func (db *DB) GetAPIKeyByHash(keyHash string) (*APIKey, error) { 946 + var k APIKey 947 + err := db.QueryRow(db.Rebind(` 948 + SELECT id, owner_did, name, key_hash, created_at, last_used_at 949 + FROM api_keys 950 + WHERE key_hash = ? 951 + `), keyHash).Scan(&k.ID, &k.OwnerDID, &k.Name, &k.KeyHash, &k.CreatedAt, &k.LastUsedAt) 952 + if err != nil { 953 + return nil, err 954 + } 955 + return &k, nil 956 + } 957 + 958 + func (db *DB) DeleteAPIKey(id, ownerDID string) error { 959 + _, err := db.Exec(db.Rebind(`DELETE FROM api_keys WHERE id = ? AND owner_did = ?`), id, ownerDID) 960 + return err 961 + } 962 + 963 + func (db *DB) UpdateAPIKeyLastUsed(id string) error { 964 + _, err := db.Exec(db.Rebind(`UPDATE api_keys SET last_used_at = ? WHERE id = ?`), time.Now(), id) 965 + return err 966 + }
+46
backend/internal/db/tags.go
··· 1 + package db 2 + 3 + type TrendingTag struct { 4 + Tag string `json:"tag"` 5 + Count int `json:"count"` 6 + } 7 + 8 + func (db *DB) GetTrendingTags(limit int) ([]TrendingTag, error) { 9 + query := ` 10 + SELECT 11 + json_each.value as tag, 12 + COUNT(*) as count 13 + FROM annotations, json_each(annotations.tags_json) 14 + WHERE tags_json IS NOT NULL 15 + AND tags_json != '' 16 + AND tags_json != '[]' 17 + GROUP BY tag 18 + ORDER BY count DESC 19 + LIMIT ? 20 + ` 21 + 22 + rows, err := db.Query(db.Rebind(query), limit) 23 + if err != nil { 24 + return nil, err 25 + } 26 + defer rows.Close() 27 + 28 + var tags []TrendingTag 29 + for rows.Next() { 30 + var t TrendingTag 31 + if err := rows.Scan(&t.Tag, &t.Count); err != nil { 32 + return nil, err 33 + } 34 + tags = append(tags, t) 35 + } 36 + 37 + if err = rows.Err(); err != nil { 38 + return nil, err 39 + } 40 + 41 + if tags == nil { 42 + return []TrendingTag{}, nil 43 + } 44 + 45 + return tags, nil 46 + }
+236 -84
backend/internal/firehose/ingester.go
··· 3 3 import ( 4 4 "bytes" 5 5 "context" 6 + "encoding/binary" 6 7 "encoding/json" 7 8 "fmt" 8 9 "io" 9 10 "log" 10 - "net/http" 11 - "strings" 12 11 "time" 12 + 13 + "github.com/fxamacker/cbor/v2" 14 + "github.com/gorilla/websocket" 15 + "github.com/ipfs/go-cid" 13 16 14 17 "margin.at/internal/db" 15 18 ) ··· 56 59 return 57 60 default: 58 61 if err := i.subscribe(ctx); err != nil { 62 + log.Printf("Firehose error: %v, reconnecting in 5s...", err) 59 63 if ctx.Err() != nil { 60 64 return 61 65 } 62 - time.Sleep(30 * time.Second) 66 + time.Sleep(5 * time.Second) 63 67 } 64 68 } 65 69 } 66 70 } 67 71 72 + type FrameHeader struct { 73 + Op int `cbor:"op"` 74 + T string `cbor:"t"` 75 + } 76 + type Commit struct { 77 + Repo string `cbor:"repo"` 78 + Rev string `cbor:"rev"` 79 + Seq int64 `cbor:"seq"` 80 + Prev *cid.Cid `cbor:"prev"` 81 + Time string `cbor:"time"` 82 + Blocks []byte `cbor:"blocks"` 83 + Ops []RepoOp `cbor:"ops"` 84 + } 85 + 86 + type RepoOp struct { 87 + Action string `cbor:"action"` 88 + Path string `cbor:"path"` 89 + Cid *cid.Cid `cbor:"cid"` 90 + } 91 + 68 92 func (i *Ingester) subscribe(ctx context.Context) error { 69 93 cursor := i.getLastCursor() 70 94 ··· 73 97 url = fmt.Sprintf("%s?cursor=%d", RelayURL, cursor) 74 98 } 75 99 76 - req, err := http.NewRequestWithContext(ctx, "GET", strings.Replace(url, "wss://", "https://", 1), nil) 77 - if err != nil { 78 - return err 79 - } 100 + log.Printf("Connecting to firehose: %s", url) 80 101 81 - resp, err := http.DefaultClient.Do(req) 102 + conn, _, err := websocket.DefaultDialer.DialContext(ctx, url, nil) 82 103 if err != nil { 83 - return err 104 + return fmt.Errorf("websocket dial failed: %w", err) 84 105 } 85 - defer resp.Body.Close() 106 + defer conn.Close() 86 107 87 - if resp.StatusCode != 200 { 88 - body, _ := io.ReadAll(resp.Body) 89 - return fmt.Errorf("firehose returned %d: %s", resp.StatusCode, string(body)) 90 - } 108 + log.Printf("Connected to firehose") 91 109 92 - decoder := json.NewDecoder(resp.Body) 93 110 for { 94 111 select { 95 112 case <-ctx.Done(): ··· 97 114 default: 98 115 } 99 116 100 - var event FirehoseEvent 101 - if err := decoder.Decode(&event); err != nil { 102 - if err == io.EOF { 103 - return nil 104 - } 105 - return err 117 + _, message, err := conn.ReadMessage() 118 + if err != nil { 119 + return fmt.Errorf("websocket read failed: %w", err) 106 120 } 107 121 108 - i.handleEvent(&event) 122 + i.handleMessage(message) 109 123 } 110 124 } 111 125 112 - type FirehoseEvent struct { 113 - Repo string `json:"repo"` 114 - Collection string `json:"collection"` 115 - Rkey string `json:"rkey"` 116 - Record json.RawMessage `json:"record"` 117 - Operation string `json:"operation"` 118 - Cursor int64 `json:"cursor"` 119 - } 126 + func (i *Ingester) handleMessage(data []byte) { 127 + reader := bytes.NewReader(data) 120 128 121 - func (i *Ingester) handleEvent(event *FirehoseEvent) { 122 - uri := fmt.Sprintf("at://%s/%s/%s", event.Repo, event.Collection, event.Rkey) 129 + var header FrameHeader 130 + decoder := cbor.NewDecoder(reader) 131 + if err := decoder.Decode(&header); err != nil { 132 + return 133 + } 123 134 124 - switch event.Collection { 125 - case CollectionAnnotation: 126 - switch event.Operation { 127 - case "create", "update": 128 - i.handleAnnotation(event) 129 - case "delete": 130 - i.db.DeleteAnnotation(uri) 135 + if header.Op != 1 { 136 + return 137 + } 138 + 139 + if header.T != "#commit" { 140 + return 141 + } 142 + 143 + var commit Commit 144 + if err := decoder.Decode(&commit); err != nil { 145 + return 146 + } 147 + 148 + for _, op := range commit.Ops { 149 + collection, rkey := parseOpPath(op.Path) 150 + if !isMarginCollection(collection) { 151 + continue 131 152 } 132 - case CollectionHighlight: 133 - switch event.Operation { 153 + 154 + uri := fmt.Sprintf("at://%s/%s/%s", commit.Repo, collection, rkey) 155 + 156 + switch op.Action { 134 157 case "create", "update": 135 - i.handleHighlight(event) 158 + if op.Cid != nil && len(commit.Blocks) > 0 { 159 + record := extractRecord(commit.Blocks, *op.Cid) 160 + if record != nil { 161 + i.handleRecord(commit.Repo, collection, rkey, record, commit.Seq) 162 + } 163 + } 136 164 case "delete": 137 - i.db.DeleteHighlight(uri) 165 + i.handleDelete(collection, uri) 138 166 } 139 - case CollectionBookmark: 140 - switch event.Operation { 141 - case "create", "update": 142 - i.handleBookmark(event) 143 - case "delete": 144 - i.db.DeleteBookmark(uri) 167 + } 168 + 169 + if commit.Seq > 0 { 170 + if err := i.db.SetCursor("firehose_cursor", commit.Seq); err != nil { 171 + log.Printf("Failed to save cursor: %v", err) 145 172 } 146 - case CollectionReply: 147 - switch event.Operation { 148 - case "create", "update": 149 - i.handleReply(event) 150 - case "delete": 151 - i.db.DeleteReply(uri) 173 + } 174 + } 175 + 176 + func parseOpPath(path string) (collection, rkey string) { 177 + for i := len(path) - 1; i >= 0; i-- { 178 + if path[i] == '/' { 179 + return path[:i], path[i+1:] 152 180 } 153 - case CollectionLike: 154 - switch event.Operation { 155 - case "create": 156 - i.handleLike(event) 157 - case "delete": 158 - i.db.DeleteLike(uri) 181 + } 182 + return path, "" 183 + } 184 + 185 + func isMarginCollection(collection string) bool { 186 + switch collection { 187 + case CollectionAnnotation, CollectionHighlight, CollectionBookmark, 188 + CollectionReply, CollectionLike, CollectionCollection, CollectionCollectionItem: 189 + return true 190 + } 191 + return false 192 + } 193 + 194 + func extractRecord(blocks []byte, targetCid cid.Cid) map[string]interface{} { 195 + reader := bytes.NewReader(blocks) 196 + 197 + headerLen, err := binary.ReadUvarint(reader) 198 + if err != nil { 199 + return nil 200 + } 201 + reader.Seek(int64(headerLen), io.SeekCurrent) 202 + 203 + for reader.Len() > 0 { 204 + blockLen, err := binary.ReadUvarint(reader) 205 + if err != nil { 206 + break 159 207 } 160 - case CollectionCollection: 161 - switch event.Operation { 162 - case "create", "update": 163 - i.handleCollection(event) 164 - case "delete": 165 - i.db.DeleteCollection(uri) 208 + 209 + blockData := make([]byte, blockLen) 210 + if _, err := io.ReadFull(reader, blockData); err != nil { 211 + break 166 212 } 167 - case CollectionCollectionItem: 168 - switch event.Operation { 169 - case "create", "update": 170 - i.handleCollectionItem(event) 171 - case "delete": 172 - i.db.RemoveFromCollection(uri) 213 + 214 + blockCid, cidLen, err := parseCidFromBlock(blockData) 215 + if err != nil { 216 + continue 217 + } 218 + 219 + if blockCid.Equals(targetCid) { 220 + var record map[string]interface{} 221 + if err := cbor.Unmarshal(blockData[cidLen:], &record); err != nil { 222 + return nil 223 + } 224 + return record 173 225 } 174 226 } 175 227 176 - if event.Cursor > 0 { 177 - if err := i.db.SetCursor("firehose_cursor", event.Cursor); err != nil { 178 - log.Printf("Failed to save cursor: %v", err) 228 + return nil 229 + } 230 + 231 + func parseCidFromBlock(data []byte) (cid.Cid, int, error) { 232 + if len(data) < 2 { 233 + return cid.Cid{}, 0, fmt.Errorf("data too short") 234 + } 235 + version, n1 := binary.Uvarint(data) 236 + if n1 <= 0 { 237 + return cid.Cid{}, 0, fmt.Errorf("invalid version varint") 238 + } 239 + 240 + if version == 1 { 241 + codec, n2 := binary.Uvarint(data[n1:]) 242 + if n2 <= 0 { 243 + return cid.Cid{}, 0, fmt.Errorf("invalid codec varint") 179 244 } 245 + 246 + mhStart := n1 + n2 247 + hashType, n3 := binary.Uvarint(data[mhStart:]) 248 + if n3 <= 0 { 249 + return cid.Cid{}, 0, fmt.Errorf("invalid hash type varint") 250 + } 251 + 252 + hashLen, n4 := binary.Uvarint(data[mhStart+n3:]) 253 + if n4 <= 0 { 254 + return cid.Cid{}, 0, fmt.Errorf("invalid hash length varint") 255 + } 256 + 257 + totalCidLen := mhStart + n3 + n4 + int(hashLen) 258 + 259 + c, err := cid.Cast(data[:totalCidLen]) 260 + if err != nil { 261 + return cid.Cid{}, 0, err 262 + } 263 + 264 + _ = codec 265 + _ = hashType 266 + 267 + return c, totalCidLen, nil 180 268 } 269 + 270 + return cid.Cid{}, 0, fmt.Errorf("unsupported CID version") 181 271 } 182 272 183 - func (i *Ingester) handleAnnotation(event *FirehoseEvent) { 273 + func (i *Ingester) handleDelete(collection, uri string) { 274 + switch collection { 275 + case CollectionAnnotation: 276 + i.db.DeleteAnnotation(uri) 277 + case CollectionHighlight: 278 + i.db.DeleteHighlight(uri) 279 + case CollectionBookmark: 280 + i.db.DeleteBookmark(uri) 281 + case CollectionReply: 282 + i.db.DeleteReply(uri) 283 + case CollectionLike: 284 + i.db.DeleteLike(uri) 285 + case CollectionCollection: 286 + i.db.DeleteCollection(uri) 287 + case CollectionCollectionItem: 288 + i.db.RemoveFromCollection(uri) 289 + } 290 + } 184 291 292 + func (i *Ingester) handleRecord(repo, collection, rkey string, record map[string]interface{}, seq int64) { 293 + _ = fmt.Sprintf("at://%s/%s/%s", repo, collection, rkey) 294 + 295 + recordJSON, err := json.Marshal(record) 296 + if err != nil { 297 + return 298 + } 299 + 300 + event := &FirehoseEvent{ 301 + Repo: repo, 302 + Collection: collection, 303 + Rkey: rkey, 304 + Record: recordJSON, 305 + Operation: "create", 306 + Cursor: seq, 307 + } 308 + 309 + switch collection { 310 + case CollectionAnnotation: 311 + i.handleAnnotation(event) 312 + case CollectionHighlight: 313 + i.handleHighlight(event) 314 + case CollectionBookmark: 315 + i.handleBookmark(event) 316 + case CollectionReply: 317 + i.handleReply(event) 318 + case CollectionLike: 319 + i.handleLike(event) 320 + case CollectionCollection: 321 + i.handleCollection(event) 322 + case CollectionCollectionItem: 323 + i.handleCollectionItem(event) 324 + } 325 + } 326 + 327 + type FirehoseEvent struct { 328 + Repo string `json:"repo"` 329 + Collection string `json:"collection"` 330 + Rkey string `json:"rkey"` 331 + Record json.RawMessage `json:"record"` 332 + Operation string `json:"operation"` 333 + Cursor int64 `json:"cursor"` 334 + } 335 + 336 + func (i *Ingester) handleAnnotation(event *FirehoseEvent) { 185 337 var record struct { 186 338 Motivation string `json:"motivation"` 187 339 Body struct { ··· 205 357 Title string `json:"title"` 206 358 } 207 359 208 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 360 + if err := json.Unmarshal(event.Record, &record); err != nil { 209 361 return 210 362 } 211 363 ··· 302 454 CreatedAt string `json:"createdAt"` 303 455 } 304 456 305 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 457 + if err := json.Unmarshal(event.Record, &record); err != nil { 306 458 return 307 459 } 308 460 ··· 334 486 CreatedAt string `json:"createdAt"` 335 487 } 336 488 337 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 489 + if err := json.Unmarshal(event.Record, &record); err != nil { 338 490 return 339 491 } 340 492 ··· 369 521 CreatedAt string `json:"createdAt"` 370 522 } 371 523 372 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 524 + if err := json.Unmarshal(event.Record, &record); err != nil { 373 525 return 374 526 } 375 527 ··· 432 584 CreatedAt string `json:"createdAt"` 433 585 } 434 586 435 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 587 + if err := json.Unmarshal(event.Record, &record); err != nil { 436 588 return 437 589 } 438 590 ··· 488 640 CreatedAt string `json:"createdAt"` 489 641 } 490 642 491 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 643 + if err := json.Unmarshal(event.Record, &record); err != nil { 492 644 return 493 645 } 494 646 ··· 532 684 CreatedAt string `json:"createdAt"` 533 685 } 534 686 535 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 687 + if err := json.Unmarshal(event.Record, &record); err != nil { 536 688 return 537 689 } 538 690
+2 -1
docker-compose.yml
··· 9 9 - OAUTH_KEY_PATH=/data/oauth_private_key.pem 10 10 env_file: 11 11 - .env 12 + volumes: 13 + - margin-data:/data 12 14 depends_on: 13 15 db: 14 16 condition: service_healthy ··· 23 25 24 26 volumes: 25 27 - db-data:/var/lib/postgresql/data 26 - - margin-data:/data 27 28 healthcheck: 28 29 test: ["CMD-SHELL", "pg_isready -U margin"] 29 30 interval: 5s
+123 -51
extension/background/service-worker.js
··· 6 6 const hasSidebarAction = 7 7 typeof browser !== "undefined" && 8 8 typeof browser.sidebarAction !== "undefined"; 9 - const hasSessionStorage = 10 - typeof chrome !== "undefined" && 11 - chrome.storage && 12 - typeof chrome.storage.session !== "undefined"; 13 9 const hasNotifications = 14 10 typeof chrome !== "undefined" && typeof chrome.notifications !== "undefined"; 15 11 ··· 43 39 } 44 40 } 45 41 46 - async function openAnnotationUI(tabId) { 42 + async function openAnnotationUI(tabId, windowId) { 47 43 if (hasSidePanel) { 48 44 try { 49 - const tab = await chrome.tabs.get(tabId); 50 - await chrome.sidePanel.setOptions({ 51 - tabId: tabId, 52 - path: "sidepanel/sidepanel.html", 53 - enabled: true, 54 - }); 55 - await chrome.sidePanel.open({ windowId: tab.windowId }); 45 + let targetWindowId = windowId; 46 + 47 + if (!targetWindowId) { 48 + const tab = await chrome.tabs.get(tabId); 49 + targetWindowId = tab.windowId; 50 + } 51 + 52 + await chrome.sidePanel.open({ windowId: targetWindowId }); 56 53 return true; 57 54 } catch (err) { 58 55 console.error("Could not open Chrome side panel:", err); ··· 71 68 return false; 72 69 } 73 70 74 - async function storePendingAnnotation(data) { 75 - if (hasSessionStorage) { 76 - await chrome.storage.session.set({ pendingAnnotation: data }); 77 - } else { 78 - await chrome.storage.local.set({ 79 - pendingAnnotation: data, 80 - pendingAnnotationExpiry: Date.now() + 60000, 81 - }); 82 - } 83 - } 84 - 85 71 chrome.runtime.onInstalled.addListener(async () => { 86 72 const stored = await chrome.storage.local.get(["apiUrl"]); 87 73 if (!stored.apiUrl) { ··· 118 104 if (hasSidebarAction) { 119 105 try { 120 106 await browser.sidebarAction.close(); 121 - } catch (e) {} 107 + } catch { 108 + /* ignore */ 109 + } 122 110 } 123 111 }); 124 112 125 - chrome.action.onClicked.addListener(async (tab) => { 113 + chrome.action.onClicked.addListener(async () => { 126 114 const stored = await chrome.storage.local.get(["apiUrl"]); 127 115 const webUrl = stored.apiUrl || WEB_BASE; 128 116 chrome.tabs.create({ url: webUrl }); ··· 130 118 131 119 chrome.contextMenus.onClicked.addListener(async (info, tab) => { 132 120 if (info.menuItemId === "margin-open-sidebar") { 133 - if (hasSidePanel && chrome.sidePanel && chrome.sidePanel.open) { 134 - try { 135 - await chrome.sidePanel.open({ windowId: tab.windowId }); 136 - } catch (err) { 137 - console.error("Failed to open side panel:", err); 138 - } 139 - } else if (hasSidebarAction) { 140 - try { 141 - await browser.sidebarAction.open(); 142 - } catch (err) { 143 - console.error("Failed to open Firefox sidebar:", err); 144 - } 145 - } 121 + await openAnnotationUI(tab.id, tab.windowId); 146 122 return; 147 123 } 148 124 ··· 189 165 selectionText: info.selectionText, 190 166 }); 191 167 selector = response?.selector; 192 - } catch (err) {} 193 - 194 - if (selector && (hasSidePanel || hasSidebarAction)) { 195 - await storePendingAnnotation({ 196 - url: tab.url, 197 - title: tab.title, 198 - selector: selector, 199 - }); 200 - const opened = await openAnnotationUI(tab.id); 201 - if (opened) return; 168 + } catch { 169 + /* ignore */ 202 170 } 203 171 204 172 if (!selector && info.selectionText) { ··· 208 176 }; 209 177 } 210 178 179 + if (selector) { 180 + try { 181 + await chrome.tabs.sendMessage(tab.id, { 182 + type: "SHOW_INLINE_ANNOTATE", 183 + data: { 184 + url: tab.url, 185 + title: tab.title, 186 + selector: selector, 187 + }, 188 + }); 189 + return; 190 + } catch (e) { 191 + console.debug("Inline annotate failed, falling back to new tab:", e); 192 + } 193 + } 194 + 211 195 if (WEB_BASE) { 212 196 let composeUrl = `${WEB_BASE}/new?url=${encodeURIComponent(tab.url)}`; 213 197 if (selector) { ··· 227 211 selectionText: info.selectionText, 228 212 }); 229 213 if (response && response.success) return; 230 - } catch (err) {} 214 + } catch { 215 + /* ignore */ 216 + } 231 217 232 218 if (info.selectionText) { 233 219 selector = { ··· 334 320 } 335 321 336 322 case "GET_ANNOTATIONS": { 323 + const stored = await chrome.storage.local.get(["apiUrl"]); 324 + const currentApiUrl = stored.apiUrl 325 + ? stored.apiUrl.replace(/\/$/, "") 326 + : API_BASE; 327 + 337 328 const pageUrl = request.data.url; 338 329 const res = await fetch( 339 - `${API_BASE}/api/targets?source=${encodeURIComponent(pageUrl)}`, 330 + `${currentApiUrl}/api/targets?source=${encodeURIComponent(pageUrl)}`, 340 331 ); 341 332 const data = await res.json(); 342 333 ··· 422 413 return; 423 414 } 424 415 const { url, selector } = request.data; 425 - 426 416 let composeUrl = `${WEB_BASE}/new?url=${encodeURIComponent(url)}`; 427 417 if (selector) { 428 418 composeUrl += `&selector=${encodeURIComponent(JSON.stringify(selector))}`; ··· 430 420 chrome.tabs.create({ url: composeUrl }); 431 421 break; 432 422 } 423 + 424 + case "OPEN_APP_URL": { 425 + if (!WEB_BASE) { 426 + chrome.runtime.openOptionsPage(); 427 + return; 428 + } 429 + const path = request.data.path; 430 + const safePath = path.startsWith("/") ? path : `/${path}`; 431 + chrome.tabs.create({ url: `${WEB_BASE}${safePath}` }); 432 + break; 433 + } 434 + 435 + case "OPEN_SIDE_PANEL": 436 + if (sender.tab && sender.tab.windowId) { 437 + chrome.sidePanel 438 + .open({ windowId: sender.tab.windowId }) 439 + .catch((err) => console.error("Failed to open side panel", err)); 440 + } 441 + break; 433 442 434 443 case "CREATE_BOOKMARK": { 435 444 if (!API_BASE) { ··· 634 643 throw new Error( 635 644 `Failed to add to collection: ${res.status} ${errText}`, 636 645 ); 646 + } 647 + 648 + const data = await res.json(); 649 + sendResponse({ success: true, data }); 650 + break; 651 + } 652 + 653 + case "GET_REPLIES": { 654 + if (!API_BASE) { 655 + sendResponse({ success: false, error: "API URL not configured" }); 656 + return; 657 + } 658 + 659 + const uri = request.data.uri; 660 + const res = await fetch( 661 + `${API_BASE}/api/replies?uri=${encodeURIComponent(uri)}`, 662 + ); 663 + 664 + if (!res.ok) { 665 + throw new Error(`Failed to fetch replies: ${res.status}`); 666 + } 667 + 668 + const data = await res.json(); 669 + sendResponse({ success: true, data: data.items || [] }); 670 + break; 671 + } 672 + 673 + case "CREATE_REPLY": { 674 + if (!API_BASE) { 675 + sendResponse({ success: false, error: "API URL not configured" }); 676 + return; 677 + } 678 + 679 + const cookie = await chrome.cookies.get({ 680 + url: API_BASE, 681 + name: "margin_session", 682 + }); 683 + 684 + if (!cookie) { 685 + sendResponse({ success: false, error: "Not authenticated" }); 686 + return; 687 + } 688 + 689 + const { parentUri, parentCid, rootUri, rootCid, text } = request.data; 690 + const res = await fetch(`${API_BASE}/api/annotations/reply`, { 691 + method: "POST", 692 + credentials: "include", 693 + headers: { 694 + "Content-Type": "application/json", 695 + "X-Session-Token": cookie.value, 696 + }, 697 + body: JSON.stringify({ 698 + parentUri, 699 + parentCid, 700 + rootUri, 701 + rootCid, 702 + text, 703 + }), 704 + }); 705 + 706 + if (!res.ok) { 707 + const errText = await res.text(); 708 + throw new Error(`Failed to create reply: ${res.status} ${errText}`); 637 709 } 638 710 639 711 const data = await res.json();
+993 -240
extension/content/content.js
··· 1 1 (() => { 2 - function buildTextQuoteSelector(selection) { 3 - const exact = selection.toString().trim(); 4 - if (!exact) return null; 2 + let sidebarHost = null; 3 + let sidebarShadow = null; 4 + let popoverEl = null; 5 5 6 - const range = selection.getRangeAt(0); 7 - const contextLength = 32; 6 + let activeItems = []; 7 + let currentSelection = null; 8 + 9 + const OVERLAY_STYLES = ` 10 + :host { all: initial; } 11 + .margin-overlay { 12 + position: absolute; 13 + top: 0; 14 + left: 0; 15 + width: 100%; 16 + height: 100%; 17 + pointer-events: none; 18 + } 8 19 9 - let prefix = ""; 10 - try { 11 - const preRange = document.createRange(); 12 - preRange.selectNodeContents(document.body); 13 - preRange.setEnd(range.startContainer, range.startOffset); 14 - const preText = preRange.toString(); 15 - prefix = preText.slice(-contextLength).trim(); 16 - } catch (e) { 17 - console.warn("Could not get prefix:", e); 20 + .margin-popover { 21 + position: absolute; 22 + width: 320px; 23 + background: #09090b; 24 + border: 1px solid #27272a; 25 + border-radius: 12px; 26 + padding: 0; 27 + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2); 28 + display: flex; 29 + flex-direction: column; 30 + pointer-events: auto; 31 + z-index: 2147483647; 32 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 33 + color: #e4e4e7; 34 + opacity: 0; 35 + transform: scale(0.95); 36 + animation: popover-in 0.15s forwards; 37 + max-height: 480px; 38 + overflow: hidden; 39 + } 40 + @keyframes popover-in { to { opacity: 1; transform: scale(1); } } 41 + .popover-header { 42 + padding: 12px 16px; 43 + border-bottom: 1px solid #27272a; 44 + display: flex; 45 + justify-content: space-between; 46 + align-items: center; 47 + background: #0f0f12; 48 + border-radius: 12px 12px 0 0; 49 + font-weight: 600; 50 + font-size: 13px; 51 + } 52 + .popover-scroll-area { 53 + overflow-y: auto; 54 + max-height: 400px; 55 + } 56 + .popover-item-block { 57 + border-bottom: 1px solid #27272a; 58 + margin-bottom: 0; 59 + animation: fade-in 0.2s; 60 + } 61 + .popover-item-block:last-child { 62 + border-bottom: none; 63 + } 64 + .popover-item-header { 65 + padding: 12px 16px 4px; 66 + display: flex; 67 + align-items: center; 68 + gap: 8px; 69 + } 70 + .popover-avatar { 71 + width: 24px; height: 24px; border-radius: 50%; background: #27272a; 72 + display: flex; align-items: center; justify-content: center; 73 + font-size: 10px; color: #a1a1aa; 74 + } 75 + .popover-handle { font-size: 12px; font-weight: 600; color: #e4e4e7; } 76 + .popover-close { background: none; border: none; color: #71717a; cursor: pointer; padding: 4px; } 77 + .popover-close:hover { color: #e4e4e7; } 78 + .popover-content { padding: 4px 16px 12px; font-size: 13px; line-height: 1.5; color: #e4e4e7; } 79 + .popover-quote { 80 + margin-top: 8px; padding: 6px 10px; background: #18181b; 81 + border-left: 2px solid #6366f1; border-radius: 4px; 82 + font-size: 11px; color: #a1a1aa; font-style: italic; 83 + } 84 + .popover-actions { 85 + padding: 8px 16px; 86 + display: flex; justify-content: flex-end; gap: 8px; 87 + } 88 + .btn-action { 89 + background: none; border: 1px solid #27272a; border-radius: 4px; 90 + padding: 4px 8px; color: #a1a1aa; font-size: 11px; cursor: pointer; 91 + } 92 + .btn-action:hover { background: #27272a; color: #e4e4e7; } 93 + 94 + .margin-selection-popup { 95 + position: fixed; 96 + display: flex; 97 + gap: 4px; 98 + padding: 6px; 99 + background: #09090b; 100 + border: 1px solid #27272a; 101 + border-radius: 8px; 102 + box-shadow: 0 8px 16px rgba(0,0,0,0.4); 103 + z-index: 2147483647; 104 + pointer-events: auto; 105 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 106 + animation: popover-in 0.15s forwards; 107 + } 108 + .selection-btn { 109 + display: flex; 110 + align-items: center; 111 + gap: 6px; 112 + padding: 6px 12px; 113 + background: transparent; 114 + border: none; 115 + border-radius: 6px; 116 + color: #e4e4e7; 117 + font-size: 12px; 118 + font-weight: 500; 119 + cursor: pointer; 120 + transition: background 0.15s; 121 + } 122 + .selection-btn:hover { 123 + background: #27272a; 124 + } 125 + .selection-btn svg { 126 + width: 14px; 127 + height: 14px; 128 + } 129 + .inline-compose-modal { 130 + position: fixed; 131 + width: 340px; 132 + max-width: calc(100vw - 40px); 133 + background: #09090b; 134 + border: 1px solid #27272a; 135 + border-radius: 12px; 136 + padding: 16px; 137 + box-sizing: border-box; 138 + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5); 139 + z-index: 2147483647; 140 + pointer-events: auto; 141 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 142 + color: #e4e4e7; 143 + animation: popover-in 0.15s forwards; 144 + overflow: hidden; 145 + } 146 + .inline-compose-modal * { 147 + box-sizing: border-box; 148 + } 149 + .inline-compose-quote { 150 + padding: 8px 12px; 151 + background: #18181b; 152 + border-left: 3px solid #6366f1; 153 + border-radius: 4px; 154 + font-size: 12px; 155 + color: #a1a1aa; 156 + font-style: italic; 157 + margin-bottom: 12px; 158 + max-height: 60px; 159 + overflow: hidden; 160 + word-break: break-word; 161 + } 162 + .inline-compose-textarea { 163 + width: 100%; 164 + min-height: 80px; 165 + padding: 10px 12px; 166 + background: #18181b; 167 + border: 1px solid #27272a; 168 + border-radius: 8px; 169 + color: #e4e4e7; 170 + font-family: inherit; 171 + font-size: 13px; 172 + resize: vertical; 173 + margin-bottom: 12px; 174 + box-sizing: border-box; 175 + } 176 + .inline-compose-textarea:focus { 177 + outline: none; 178 + border-color: #6366f1; 179 + } 180 + .inline-compose-actions { 181 + display: flex; 182 + justify-content: flex-end; 183 + gap: 8px; 184 + } 185 + .btn-cancel { 186 + padding: 8px 16px; 187 + background: transparent; 188 + border: 1px solid #27272a; 189 + border-radius: 6px; 190 + color: #a1a1aa; 191 + font-size: 13px; 192 + cursor: pointer; 193 + } 194 + .btn-cancel:hover { 195 + background: #27272a; 196 + color: #e4e4e7; 18 197 } 198 + .btn-submit { 199 + padding: 8px 16px; 200 + background: #6366f1; 201 + border: none; 202 + border-radius: 6px; 203 + color: white; 204 + font-size: 13px; 205 + font-weight: 500; 206 + cursor: pointer; 207 + } 208 + .btn-submit:hover { 209 + background: #4f46e5; 210 + } 211 + .btn-submit:disabled { 212 + opacity: 0.5; 213 + cursor: not-allowed; 214 + } 215 + .reply-section { 216 + border-top: 1px solid #27272a; 217 + padding: 12px 16px; 218 + background: #0f0f12; 219 + border-radius: 0 0 12px 12px; 220 + } 221 + .reply-textarea { 222 + width: 100%; 223 + min-height: 60px; 224 + padding: 8px 10px; 225 + background: #18181b; 226 + border: 1px solid #27272a; 227 + border-radius: 6px; 228 + color: #e4e4e7; 229 + font-family: inherit; 230 + font-size: 12px; 231 + resize: none; 232 + margin-bottom: 8px; 233 + } 234 + .reply-textarea:focus { 235 + outline: none; 236 + border-color: #6366f1; 237 + } 238 + .reply-submit { 239 + padding: 6px 12px; 240 + background: #6366f1; 241 + border: none; 242 + border-radius: 4px; 243 + color: white; 244 + font-size: 11px; 245 + font-weight: 500; 246 + cursor: pointer; 247 + float: right; 248 + } 249 + .reply-submit:disabled { 250 + opacity: 0.5; 251 + } 252 + .reply-item { 253 + padding: 8px 0; 254 + border-top: 1px solid #27272a; 255 + } 256 + .reply-item:first-child { 257 + border-top: none; 258 + } 259 + .reply-author { 260 + font-size: 11px; 261 + font-weight: 600; 262 + color: #a1a1aa; 263 + margin-bottom: 4px; 264 + } 265 + .reply-text { 266 + font-size: 12px; 267 + color: #e4e4e7; 268 + line-height: 1.4; 269 + } 270 + `; 19 271 20 - let suffix = ""; 21 - try { 22 - const postRange = document.createRange(); 23 - postRange.selectNodeContents(document.body); 24 - postRange.setStart(range.endContainer, range.endOffset); 25 - const postText = postRange.toString(); 26 - suffix = postText.slice(0, contextLength).trim(); 27 - } catch (e) { 28 - console.warn("Could not get suffix:", e); 272 + class DOMTextMatcher { 273 + constructor() { 274 + this.textNodes = []; 275 + this.corpus = ""; 276 + this.indices = []; 277 + this.buildMap(); 29 278 } 30 279 31 - return { 32 - type: "TextQuoteSelector", 33 - exact: exact, 34 - prefix: prefix || undefined, 35 - suffix: suffix || undefined, 36 - }; 37 - } 280 + buildMap() { 281 + const walker = document.createTreeWalker( 282 + document.body, 283 + NodeFilter.SHOW_TEXT, 284 + { 285 + acceptNode: (node) => { 286 + if (!node.parentNode) return NodeFilter.FILTER_REJECT; 287 + const tag = node.parentNode.tagName; 288 + if ( 289 + ["SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA", "INPUT"].includes(tag) 290 + ) 291 + return NodeFilter.FILTER_REJECT; 292 + if (node.textContent.trim().length === 0) 293 + return NodeFilter.FILTER_SKIP; 38 294 39 - function findAndScrollToText(selector) { 40 - if (!selector || !selector.exact) return false; 295 + if (node.parentNode.offsetParent === null) 296 + return NodeFilter.FILTER_REJECT; 41 297 42 - const searchText = selector.exact.trim(); 43 - const normalizedSearch = searchText.replace(/\s+/g, " "); 298 + return NodeFilter.FILTER_ACCEPT; 299 + }, 300 + }, 301 + ); 302 + 303 + let currentNode; 304 + let index = 0; 305 + while ((currentNode = walker.nextNode())) { 306 + const text = currentNode.textContent; 307 + this.textNodes.push(currentNode); 308 + this.corpus += text; 309 + this.indices.push({ 310 + start: index, 311 + node: currentNode, 312 + length: text.length, 313 + }); 314 + index += text.length; 315 + } 316 + } 44 317 45 - const treeWalker = document.createTreeWalker( 46 - document.body, 47 - NodeFilter.SHOW_TEXT, 48 - null, 49 - false, 50 - ); 318 + findRange(searchText) { 319 + if (!searchText) return null; 51 320 52 - let currentNode; 53 - while ((currentNode = treeWalker.nextNode())) { 54 - const nodeText = currentNode.textContent; 55 - const normalizedNode = nodeText.replace(/\s+/g, " "); 321 + let matchIndex = this.corpus.indexOf(searchText); 56 322 57 - let index = nodeText.indexOf(searchText); 323 + if (matchIndex === -1) { 324 + const normalizedSearch = searchText.replace(/\s+/g, " ").trim(); 325 + matchIndex = this.corpus.indexOf(normalizedSearch); 58 326 59 - if (index === -1) { 60 - const normIndex = normalizedNode.indexOf(normalizedSearch); 61 - if (normIndex !== -1) { 62 - index = nodeText.indexOf(searchText.substring(0, 20)); 63 - if (index === -1) index = 0; 327 + if (matchIndex === -1) { 328 + const fuzzyMatch = this.fuzzyFindInCorpus(searchText); 329 + if (fuzzyMatch) { 330 + const start = this.mapIndexToPoint(fuzzyMatch.start); 331 + const end = this.mapIndexToPoint(fuzzyMatch.end); 332 + if (start && end) { 333 + const range = document.createRange(); 334 + range.setStart(start.node, start.offset); 335 + range.setEnd(end.node, end.offset); 336 + return range; 337 + } 338 + } 339 + return null; 64 340 } 65 341 } 66 342 67 - if (index !== -1 && nodeText.trim().length > 0) { 68 - try { 69 - const range = document.createRange(); 70 - const endIndex = Math.min(index + searchText.length, nodeText.length); 71 - range.setStart(currentNode, index); 72 - range.setEnd(currentNode, endIndex); 343 + const start = this.mapIndexToPoint(matchIndex); 344 + const end = this.mapIndexToPoint(matchIndex + searchText.length); 73 345 74 - if (typeof CSS !== "undefined" && CSS.highlights) { 75 - const highlight = new Highlight(range); 76 - CSS.highlights.set("margin-scroll-highlight", highlight); 346 + if (start && end) { 347 + const range = document.createRange(); 348 + range.setStart(start.node, start.offset); 349 + range.setEnd(end.node, end.offset); 350 + return range; 351 + } 352 + return null; 353 + } 354 + 355 + fuzzyFindInCorpus(searchText) { 356 + const searchWords = searchText 357 + .trim() 358 + .split(/\s+/) 359 + .filter((w) => w.length > 0); 360 + if (searchWords.length === 0) return null; 361 + 362 + const corpusLower = this.corpus.toLowerCase(); 363 + 364 + const firstWord = searchWords[0].toLowerCase(); 365 + let searchStart = 0; 366 + 367 + while (searchStart < corpusLower.length) { 368 + const wordStart = corpusLower.indexOf(firstWord, searchStart); 369 + if (wordStart === -1) break; 77 370 78 - setTimeout(() => { 79 - CSS.highlights.delete("margin-scroll-highlight"); 80 - }, 3000); 371 + let corpusPos = wordStart; 372 + let matched = true; 373 + let lastMatchEnd = wordStart; 374 + 375 + for (const word of searchWords) { 376 + const wordLower = word.toLowerCase(); 377 + while ( 378 + corpusPos < corpusLower.length && 379 + /\s/.test(this.corpus[corpusPos]) 380 + ) { 381 + corpusPos++; 382 + } 383 + const corpusSlice = corpusLower.slice( 384 + corpusPos, 385 + corpusPos + wordLower.length, 386 + ); 387 + if (corpusSlice !== wordLower) { 388 + matched = false; 389 + break; 81 390 } 82 391 83 - const rect = range.getBoundingClientRect(); 84 - window.scrollTo({ 85 - top: window.scrollY + rect.top - window.innerHeight / 3, 86 - behavior: "smooth", 87 - }); 88 - 89 - window.scrollTo({ 90 - top: window.scrollY + rect.top - window.innerHeight / 3, 91 - behavior: "smooth", 92 - }); 392 + corpusPos += wordLower.length; 393 + lastMatchEnd = corpusPos; 394 + } 93 395 94 - return true; 95 - } catch (e) { 96 - console.warn("Could not create range:", e); 396 + if (matched) { 397 + return { start: wordStart, end: lastMatchEnd }; 97 398 } 399 + 400 + searchStart = wordStart + 1; 98 401 } 402 + 403 + return null; 99 404 } 100 405 101 - if (window.find) { 102 - window.getSelection()?.removeAllRanges(); 103 - const found = window.find(searchText, false, false, true, false); 104 - if (found) { 105 - const selection = window.getSelection(); 106 - if (selection && selection.rangeCount > 0) { 107 - const range = selection.getRangeAt(0); 108 - const rect = range.getBoundingClientRect(); 109 - window.scrollTo({ 110 - top: window.scrollY + rect.top - window.innerHeight / 3, 111 - behavior: "smooth", 112 - }); 406 + mapIndexToPoint(corpusIndex) { 407 + for (const info of this.indices) { 408 + if ( 409 + corpusIndex >= info.start && 410 + corpusIndex < info.start + info.length 411 + ) { 412 + return { node: info.node, offset: corpusIndex - info.start }; 113 413 } 114 - return true; 115 414 } 415 + if (this.indices.length > 0) { 416 + const last = this.indices[this.indices.length - 1]; 417 + if (corpusIndex === last.start + last.length) { 418 + return { node: last.node, offset: last.length }; 419 + } 420 + } 421 + return null; 116 422 } 423 + } 117 424 118 - return false; 425 + function initOverlay() { 426 + sidebarHost = document.createElement("div"); 427 + sidebarHost.id = "margin-overlay-host"; 428 + const getScrollHeight = () => { 429 + const bodyH = document.body?.scrollHeight || 0; 430 + const docH = document.documentElement?.scrollHeight || 0; 431 + return Math.max(bodyH, docH); 432 + }; 433 + 434 + sidebarHost.style.cssText = ` 435 + position: absolute; top: 0; left: 0; width: 100%; 436 + height: ${getScrollHeight()}px; 437 + pointer-events: none; z-index: 2147483647; 438 + `; 439 + document.body?.appendChild(sidebarHost) || 440 + document.documentElement.appendChild(sidebarHost); 441 + 442 + sidebarShadow = sidebarHost.attachShadow({ mode: "open" }); 443 + const styleEl = document.createElement("style"); 444 + styleEl.textContent = OVERLAY_STYLES; 445 + sidebarShadow.appendChild(styleEl); 446 + 447 + const container = document.createElement("div"); 448 + container.className = "margin-overlay"; 449 + container.id = "margin-overlay-container"; 450 + sidebarShadow.appendChild(container); 451 + 452 + const observer = new ResizeObserver(() => { 453 + sidebarHost.style.height = `${getScrollHeight()}px`; 454 + }); 455 + if (document.body) observer.observe(document.body); 456 + if (document.documentElement) observer.observe(document.documentElement); 457 + 458 + if (typeof chrome !== "undefined" && chrome.storage) { 459 + chrome.storage.local.get(["showOverlay"], (result) => { 460 + if (result.showOverlay === false) { 461 + sidebarHost.style.display = "none"; 462 + } else { 463 + fetchAnnotations(); 464 + } 465 + }); 466 + } else { 467 + fetchAnnotations(); 468 + } 469 + 470 + document.addEventListener("mousemove", handleMouseMove); 471 + document.addEventListener("click", handleDocumentClick, true); 119 472 } 120 473 121 - function renderPageHighlights(highlights) { 122 - if (!highlights || !Array.isArray(highlights) || !CSS.highlights) return; 474 + function showInlineComposeModal() { 475 + if (!sidebarShadow || !currentSelection) return; 123 476 124 - const ranges = []; 477 + const container = sidebarShadow.getElementById("margin-overlay-container"); 478 + if (!container) return; 125 479 126 - highlights.forEach((item) => { 127 - const selector = item.target?.selector; 128 - if (!selector?.exact) return; 480 + const existingModal = container.querySelector(".inline-compose-modal"); 481 + if (existingModal) existingModal.remove(); 482 + 483 + const modal = document.createElement("div"); 484 + modal.className = "inline-compose-modal"; 485 + 486 + modal.style.left = `${Math.max(20, (window.innerWidth - 340) / 2)}px`; 487 + modal.style.top = `${Math.min(200, window.innerHeight / 4)}px`; 488 + 489 + const truncatedQuote = 490 + currentSelection.text.length > 100 491 + ? currentSelection.text.substring(0, 100) + "..." 492 + : currentSelection.text; 493 + 494 + modal.innerHTML = ` 495 + <div class="inline-compose-quote">"${truncatedQuote}"</div> 496 + <textarea class="inline-compose-textarea" placeholder="Add your annotation..." autofocus></textarea> 497 + <div class="inline-compose-actions"> 498 + <button class="btn-cancel">Cancel</button> 499 + <button class="btn-submit">Post Annotation</button> 500 + </div> 501 + `; 502 + 503 + const textarea = modal.querySelector("textarea"); 504 + const submitBtn = modal.querySelector(".btn-submit"); 505 + const cancelBtn = modal.querySelector(".btn-cancel"); 506 + 507 + cancelBtn.addEventListener("click", () => { 508 + modal.remove(); 509 + }); 510 + 511 + submitBtn.addEventListener("click", async () => { 512 + const text = textarea.value.trim(); 513 + if (!text) return; 514 + 515 + submitBtn.disabled = true; 516 + submitBtn.textContent = "Posting..."; 129 517 130 - const searchText = selector.exact; 131 - const treeWalker = document.createTreeWalker( 132 - document.body, 133 - NodeFilter.SHOW_TEXT, 134 - null, 135 - false, 518 + chrome.runtime.sendMessage( 519 + { 520 + type: "CREATE_ANNOTATION", 521 + data: { 522 + url: currentSelection.url || window.location.href, 523 + title: currentSelection.title || document.title, 524 + text: text, 525 + selector: currentSelection.selector, 526 + }, 527 + }, 528 + (res) => { 529 + if (res && res.success) { 530 + modal.remove(); 531 + fetchAnnotations(); 532 + } else { 533 + submitBtn.disabled = false; 534 + submitBtn.textContent = "Post Annotation"; 535 + alert( 536 + "Failed to create annotation: " + (res?.error || "Unknown error"), 537 + ); 538 + } 539 + }, 136 540 ); 541 + }); 137 542 138 - let currentNode; 139 - while ((currentNode = treeWalker.nextNode())) { 140 - const nodeText = currentNode.textContent; 141 - const index = nodeText.indexOf(searchText); 543 + container.appendChild(modal); 544 + textarea.focus(); 545 + 546 + const handleEscape = (e) => { 547 + if (e.key === "Escape") { 548 + modal.remove(); 549 + document.removeEventListener("keydown", handleEscape); 550 + } 551 + }; 552 + document.addEventListener("keydown", handleEscape); 553 + } 142 554 143 - if (index !== -1) { 144 - try { 145 - const range = document.createRange(); 146 - range.setStart(currentNode, index); 147 - range.setEnd(currentNode, index + searchText.length); 148 - ranges.push(range); 149 - } catch (e) { 150 - console.warn("Could not create range for highlight:", e); 555 + let hoverIndicator = null; 556 + 557 + function handleMouseMove(e) { 558 + const x = e.clientX; 559 + const y = e.clientY; 560 + let foundItems = []; 561 + let firstRange = null; 562 + for (const { range, item } of activeItems) { 563 + const rects = range.getClientRects(); 564 + for (const rect of rects) { 565 + if ( 566 + x >= rect.left && 567 + x <= rect.right && 568 + y >= rect.top && 569 + y <= rect.bottom 570 + ) { 571 + if (!firstRange) firstRange = range; 572 + if (!foundItems.some((f) => f.item === item)) { 573 + foundItems.push({ range, item, rect }); 151 574 } 152 575 break; 153 576 } 154 577 } 155 - }); 578 + } 156 579 157 - if (ranges.length > 0) { 158 - const highlight = new Highlight(...ranges); 159 - CSS.highlights.set("margin-page-highlights", highlight); 160 - } 161 - } 580 + if (foundItems.length > 0) { 581 + document.body.style.cursor = "pointer"; 162 582 163 - chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 164 - if (request.type === "GET_SELECTOR_FOR_ANNOTATE_INLINE") { 165 - const selection = window.getSelection(); 166 - if (!selection || selection.toString().trim().length === 0) { 167 - sendResponse({ selector: null }); 168 - return true; 583 + if (!hoverIndicator && sidebarShadow) { 584 + const container = sidebarShadow.getElementById( 585 + "margin-overlay-container", 586 + ); 587 + if (container) { 588 + hoverIndicator = document.createElement("div"); 589 + hoverIndicator.className = "margin-hover-indicator"; 590 + hoverIndicator.style.cssText = ` 591 + position: fixed; 592 + display: flex; 593 + align-items: center; 594 + pointer-events: none; 595 + z-index: 2147483647; 596 + opacity: 0; 597 + transition: opacity 0.15s, transform 0.15s; 598 + transform: scale(0.8); 599 + `; 600 + container.appendChild(hoverIndicator); 601 + } 169 602 } 170 603 171 - const selector = buildTextQuoteSelector(selection); 172 - sendResponse({ selector: selector }); 173 - return true; 604 + if (hoverIndicator) { 605 + const authorsMap = new Map(); 606 + foundItems.forEach(({ item }) => { 607 + const author = item.author || item.creator || {}; 608 + const id = author.did || author.handle || "unknown"; 609 + if (!authorsMap.has(id)) { 610 + authorsMap.set(id, author); 611 + } 612 + }); 613 + const uniqueAuthors = Array.from(authorsMap.values()); 614 + 615 + const maxShow = 3; 616 + const displayAuthors = uniqueAuthors.slice(0, maxShow); 617 + const overflow = uniqueAuthors.length - maxShow; 618 + 619 + let html = displayAuthors 620 + .map((author, i) => { 621 + const avatar = author.avatar; 622 + const handle = author.handle || "U"; 623 + const marginLeft = i === 0 ? "0" : "-8px"; 624 + 625 + if (avatar) { 626 + return `<img src="${avatar}" style="width: 24px; height: 24px; border-radius: 50%; object-fit: cover; border: 2px solid #09090b; margin-left: ${marginLeft};">`; 627 + } else { 628 + return `<div style="width: 24px; height: 24px; border-radius: 50%; background: #6366f1; color: white; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; font-family: -apple-system, sans-serif; border: 2px solid #09090b; margin-left: ${marginLeft};">${handle[0]?.toUpperCase() || "U"}</div>`; 629 + } 630 + }) 631 + .join(""); 632 + 633 + if (overflow > 0) { 634 + html += `<div style="width: 24px; height: 24px; border-radius: 50%; background: #27272a; color: #a1a1aa; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; font-family: -apple-system, sans-serif; border: 2px solid #09090b; margin-left: -8px;">+${overflow}</div>`; 635 + } 636 + 637 + hoverIndicator.innerHTML = html; 638 + 639 + const firstRect = firstRange.getClientRects()[0]; 640 + const totalWidth = 641 + Math.min(uniqueAuthors.length, maxShow + (overflow > 0 ? 1 : 0)) * 642 + 18 + 643 + 8; 644 + const leftPos = firstRect.left - totalWidth; 645 + const topPos = firstRect.top + firstRect.height / 2 - 12; 646 + 647 + hoverIndicator.style.left = `${leftPos}px`; 648 + hoverIndicator.style.top = `${topPos}px`; 649 + hoverIndicator.style.opacity = "1"; 650 + hoverIndicator.style.transform = "scale(1)"; 651 + } 652 + } else { 653 + document.body.style.cursor = ""; 654 + if (hoverIndicator) { 655 + hoverIndicator.style.opacity = "0"; 656 + hoverIndicator.style.transform = "scale(0.8)"; 657 + } 174 658 } 659 + } 175 660 176 - if (request.type === "GET_SELECTOR_FOR_ANNOTATE") { 177 - const selection = window.getSelection(); 178 - if (!selection || selection.toString().trim().length === 0) { 661 + function handleDocumentClick(e) { 662 + const x = e.clientX; 663 + const y = e.clientY; 664 + if (popoverEl && sidebarShadow) { 665 + const rect = popoverEl.getBoundingClientRect(); 666 + if ( 667 + x >= rect.left && 668 + x <= rect.right && 669 + y >= rect.top && 670 + y <= rect.bottom 671 + ) { 179 672 return; 180 673 } 674 + } 181 675 182 - const selector = buildTextQuoteSelector(selection); 183 - if (selector) { 184 - chrome.runtime.sendMessage({ 185 - type: "OPEN_COMPOSE", 186 - data: { 187 - url: window.location.href, 188 - selector: selector, 189 - }, 190 - }); 676 + let clickedItems = []; 677 + for (const { range, item } of activeItems) { 678 + const rects = range.getClientRects(); 679 + for (const rect of rects) { 680 + if ( 681 + x >= rect.left && 682 + x <= rect.right && 683 + y >= rect.top && 684 + y <= rect.bottom 685 + ) { 686 + if (!clickedItems.includes(item)) { 687 + clickedItems.push(item); 688 + } 689 + break; 690 + } 191 691 } 192 692 } 193 693 194 - if (request.type === "GET_SELECTOR_FOR_HIGHLIGHT") { 195 - const selection = window.getSelection(); 196 - if (!selection || selection.toString().trim().length === 0) { 197 - sendResponse({ success: false, error: "No text selected" }); 198 - return true; 199 - } 694 + if (clickedItems.length > 0) { 695 + e.preventDefault(); 696 + e.stopPropagation(); 200 697 201 - const selector = buildTextQuoteSelector(selection); 202 - if (selector) { 203 - chrome.runtime 204 - .sendMessage({ 205 - type: "CREATE_HIGHLIGHT", 206 - data: { 207 - url: window.location.href, 208 - title: document.title, 209 - selector: selector, 210 - }, 211 - }) 212 - .then((response) => { 213 - if (response?.success) { 214 - showNotification("Text highlighted!", "success"); 698 + if (popoverEl) { 699 + const currentIds = popoverEl.dataset.itemIds; 700 + const newIds = clickedItems 701 + .map((i) => i.uri || i.id) 702 + .sort() 703 + .join(","); 215 704 216 - if (CSS.highlights) { 217 - try { 218 - const range = selection.getRangeAt(0); 219 - const highlight = new Highlight(range); 220 - CSS.highlights.set("margin-highlight-preview", highlight); 221 - } catch (e) { 222 - console.warn("Could not visually highlight:", e); 223 - } 224 - } 705 + if (currentIds === newIds) { 706 + popoverEl.remove(); 707 + popoverEl = null; 708 + return; 709 + } 710 + } 225 711 226 - window.getSelection().removeAllRanges(); 227 - } else { 228 - showNotification( 229 - "Failed to highlight: " + (response?.error || "Unknown error"), 230 - "error", 231 - ); 232 - } 233 - sendResponse(response); 234 - }) 235 - .catch((err) => { 236 - console.error("Highlight error:", err); 237 - showNotification("Error creating highlight", "error"); 238 - sendResponse({ success: false, error: err.message }); 239 - }); 240 - return true; 712 + const firstItem = clickedItems[0]; 713 + const match = activeItems.find((x) => x.item === firstItem); 714 + if (match) { 715 + const rects = match.range.getClientRects(); 716 + if (rects.length > 0) { 717 + const rect = rects[0]; 718 + const top = rect.top + window.scrollY; 719 + const left = rect.left + window.scrollX; 720 + showPopover(clickedItems, top, left); 721 + } 241 722 } 242 - sendResponse({ success: false, error: "Could not build selector" }); 243 - return true; 723 + } else { 724 + if (popoverEl) { 725 + popoverEl.remove(); 726 + popoverEl = null; 727 + } 244 728 } 729 + } 245 730 246 - if (request.type === "SCROLL_TO_TEXT") { 247 - const found = findAndScrollToText(request.selector); 248 - if (!found) { 249 - showNotification("Could not find text on page", "error"); 731 + function renderBadges(annotations) { 732 + if (!sidebarShadow) return; 733 + 734 + const itemsToRender = annotations || []; 735 + activeItems = []; 736 + const rangesByColor = {}; 737 + 738 + const matcher = new DOMTextMatcher(); 739 + 740 + itemsToRender.forEach((item) => { 741 + const selector = item.target?.selector || item.selector; 742 + if (!selector?.exact) return; 743 + 744 + const range = matcher.findRange(selector.exact); 745 + if (range) { 746 + activeItems.push({ range, item }); 747 + 748 + const color = item.color || "#6366f1"; 749 + if (!rangesByColor[color]) rangesByColor[color] = []; 750 + rangesByColor[color].push(range); 751 + } 752 + }); 753 + 754 + if (typeof CSS !== "undefined" && CSS.highlights) { 755 + CSS.highlights.clear(); 756 + for (const [color, ranges] of Object.entries(rangesByColor)) { 757 + const highlight = new Highlight(...ranges); 758 + const safeColor = color.replace(/[^a-zA-Z0-9]/g, ""); 759 + const name = `margin-hl-${safeColor}`; 760 + CSS.highlights.set(name, highlight); 761 + injectHighlightStyle(name, color); 250 762 } 251 763 } 764 + } 252 765 253 - if (request.type === "RENDER_HIGHLIGHTS") { 254 - renderPageHighlights(request.highlights); 766 + const injectedStyles = new Set(); 767 + function injectHighlightStyle(name, color) { 768 + if (injectedStyles.has(name)) return; 769 + const style = document.createElement("style"); 770 + style.textContent = ` 771 + ::highlight(${name}) { 772 + text-decoration: underline; 773 + text-decoration-color: ${color}; 774 + text-decoration-thickness: 2px; 775 + text-underline-offset: 2px; 776 + cursor: pointer; 777 + } 778 + `; 779 + document.head.appendChild(style); 780 + injectedStyles.add(name); 781 + } 782 + 783 + function showPopover(items, top, left) { 784 + if (popoverEl) popoverEl.remove(); 785 + const container = sidebarShadow.getElementById("margin-overlay-container"); 786 + popoverEl = document.createElement("div"); 787 + popoverEl.className = "margin-popover"; 788 + 789 + const ids = items 790 + .map((i) => i.uri || i.id) 791 + .sort() 792 + .join(","); 793 + popoverEl.dataset.itemIds = ids; 794 + 795 + const popWidth = 320; 796 + const screenWidth = window.innerWidth; 797 + let finalLeft = left; 798 + if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20; 799 + 800 + popoverEl.style.top = `${top + 20}px`; 801 + popoverEl.style.left = `${finalLeft}px`; 802 + 803 + const hasHighlights = items.some((item) => item.type === "Highlight"); 804 + const hasAnnotations = items.some((item) => item.type !== "Highlight"); 805 + let title; 806 + if (items.length > 1) { 807 + if (hasHighlights && hasAnnotations) { 808 + title = `${items.length} Items`; 809 + } else if (hasHighlights) { 810 + title = `${items.length} Highlights`; 811 + } else { 812 + title = `${items.length} Annotations`; 813 + } 814 + } else { 815 + title = items[0]?.type === "Highlight" ? "Highlight" : "Annotation"; 255 816 } 256 817 257 - return true; 258 - }); 818 + let contentHtml = items 819 + .map((item) => { 820 + const author = item.author || item.creator || {}; 821 + const handle = author.handle || "User"; 822 + const avatar = author.avatar; 823 + const text = item.body?.value || item.text || ""; 824 + const quote = 825 + item.target?.selector?.exact || item.selector?.exact || ""; 826 + const id = item.id || item.uri; 827 + 828 + let avatarHtml = `<div class="popover-avatar">${handle[0]?.toUpperCase() || "U"}</div>`; 829 + if (avatar) { 830 + avatarHtml = `<img src="${avatar}" class="popover-avatar" style="object-fit: cover;">`; 831 + } 259 832 260 - function showNotification(message, type = "info") { 261 - const existing = document.querySelector(".margin-notification"); 262 - if (existing) existing.remove(); 833 + const isHighlight = item.type === "Highlight"; 263 834 264 - const notification = document.createElement("div"); 265 - notification.className = "margin-notification"; 266 - notification.textContent = message; 835 + let bodyHtml = ""; 836 + if (isHighlight) { 837 + bodyHtml = `<div class="popover-text" style="font-style: italic; color: #a1a1aa;">"${quote}"</div>`; 838 + } else { 839 + bodyHtml = `<div class="popover-text">${text}</div>`; 840 + if (quote) { 841 + bodyHtml += `<div class="popover-quote">"${quote}"</div>`; 842 + } 843 + } 267 844 268 - const bgColor = 269 - type === "success" ? "#10b981" : type === "error" ? "#ef4444" : "#6366f1"; 270 - notification.style.cssText = ` 271 - position: fixed; 272 - bottom: 24px; 273 - right: 24px; 274 - padding: 12px 20px; 275 - background: ${bgColor}; 276 - color: white; 277 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 278 - font-size: 14px; 279 - font-weight: 500; 280 - border-radius: 8px; 281 - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); 282 - z-index: 999999; 283 - animation: margin-slide-in 0.2s ease; 845 + return ` 846 + <div class="popover-item-block"> 847 + <div class="popover-item-header"> 848 + <div class="popover-author"> 849 + ${avatarHtml} 850 + <span class="popover-handle">@${handle}</span> 851 + </div> 852 + </div> 853 + <div class="popover-content"> 854 + ${bodyHtml} 855 + </div> 856 + <div class="popover-actions"> 857 + ${!isHighlight ? `<button class="btn-action btn-reply" data-id="${id}">Reply</button>` : ""} 858 + <button class="btn-action btn-share" data-id="${id}" data-text="${text}" data-quote="${quote}">Share</button> 859 + </div> 860 + </div> 284 861 `; 862 + }) 863 + .join(""); 864 + 865 + popoverEl.innerHTML = ` 866 + <div class="popover-header"> 867 + <span>${title}</span> 868 + <button class="popover-close">โœ•</button> 869 + </div> 870 + <div class="popover-scroll-area"> 871 + ${contentHtml} 872 + </div> 873 + `; 874 + 875 + popoverEl.querySelector(".popover-close").addEventListener("click", (e) => { 876 + e.stopPropagation(); 877 + popoverEl.remove(); 878 + popoverEl = null; 879 + }); 880 + 881 + const replyBtns = popoverEl.querySelectorAll(".btn-reply"); 882 + replyBtns.forEach((btn) => { 883 + btn.addEventListener("click", (e) => { 884 + e.stopPropagation(); 885 + const id = btn.getAttribute("data-id"); 886 + if (id) { 887 + chrome.runtime.sendMessage({ 888 + type: "OPEN_APP_URL", 889 + data: { path: `/annotation/${encodeURIComponent(id)}` }, 890 + }); 891 + } 892 + }); 893 + }); 285 894 286 - document.body.appendChild(notification); 895 + const shareBtns = popoverEl.querySelectorAll(".btn-share"); 896 + shareBtns.forEach((btn) => { 897 + btn.addEventListener("click", async () => { 898 + const id = btn.getAttribute("data-id"); 899 + const text = btn.getAttribute("data-text"); 900 + const quote = btn.getAttribute("data-quote"); 901 + const u = `https://margin.at/annotation/${encodeURIComponent(id)}`; 902 + const shareText = `${text ? text + "\n" : ""}${quote ? `"${quote}"\n` : ""}${u}`; 903 + 904 + try { 905 + await navigator.clipboard.writeText(shareText); 906 + const originalText = btn.innerText; 907 + btn.innerText = "Copied!"; 908 + setTimeout(() => (btn.innerText = originalText), 2000); 909 + } catch (e) { 910 + console.error("Failed to copy", e); 911 + } 912 + }); 913 + }); 914 + 915 + container.appendChild(popoverEl); 287 916 288 917 setTimeout(() => { 289 - notification.style.animation = "margin-slide-out 0.2s ease forwards"; 290 - setTimeout(() => notification.remove(), 200); 291 - }, 3000); 918 + document.addEventListener("click", closePopoverOutside); 919 + }, 0); 292 920 } 293 921 294 - const style = document.createElement("style"); 295 - style.textContent = ` 296 - @keyframes margin-slide-in { 297 - from { opacity: 0; transform: translateY(10px); } 298 - to { opacity: 1; transform: translateY(0); } 299 - } 300 - @keyframes margin-slide-out { 301 - from { opacity: 1; transform: translateY(0); } 302 - to { opacity: 0; transform: translateY(10px); } 922 + function closePopoverOutside() { 923 + if (popoverEl) { 924 + popoverEl.remove(); 925 + popoverEl = null; 926 + document.removeEventListener("click", closePopoverOutside); 927 + } 928 + } 929 + 930 + function fetchAnnotations(retryCount = 0) { 931 + if (typeof chrome !== "undefined" && chrome.runtime) { 932 + chrome.runtime.sendMessage( 933 + { 934 + type: "GET_ANNOTATIONS", 935 + data: { url: window.location.href }, 936 + }, 937 + (res) => { 938 + if (res && res.success && res.data && res.data.length > 0) { 939 + renderBadges(res.data); 940 + } else if (retryCount < 3) { 941 + setTimeout( 942 + () => fetchAnnotations(retryCount + 1), 943 + 1000 * (retryCount + 1), 944 + ); 945 + } 946 + }, 947 + ); 948 + } 949 + } 950 + 951 + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 952 + if (request.type === "GET_SELECTOR_FOR_ANNOTATE_INLINE") { 953 + const sel = window.getSelection(); 954 + if (!sel || !sel.toString()) { 955 + sendResponse({ selector: null }); 956 + return true; 957 + } 958 + const exact = sel.toString().trim(); 959 + sendResponse({ selector: { type: "TextQuoteSelector", exact } }); 960 + return true; 961 + } 962 + 963 + if (request.type === "SHOW_INLINE_ANNOTATE") { 964 + currentSelection = { 965 + text: request.data.selector?.exact || "", 966 + selector: request.data.selector, 967 + url: request.data.url, 968 + title: request.data.title, 969 + }; 970 + showInlineComposeModal(); 971 + sendResponse({ success: true }); 972 + return true; 973 + } 974 + 975 + if (request.type === "UPDATE_OVERLAY_VISIBILITY") { 976 + if (sidebarHost) { 977 + sidebarHost.style.display = request.show ? "block" : "none"; 978 + } 979 + if (request.show) { 980 + fetchAnnotations(); 981 + } else { 982 + if (typeof CSS !== "undefined" && CSS.highlights) { 983 + CSS.highlights.clear(); 303 984 } 304 - ::highlight(margin-highlight-preview) { 305 - background-color: rgba(168, 85, 247, 0.3); 306 - color: inherit; 985 + } 986 + sendResponse({ success: true }); 987 + return true; 988 + } 989 + 990 + if (request.type === "SCROLL_TO_TEXT") { 991 + const selector = request.selector; 992 + if (selector?.exact) { 993 + const matcher = new DOMTextMatcher(); 994 + const range = matcher.findRange(selector.exact); 995 + if (range) { 996 + const rect = range.getBoundingClientRect(); 997 + window.scrollTo({ 998 + top: window.scrollY + rect.top - window.innerHeight / 3, 999 + behavior: "smooth", 1000 + }); 1001 + const highlight = new Highlight(range); 1002 + CSS.highlights.set("margin-scroll-flash", highlight); 1003 + injectHighlightStyle("margin-scroll-flash", "#8b5cf6"); 1004 + setTimeout(() => CSS.highlights.delete("margin-scroll-flash"), 2000); 307 1005 } 308 - ::highlight(margin-scroll-highlight) { 309 - background-color: rgba(99, 102, 241, 0.4); 310 - color: inherit; 1006 + } 1007 + } 1008 + return true; 1009 + }); 1010 + 1011 + if (document.readyState === "loading") { 1012 + document.addEventListener("DOMContentLoaded", initOverlay); 1013 + } else { 1014 + initOverlay(); 1015 + } 1016 + 1017 + window.addEventListener("load", () => { 1018 + if (typeof chrome !== "undefined" && chrome.storage) { 1019 + chrome.storage.local.get(["showOverlay"], (result) => { 1020 + if (result.showOverlay !== false) { 1021 + setTimeout(() => fetchAnnotations(), 500); 311 1022 } 312 - ::highlight(margin-page-highlights) { 313 - background-color: rgba(252, 211, 77, 0.3); 314 - color: inherit; 1023 + }); 1024 + } else { 1025 + setTimeout(() => fetchAnnotations(), 500); 1026 + } 1027 + }); 1028 + 1029 + let lastUrl = window.location.href; 1030 + 1031 + function checkUrlChange() { 1032 + if (window.location.href !== lastUrl) { 1033 + lastUrl = window.location.href; 1034 + onUrlChange(); 1035 + } 1036 + } 1037 + 1038 + function onUrlChange() { 1039 + if (typeof CSS !== "undefined" && CSS.highlights) { 1040 + CSS.highlights.clear(); 1041 + } 1042 + activeItems = []; 1043 + 1044 + if (typeof chrome !== "undefined" && chrome.storage) { 1045 + chrome.storage.local.get(["showOverlay"], (result) => { 1046 + if (result.showOverlay !== false) { 1047 + fetchAnnotations(); 315 1048 } 316 - `; 317 - document.head.appendChild(style); 1049 + }); 1050 + } else { 1051 + fetchAnnotations(); 1052 + } 1053 + } 1054 + 1055 + window.addEventListener("popstate", onUrlChange); 1056 + 1057 + const originalPushState = history.pushState; 1058 + const originalReplaceState = history.replaceState; 1059 + 1060 + history.pushState = function (...args) { 1061 + originalPushState.apply(this, args); 1062 + checkUrlChange(); 1063 + }; 1064 + 1065 + history.replaceState = function (...args) { 1066 + originalReplaceState.apply(this, args); 1067 + checkUrlChange(); 1068 + }; 1069 + 1070 + setInterval(checkUrlChange, 1000); 318 1071 })();
+25
extension/eslint.config.js
··· 1 + import js from "@eslint/js"; 2 + import globals from "globals"; 3 + 4 + export default [ 5 + { ignores: ["dist"] }, 6 + { 7 + files: ["**/*.js"], 8 + languageOptions: { 9 + ecmaVersion: 2020, 10 + globals: { 11 + ...globals.browser, 12 + ...globals.webextensions, 13 + }, 14 + parserOptions: { 15 + ecmaVersion: "latest", 16 + sourceType: "module", 17 + }, 18 + }, 19 + rules: { 20 + ...js.configs.recommended.rules, 21 + "no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], 22 + "no-undef": "warn", 23 + }, 24 + }, 25 + ];
+19 -1
extension/icons/site.webmanifest
··· 1 - {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} 1 + { 2 + "name": "Margin", 3 + "short_name": "Margin", 4 + "icons": [ 5 + { 6 + "src": "/android-chrome-192x192.png", 7 + "sizes": "192x192", 8 + "type": "image/png" 9 + }, 10 + { 11 + "src": "/android-chrome-512x512.png", 12 + "sizes": "512x512", 13 + "type": "image/png" 14 + } 15 + ], 16 + "theme_color": "#ffffff", 17 + "background_color": "#ffffff", 18 + "display": "standalone" 19 + }
+1091
extension/package-lock.json
··· 1 + { 2 + "name": "margin-extension", 3 + "version": "0.1.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "margin-extension", 9 + "version": "0.1.0", 10 + "devDependencies": { 11 + "@eslint/js": "^9.39.2", 12 + "eslint": "^9.39.2", 13 + "globals": "^17.0.0" 14 + } 15 + }, 16 + "node_modules/@eslint-community/eslint-utils": { 17 + "version": "4.9.1", 18 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", 19 + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", 20 + "dev": true, 21 + "license": "MIT", 22 + "dependencies": { 23 + "eslint-visitor-keys": "^3.4.3" 24 + }, 25 + "engines": { 26 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 27 + }, 28 + "funding": { 29 + "url": "https://opencollective.com/eslint" 30 + }, 31 + "peerDependencies": { 32 + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 33 + } 34 + }, 35 + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { 36 + "version": "3.4.3", 37 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 38 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 39 + "dev": true, 40 + "license": "Apache-2.0", 41 + "engines": { 42 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 43 + }, 44 + "funding": { 45 + "url": "https://opencollective.com/eslint" 46 + } 47 + }, 48 + "node_modules/@eslint-community/regexpp": { 49 + "version": "4.12.2", 50 + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", 51 + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", 52 + "dev": true, 53 + "license": "MIT", 54 + "engines": { 55 + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 56 + } 57 + }, 58 + "node_modules/@eslint/config-array": { 59 + "version": "0.21.1", 60 + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", 61 + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", 62 + "dev": true, 63 + "license": "Apache-2.0", 64 + "dependencies": { 65 + "@eslint/object-schema": "^2.1.7", 66 + "debug": "^4.3.1", 67 + "minimatch": "^3.1.2" 68 + }, 69 + "engines": { 70 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 71 + } 72 + }, 73 + "node_modules/@eslint/config-helpers": { 74 + "version": "0.4.2", 75 + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", 76 + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", 77 + "dev": true, 78 + "license": "Apache-2.0", 79 + "dependencies": { 80 + "@eslint/core": "^0.17.0" 81 + }, 82 + "engines": { 83 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 84 + } 85 + }, 86 + "node_modules/@eslint/core": { 87 + "version": "0.17.0", 88 + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", 89 + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", 90 + "dev": true, 91 + "license": "Apache-2.0", 92 + "dependencies": { 93 + "@types/json-schema": "^7.0.15" 94 + }, 95 + "engines": { 96 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 97 + } 98 + }, 99 + "node_modules/@eslint/eslintrc": { 100 + "version": "3.3.3", 101 + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", 102 + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", 103 + "dev": true, 104 + "license": "MIT", 105 + "dependencies": { 106 + "ajv": "^6.12.4", 107 + "debug": "^4.3.2", 108 + "espree": "^10.0.1", 109 + "globals": "^14.0.0", 110 + "ignore": "^5.2.0", 111 + "import-fresh": "^3.2.1", 112 + "js-yaml": "^4.1.1", 113 + "minimatch": "^3.1.2", 114 + "strip-json-comments": "^3.1.1" 115 + }, 116 + "engines": { 117 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 118 + }, 119 + "funding": { 120 + "url": "https://opencollective.com/eslint" 121 + } 122 + }, 123 + "node_modules/@eslint/eslintrc/node_modules/globals": { 124 + "version": "14.0.0", 125 + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 126 + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 127 + "dev": true, 128 + "license": "MIT", 129 + "engines": { 130 + "node": ">=18" 131 + }, 132 + "funding": { 133 + "url": "https://github.com/sponsors/sindresorhus" 134 + } 135 + }, 136 + "node_modules/@eslint/js": { 137 + "version": "9.39.2", 138 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", 139 + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", 140 + "dev": true, 141 + "license": "MIT", 142 + "engines": { 143 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 144 + }, 145 + "funding": { 146 + "url": "https://eslint.org/donate" 147 + } 148 + }, 149 + "node_modules/@eslint/object-schema": { 150 + "version": "2.1.7", 151 + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", 152 + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", 153 + "dev": true, 154 + "license": "Apache-2.0", 155 + "engines": { 156 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 157 + } 158 + }, 159 + "node_modules/@eslint/plugin-kit": { 160 + "version": "0.4.1", 161 + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", 162 + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", 163 + "dev": true, 164 + "license": "Apache-2.0", 165 + "dependencies": { 166 + "@eslint/core": "^0.17.0", 167 + "levn": "^0.4.1" 168 + }, 169 + "engines": { 170 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 171 + } 172 + }, 173 + "node_modules/@humanfs/core": { 174 + "version": "0.19.1", 175 + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 176 + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 177 + "dev": true, 178 + "license": "Apache-2.0", 179 + "engines": { 180 + "node": ">=18.18.0" 181 + } 182 + }, 183 + "node_modules/@humanfs/node": { 184 + "version": "0.16.7", 185 + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", 186 + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", 187 + "dev": true, 188 + "license": "Apache-2.0", 189 + "dependencies": { 190 + "@humanfs/core": "^0.19.1", 191 + "@humanwhocodes/retry": "^0.4.0" 192 + }, 193 + "engines": { 194 + "node": ">=18.18.0" 195 + } 196 + }, 197 + "node_modules/@humanwhocodes/module-importer": { 198 + "version": "1.0.1", 199 + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 200 + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 201 + "dev": true, 202 + "license": "Apache-2.0", 203 + "engines": { 204 + "node": ">=12.22" 205 + }, 206 + "funding": { 207 + "type": "github", 208 + "url": "https://github.com/sponsors/nzakas" 209 + } 210 + }, 211 + "node_modules/@humanwhocodes/retry": { 212 + "version": "0.4.3", 213 + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", 214 + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", 215 + "dev": true, 216 + "license": "Apache-2.0", 217 + "engines": { 218 + "node": ">=18.18" 219 + }, 220 + "funding": { 221 + "type": "github", 222 + "url": "https://github.com/sponsors/nzakas" 223 + } 224 + }, 225 + "node_modules/@types/estree": { 226 + "version": "1.0.8", 227 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 228 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 229 + "dev": true, 230 + "license": "MIT" 231 + }, 232 + "node_modules/@types/json-schema": { 233 + "version": "7.0.15", 234 + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 235 + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 236 + "dev": true, 237 + "license": "MIT" 238 + }, 239 + "node_modules/acorn": { 240 + "version": "8.15.0", 241 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", 242 + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 243 + "dev": true, 244 + "license": "MIT", 245 + "peer": true, 246 + "bin": { 247 + "acorn": "bin/acorn" 248 + }, 249 + "engines": { 250 + "node": ">=0.4.0" 251 + } 252 + }, 253 + "node_modules/acorn-jsx": { 254 + "version": "5.3.2", 255 + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 256 + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 257 + "dev": true, 258 + "license": "MIT", 259 + "peerDependencies": { 260 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 261 + } 262 + }, 263 + "node_modules/ajv": { 264 + "version": "6.12.6", 265 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 266 + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 267 + "dev": true, 268 + "license": "MIT", 269 + "dependencies": { 270 + "fast-deep-equal": "^3.1.1", 271 + "fast-json-stable-stringify": "^2.0.0", 272 + "json-schema-traverse": "^0.4.1", 273 + "uri-js": "^4.2.2" 274 + }, 275 + "funding": { 276 + "type": "github", 277 + "url": "https://github.com/sponsors/epoberezkin" 278 + } 279 + }, 280 + "node_modules/ansi-styles": { 281 + "version": "4.3.0", 282 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 283 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 284 + "dev": true, 285 + "license": "MIT", 286 + "dependencies": { 287 + "color-convert": "^2.0.1" 288 + }, 289 + "engines": { 290 + "node": ">=8" 291 + }, 292 + "funding": { 293 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 294 + } 295 + }, 296 + "node_modules/argparse": { 297 + "version": "2.0.1", 298 + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 299 + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 300 + "dev": true, 301 + "license": "Python-2.0" 302 + }, 303 + "node_modules/balanced-match": { 304 + "version": "1.0.2", 305 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 306 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 307 + "dev": true, 308 + "license": "MIT" 309 + }, 310 + "node_modules/brace-expansion": { 311 + "version": "1.1.12", 312 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 313 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 314 + "dev": true, 315 + "license": "MIT", 316 + "dependencies": { 317 + "balanced-match": "^1.0.0", 318 + "concat-map": "0.0.1" 319 + } 320 + }, 321 + "node_modules/callsites": { 322 + "version": "3.1.0", 323 + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 324 + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 325 + "dev": true, 326 + "license": "MIT", 327 + "engines": { 328 + "node": ">=6" 329 + } 330 + }, 331 + "node_modules/chalk": { 332 + "version": "4.1.2", 333 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 334 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 335 + "dev": true, 336 + "license": "MIT", 337 + "dependencies": { 338 + "ansi-styles": "^4.1.0", 339 + "supports-color": "^7.1.0" 340 + }, 341 + "engines": { 342 + "node": ">=10" 343 + }, 344 + "funding": { 345 + "url": "https://github.com/chalk/chalk?sponsor=1" 346 + } 347 + }, 348 + "node_modules/color-convert": { 349 + "version": "2.0.1", 350 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 351 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 352 + "dev": true, 353 + "license": "MIT", 354 + "dependencies": { 355 + "color-name": "~1.1.4" 356 + }, 357 + "engines": { 358 + "node": ">=7.0.0" 359 + } 360 + }, 361 + "node_modules/color-name": { 362 + "version": "1.1.4", 363 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 364 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 365 + "dev": true, 366 + "license": "MIT" 367 + }, 368 + "node_modules/concat-map": { 369 + "version": "0.0.1", 370 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 371 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 372 + "dev": true, 373 + "license": "MIT" 374 + }, 375 + "node_modules/cross-spawn": { 376 + "version": "7.0.6", 377 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 378 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 379 + "dev": true, 380 + "license": "MIT", 381 + "dependencies": { 382 + "path-key": "^3.1.0", 383 + "shebang-command": "^2.0.0", 384 + "which": "^2.0.1" 385 + }, 386 + "engines": { 387 + "node": ">= 8" 388 + } 389 + }, 390 + "node_modules/debug": { 391 + "version": "4.4.3", 392 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 393 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 394 + "dev": true, 395 + "license": "MIT", 396 + "dependencies": { 397 + "ms": "^2.1.3" 398 + }, 399 + "engines": { 400 + "node": ">=6.0" 401 + }, 402 + "peerDependenciesMeta": { 403 + "supports-color": { 404 + "optional": true 405 + } 406 + } 407 + }, 408 + "node_modules/deep-is": { 409 + "version": "0.1.4", 410 + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 411 + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 412 + "dev": true, 413 + "license": "MIT" 414 + }, 415 + "node_modules/escape-string-regexp": { 416 + "version": "4.0.0", 417 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 418 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 419 + "dev": true, 420 + "license": "MIT", 421 + "engines": { 422 + "node": ">=10" 423 + }, 424 + "funding": { 425 + "url": "https://github.com/sponsors/sindresorhus" 426 + } 427 + }, 428 + "node_modules/eslint": { 429 + "version": "9.39.2", 430 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", 431 + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", 432 + "dev": true, 433 + "license": "MIT", 434 + "peer": true, 435 + "dependencies": { 436 + "@eslint-community/eslint-utils": "^4.8.0", 437 + "@eslint-community/regexpp": "^4.12.1", 438 + "@eslint/config-array": "^0.21.1", 439 + "@eslint/config-helpers": "^0.4.2", 440 + "@eslint/core": "^0.17.0", 441 + "@eslint/eslintrc": "^3.3.1", 442 + "@eslint/js": "9.39.2", 443 + "@eslint/plugin-kit": "^0.4.1", 444 + "@humanfs/node": "^0.16.6", 445 + "@humanwhocodes/module-importer": "^1.0.1", 446 + "@humanwhocodes/retry": "^0.4.2", 447 + "@types/estree": "^1.0.6", 448 + "ajv": "^6.12.4", 449 + "chalk": "^4.0.0", 450 + "cross-spawn": "^7.0.6", 451 + "debug": "^4.3.2", 452 + "escape-string-regexp": "^4.0.0", 453 + "eslint-scope": "^8.4.0", 454 + "eslint-visitor-keys": "^4.2.1", 455 + "espree": "^10.4.0", 456 + "esquery": "^1.5.0", 457 + "esutils": "^2.0.2", 458 + "fast-deep-equal": "^3.1.3", 459 + "file-entry-cache": "^8.0.0", 460 + "find-up": "^5.0.0", 461 + "glob-parent": "^6.0.2", 462 + "ignore": "^5.2.0", 463 + "imurmurhash": "^0.1.4", 464 + "is-glob": "^4.0.0", 465 + "json-stable-stringify-without-jsonify": "^1.0.1", 466 + "lodash.merge": "^4.6.2", 467 + "minimatch": "^3.1.2", 468 + "natural-compare": "^1.4.0", 469 + "optionator": "^0.9.3" 470 + }, 471 + "bin": { 472 + "eslint": "bin/eslint.js" 473 + }, 474 + "engines": { 475 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 476 + }, 477 + "funding": { 478 + "url": "https://eslint.org/donate" 479 + }, 480 + "peerDependencies": { 481 + "jiti": "*" 482 + }, 483 + "peerDependenciesMeta": { 484 + "jiti": { 485 + "optional": true 486 + } 487 + } 488 + }, 489 + "node_modules/eslint-scope": { 490 + "version": "8.4.0", 491 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", 492 + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", 493 + "dev": true, 494 + "license": "BSD-2-Clause", 495 + "dependencies": { 496 + "esrecurse": "^4.3.0", 497 + "estraverse": "^5.2.0" 498 + }, 499 + "engines": { 500 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 501 + }, 502 + "funding": { 503 + "url": "https://opencollective.com/eslint" 504 + } 505 + }, 506 + "node_modules/eslint-visitor-keys": { 507 + "version": "4.2.1", 508 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", 509 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", 510 + "dev": true, 511 + "license": "Apache-2.0", 512 + "engines": { 513 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 514 + }, 515 + "funding": { 516 + "url": "https://opencollective.com/eslint" 517 + } 518 + }, 519 + "node_modules/espree": { 520 + "version": "10.4.0", 521 + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", 522 + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", 523 + "dev": true, 524 + "license": "BSD-2-Clause", 525 + "dependencies": { 526 + "acorn": "^8.15.0", 527 + "acorn-jsx": "^5.3.2", 528 + "eslint-visitor-keys": "^4.2.1" 529 + }, 530 + "engines": { 531 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 532 + }, 533 + "funding": { 534 + "url": "https://opencollective.com/eslint" 535 + } 536 + }, 537 + "node_modules/esquery": { 538 + "version": "1.7.0", 539 + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", 540 + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", 541 + "dev": true, 542 + "license": "BSD-3-Clause", 543 + "dependencies": { 544 + "estraverse": "^5.1.0" 545 + }, 546 + "engines": { 547 + "node": ">=0.10" 548 + } 549 + }, 550 + "node_modules/esrecurse": { 551 + "version": "4.3.0", 552 + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 553 + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 554 + "dev": true, 555 + "license": "BSD-2-Clause", 556 + "dependencies": { 557 + "estraverse": "^5.2.0" 558 + }, 559 + "engines": { 560 + "node": ">=4.0" 561 + } 562 + }, 563 + "node_modules/estraverse": { 564 + "version": "5.3.0", 565 + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 566 + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 567 + "dev": true, 568 + "license": "BSD-2-Clause", 569 + "engines": { 570 + "node": ">=4.0" 571 + } 572 + }, 573 + "node_modules/esutils": { 574 + "version": "2.0.3", 575 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 576 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 577 + "dev": true, 578 + "license": "BSD-2-Clause", 579 + "engines": { 580 + "node": ">=0.10.0" 581 + } 582 + }, 583 + "node_modules/fast-deep-equal": { 584 + "version": "3.1.3", 585 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 586 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 587 + "dev": true, 588 + "license": "MIT" 589 + }, 590 + "node_modules/fast-json-stable-stringify": { 591 + "version": "2.1.0", 592 + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 593 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 594 + "dev": true, 595 + "license": "MIT" 596 + }, 597 + "node_modules/fast-levenshtein": { 598 + "version": "2.0.6", 599 + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 600 + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 601 + "dev": true, 602 + "license": "MIT" 603 + }, 604 + "node_modules/file-entry-cache": { 605 + "version": "8.0.0", 606 + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 607 + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 608 + "dev": true, 609 + "license": "MIT", 610 + "dependencies": { 611 + "flat-cache": "^4.0.0" 612 + }, 613 + "engines": { 614 + "node": ">=16.0.0" 615 + } 616 + }, 617 + "node_modules/find-up": { 618 + "version": "5.0.0", 619 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 620 + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 621 + "dev": true, 622 + "license": "MIT", 623 + "dependencies": { 624 + "locate-path": "^6.0.0", 625 + "path-exists": "^4.0.0" 626 + }, 627 + "engines": { 628 + "node": ">=10" 629 + }, 630 + "funding": { 631 + "url": "https://github.com/sponsors/sindresorhus" 632 + } 633 + }, 634 + "node_modules/flat-cache": { 635 + "version": "4.0.1", 636 + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 637 + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 638 + "dev": true, 639 + "license": "MIT", 640 + "dependencies": { 641 + "flatted": "^3.2.9", 642 + "keyv": "^4.5.4" 643 + }, 644 + "engines": { 645 + "node": ">=16" 646 + } 647 + }, 648 + "node_modules/flatted": { 649 + "version": "3.3.3", 650 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", 651 + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", 652 + "dev": true, 653 + "license": "ISC" 654 + }, 655 + "node_modules/glob-parent": { 656 + "version": "6.0.2", 657 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 658 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 659 + "dev": true, 660 + "license": "ISC", 661 + "dependencies": { 662 + "is-glob": "^4.0.3" 663 + }, 664 + "engines": { 665 + "node": ">=10.13.0" 666 + } 667 + }, 668 + "node_modules/globals": { 669 + "version": "17.0.0", 670 + "resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz", 671 + "integrity": "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==", 672 + "dev": true, 673 + "license": "MIT", 674 + "engines": { 675 + "node": ">=18" 676 + }, 677 + "funding": { 678 + "url": "https://github.com/sponsors/sindresorhus" 679 + } 680 + }, 681 + "node_modules/has-flag": { 682 + "version": "4.0.0", 683 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 684 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 685 + "dev": true, 686 + "license": "MIT", 687 + "engines": { 688 + "node": ">=8" 689 + } 690 + }, 691 + "node_modules/ignore": { 692 + "version": "5.3.2", 693 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 694 + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 695 + "dev": true, 696 + "license": "MIT", 697 + "engines": { 698 + "node": ">= 4" 699 + } 700 + }, 701 + "node_modules/import-fresh": { 702 + "version": "3.3.1", 703 + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", 704 + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 705 + "dev": true, 706 + "license": "MIT", 707 + "dependencies": { 708 + "parent-module": "^1.0.0", 709 + "resolve-from": "^4.0.0" 710 + }, 711 + "engines": { 712 + "node": ">=6" 713 + }, 714 + "funding": { 715 + "url": "https://github.com/sponsors/sindresorhus" 716 + } 717 + }, 718 + "node_modules/imurmurhash": { 719 + "version": "0.1.4", 720 + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 721 + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 722 + "dev": true, 723 + "license": "MIT", 724 + "engines": { 725 + "node": ">=0.8.19" 726 + } 727 + }, 728 + "node_modules/is-extglob": { 729 + "version": "2.1.1", 730 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 731 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 732 + "dev": true, 733 + "license": "MIT", 734 + "engines": { 735 + "node": ">=0.10.0" 736 + } 737 + }, 738 + "node_modules/is-glob": { 739 + "version": "4.0.3", 740 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 741 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 742 + "dev": true, 743 + "license": "MIT", 744 + "dependencies": { 745 + "is-extglob": "^2.1.1" 746 + }, 747 + "engines": { 748 + "node": ">=0.10.0" 749 + } 750 + }, 751 + "node_modules/isexe": { 752 + "version": "2.0.0", 753 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 754 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 755 + "dev": true, 756 + "license": "ISC" 757 + }, 758 + "node_modules/js-yaml": { 759 + "version": "4.1.1", 760 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", 761 + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", 762 + "dev": true, 763 + "license": "MIT", 764 + "dependencies": { 765 + "argparse": "^2.0.1" 766 + }, 767 + "bin": { 768 + "js-yaml": "bin/js-yaml.js" 769 + } 770 + }, 771 + "node_modules/json-buffer": { 772 + "version": "3.0.1", 773 + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 774 + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 775 + "dev": true, 776 + "license": "MIT" 777 + }, 778 + "node_modules/json-schema-traverse": { 779 + "version": "0.4.1", 780 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 781 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 782 + "dev": true, 783 + "license": "MIT" 784 + }, 785 + "node_modules/json-stable-stringify-without-jsonify": { 786 + "version": "1.0.1", 787 + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 788 + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 789 + "dev": true, 790 + "license": "MIT" 791 + }, 792 + "node_modules/keyv": { 793 + "version": "4.5.4", 794 + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 795 + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 796 + "dev": true, 797 + "license": "MIT", 798 + "dependencies": { 799 + "json-buffer": "3.0.1" 800 + } 801 + }, 802 + "node_modules/levn": { 803 + "version": "0.4.1", 804 + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 805 + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 806 + "dev": true, 807 + "license": "MIT", 808 + "dependencies": { 809 + "prelude-ls": "^1.2.1", 810 + "type-check": "~0.4.0" 811 + }, 812 + "engines": { 813 + "node": ">= 0.8.0" 814 + } 815 + }, 816 + "node_modules/locate-path": { 817 + "version": "6.0.0", 818 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 819 + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 820 + "dev": true, 821 + "license": "MIT", 822 + "dependencies": { 823 + "p-locate": "^5.0.0" 824 + }, 825 + "engines": { 826 + "node": ">=10" 827 + }, 828 + "funding": { 829 + "url": "https://github.com/sponsors/sindresorhus" 830 + } 831 + }, 832 + "node_modules/lodash.merge": { 833 + "version": "4.6.2", 834 + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 835 + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 836 + "dev": true, 837 + "license": "MIT" 838 + }, 839 + "node_modules/minimatch": { 840 + "version": "3.1.2", 841 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 842 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 843 + "dev": true, 844 + "license": "ISC", 845 + "dependencies": { 846 + "brace-expansion": "^1.1.7" 847 + }, 848 + "engines": { 849 + "node": "*" 850 + } 851 + }, 852 + "node_modules/ms": { 853 + "version": "2.1.3", 854 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 855 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 856 + "dev": true, 857 + "license": "MIT" 858 + }, 859 + "node_modules/natural-compare": { 860 + "version": "1.4.0", 861 + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 862 + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 863 + "dev": true, 864 + "license": "MIT" 865 + }, 866 + "node_modules/optionator": { 867 + "version": "0.9.4", 868 + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", 869 + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", 870 + "dev": true, 871 + "license": "MIT", 872 + "dependencies": { 873 + "deep-is": "^0.1.3", 874 + "fast-levenshtein": "^2.0.6", 875 + "levn": "^0.4.1", 876 + "prelude-ls": "^1.2.1", 877 + "type-check": "^0.4.0", 878 + "word-wrap": "^1.2.5" 879 + }, 880 + "engines": { 881 + "node": ">= 0.8.0" 882 + } 883 + }, 884 + "node_modules/p-limit": { 885 + "version": "3.1.0", 886 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 887 + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 888 + "dev": true, 889 + "license": "MIT", 890 + "dependencies": { 891 + "yocto-queue": "^0.1.0" 892 + }, 893 + "engines": { 894 + "node": ">=10" 895 + }, 896 + "funding": { 897 + "url": "https://github.com/sponsors/sindresorhus" 898 + } 899 + }, 900 + "node_modules/p-locate": { 901 + "version": "5.0.0", 902 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 903 + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 904 + "dev": true, 905 + "license": "MIT", 906 + "dependencies": { 907 + "p-limit": "^3.0.2" 908 + }, 909 + "engines": { 910 + "node": ">=10" 911 + }, 912 + "funding": { 913 + "url": "https://github.com/sponsors/sindresorhus" 914 + } 915 + }, 916 + "node_modules/parent-module": { 917 + "version": "1.0.1", 918 + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 919 + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 920 + "dev": true, 921 + "license": "MIT", 922 + "dependencies": { 923 + "callsites": "^3.0.0" 924 + }, 925 + "engines": { 926 + "node": ">=6" 927 + } 928 + }, 929 + "node_modules/path-exists": { 930 + "version": "4.0.0", 931 + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 932 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 933 + "dev": true, 934 + "license": "MIT", 935 + "engines": { 936 + "node": ">=8" 937 + } 938 + }, 939 + "node_modules/path-key": { 940 + "version": "3.1.1", 941 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 942 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 943 + "dev": true, 944 + "license": "MIT", 945 + "engines": { 946 + "node": ">=8" 947 + } 948 + }, 949 + "node_modules/prelude-ls": { 950 + "version": "1.2.1", 951 + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 952 + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 953 + "dev": true, 954 + "license": "MIT", 955 + "engines": { 956 + "node": ">= 0.8.0" 957 + } 958 + }, 959 + "node_modules/punycode": { 960 + "version": "2.3.1", 961 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 962 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 963 + "dev": true, 964 + "license": "MIT", 965 + "engines": { 966 + "node": ">=6" 967 + } 968 + }, 969 + "node_modules/resolve-from": { 970 + "version": "4.0.0", 971 + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 972 + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 973 + "dev": true, 974 + "license": "MIT", 975 + "engines": { 976 + "node": ">=4" 977 + } 978 + }, 979 + "node_modules/shebang-command": { 980 + "version": "2.0.0", 981 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 982 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 983 + "dev": true, 984 + "license": "MIT", 985 + "dependencies": { 986 + "shebang-regex": "^3.0.0" 987 + }, 988 + "engines": { 989 + "node": ">=8" 990 + } 991 + }, 992 + "node_modules/shebang-regex": { 993 + "version": "3.0.0", 994 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 995 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 996 + "dev": true, 997 + "license": "MIT", 998 + "engines": { 999 + "node": ">=8" 1000 + } 1001 + }, 1002 + "node_modules/strip-json-comments": { 1003 + "version": "3.1.1", 1004 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1005 + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1006 + "dev": true, 1007 + "license": "MIT", 1008 + "engines": { 1009 + "node": ">=8" 1010 + }, 1011 + "funding": { 1012 + "url": "https://github.com/sponsors/sindresorhus" 1013 + } 1014 + }, 1015 + "node_modules/supports-color": { 1016 + "version": "7.2.0", 1017 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1018 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1019 + "dev": true, 1020 + "license": "MIT", 1021 + "dependencies": { 1022 + "has-flag": "^4.0.0" 1023 + }, 1024 + "engines": { 1025 + "node": ">=8" 1026 + } 1027 + }, 1028 + "node_modules/type-check": { 1029 + "version": "0.4.0", 1030 + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 1031 + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 1032 + "dev": true, 1033 + "license": "MIT", 1034 + "dependencies": { 1035 + "prelude-ls": "^1.2.1" 1036 + }, 1037 + "engines": { 1038 + "node": ">= 0.8.0" 1039 + } 1040 + }, 1041 + "node_modules/uri-js": { 1042 + "version": "4.4.1", 1043 + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1044 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1045 + "dev": true, 1046 + "license": "BSD-2-Clause", 1047 + "dependencies": { 1048 + "punycode": "^2.1.0" 1049 + } 1050 + }, 1051 + "node_modules/which": { 1052 + "version": "2.0.2", 1053 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1054 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1055 + "dev": true, 1056 + "license": "ISC", 1057 + "dependencies": { 1058 + "isexe": "^2.0.0" 1059 + }, 1060 + "bin": { 1061 + "node-which": "bin/node-which" 1062 + }, 1063 + "engines": { 1064 + "node": ">= 8" 1065 + } 1066 + }, 1067 + "node_modules/word-wrap": { 1068 + "version": "1.2.5", 1069 + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 1070 + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 1071 + "dev": true, 1072 + "license": "MIT", 1073 + "engines": { 1074 + "node": ">=0.10.0" 1075 + } 1076 + }, 1077 + "node_modules/yocto-queue": { 1078 + "version": "0.1.0", 1079 + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1080 + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1081 + "dev": true, 1082 + "license": "MIT", 1083 + "engines": { 1084 + "node": ">=10" 1085 + }, 1086 + "funding": { 1087 + "url": "https://github.com/sponsors/sindresorhus" 1088 + } 1089 + } 1090 + } 1091 + }
+14
extension/package.json
··· 1 + { 2 + "name": "margin-extension", 3 + "version": "0.1.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "lint": "eslint ." 8 + }, 9 + "devDependencies": { 10 + "@eslint/js": "^9.39.2", 11 + "eslint": "^9.39.2", 12 + "globals": "^17.0.0" 13 + } 14 + }
+86 -20
extension/popup/popup.css
··· 1 1 :root { 2 - --bg-primary: #0c0a14; 3 - --bg-secondary: #14111f; 4 - --bg-tertiary: #1a1528; 5 - --bg-card: #14111f; 6 - --bg-hover: #1e1932; 7 - 8 - --text-primary: #f4f0ff; 9 - --text-secondary: #a89ec8; 10 - --text-tertiary: #6b5f8a; 11 - 12 - --accent: #a855f7; 13 - --accent-hover: #c084fc; 14 - --accent-subtle: rgba(168, 85, 247, 0.15); 2 + --bg-primary: #09090b; 3 + --bg-secondary: #0f0f12; 4 + --bg-tertiary: #18181b; 5 + --bg-card: #09090b; 6 + --bg-elevated: #18181b; 7 + --bg-hover: #27272a; 15 8 16 - --border: #2d2640; 17 - --border-hover: #3d3560; 9 + --text-primary: #e4e4e7; 10 + --text-secondary: #a1a1aa; 11 + --text-tertiary: #71717a; 12 + --border: #27272a; 13 + --border-hover: #3f3f46; 18 14 19 - --success: #22c55e; 20 - --danger: #ef4444; 15 + --accent: #6366f1; 16 + --accent-hover: #4f46e5; 17 + --accent-subtle: rgba(99, 102, 241, 0.1); 18 + --accent-text: #818cf8; 19 + --success: #10b981; 20 + --error: #ef4444; 21 21 --warning: #f59e0b; 22 22 23 - --radius-sm: 6px; 24 - --radius-md: 10px; 25 - --radius-lg: 16px; 23 + --radius-sm: 4px; 24 + --radius-md: 6px; 25 + --radius-lg: 8px; 26 + --radius-full: 9999px; 27 + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 28 + --shadow-md: 29 + 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 26 30 } 27 31 28 32 * { ··· 644 648 gap: 8px; 645 649 margin-left: auto; 646 650 } 651 + 652 + .toggle-switch { 653 + position: relative; 654 + display: inline-block; 655 + width: 44px; 656 + height: 24px; 657 + flex-shrink: 0; 658 + } 659 + 660 + .toggle-switch input { 661 + opacity: 0; 662 + width: 0; 663 + height: 0; 664 + } 665 + 666 + .toggle-slider { 667 + position: absolute; 668 + cursor: pointer; 669 + top: 0; 670 + left: 0; 671 + right: 0; 672 + bottom: 0; 673 + background-color: var(--border); 674 + transition: 0.2s; 675 + border-radius: 24px; 676 + } 677 + 678 + .toggle-slider:before { 679 + position: absolute; 680 + content: ""; 681 + height: 18px; 682 + width: 18px; 683 + left: 3px; 684 + bottom: 3px; 685 + background-color: var(--text-secondary); 686 + transition: 0.2s; 687 + border-radius: 50%; 688 + } 689 + 690 + .toggle-switch input:checked + .toggle-slider { 691 + background-color: var(--accent); 692 + } 693 + 694 + .toggle-switch input:checked + .toggle-slider:before { 695 + transform: translateX(20px); 696 + background-color: white; 697 + } 698 + 699 + .settings-input { 700 + width: 100%; 701 + padding: 10px 12px; 702 + background: var(--bg-tertiary); 703 + border: 1px solid var(--border); 704 + border-radius: var(--radius-md); 705 + color: var(--text-primary); 706 + font-size: 13px; 707 + } 708 + 709 + .settings-input:focus { 710 + outline: none; 711 + border-color: var(--accent); 712 + }
+20
extension/popup/popup.html
··· 218 218 <button id="close-settings" class="btn-icon">ร—</button> 219 219 </div> 220 220 <div class="setting-item"> 221 + <div 222 + style=" 223 + display: flex; 224 + justify-content: space-between; 225 + align-items: center; 226 + " 227 + > 228 + <div> 229 + <label>Show page overlays</label> 230 + <p class="setting-help" style="margin-top: 2px"> 231 + Highlights, badges, and tooltips on pages 232 + </p> 233 + </div> 234 + <label class="toggle-switch"> 235 + <input type="checkbox" id="overlay-toggle" checked /> 236 + <span class="toggle-slider"></span> 237 + </label> 238 + </div> 239 + </div> 240 + <div class="setting-item"> 221 241 <label for="api-url">API URL (for self-hosting)</label> 222 242 <input 223 243 type="url"
+37 -18
extension/popup/popup.js
··· 39 39 collectionList: document.getElementById("collection-list"), 40 40 collectionLoading: document.getElementById("collection-loading"), 41 41 collectionsEmpty: document.getElementById("collections-empty"), 42 + overlayToggle: document.getElementById("overlay-toggle"), 42 43 }; 43 44 44 45 let currentTab = null; 45 46 let apiUrl = "https://margin.at"; 46 47 let currentUserDid = null; 47 48 let pendingSelector = null; 48 - let activeAnnotationUriForCollection = null; 49 + // let _activeAnnotationUriForCollection = null; 49 50 50 - const storage = await browserAPI.storage.local.get(["apiUrl"]); 51 + const storage = await browserAPI.storage.local.get(["apiUrl", "showOverlay"]); 51 52 if (storage.apiUrl) { 52 53 apiUrl = storage.apiUrl; 53 54 } 54 55 els.apiUrlInput.value = apiUrl; 56 + 57 + if (els.overlayToggle) { 58 + els.overlayToggle.checked = storage.showOverlay !== false; 59 + } 55 60 56 61 try { 57 62 const [tab] = await browserAPI.tabs.query({ ··· 74 79 pendingData = sessionData.pendingAnnotation; 75 80 await browserAPI.storage.session.remove(["pendingAnnotation"]); 76 81 } 77 - } catch (e) {} 82 + } catch { 83 + /* ignore */ 84 + } 78 85 } 79 86 80 87 if (!pendingData) { ··· 209 216 210 217 els.saveSettings?.addEventListener("click", async () => { 211 218 const newUrl = els.apiUrlInput.value.replace(/\/$/, ""); 219 + const showOverlay = els.overlayToggle?.checked ?? true; 220 + 221 + await browserAPI.storage.local.set({ apiUrl: newUrl, showOverlay }); 212 222 if (newUrl) { 213 - await browserAPI.storage.local.set({ apiUrl: newUrl }); 214 223 apiUrl = newUrl; 215 - await sendMessage({ type: "UPDATE_SETTINGS" }); 216 - views.settings.style.display = "none"; 217 - checkSession(); 224 + } 225 + await sendMessage({ type: "UPDATE_SETTINGS" }); 226 + 227 + const tabs = await browserAPI.tabs.query({}); 228 + for (const tab of tabs) { 229 + if (tab.id) { 230 + try { 231 + await browserAPI.tabs.sendMessage(tab.id, { 232 + type: "UPDATE_OVERLAY_VISIBILITY", 233 + show: showOverlay, 234 + }); 235 + } catch { 236 + /* ignore */ 237 + } 238 + } 218 239 } 240 + 241 + views.settings.style.display = "none"; 242 + checkSession(); 219 243 }); 220 244 221 245 els.closeCollectionSelector?.addEventListener("click", () => { 222 246 views.collectionSelector.style.display = "none"; 223 - activeAnnotationUriForCollection = null; 224 247 }); 225 248 226 249 async function openCollectionSelector(annotationUri) { ··· 228 251 console.error("No currentUserDid, returning early"); 229 252 return; 230 253 } 231 - activeAnnotationUriForCollection = annotationUri; 232 254 views.collectionSelector.style.display = "flex"; 233 255 els.collectionList.innerHTML = ""; 234 256 els.collectionLoading.style.display = "block"; ··· 358 380 const res = await sendMessage({ type: "CHECK_SESSION" }); 359 381 360 382 if (res.success && res.data?.authenticated) { 361 - if (els.userHandle) els.userHandle.textContent = "@" + res.data.handle; 383 + if (els.userHandle) { 384 + const handle = res.data.handle || res.data.email || "User"; 385 + els.userHandle.textContent = "@" + handle; 386 + } 362 387 els.userInfo.style.display = "flex"; 363 388 currentUserDid = res.data.did; 364 389 showView("main"); ··· 504 529 const actions = document.createElement("div"); 505 530 actions.className = "annotation-item-actions"; 506 531 507 - if ( 508 - item.author?.did === currentUserDid || 509 - item.creator?.did === currentUserDid 510 - ) { 532 + if (currentUserDid) { 511 533 const folderBtn = document.createElement("button"); 512 534 folderBtn.className = "btn-icon"; 513 535 folderBtn.innerHTML = ··· 577 599 578 600 row.appendChild(content); 579 601 580 - if ( 581 - item.author?.did === currentUserDid || 582 - item.creator?.did === currentUserDid 583 - ) { 602 + if (currentUserDid) { 584 603 const folderBtn = document.createElement("button"); 585 604 folderBtn.className = "btn-icon"; 586 605 folderBtn.innerHTML =
+217 -20
extension/sidepanel/sidepanel.css
··· 1 1 :root { 2 - --bg-primary: #0c0a14; 3 - --bg-secondary: #110e1c; 4 - --bg-tertiary: #1a1528; 5 - --bg-card: #14111f; 6 - --bg-hover: #1e1932; 7 - --bg-elevated: #1a1528; 2 + --bg-primary: #09090b; 3 + --bg-secondary: #0f0f12; 4 + --bg-tertiary: #18181b; 5 + --bg-card: #09090b; 6 + --bg-hover: #18181b; 7 + --bg-elevated: #18181b; 8 8 9 - --text-primary: #f4f0ff; 10 - --text-secondary: #a89ec8; 11 - --text-tertiary: #6b5f8a; 9 + --text-primary: #e4e4e7; 10 + --text-secondary: #a1a1aa; 11 + --text-tertiary: #71717a; 12 12 13 - --accent: #a855f7; 14 - --accent-hover: #c084fc; 15 - --accent-subtle: rgba(168, 85, 247, 0.15); 13 + --accent: #6366f1; 14 + --accent-hover: #4f46e5; 15 + --accent-subtle: rgba(99, 102, 241, 0.1); 16 + --accent-text: #818cf8; 16 17 17 - --border: #2d2640; 18 - --border-hover: #3d3560; 18 + --border: #27272a; 19 + --border-hover: #3f3f46; 19 20 20 - --success: #22c55e; 21 + --success: #10b981; 21 22 --error: #ef4444; 22 23 --warning: #f59e0b; 23 24 24 - --radius-sm: 6px; 25 - --radius-md: 10px; 26 - --radius-lg: 16px; 25 + --radius-sm: 4px; 26 + --radius-md: 6px; 27 + --radius-lg: 8px; 27 28 --radius-full: 9999px; 28 29 29 - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 30 - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 30 + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 31 + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 32 + } 33 + 34 + * { 35 + margin: 0; 36 + padding: 0; 37 + box-sizing: border-box; 38 + } 39 + 40 + body { 41 + font-family: 42 + "Inter", 43 + -apple-system, 44 + BlinkMacSystemFont, 45 + "Segoe UI", 46 + sans-serif; 47 + background: var(--bg-primary); 48 + color: var(--text-primary); 49 + min-height: 100vh; 50 + -webkit-font-smoothing: antialiased; 51 + } 52 + 53 + .sidebar { 54 + display: flex; 55 + flex-direction: column; 56 + height: 100vh; 57 + background: var(--bg-primary); 58 + } 59 + 60 + .sidebar-header { 61 + display: flex; 62 + align-items: center; 63 + justify-content: space-between; 64 + padding: 14px 16px; 65 + border-bottom: 1px solid var(--border); 66 + background: var(--bg-primary); 67 + } 68 + 69 + .user-handle { 70 + font-size: 12px; 71 + color: var(--text-secondary); 72 + background: var(--bg-tertiary); 73 + padding: 4px 8px; 74 + border-radius: var(--radius-sm); 75 + } 76 + 77 + .current-page-info { 78 + display: flex; 79 + align-items: center; 80 + gap: 8px; 81 + padding: 10px 16px; 82 + background: var(--bg-primary); 83 + border-bottom: 1px solid var(--border); 84 + } 85 + 86 + .tabs { 87 + display: flex; 88 + border-bottom: 1px solid var(--border); 89 + background: var(--bg-primary); 90 + padding: 4px; 91 + gap: 4px; 92 + margin: 0; 93 + } 94 + 95 + .tab-btn { 96 + flex: 1; 97 + padding: 10px 8px; 98 + background: transparent; 99 + border: none; 100 + font-size: 12px; 101 + font-weight: 500; 102 + color: var(--text-secondary); 103 + cursor: pointer; 104 + border-radius: var(--radius-sm); 105 + transition: all 0.15s; 106 + } 107 + 108 + .tab-btn:hover { 109 + color: var(--text-primary); 110 + background: var(--bg-hover); 111 + } 112 + 113 + .tab-btn.active { 114 + color: var(--text-primary); 115 + background: var(--bg-tertiary); 116 + box-shadow: none; 117 + } 118 + 119 + .quick-actions { 120 + display: flex; 121 + gap: 8px; 122 + padding: 12px 16px; 123 + border-bottom: 1px solid var(--border); 124 + background: var(--bg-primary); 125 + } 126 + 127 + .create-form { 128 + padding: 16px; 129 + border-bottom: 1px solid var(--border); 130 + background: var(--bg-primary); 131 + } 132 + 133 + .section-header { 134 + display: flex; 135 + justify-content: space-between; 136 + align-items: center; 137 + padding: 14px 16px; 138 + background: var(--bg-primary); 139 + border-bottom: 1px solid var(--border); 140 + } 141 + 142 + .annotation-item { 143 + border: 1px solid var(--border); 144 + border-radius: var(--radius-md); 145 + padding: 12px; 146 + background: var(--bg-primary); 147 + transition: border-color 0.15s; 148 + } 149 + 150 + .annotation-item:hover { 151 + border-color: var(--border-hover); 152 + background: var(--bg-hover); 153 + } 154 + 155 + .sidebar-footer { 156 + display: flex; 157 + align-items: center; 158 + justify-content: space-between; 159 + padding: 12px 16px; 160 + border-top: 1px solid var(--border); 161 + background: var(--bg-primary); 162 + } 163 + 164 + ::-webkit-scrollbar { 165 + width: 10px; 166 + height: 10px; 167 + } 168 + 169 + ::-webkit-scrollbar-track { 170 + background: transparent; 171 + } 172 + 173 + ::-webkit-scrollbar-thumb { 174 + background: var(--border); 175 + border-radius: 5px; 176 + border: 2px solid var(--bg-primary); 177 + } 178 + 179 + ::-webkit-scrollbar-thumb:hover { 180 + background: var(--border-hover); 31 181 } 32 182 33 183 * { ··· 732 882 gap: 8px; 733 883 margin-left: auto; 734 884 } 885 + 886 + .toggle-switch { 887 + position: relative; 888 + display: inline-block; 889 + width: 44px; 890 + height: 24px; 891 + flex-shrink: 0; 892 + } 893 + 894 + .toggle-switch input { 895 + opacity: 0; 896 + width: 0; 897 + height: 0; 898 + } 899 + 900 + .toggle-slider { 901 + position: absolute; 902 + cursor: pointer; 903 + top: 0; 904 + left: 0; 905 + right: 0; 906 + bottom: 0; 907 + background-color: var(--border); 908 + transition: 0.2s; 909 + border-radius: 24px; 910 + } 911 + 912 + .toggle-slider:before { 913 + position: absolute; 914 + content: ""; 915 + height: 18px; 916 + width: 18px; 917 + left: 3px; 918 + bottom: 3px; 919 + background-color: var(--text-secondary); 920 + transition: 0.2s; 921 + border-radius: 50%; 922 + } 923 + 924 + .toggle-switch input:checked + .toggle-slider { 925 + background-color: var(--accent); 926 + } 927 + 928 + .toggle-switch input:checked + .toggle-slider:before { 929 + transform: translateX(20px); 930 + background-color: white; 931 + }
+20
extension/sidepanel/sidepanel.html
··· 250 250 <button id="close-settings" class="btn-icon">ร—</button> 251 251 </div> 252 252 <div class="setting-item"> 253 + <div 254 + style=" 255 + display: flex; 256 + justify-content: space-between; 257 + align-items: center; 258 + " 259 + > 260 + <div> 261 + <label>Show page overlays</label> 262 + <p class="setting-help" style="margin-top: 2px"> 263 + Display highlights, badges, and tooltips on pages 264 + </p> 265 + </div> 266 + <label class="toggle-switch"> 267 + <input type="checkbox" id="overlay-toggle" checked /> 268 + <span class="toggle-slider"></span> 269 + </label> 270 + </div> 271 + </div> 272 + <div class="setting-item"> 253 273 <label for="api-url">API URL</label> 254 274 <input 255 275 type="url"
+36 -21
extension/sidepanel/sidepanel.js
··· 37 37 collectionList: document.getElementById("collection-list"), 38 38 collectionLoading: document.getElementById("collection-loading"), 39 39 collectionsEmpty: document.getElementById("collections-empty"), 40 + overlayToggle: document.getElementById("overlay-toggle"), 40 41 }; 41 42 42 43 let currentTab = null; 43 44 let apiUrl = ""; 44 45 let currentUserDid = null; 45 46 let pendingSelector = null; 46 - let activeAnnotationUriForCollection = null; 47 47 48 48 const storage = await chrome.storage.local.get(["apiUrl"]); 49 49 if (storage.apiUrl) { ··· 51 51 } 52 52 53 53 els.apiUrlInput.value = apiUrl; 54 + 55 + const overlayStorage = await chrome.storage.local.get(["showOverlay"]); 56 + if (els.overlayToggle) { 57 + els.overlayToggle.checked = overlayStorage.showOverlay !== false; 58 + } 54 59 55 60 chrome.storage.onChanged.addListener((changes, area) => { 56 61 if (area === "local" && changes.apiUrl) { ··· 253 258 254 259 els.closeCollectionSelector?.addEventListener("click", () => { 255 260 views.collectionSelector.style.display = "none"; 256 - activeAnnotationUriForCollection = null; 257 261 }); 258 262 259 263 els.saveSettings?.addEventListener("click", async () => { 260 264 const newUrl = els.apiUrlInput.value.replace(/\/$/, ""); 265 + const showOverlay = els.overlayToggle?.checked ?? true; 266 + 267 + await chrome.storage.local.set({ apiUrl: newUrl, showOverlay }); 261 268 if (newUrl) { 262 - await chrome.storage.local.set({ apiUrl: newUrl }); 263 269 apiUrl = newUrl; 264 - await sendMessage({ type: "UPDATE_SETTINGS" }); 265 - views.settings.style.display = "none"; 266 - checkSession(); 270 + } 271 + await sendMessage({ type: "UPDATE_SETTINGS" }); 272 + 273 + const tabs = await chrome.tabs.query({}); 274 + for (const tab of tabs) { 275 + if (tab.id) { 276 + try { 277 + await chrome.tabs.sendMessage(tab.id, { 278 + type: "UPDATE_OVERLAY_VISIBILITY", 279 + show: showOverlay, 280 + }); 281 + } catch { 282 + /* ignore */ 283 + } 284 + } 267 285 } 286 + 287 + views.settings.style.display = "none"; 288 + checkSession(); 268 289 }); 269 290 270 291 els.signOutBtn?.addEventListener("click", async () => { ··· 367 388 console.error("No currentUserDid, returning early"); 368 389 return; 369 390 } 370 - activeAnnotationUriForCollection = annotationUri; 371 391 views.collectionSelector.style.display = "flex"; 372 392 els.collectionList.innerHTML = ""; 373 393 els.collectionLoading.style.display = "block"; ··· 561 581 header.appendChild(badge); 562 582 } 563 583 564 - if ( 565 - item.author?.did === currentUserDid || 566 - item.creator?.did === currentUserDid 567 - ) { 584 + if (currentUserDid) { 568 585 const actions = document.createElement("div"); 569 586 actions.className = "annotation-item-actions"; 570 587 ··· 635 652 let hostname = item.source; 636 653 try { 637 654 hostname = new URL(item.source).hostname; 638 - } catch {} 655 + } catch { 656 + /* ignore */ 657 + } 639 658 640 659 const row = document.createElement("div"); 641 660 row.style.display = "flex"; ··· 658 677 659 678 row.appendChild(content); 660 679 661 - if ( 662 - item.author?.did === currentUserDid || 663 - item.creator?.did === currentUserDid 664 - ) { 680 + if (currentUserDid) { 665 681 const folderBtn = document.createElement("button"); 666 682 folderBtn.className = "btn-icon"; 667 683 folderBtn.innerHTML = ··· 701 717 let hostname = url; 702 718 try { 703 719 hostname = new URL(url).hostname; 704 - } catch {} 720 + } catch { 721 + /* ignore */ 722 + } 705 723 706 724 const header = document.createElement("div"); 707 725 header.className = "annotation-item-header"; ··· 721 739 722 740 header.appendChild(meta); 723 741 724 - if ( 725 - item.author?.did === currentUserDid || 726 - item.creator?.did === currentUserDid 727 - ) { 742 + if (currentUserDid) { 728 743 const actions = document.createElement("div"); 729 744 actions.className = "annotation-item-actions"; 730 745
+9 -28
lexicons/at/margin/annotation.json
··· 10 10 "key": "tid", 11 11 "record": { 12 12 "type": "object", 13 - "required": [ 14 - "target", 15 - "createdAt" 16 - ], 13 + "required": ["target", "createdAt"], 17 14 "properties": { 18 15 "motivation": { 19 16 "type": "string", ··· 87 84 "target": { 88 85 "type": "object", 89 86 "description": "W3C SpecificResource - the target with optional selector", 90 - "required": [ 91 - "source" 92 - ], 87 + "required": ["source"], 93 88 "properties": { 94 89 "source": { 95 90 "type": "string", ··· 127 122 "textQuoteSelector": { 128 123 "type": "object", 129 124 "description": "W3C TextQuoteSelector - select text by quoting it with context", 130 - "required": [ 131 - "exact" 132 - ], 125 + "required": ["exact"], 133 126 "properties": { 134 127 "type": { 135 128 "type": "string", ··· 158 151 "textPositionSelector": { 159 152 "type": "object", 160 153 "description": "W3C TextPositionSelector - select by character offsets", 161 - "required": [ 162 - "start", 163 - "end" 164 - ], 154 + "required": ["start", "end"], 165 155 "properties": { 166 156 "type": { 167 157 "type": "string", ··· 182 172 "cssSelector": { 183 173 "type": "object", 184 174 "description": "W3C CssSelector - select DOM elements by CSS selector", 185 - "required": [ 186 - "value" 187 - ], 175 + "required": ["value"], 188 176 "properties": { 189 177 "type": { 190 178 "type": "string", ··· 200 188 "xpathSelector": { 201 189 "type": "object", 202 190 "description": "W3C XPathSelector - select by XPath expression", 203 - "required": [ 204 - "value" 205 - ], 191 + "required": ["value"], 206 192 "properties": { 207 193 "type": { 208 194 "type": "string", ··· 218 204 "fragmentSelector": { 219 205 "type": "object", 220 206 "description": "W3C FragmentSelector - select by URI fragment", 221 - "required": [ 222 - "value" 223 - ], 207 + "required": ["value"], 224 208 "properties": { 225 209 "type": { 226 210 "type": "string", ··· 241 225 "rangeSelector": { 242 226 "type": "object", 243 227 "description": "W3C RangeSelector - select range between two selectors", 244 - "required": [ 245 - "startSelector", 246 - "endSelector" 247 - ], 228 + "required": ["startSelector", "endSelector"], 248 229 "properties": { 249 230 "type": { 250 231 "type": "string", ··· 289 270 } 290 271 } 291 272 } 292 - } 273 + }
+49 -52
lexicons/at/margin/bookmark.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "at.margin.bookmark", 4 - "description": "A bookmark record - save URL for later", 5 - "defs": { 6 - "main": { 7 - "type": "record", 8 - "description": "A bookmarked URL (motivation: bookmarking)", 9 - "key": "tid", 10 - "record": { 11 - "type": "object", 12 - "required": [ 13 - "source", 14 - "createdAt" 15 - ], 16 - "properties": { 17 - "source": { 18 - "type": "string", 19 - "format": "uri", 20 - "description": "The bookmarked URL" 21 - }, 22 - "sourceHash": { 23 - "type": "string", 24 - "description": "SHA256 hash of normalized URL for indexing" 25 - }, 26 - "title": { 27 - "type": "string", 28 - "maxLength": 500, 29 - "description": "Page title" 30 - }, 31 - "description": { 32 - "type": "string", 33 - "maxLength": 1000, 34 - "maxGraphemes": 300, 35 - "description": "Optional description/note" 36 - }, 37 - "tags": { 38 - "type": "array", 39 - "description": "Tags for categorization", 40 - "items": { 41 - "type": "string", 42 - "maxLength": 64, 43 - "maxGraphemes": 32 44 - }, 45 - "maxLength": 10 46 - }, 47 - "createdAt": { 48 - "type": "string", 49 - "format": "datetime" 50 - } 51 - } 52 - } 2 + "lexicon": 1, 3 + "id": "at.margin.bookmark", 4 + "description": "A bookmark record - save URL for later", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "description": "A bookmarked URL (motivation: bookmarking)", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": ["source", "createdAt"], 13 + "properties": { 14 + "source": { 15 + "type": "string", 16 + "format": "uri", 17 + "description": "The bookmarked URL" 18 + }, 19 + "sourceHash": { 20 + "type": "string", 21 + "description": "SHA256 hash of normalized URL for indexing" 22 + }, 23 + "title": { 24 + "type": "string", 25 + "maxLength": 500, 26 + "description": "Page title" 27 + }, 28 + "description": { 29 + "type": "string", 30 + "maxLength": 1000, 31 + "maxGraphemes": 300, 32 + "description": "Optional description/note" 33 + }, 34 + "tags": { 35 + "type": "array", 36 + "description": "Tags for categorization", 37 + "items": { 38 + "type": "string", 39 + "maxLength": 64, 40 + "maxGraphemes": 32 41 + }, 42 + "maxLength": 10 43 + }, 44 + "createdAt": { 45 + "type": "string", 46 + "format": "datetime" 47 + } 53 48 } 49 + } 54 50 } 55 - } 51 + } 52 + }
+37 -40
lexicons/at/margin/collection.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "at.margin.collection", 4 - "description": "A collection of annotations (like a folder or notebook)", 5 - "defs": { 6 - "main": { 7 - "type": "record", 8 - "description": "A named collection for organizing annotations", 9 - "key": "tid", 10 - "record": { 11 - "type": "object", 12 - "required": [ 13 - "name", 14 - "createdAt" 15 - ], 16 - "properties": { 17 - "name": { 18 - "type": "string", 19 - "maxLength": 100, 20 - "maxGraphemes": 50, 21 - "description": "Collection name" 22 - }, 23 - "description": { 24 - "type": "string", 25 - "maxLength": 500, 26 - "maxGraphemes": 150, 27 - "description": "Collection description" 28 - }, 29 - "icon": { 30 - "type": "string", 31 - "maxLength": 10, 32 - "maxGraphemes": 2, 33 - "description": "Emoji icon for the collection" 34 - }, 35 - "createdAt": { 36 - "type": "string", 37 - "format": "datetime" 38 - } 39 - } 40 - } 2 + "lexicon": 1, 3 + "id": "at.margin.collection", 4 + "description": "A collection of annotations (like a folder or notebook)", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "description": "A named collection for organizing annotations", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": ["name", "createdAt"], 13 + "properties": { 14 + "name": { 15 + "type": "string", 16 + "maxLength": 100, 17 + "maxGraphemes": 50, 18 + "description": "Collection name" 19 + }, 20 + "description": { 21 + "type": "string", 22 + "maxLength": 500, 23 + "maxGraphemes": 150, 24 + "description": "Collection description" 25 + }, 26 + "icon": { 27 + "type": "string", 28 + "maxLength": 10, 29 + "maxGraphemes": 2, 30 + "description": "Emoji icon for the collection" 31 + }, 32 + "createdAt": { 33 + "type": "string", 34 + "format": "datetime" 35 + } 41 36 } 37 + } 42 38 } 43 - } 39 + } 40 + }
+34 -38
lexicons/at/margin/collectionItem.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "at.margin.collectionItem", 4 - "description": "An item in a collection (links annotation to collection)", 5 - "defs": { 6 - "main": { 7 - "type": "record", 8 - "description": "Associates an annotation with a collection", 9 - "key": "tid", 10 - "record": { 11 - "type": "object", 12 - "required": [ 13 - "collection", 14 - "annotation", 15 - "createdAt" 16 - ], 17 - "properties": { 18 - "collection": { 19 - "type": "string", 20 - "format": "at-uri", 21 - "description": "AT URI of the collection" 22 - }, 23 - "annotation": { 24 - "type": "string", 25 - "format": "at-uri", 26 - "description": "AT URI of the annotation, highlight, or bookmark" 27 - }, 28 - "position": { 29 - "type": "integer", 30 - "minimum": 0, 31 - "description": "Sort order within the collection" 32 - }, 33 - "createdAt": { 34 - "type": "string", 35 - "format": "datetime" 36 - } 37 - } 38 - } 2 + "lexicon": 1, 3 + "id": "at.margin.collectionItem", 4 + "description": "An item in a collection (links annotation to collection)", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "description": "Associates an annotation with a collection", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": ["collection", "annotation", "createdAt"], 13 + "properties": { 14 + "collection": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "AT URI of the collection" 18 + }, 19 + "annotation": { 20 + "type": "string", 21 + "format": "at-uri", 22 + "description": "AT URI of the annotation, highlight, or bookmark" 23 + }, 24 + "position": { 25 + "type": "integer", 26 + "minimum": 0, 27 + "description": "Sort order within the collection" 28 + }, 29 + "createdAt": { 30 + "type": "string", 31 + "format": "datetime" 32 + } 39 33 } 34 + } 40 35 } 41 - } 36 + } 37 + }
+39 -42
lexicons/at/margin/highlight.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "at.margin.highlight", 4 - "description": "A lightweight highlight record - annotation without body text", 5 - "defs": { 6 - "main": { 7 - "type": "record", 8 - "description": "A highlight on a web page (motivation: highlighting)", 9 - "key": "tid", 10 - "record": { 11 - "type": "object", 12 - "required": [ 13 - "target", 14 - "createdAt" 15 - ], 16 - "properties": { 17 - "target": { 18 - "type": "ref", 19 - "ref": "at.margin.annotation#target", 20 - "description": "The resource and segment being highlighted" 21 - }, 22 - "color": { 23 - "type": "string", 24 - "description": "Highlight color (hex or named)", 25 - "maxLength": 20 26 - }, 27 - "tags": { 28 - "type": "array", 29 - "description": "Tags for categorization", 30 - "items": { 31 - "type": "string", 32 - "maxLength": 64, 33 - "maxGraphemes": 32 34 - }, 35 - "maxLength": 10 36 - }, 37 - "createdAt": { 38 - "type": "string", 39 - "format": "datetime" 40 - } 41 - } 42 - } 2 + "lexicon": 1, 3 + "id": "at.margin.highlight", 4 + "description": "A lightweight highlight record - annotation without body text", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "description": "A highlight on a web page (motivation: highlighting)", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": ["target", "createdAt"], 13 + "properties": { 14 + "target": { 15 + "type": "ref", 16 + "ref": "at.margin.annotation#target", 17 + "description": "The resource and segment being highlighted" 18 + }, 19 + "color": { 20 + "type": "string", 21 + "description": "Highlight color (hex or named)", 22 + "maxLength": 20 23 + }, 24 + "tags": { 25 + "type": "array", 26 + "description": "Tags for categorization", 27 + "items": { 28 + "type": "string", 29 + "maxLength": 64, 30 + "maxGraphemes": 32 31 + }, 32 + "maxLength": 10 33 + }, 34 + "createdAt": { 35 + "type": "string", 36 + "format": "datetime" 37 + } 43 38 } 39 + } 44 40 } 45 - } 41 + } 42 + }
+36 -42
lexicons/at/margin/like.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "at.margin.like", 4 - "defs": { 5 - "main": { 6 - "type": "record", 7 - "description": "A like on an annotation or reply", 8 - "key": "tid", 9 - "record": { 10 - "type": "object", 11 - "required": [ 12 - "subject", 13 - "createdAt" 14 - ], 15 - "properties": { 16 - "subject": { 17 - "type": "ref", 18 - "ref": "#subjectRef", 19 - "description": "Reference to the annotation or reply being liked" 20 - }, 21 - "createdAt": { 22 - "type": "string", 23 - "format": "datetime" 24 - } 25 - } 26 - } 2 + "lexicon": 1, 3 + "id": "at.margin.like", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A like on an annotation or reply", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["subject", "createdAt"], 12 + "properties": { 13 + "subject": { 14 + "type": "ref", 15 + "ref": "#subjectRef", 16 + "description": "Reference to the annotation or reply being liked" 17 + }, 18 + "createdAt": { 19 + "type": "string", 20 + "format": "datetime" 21 + } 22 + } 23 + } 24 + }, 25 + "subjectRef": { 26 + "type": "object", 27 + "required": ["uri", "cid"], 28 + "properties": { 29 + "uri": { 30 + "type": "string", 31 + "format": "at-uri" 27 32 }, 28 - "subjectRef": { 29 - "type": "object", 30 - "required": [ 31 - "uri", 32 - "cid" 33 - ], 34 - "properties": { 35 - "uri": { 36 - "type": "string", 37 - "format": "at-uri" 38 - }, 39 - "cid": { 40 - "type": "string", 41 - "format": "cid" 42 - } 43 - } 33 + "cid": { 34 + "type": "string", 35 + "format": "cid" 44 36 } 37 + } 45 38 } 46 - } 39 + } 40 + }
+55 -63
lexicons/at/margin/reply.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "at.margin.reply", 4 - "revision": 2, 5 - "description": "A reply to an annotation or another reply", 6 - "defs": { 7 - "main": { 8 - "type": "record", 9 - "description": "A reply to an annotation (motivation: replying)", 10 - "key": "tid", 11 - "record": { 12 - "type": "object", 13 - "required": [ 14 - "parent", 15 - "root", 16 - "text", 17 - "createdAt" 18 - ], 19 - "properties": { 20 - "parent": { 21 - "type": "ref", 22 - "ref": "#replyRef", 23 - "description": "Reference to the parent annotation or reply" 24 - }, 25 - "root": { 26 - "type": "ref", 27 - "ref": "#replyRef", 28 - "description": "Reference to the root annotation of the thread" 29 - }, 30 - "text": { 31 - "type": "string", 32 - "maxLength": 10000, 33 - "maxGraphemes": 3000, 34 - "description": "Reply text content" 35 - }, 36 - "format": { 37 - "type": "string", 38 - "description": "MIME type of the text content", 39 - "default": "text/plain" 40 - }, 41 - "createdAt": { 42 - "type": "string", 43 - "format": "datetime" 44 - } 45 - } 46 - } 2 + "lexicon": 1, 3 + "id": "at.margin.reply", 4 + "revision": 2, 5 + "description": "A reply to an annotation or another reply", 6 + "defs": { 7 + "main": { 8 + "type": "record", 9 + "description": "A reply to an annotation (motivation: replying)", 10 + "key": "tid", 11 + "record": { 12 + "type": "object", 13 + "required": ["parent", "root", "text", "createdAt"], 14 + "properties": { 15 + "parent": { 16 + "type": "ref", 17 + "ref": "#replyRef", 18 + "description": "Reference to the parent annotation or reply" 19 + }, 20 + "root": { 21 + "type": "ref", 22 + "ref": "#replyRef", 23 + "description": "Reference to the root annotation of the thread" 24 + }, 25 + "text": { 26 + "type": "string", 27 + "maxLength": 10000, 28 + "maxGraphemes": 3000, 29 + "description": "Reply text content" 30 + }, 31 + "format": { 32 + "type": "string", 33 + "description": "MIME type of the text content", 34 + "default": "text/plain" 35 + }, 36 + "createdAt": { 37 + "type": "string", 38 + "format": "datetime" 39 + } 40 + } 41 + } 42 + }, 43 + "replyRef": { 44 + "type": "object", 45 + "description": "Strong reference to an annotation or reply", 46 + "required": ["uri", "cid"], 47 + "properties": { 48 + "uri": { 49 + "type": "string", 50 + "format": "at-uri" 47 51 }, 48 - "replyRef": { 49 - "type": "object", 50 - "description": "Strong reference to an annotation or reply", 51 - "required": [ 52 - "uri", 53 - "cid" 54 - ], 55 - "properties": { 56 - "uri": { 57 - "type": "string", 58 - "format": "at-uri" 59 - }, 60 - "cid": { 61 - "type": "string", 62 - "format": "cid" 63 - } 64 - } 52 + "cid": { 53 + "type": "string", 54 + "format": "cid" 65 55 } 56 + } 66 57 } 67 - } 58 + } 59 + }
+40
web/eslint.config.js
··· 1 + import js from "@eslint/js"; 2 + import globals from "globals"; 3 + import react from "eslint-plugin-react"; 4 + import reactHooks from "eslint-plugin-react-hooks"; 5 + import reactRefresh from "eslint-plugin-react-refresh"; 6 + 7 + export default [ 8 + { ignores: ["dist"] }, 9 + { 10 + files: ["**/*.{js,jsx}"], 11 + languageOptions: { 12 + ecmaVersion: 2020, 13 + globals: globals.browser, 14 + parserOptions: { 15 + ecmaVersion: "latest", 16 + ecmaFeatures: { jsx: true }, 17 + sourceType: "module", 18 + }, 19 + }, 20 + settings: { react: { version: "18.3" } }, 21 + plugins: { 22 + react, 23 + "react-hooks": reactHooks, 24 + "react-refresh": reactRefresh, 25 + }, 26 + rules: { 27 + ...js.configs.recommended.rules, 28 + ...react.configs.recommended.rules, 29 + ...react.configs["jsx-runtime"].rules, 30 + ...reactHooks.configs.recommended.rules, 31 + "react/jsx-no-target-blank": "off", 32 + "react-refresh/only-export-components": [ 33 + "warn", 34 + { allowConstantExport: true }, 35 + ], 36 + "no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], 37 + "react/prop-types": "off", 38 + }, 39 + }, 40 + ];
+3051 -12
web/package-lock.json
··· 15 15 "react-router-dom": "^6.28.0" 16 16 }, 17 17 "devDependencies": { 18 + "@eslint/js": "^9.39.2", 18 19 "@types/react": "^18.3.12", 19 20 "@types/react-dom": "^18.3.1", 20 21 "@vitejs/plugin-react": "^4.3.3", 22 + "eslint": "^9.39.2", 23 + "eslint-plugin-react": "^7.37.5", 24 + "eslint-plugin-react-hooks": "^7.0.1", 25 + "eslint-plugin-react-refresh": "^0.4.26", 26 + "globals": "^17.0.0", 21 27 "vite": "^6.0.3" 22 28 } 23 29 }, ··· 746 752 "node": ">=18" 747 753 } 748 754 }, 755 + "node_modules/@eslint-community/eslint-utils": { 756 + "version": "4.9.1", 757 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", 758 + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", 759 + "dev": true, 760 + "license": "MIT", 761 + "dependencies": { 762 + "eslint-visitor-keys": "^3.4.3" 763 + }, 764 + "engines": { 765 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 766 + }, 767 + "funding": { 768 + "url": "https://opencollective.com/eslint" 769 + }, 770 + "peerDependencies": { 771 + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 772 + } 773 + }, 774 + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { 775 + "version": "3.4.3", 776 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 777 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 778 + "dev": true, 779 + "license": "Apache-2.0", 780 + "engines": { 781 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 782 + }, 783 + "funding": { 784 + "url": "https://opencollective.com/eslint" 785 + } 786 + }, 787 + "node_modules/@eslint-community/regexpp": { 788 + "version": "4.12.2", 789 + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", 790 + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", 791 + "dev": true, 792 + "license": "MIT", 793 + "engines": { 794 + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 795 + } 796 + }, 797 + "node_modules/@eslint/config-array": { 798 + "version": "0.21.1", 799 + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", 800 + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", 801 + "dev": true, 802 + "license": "Apache-2.0", 803 + "dependencies": { 804 + "@eslint/object-schema": "^2.1.7", 805 + "debug": "^4.3.1", 806 + "minimatch": "^3.1.2" 807 + }, 808 + "engines": { 809 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 810 + } 811 + }, 812 + "node_modules/@eslint/config-helpers": { 813 + "version": "0.4.2", 814 + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", 815 + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", 816 + "dev": true, 817 + "license": "Apache-2.0", 818 + "dependencies": { 819 + "@eslint/core": "^0.17.0" 820 + }, 821 + "engines": { 822 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 823 + } 824 + }, 825 + "node_modules/@eslint/core": { 826 + "version": "0.17.0", 827 + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", 828 + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", 829 + "dev": true, 830 + "license": "Apache-2.0", 831 + "dependencies": { 832 + "@types/json-schema": "^7.0.15" 833 + }, 834 + "engines": { 835 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 836 + } 837 + }, 838 + "node_modules/@eslint/eslintrc": { 839 + "version": "3.3.3", 840 + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", 841 + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", 842 + "dev": true, 843 + "license": "MIT", 844 + "dependencies": { 845 + "ajv": "^6.12.4", 846 + "debug": "^4.3.2", 847 + "espree": "^10.0.1", 848 + "globals": "^14.0.0", 849 + "ignore": "^5.2.0", 850 + "import-fresh": "^3.2.1", 851 + "js-yaml": "^4.1.1", 852 + "minimatch": "^3.1.2", 853 + "strip-json-comments": "^3.1.1" 854 + }, 855 + "engines": { 856 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 857 + }, 858 + "funding": { 859 + "url": "https://opencollective.com/eslint" 860 + } 861 + }, 862 + "node_modules/@eslint/eslintrc/node_modules/globals": { 863 + "version": "14.0.0", 864 + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 865 + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 866 + "dev": true, 867 + "license": "MIT", 868 + "engines": { 869 + "node": ">=18" 870 + }, 871 + "funding": { 872 + "url": "https://github.com/sponsors/sindresorhus" 873 + } 874 + }, 875 + "node_modules/@eslint/js": { 876 + "version": "9.39.2", 877 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", 878 + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", 879 + "dev": true, 880 + "license": "MIT", 881 + "engines": { 882 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 883 + }, 884 + "funding": { 885 + "url": "https://eslint.org/donate" 886 + } 887 + }, 888 + "node_modules/@eslint/object-schema": { 889 + "version": "2.1.7", 890 + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", 891 + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", 892 + "dev": true, 893 + "license": "Apache-2.0", 894 + "engines": { 895 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 896 + } 897 + }, 898 + "node_modules/@eslint/plugin-kit": { 899 + "version": "0.4.1", 900 + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", 901 + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", 902 + "dev": true, 903 + "license": "Apache-2.0", 904 + "dependencies": { 905 + "@eslint/core": "^0.17.0", 906 + "levn": "^0.4.1" 907 + }, 908 + "engines": { 909 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 910 + } 911 + }, 912 + "node_modules/@humanfs/core": { 913 + "version": "0.19.1", 914 + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 915 + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 916 + "dev": true, 917 + "license": "Apache-2.0", 918 + "engines": { 919 + "node": ">=18.18.0" 920 + } 921 + }, 922 + "node_modules/@humanfs/node": { 923 + "version": "0.16.7", 924 + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", 925 + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", 926 + "dev": true, 927 + "license": "Apache-2.0", 928 + "dependencies": { 929 + "@humanfs/core": "^0.19.1", 930 + "@humanwhocodes/retry": "^0.4.0" 931 + }, 932 + "engines": { 933 + "node": ">=18.18.0" 934 + } 935 + }, 936 + "node_modules/@humanwhocodes/module-importer": { 937 + "version": "1.0.1", 938 + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 939 + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 940 + "dev": true, 941 + "license": "Apache-2.0", 942 + "engines": { 943 + "node": ">=12.22" 944 + }, 945 + "funding": { 946 + "type": "github", 947 + "url": "https://github.com/sponsors/nzakas" 948 + } 949 + }, 950 + "node_modules/@humanwhocodes/retry": { 951 + "version": "0.4.3", 952 + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", 953 + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", 954 + "dev": true, 955 + "license": "Apache-2.0", 956 + "engines": { 957 + "node": ">=18.18" 958 + }, 959 + "funding": { 960 + "type": "github", 961 + "url": "https://github.com/sponsors/nzakas" 962 + } 963 + }, 749 964 "node_modules/@jridgewell/gen-mapping": { 750 965 "version": "0.3.13", 751 966 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", ··· 797 1012 } 798 1013 }, 799 1014 "node_modules/@remix-run/router": { 800 - "version": "1.23.1", 801 - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", 802 - "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", 1015 + "version": "1.23.2", 1016 + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", 1017 + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", 803 1018 "license": "MIT", 804 1019 "engines": { 805 1020 "node": ">=14.0.0" ··· 1172 1387 "dev": true, 1173 1388 "license": "MIT" 1174 1389 }, 1390 + "node_modules/@types/json-schema": { 1391 + "version": "7.0.15", 1392 + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 1393 + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 1394 + "dev": true, 1395 + "license": "MIT" 1396 + }, 1175 1397 "node_modules/@types/prop-types": { 1176 1398 "version": "15.7.15", 1177 1399 "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", ··· 1222 1444 "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" 1223 1445 } 1224 1446 }, 1447 + "node_modules/acorn": { 1448 + "version": "8.15.0", 1449 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", 1450 + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 1451 + "dev": true, 1452 + "license": "MIT", 1453 + "peer": true, 1454 + "bin": { 1455 + "acorn": "bin/acorn" 1456 + }, 1457 + "engines": { 1458 + "node": ">=0.4.0" 1459 + } 1460 + }, 1461 + "node_modules/acorn-jsx": { 1462 + "version": "5.3.2", 1463 + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 1464 + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 1465 + "dev": true, 1466 + "license": "MIT", 1467 + "peerDependencies": { 1468 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 1469 + } 1470 + }, 1471 + "node_modules/ajv": { 1472 + "version": "6.12.6", 1473 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 1474 + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 1475 + "dev": true, 1476 + "license": "MIT", 1477 + "dependencies": { 1478 + "fast-deep-equal": "^3.1.1", 1479 + "fast-json-stable-stringify": "^2.0.0", 1480 + "json-schema-traverse": "^0.4.1", 1481 + "uri-js": "^4.2.2" 1482 + }, 1483 + "funding": { 1484 + "type": "github", 1485 + "url": "https://github.com/sponsors/epoberezkin" 1486 + } 1487 + }, 1488 + "node_modules/ansi-styles": { 1489 + "version": "4.3.0", 1490 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1491 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1492 + "dev": true, 1493 + "license": "MIT", 1494 + "dependencies": { 1495 + "color-convert": "^2.0.1" 1496 + }, 1497 + "engines": { 1498 + "node": ">=8" 1499 + }, 1500 + "funding": { 1501 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1502 + } 1503 + }, 1504 + "node_modules/argparse": { 1505 + "version": "2.0.1", 1506 + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 1507 + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 1508 + "dev": true, 1509 + "license": "Python-2.0" 1510 + }, 1511 + "node_modules/array-buffer-byte-length": { 1512 + "version": "1.0.2", 1513 + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", 1514 + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", 1515 + "dev": true, 1516 + "license": "MIT", 1517 + "dependencies": { 1518 + "call-bound": "^1.0.3", 1519 + "is-array-buffer": "^3.0.5" 1520 + }, 1521 + "engines": { 1522 + "node": ">= 0.4" 1523 + }, 1524 + "funding": { 1525 + "url": "https://github.com/sponsors/ljharb" 1526 + } 1527 + }, 1528 + "node_modules/array-includes": { 1529 + "version": "3.1.9", 1530 + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", 1531 + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", 1532 + "dev": true, 1533 + "license": "MIT", 1534 + "dependencies": { 1535 + "call-bind": "^1.0.8", 1536 + "call-bound": "^1.0.4", 1537 + "define-properties": "^1.2.1", 1538 + "es-abstract": "^1.24.0", 1539 + "es-object-atoms": "^1.1.1", 1540 + "get-intrinsic": "^1.3.0", 1541 + "is-string": "^1.1.1", 1542 + "math-intrinsics": "^1.1.0" 1543 + }, 1544 + "engines": { 1545 + "node": ">= 0.4" 1546 + }, 1547 + "funding": { 1548 + "url": "https://github.com/sponsors/ljharb" 1549 + } 1550 + }, 1551 + "node_modules/array.prototype.findlast": { 1552 + "version": "1.2.5", 1553 + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", 1554 + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", 1555 + "dev": true, 1556 + "license": "MIT", 1557 + "dependencies": { 1558 + "call-bind": "^1.0.7", 1559 + "define-properties": "^1.2.1", 1560 + "es-abstract": "^1.23.2", 1561 + "es-errors": "^1.3.0", 1562 + "es-object-atoms": "^1.0.0", 1563 + "es-shim-unscopables": "^1.0.2" 1564 + }, 1565 + "engines": { 1566 + "node": ">= 0.4" 1567 + }, 1568 + "funding": { 1569 + "url": "https://github.com/sponsors/ljharb" 1570 + } 1571 + }, 1572 + "node_modules/array.prototype.flat": { 1573 + "version": "1.3.3", 1574 + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", 1575 + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", 1576 + "dev": true, 1577 + "license": "MIT", 1578 + "dependencies": { 1579 + "call-bind": "^1.0.8", 1580 + "define-properties": "^1.2.1", 1581 + "es-abstract": "^1.23.5", 1582 + "es-shim-unscopables": "^1.0.2" 1583 + }, 1584 + "engines": { 1585 + "node": ">= 0.4" 1586 + }, 1587 + "funding": { 1588 + "url": "https://github.com/sponsors/ljharb" 1589 + } 1590 + }, 1591 + "node_modules/array.prototype.flatmap": { 1592 + "version": "1.3.3", 1593 + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", 1594 + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", 1595 + "dev": true, 1596 + "license": "MIT", 1597 + "dependencies": { 1598 + "call-bind": "^1.0.8", 1599 + "define-properties": "^1.2.1", 1600 + "es-abstract": "^1.23.5", 1601 + "es-shim-unscopables": "^1.0.2" 1602 + }, 1603 + "engines": { 1604 + "node": ">= 0.4" 1605 + }, 1606 + "funding": { 1607 + "url": "https://github.com/sponsors/ljharb" 1608 + } 1609 + }, 1610 + "node_modules/array.prototype.tosorted": { 1611 + "version": "1.1.4", 1612 + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", 1613 + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", 1614 + "dev": true, 1615 + "license": "MIT", 1616 + "dependencies": { 1617 + "call-bind": "^1.0.7", 1618 + "define-properties": "^1.2.1", 1619 + "es-abstract": "^1.23.3", 1620 + "es-errors": "^1.3.0", 1621 + "es-shim-unscopables": "^1.0.2" 1622 + }, 1623 + "engines": { 1624 + "node": ">= 0.4" 1625 + } 1626 + }, 1627 + "node_modules/arraybuffer.prototype.slice": { 1628 + "version": "1.0.4", 1629 + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", 1630 + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", 1631 + "dev": true, 1632 + "license": "MIT", 1633 + "dependencies": { 1634 + "array-buffer-byte-length": "^1.0.1", 1635 + "call-bind": "^1.0.8", 1636 + "define-properties": "^1.2.1", 1637 + "es-abstract": "^1.23.5", 1638 + "es-errors": "^1.3.0", 1639 + "get-intrinsic": "^1.2.6", 1640 + "is-array-buffer": "^3.0.4" 1641 + }, 1642 + "engines": { 1643 + "node": ">= 0.4" 1644 + }, 1645 + "funding": { 1646 + "url": "https://github.com/sponsors/ljharb" 1647 + } 1648 + }, 1649 + "node_modules/async-function": { 1650 + "version": "1.0.0", 1651 + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", 1652 + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", 1653 + "dev": true, 1654 + "license": "MIT", 1655 + "engines": { 1656 + "node": ">= 0.4" 1657 + } 1658 + }, 1659 + "node_modules/available-typed-arrays": { 1660 + "version": "1.0.7", 1661 + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", 1662 + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", 1663 + "dev": true, 1664 + "license": "MIT", 1665 + "dependencies": { 1666 + "possible-typed-array-names": "^1.0.0" 1667 + }, 1668 + "engines": { 1669 + "node": ">= 0.4" 1670 + }, 1671 + "funding": { 1672 + "url": "https://github.com/sponsors/ljharb" 1673 + } 1674 + }, 1675 + "node_modules/balanced-match": { 1676 + "version": "1.0.2", 1677 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1678 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1679 + "dev": true, 1680 + "license": "MIT" 1681 + }, 1225 1682 "node_modules/baseline-browser-mapping": { 1226 1683 "version": "2.9.11", 1227 1684 "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", ··· 1230 1687 "license": "Apache-2.0", 1231 1688 "bin": { 1232 1689 "baseline-browser-mapping": "dist/cli.js" 1690 + } 1691 + }, 1692 + "node_modules/brace-expansion": { 1693 + "version": "1.1.12", 1694 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 1695 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 1696 + "dev": true, 1697 + "license": "MIT", 1698 + "dependencies": { 1699 + "balanced-match": "^1.0.0", 1700 + "concat-map": "0.0.1" 1233 1701 } 1234 1702 }, 1235 1703 "node_modules/browserslist": { ··· 1267 1735 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 1268 1736 } 1269 1737 }, 1738 + "node_modules/call-bind": { 1739 + "version": "1.0.8", 1740 + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", 1741 + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", 1742 + "dev": true, 1743 + "license": "MIT", 1744 + "dependencies": { 1745 + "call-bind-apply-helpers": "^1.0.0", 1746 + "es-define-property": "^1.0.0", 1747 + "get-intrinsic": "^1.2.4", 1748 + "set-function-length": "^1.2.2" 1749 + }, 1750 + "engines": { 1751 + "node": ">= 0.4" 1752 + }, 1753 + "funding": { 1754 + "url": "https://github.com/sponsors/ljharb" 1755 + } 1756 + }, 1757 + "node_modules/call-bind-apply-helpers": { 1758 + "version": "1.0.2", 1759 + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 1760 + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 1761 + "dev": true, 1762 + "license": "MIT", 1763 + "dependencies": { 1764 + "es-errors": "^1.3.0", 1765 + "function-bind": "^1.1.2" 1766 + }, 1767 + "engines": { 1768 + "node": ">= 0.4" 1769 + } 1770 + }, 1771 + "node_modules/call-bound": { 1772 + "version": "1.0.4", 1773 + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 1774 + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 1775 + "dev": true, 1776 + "license": "MIT", 1777 + "dependencies": { 1778 + "call-bind-apply-helpers": "^1.0.2", 1779 + "get-intrinsic": "^1.3.0" 1780 + }, 1781 + "engines": { 1782 + "node": ">= 0.4" 1783 + }, 1784 + "funding": { 1785 + "url": "https://github.com/sponsors/ljharb" 1786 + } 1787 + }, 1788 + "node_modules/callsites": { 1789 + "version": "3.1.0", 1790 + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 1791 + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 1792 + "dev": true, 1793 + "license": "MIT", 1794 + "engines": { 1795 + "node": ">=6" 1796 + } 1797 + }, 1270 1798 "node_modules/caniuse-lite": { 1271 1799 "version": "1.0.30001762", 1272 1800 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", ··· 1288 1816 ], 1289 1817 "license": "CC-BY-4.0" 1290 1818 }, 1819 + "node_modules/chalk": { 1820 + "version": "4.1.2", 1821 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1822 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1823 + "dev": true, 1824 + "license": "MIT", 1825 + "dependencies": { 1826 + "ansi-styles": "^4.1.0", 1827 + "supports-color": "^7.1.0" 1828 + }, 1829 + "engines": { 1830 + "node": ">=10" 1831 + }, 1832 + "funding": { 1833 + "url": "https://github.com/chalk/chalk?sponsor=1" 1834 + } 1835 + }, 1836 + "node_modules/color-convert": { 1837 + "version": "2.0.1", 1838 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1839 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1840 + "dev": true, 1841 + "license": "MIT", 1842 + "dependencies": { 1843 + "color-name": "~1.1.4" 1844 + }, 1845 + "engines": { 1846 + "node": ">=7.0.0" 1847 + } 1848 + }, 1849 + "node_modules/color-name": { 1850 + "version": "1.1.4", 1851 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1852 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1853 + "dev": true, 1854 + "license": "MIT" 1855 + }, 1856 + "node_modules/concat-map": { 1857 + "version": "0.0.1", 1858 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1859 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 1860 + "dev": true, 1861 + "license": "MIT" 1862 + }, 1291 1863 "node_modules/convert-source-map": { 1292 1864 "version": "2.0.0", 1293 1865 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", ··· 1295 1867 "dev": true, 1296 1868 "license": "MIT" 1297 1869 }, 1870 + "node_modules/cross-spawn": { 1871 + "version": "7.0.6", 1872 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 1873 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 1874 + "dev": true, 1875 + "license": "MIT", 1876 + "dependencies": { 1877 + "path-key": "^3.1.0", 1878 + "shebang-command": "^2.0.0", 1879 + "which": "^2.0.1" 1880 + }, 1881 + "engines": { 1882 + "node": ">= 8" 1883 + } 1884 + }, 1298 1885 "node_modules/csstype": { 1299 1886 "version": "3.2.3", 1300 1887 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", ··· 1302 1889 "dev": true, 1303 1890 "license": "MIT" 1304 1891 }, 1892 + "node_modules/data-view-buffer": { 1893 + "version": "1.0.2", 1894 + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", 1895 + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", 1896 + "dev": true, 1897 + "license": "MIT", 1898 + "dependencies": { 1899 + "call-bound": "^1.0.3", 1900 + "es-errors": "^1.3.0", 1901 + "is-data-view": "^1.0.2" 1902 + }, 1903 + "engines": { 1904 + "node": ">= 0.4" 1905 + }, 1906 + "funding": { 1907 + "url": "https://github.com/sponsors/ljharb" 1908 + } 1909 + }, 1910 + "node_modules/data-view-byte-length": { 1911 + "version": "1.0.2", 1912 + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", 1913 + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", 1914 + "dev": true, 1915 + "license": "MIT", 1916 + "dependencies": { 1917 + "call-bound": "^1.0.3", 1918 + "es-errors": "^1.3.0", 1919 + "is-data-view": "^1.0.2" 1920 + }, 1921 + "engines": { 1922 + "node": ">= 0.4" 1923 + }, 1924 + "funding": { 1925 + "url": "https://github.com/sponsors/inspect-js" 1926 + } 1927 + }, 1928 + "node_modules/data-view-byte-offset": { 1929 + "version": "1.0.1", 1930 + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", 1931 + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", 1932 + "dev": true, 1933 + "license": "MIT", 1934 + "dependencies": { 1935 + "call-bound": "^1.0.2", 1936 + "es-errors": "^1.3.0", 1937 + "is-data-view": "^1.0.1" 1938 + }, 1939 + "engines": { 1940 + "node": ">= 0.4" 1941 + }, 1942 + "funding": { 1943 + "url": "https://github.com/sponsors/ljharb" 1944 + } 1945 + }, 1305 1946 "node_modules/debug": { 1306 1947 "version": "4.4.3", 1307 1948 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", ··· 1320 1961 } 1321 1962 } 1322 1963 }, 1964 + "node_modules/deep-is": { 1965 + "version": "0.1.4", 1966 + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 1967 + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 1968 + "dev": true, 1969 + "license": "MIT" 1970 + }, 1971 + "node_modules/define-data-property": { 1972 + "version": "1.1.4", 1973 + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 1974 + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 1975 + "dev": true, 1976 + "license": "MIT", 1977 + "dependencies": { 1978 + "es-define-property": "^1.0.0", 1979 + "es-errors": "^1.3.0", 1980 + "gopd": "^1.0.1" 1981 + }, 1982 + "engines": { 1983 + "node": ">= 0.4" 1984 + }, 1985 + "funding": { 1986 + "url": "https://github.com/sponsors/ljharb" 1987 + } 1988 + }, 1989 + "node_modules/define-properties": { 1990 + "version": "1.2.1", 1991 + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", 1992 + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", 1993 + "dev": true, 1994 + "license": "MIT", 1995 + "dependencies": { 1996 + "define-data-property": "^1.0.1", 1997 + "has-property-descriptors": "^1.0.0", 1998 + "object-keys": "^1.1.1" 1999 + }, 2000 + "engines": { 2001 + "node": ">= 0.4" 2002 + }, 2003 + "funding": { 2004 + "url": "https://github.com/sponsors/ljharb" 2005 + } 2006 + }, 2007 + "node_modules/doctrine": { 2008 + "version": "2.1.0", 2009 + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", 2010 + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", 2011 + "dev": true, 2012 + "license": "Apache-2.0", 2013 + "dependencies": { 2014 + "esutils": "^2.0.2" 2015 + }, 2016 + "engines": { 2017 + "node": ">=0.10.0" 2018 + } 2019 + }, 2020 + "node_modules/dunder-proto": { 2021 + "version": "1.0.1", 2022 + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 2023 + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 2024 + "dev": true, 2025 + "license": "MIT", 2026 + "dependencies": { 2027 + "call-bind-apply-helpers": "^1.0.1", 2028 + "es-errors": "^1.3.0", 2029 + "gopd": "^1.2.0" 2030 + }, 2031 + "engines": { 2032 + "node": ">= 0.4" 2033 + } 2034 + }, 1323 2035 "node_modules/electron-to-chromium": { 1324 2036 "version": "1.5.267", 1325 2037 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", ··· 1327 2039 "dev": true, 1328 2040 "license": "ISC" 1329 2041 }, 2042 + "node_modules/es-abstract": { 2043 + "version": "1.24.1", 2044 + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", 2045 + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", 2046 + "dev": true, 2047 + "license": "MIT", 2048 + "dependencies": { 2049 + "array-buffer-byte-length": "^1.0.2", 2050 + "arraybuffer.prototype.slice": "^1.0.4", 2051 + "available-typed-arrays": "^1.0.7", 2052 + "call-bind": "^1.0.8", 2053 + "call-bound": "^1.0.4", 2054 + "data-view-buffer": "^1.0.2", 2055 + "data-view-byte-length": "^1.0.2", 2056 + "data-view-byte-offset": "^1.0.1", 2057 + "es-define-property": "^1.0.1", 2058 + "es-errors": "^1.3.0", 2059 + "es-object-atoms": "^1.1.1", 2060 + "es-set-tostringtag": "^2.1.0", 2061 + "es-to-primitive": "^1.3.0", 2062 + "function.prototype.name": "^1.1.8", 2063 + "get-intrinsic": "^1.3.0", 2064 + "get-proto": "^1.0.1", 2065 + "get-symbol-description": "^1.1.0", 2066 + "globalthis": "^1.0.4", 2067 + "gopd": "^1.2.0", 2068 + "has-property-descriptors": "^1.0.2", 2069 + "has-proto": "^1.2.0", 2070 + "has-symbols": "^1.1.0", 2071 + "hasown": "^2.0.2", 2072 + "internal-slot": "^1.1.0", 2073 + "is-array-buffer": "^3.0.5", 2074 + "is-callable": "^1.2.7", 2075 + "is-data-view": "^1.0.2", 2076 + "is-negative-zero": "^2.0.3", 2077 + "is-regex": "^1.2.1", 2078 + "is-set": "^2.0.3", 2079 + "is-shared-array-buffer": "^1.0.4", 2080 + "is-string": "^1.1.1", 2081 + "is-typed-array": "^1.1.15", 2082 + "is-weakref": "^1.1.1", 2083 + "math-intrinsics": "^1.1.0", 2084 + "object-inspect": "^1.13.4", 2085 + "object-keys": "^1.1.1", 2086 + "object.assign": "^4.1.7", 2087 + "own-keys": "^1.0.1", 2088 + "regexp.prototype.flags": "^1.5.4", 2089 + "safe-array-concat": "^1.1.3", 2090 + "safe-push-apply": "^1.0.0", 2091 + "safe-regex-test": "^1.1.0", 2092 + "set-proto": "^1.0.0", 2093 + "stop-iteration-iterator": "^1.1.0", 2094 + "string.prototype.trim": "^1.2.10", 2095 + "string.prototype.trimend": "^1.0.9", 2096 + "string.prototype.trimstart": "^1.0.8", 2097 + "typed-array-buffer": "^1.0.3", 2098 + "typed-array-byte-length": "^1.0.3", 2099 + "typed-array-byte-offset": "^1.0.4", 2100 + "typed-array-length": "^1.0.7", 2101 + "unbox-primitive": "^1.1.0", 2102 + "which-typed-array": "^1.1.19" 2103 + }, 2104 + "engines": { 2105 + "node": ">= 0.4" 2106 + }, 2107 + "funding": { 2108 + "url": "https://github.com/sponsors/ljharb" 2109 + } 2110 + }, 2111 + "node_modules/es-define-property": { 2112 + "version": "1.0.1", 2113 + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 2114 + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 2115 + "dev": true, 2116 + "license": "MIT", 2117 + "engines": { 2118 + "node": ">= 0.4" 2119 + } 2120 + }, 2121 + "node_modules/es-errors": { 2122 + "version": "1.3.0", 2123 + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 2124 + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 2125 + "dev": true, 2126 + "license": "MIT", 2127 + "engines": { 2128 + "node": ">= 0.4" 2129 + } 2130 + }, 2131 + "node_modules/es-iterator-helpers": { 2132 + "version": "1.2.2", 2133 + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", 2134 + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", 2135 + "dev": true, 2136 + "license": "MIT", 2137 + "dependencies": { 2138 + "call-bind": "^1.0.8", 2139 + "call-bound": "^1.0.4", 2140 + "define-properties": "^1.2.1", 2141 + "es-abstract": "^1.24.1", 2142 + "es-errors": "^1.3.0", 2143 + "es-set-tostringtag": "^2.1.0", 2144 + "function-bind": "^1.1.2", 2145 + "get-intrinsic": "^1.3.0", 2146 + "globalthis": "^1.0.4", 2147 + "gopd": "^1.2.0", 2148 + "has-property-descriptors": "^1.0.2", 2149 + "has-proto": "^1.2.0", 2150 + "has-symbols": "^1.1.0", 2151 + "internal-slot": "^1.1.0", 2152 + "iterator.prototype": "^1.1.5", 2153 + "safe-array-concat": "^1.1.3" 2154 + }, 2155 + "engines": { 2156 + "node": ">= 0.4" 2157 + } 2158 + }, 2159 + "node_modules/es-object-atoms": { 2160 + "version": "1.1.1", 2161 + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 2162 + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 2163 + "dev": true, 2164 + "license": "MIT", 2165 + "dependencies": { 2166 + "es-errors": "^1.3.0" 2167 + }, 2168 + "engines": { 2169 + "node": ">= 0.4" 2170 + } 2171 + }, 2172 + "node_modules/es-set-tostringtag": { 2173 + "version": "2.1.0", 2174 + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 2175 + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 2176 + "dev": true, 2177 + "license": "MIT", 2178 + "dependencies": { 2179 + "es-errors": "^1.3.0", 2180 + "get-intrinsic": "^1.2.6", 2181 + "has-tostringtag": "^1.0.2", 2182 + "hasown": "^2.0.2" 2183 + }, 2184 + "engines": { 2185 + "node": ">= 0.4" 2186 + } 2187 + }, 2188 + "node_modules/es-shim-unscopables": { 2189 + "version": "1.1.0", 2190 + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", 2191 + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", 2192 + "dev": true, 2193 + "license": "MIT", 2194 + "dependencies": { 2195 + "hasown": "^2.0.2" 2196 + }, 2197 + "engines": { 2198 + "node": ">= 0.4" 2199 + } 2200 + }, 2201 + "node_modules/es-to-primitive": { 2202 + "version": "1.3.0", 2203 + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", 2204 + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", 2205 + "dev": true, 2206 + "license": "MIT", 2207 + "dependencies": { 2208 + "is-callable": "^1.2.7", 2209 + "is-date-object": "^1.0.5", 2210 + "is-symbol": "^1.0.4" 2211 + }, 2212 + "engines": { 2213 + "node": ">= 0.4" 2214 + }, 2215 + "funding": { 2216 + "url": "https://github.com/sponsors/ljharb" 2217 + } 2218 + }, 1330 2219 "node_modules/esbuild": { 1331 2220 "version": "0.25.12", 1332 2221 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", ··· 1379 2268 "node": ">=6" 1380 2269 } 1381 2270 }, 2271 + "node_modules/escape-string-regexp": { 2272 + "version": "4.0.0", 2273 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 2274 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 2275 + "dev": true, 2276 + "license": "MIT", 2277 + "engines": { 2278 + "node": ">=10" 2279 + }, 2280 + "funding": { 2281 + "url": "https://github.com/sponsors/sindresorhus" 2282 + } 2283 + }, 2284 + "node_modules/eslint": { 2285 + "version": "9.39.2", 2286 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", 2287 + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", 2288 + "dev": true, 2289 + "license": "MIT", 2290 + "peer": true, 2291 + "dependencies": { 2292 + "@eslint-community/eslint-utils": "^4.8.0", 2293 + "@eslint-community/regexpp": "^4.12.1", 2294 + "@eslint/config-array": "^0.21.1", 2295 + "@eslint/config-helpers": "^0.4.2", 2296 + "@eslint/core": "^0.17.0", 2297 + "@eslint/eslintrc": "^3.3.1", 2298 + "@eslint/js": "9.39.2", 2299 + "@eslint/plugin-kit": "^0.4.1", 2300 + "@humanfs/node": "^0.16.6", 2301 + "@humanwhocodes/module-importer": "^1.0.1", 2302 + "@humanwhocodes/retry": "^0.4.2", 2303 + "@types/estree": "^1.0.6", 2304 + "ajv": "^6.12.4", 2305 + "chalk": "^4.0.0", 2306 + "cross-spawn": "^7.0.6", 2307 + "debug": "^4.3.2", 2308 + "escape-string-regexp": "^4.0.0", 2309 + "eslint-scope": "^8.4.0", 2310 + "eslint-visitor-keys": "^4.2.1", 2311 + "espree": "^10.4.0", 2312 + "esquery": "^1.5.0", 2313 + "esutils": "^2.0.2", 2314 + "fast-deep-equal": "^3.1.3", 2315 + "file-entry-cache": "^8.0.0", 2316 + "find-up": "^5.0.0", 2317 + "glob-parent": "^6.0.2", 2318 + "ignore": "^5.2.0", 2319 + "imurmurhash": "^0.1.4", 2320 + "is-glob": "^4.0.0", 2321 + "json-stable-stringify-without-jsonify": "^1.0.1", 2322 + "lodash.merge": "^4.6.2", 2323 + "minimatch": "^3.1.2", 2324 + "natural-compare": "^1.4.0", 2325 + "optionator": "^0.9.3" 2326 + }, 2327 + "bin": { 2328 + "eslint": "bin/eslint.js" 2329 + }, 2330 + "engines": { 2331 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2332 + }, 2333 + "funding": { 2334 + "url": "https://eslint.org/donate" 2335 + }, 2336 + "peerDependencies": { 2337 + "jiti": "*" 2338 + }, 2339 + "peerDependenciesMeta": { 2340 + "jiti": { 2341 + "optional": true 2342 + } 2343 + } 2344 + }, 2345 + "node_modules/eslint-plugin-react": { 2346 + "version": "7.37.5", 2347 + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", 2348 + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", 2349 + "dev": true, 2350 + "license": "MIT", 2351 + "dependencies": { 2352 + "array-includes": "^3.1.8", 2353 + "array.prototype.findlast": "^1.2.5", 2354 + "array.prototype.flatmap": "^1.3.3", 2355 + "array.prototype.tosorted": "^1.1.4", 2356 + "doctrine": "^2.1.0", 2357 + "es-iterator-helpers": "^1.2.1", 2358 + "estraverse": "^5.3.0", 2359 + "hasown": "^2.0.2", 2360 + "jsx-ast-utils": "^2.4.1 || ^3.0.0", 2361 + "minimatch": "^3.1.2", 2362 + "object.entries": "^1.1.9", 2363 + "object.fromentries": "^2.0.8", 2364 + "object.values": "^1.2.1", 2365 + "prop-types": "^15.8.1", 2366 + "resolve": "^2.0.0-next.5", 2367 + "semver": "^6.3.1", 2368 + "string.prototype.matchall": "^4.0.12", 2369 + "string.prototype.repeat": "^1.0.0" 2370 + }, 2371 + "engines": { 2372 + "node": ">=4" 2373 + }, 2374 + "peerDependencies": { 2375 + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" 2376 + } 2377 + }, 2378 + "node_modules/eslint-plugin-react-hooks": { 2379 + "version": "7.0.1", 2380 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", 2381 + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", 2382 + "dev": true, 2383 + "license": "MIT", 2384 + "dependencies": { 2385 + "@babel/core": "^7.24.4", 2386 + "@babel/parser": "^7.24.4", 2387 + "hermes-parser": "^0.25.1", 2388 + "zod": "^3.25.0 || ^4.0.0", 2389 + "zod-validation-error": "^3.5.0 || ^4.0.0" 2390 + }, 2391 + "engines": { 2392 + "node": ">=18" 2393 + }, 2394 + "peerDependencies": { 2395 + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" 2396 + } 2397 + }, 2398 + "node_modules/eslint-plugin-react-refresh": { 2399 + "version": "0.4.26", 2400 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", 2401 + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", 2402 + "dev": true, 2403 + "license": "MIT", 2404 + "peerDependencies": { 2405 + "eslint": ">=8.40" 2406 + } 2407 + }, 2408 + "node_modules/eslint-scope": { 2409 + "version": "8.4.0", 2410 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", 2411 + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", 2412 + "dev": true, 2413 + "license": "BSD-2-Clause", 2414 + "dependencies": { 2415 + "esrecurse": "^4.3.0", 2416 + "estraverse": "^5.2.0" 2417 + }, 2418 + "engines": { 2419 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2420 + }, 2421 + "funding": { 2422 + "url": "https://opencollective.com/eslint" 2423 + } 2424 + }, 2425 + "node_modules/eslint-visitor-keys": { 2426 + "version": "4.2.1", 2427 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", 2428 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", 2429 + "dev": true, 2430 + "license": "Apache-2.0", 2431 + "engines": { 2432 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2433 + }, 2434 + "funding": { 2435 + "url": "https://opencollective.com/eslint" 2436 + } 2437 + }, 2438 + "node_modules/espree": { 2439 + "version": "10.4.0", 2440 + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", 2441 + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", 2442 + "dev": true, 2443 + "license": "BSD-2-Clause", 2444 + "dependencies": { 2445 + "acorn": "^8.15.0", 2446 + "acorn-jsx": "^5.3.2", 2447 + "eslint-visitor-keys": "^4.2.1" 2448 + }, 2449 + "engines": { 2450 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2451 + }, 2452 + "funding": { 2453 + "url": "https://opencollective.com/eslint" 2454 + } 2455 + }, 2456 + "node_modules/esquery": { 2457 + "version": "1.7.0", 2458 + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", 2459 + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", 2460 + "dev": true, 2461 + "license": "BSD-3-Clause", 2462 + "dependencies": { 2463 + "estraverse": "^5.1.0" 2464 + }, 2465 + "engines": { 2466 + "node": ">=0.10" 2467 + } 2468 + }, 2469 + "node_modules/esrecurse": { 2470 + "version": "4.3.0", 2471 + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 2472 + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 2473 + "dev": true, 2474 + "license": "BSD-2-Clause", 2475 + "dependencies": { 2476 + "estraverse": "^5.2.0" 2477 + }, 2478 + "engines": { 2479 + "node": ">=4.0" 2480 + } 2481 + }, 2482 + "node_modules/estraverse": { 2483 + "version": "5.3.0", 2484 + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 2485 + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 2486 + "dev": true, 2487 + "license": "BSD-2-Clause", 2488 + "engines": { 2489 + "node": ">=4.0" 2490 + } 2491 + }, 2492 + "node_modules/esutils": { 2493 + "version": "2.0.3", 2494 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 2495 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 2496 + "dev": true, 2497 + "license": "BSD-2-Clause", 2498 + "engines": { 2499 + "node": ">=0.10.0" 2500 + } 2501 + }, 2502 + "node_modules/fast-deep-equal": { 2503 + "version": "3.1.3", 2504 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 2505 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 2506 + "dev": true, 2507 + "license": "MIT" 2508 + }, 2509 + "node_modules/fast-json-stable-stringify": { 2510 + "version": "2.1.0", 2511 + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 2512 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 2513 + "dev": true, 2514 + "license": "MIT" 2515 + }, 2516 + "node_modules/fast-levenshtein": { 2517 + "version": "2.0.6", 2518 + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 2519 + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 2520 + "dev": true, 2521 + "license": "MIT" 2522 + }, 1382 2523 "node_modules/fdir": { 1383 2524 "version": "6.5.0", 1384 2525 "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", ··· 1397 2538 } 1398 2539 } 1399 2540 }, 2541 + "node_modules/file-entry-cache": { 2542 + "version": "8.0.0", 2543 + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 2544 + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 2545 + "dev": true, 2546 + "license": "MIT", 2547 + "dependencies": { 2548 + "flat-cache": "^4.0.0" 2549 + }, 2550 + "engines": { 2551 + "node": ">=16.0.0" 2552 + } 2553 + }, 2554 + "node_modules/find-up": { 2555 + "version": "5.0.0", 2556 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 2557 + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 2558 + "dev": true, 2559 + "license": "MIT", 2560 + "dependencies": { 2561 + "locate-path": "^6.0.0", 2562 + "path-exists": "^4.0.0" 2563 + }, 2564 + "engines": { 2565 + "node": ">=10" 2566 + }, 2567 + "funding": { 2568 + "url": "https://github.com/sponsors/sindresorhus" 2569 + } 2570 + }, 2571 + "node_modules/flat-cache": { 2572 + "version": "4.0.1", 2573 + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 2574 + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 2575 + "dev": true, 2576 + "license": "MIT", 2577 + "dependencies": { 2578 + "flatted": "^3.2.9", 2579 + "keyv": "^4.5.4" 2580 + }, 2581 + "engines": { 2582 + "node": ">=16" 2583 + } 2584 + }, 2585 + "node_modules/flatted": { 2586 + "version": "3.3.3", 2587 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", 2588 + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", 2589 + "dev": true, 2590 + "license": "ISC" 2591 + }, 2592 + "node_modules/for-each": { 2593 + "version": "0.3.5", 2594 + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", 2595 + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", 2596 + "dev": true, 2597 + "license": "MIT", 2598 + "dependencies": { 2599 + "is-callable": "^1.2.7" 2600 + }, 2601 + "engines": { 2602 + "node": ">= 0.4" 2603 + }, 2604 + "funding": { 2605 + "url": "https://github.com/sponsors/ljharb" 2606 + } 2607 + }, 1400 2608 "node_modules/fsevents": { 1401 2609 "version": "2.3.3", 1402 2610 "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", ··· 1412 2620 "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1413 2621 } 1414 2622 }, 2623 + "node_modules/function-bind": { 2624 + "version": "1.1.2", 2625 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 2626 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 2627 + "dev": true, 2628 + "license": "MIT", 2629 + "funding": { 2630 + "url": "https://github.com/sponsors/ljharb" 2631 + } 2632 + }, 2633 + "node_modules/function.prototype.name": { 2634 + "version": "1.1.8", 2635 + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", 2636 + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", 2637 + "dev": true, 2638 + "license": "MIT", 2639 + "dependencies": { 2640 + "call-bind": "^1.0.8", 2641 + "call-bound": "^1.0.3", 2642 + "define-properties": "^1.2.1", 2643 + "functions-have-names": "^1.2.3", 2644 + "hasown": "^2.0.2", 2645 + "is-callable": "^1.2.7" 2646 + }, 2647 + "engines": { 2648 + "node": ">= 0.4" 2649 + }, 2650 + "funding": { 2651 + "url": "https://github.com/sponsors/ljharb" 2652 + } 2653 + }, 2654 + "node_modules/functions-have-names": { 2655 + "version": "1.2.3", 2656 + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", 2657 + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", 2658 + "dev": true, 2659 + "license": "MIT", 2660 + "funding": { 2661 + "url": "https://github.com/sponsors/ljharb" 2662 + } 2663 + }, 2664 + "node_modules/generator-function": { 2665 + "version": "2.0.1", 2666 + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", 2667 + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", 2668 + "dev": true, 2669 + "license": "MIT", 2670 + "engines": { 2671 + "node": ">= 0.4" 2672 + } 2673 + }, 1415 2674 "node_modules/gensync": { 1416 2675 "version": "1.0.0-beta.2", 1417 2676 "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", ··· 1422 2681 "node": ">=6.9.0" 1423 2682 } 1424 2683 }, 2684 + "node_modules/get-intrinsic": { 2685 + "version": "1.3.0", 2686 + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 2687 + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 2688 + "dev": true, 2689 + "license": "MIT", 2690 + "dependencies": { 2691 + "call-bind-apply-helpers": "^1.0.2", 2692 + "es-define-property": "^1.0.1", 2693 + "es-errors": "^1.3.0", 2694 + "es-object-atoms": "^1.1.1", 2695 + "function-bind": "^1.1.2", 2696 + "get-proto": "^1.0.1", 2697 + "gopd": "^1.2.0", 2698 + "has-symbols": "^1.1.0", 2699 + "hasown": "^2.0.2", 2700 + "math-intrinsics": "^1.1.0" 2701 + }, 2702 + "engines": { 2703 + "node": ">= 0.4" 2704 + }, 2705 + "funding": { 2706 + "url": "https://github.com/sponsors/ljharb" 2707 + } 2708 + }, 2709 + "node_modules/get-proto": { 2710 + "version": "1.0.1", 2711 + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 2712 + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 2713 + "dev": true, 2714 + "license": "MIT", 2715 + "dependencies": { 2716 + "dunder-proto": "^1.0.1", 2717 + "es-object-atoms": "^1.0.0" 2718 + }, 2719 + "engines": { 2720 + "node": ">= 0.4" 2721 + } 2722 + }, 2723 + "node_modules/get-symbol-description": { 2724 + "version": "1.1.0", 2725 + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", 2726 + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", 2727 + "dev": true, 2728 + "license": "MIT", 2729 + "dependencies": { 2730 + "call-bound": "^1.0.3", 2731 + "es-errors": "^1.3.0", 2732 + "get-intrinsic": "^1.2.6" 2733 + }, 2734 + "engines": { 2735 + "node": ">= 0.4" 2736 + }, 2737 + "funding": { 2738 + "url": "https://github.com/sponsors/ljharb" 2739 + } 2740 + }, 2741 + "node_modules/glob-parent": { 2742 + "version": "6.0.2", 2743 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 2744 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 2745 + "dev": true, 2746 + "license": "ISC", 2747 + "dependencies": { 2748 + "is-glob": "^4.0.3" 2749 + }, 2750 + "engines": { 2751 + "node": ">=10.13.0" 2752 + } 2753 + }, 2754 + "node_modules/globals": { 2755 + "version": "17.0.0", 2756 + "resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz", 2757 + "integrity": "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==", 2758 + "dev": true, 2759 + "license": "MIT", 2760 + "engines": { 2761 + "node": ">=18" 2762 + }, 2763 + "funding": { 2764 + "url": "https://github.com/sponsors/sindresorhus" 2765 + } 2766 + }, 2767 + "node_modules/globalthis": { 2768 + "version": "1.0.4", 2769 + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", 2770 + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", 2771 + "dev": true, 2772 + "license": "MIT", 2773 + "dependencies": { 2774 + "define-properties": "^1.2.1", 2775 + "gopd": "^1.0.1" 2776 + }, 2777 + "engines": { 2778 + "node": ">= 0.4" 2779 + }, 2780 + "funding": { 2781 + "url": "https://github.com/sponsors/ljharb" 2782 + } 2783 + }, 2784 + "node_modules/gopd": { 2785 + "version": "1.2.0", 2786 + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 2787 + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 2788 + "dev": true, 2789 + "license": "MIT", 2790 + "engines": { 2791 + "node": ">= 0.4" 2792 + }, 2793 + "funding": { 2794 + "url": "https://github.com/sponsors/ljharb" 2795 + } 2796 + }, 2797 + "node_modules/has-bigints": { 2798 + "version": "1.1.0", 2799 + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", 2800 + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", 2801 + "dev": true, 2802 + "license": "MIT", 2803 + "engines": { 2804 + "node": ">= 0.4" 2805 + }, 2806 + "funding": { 2807 + "url": "https://github.com/sponsors/ljharb" 2808 + } 2809 + }, 2810 + "node_modules/has-flag": { 2811 + "version": "4.0.0", 2812 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 2813 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 2814 + "dev": true, 2815 + "license": "MIT", 2816 + "engines": { 2817 + "node": ">=8" 2818 + } 2819 + }, 2820 + "node_modules/has-property-descriptors": { 2821 + "version": "1.0.2", 2822 + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 2823 + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 2824 + "dev": true, 2825 + "license": "MIT", 2826 + "dependencies": { 2827 + "es-define-property": "^1.0.0" 2828 + }, 2829 + "funding": { 2830 + "url": "https://github.com/sponsors/ljharb" 2831 + } 2832 + }, 2833 + "node_modules/has-proto": { 2834 + "version": "1.2.0", 2835 + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", 2836 + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", 2837 + "dev": true, 2838 + "license": "MIT", 2839 + "dependencies": { 2840 + "dunder-proto": "^1.0.0" 2841 + }, 2842 + "engines": { 2843 + "node": ">= 0.4" 2844 + }, 2845 + "funding": { 2846 + "url": "https://github.com/sponsors/ljharb" 2847 + } 2848 + }, 2849 + "node_modules/has-symbols": { 2850 + "version": "1.1.0", 2851 + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 2852 + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 2853 + "dev": true, 2854 + "license": "MIT", 2855 + "engines": { 2856 + "node": ">= 0.4" 2857 + }, 2858 + "funding": { 2859 + "url": "https://github.com/sponsors/ljharb" 2860 + } 2861 + }, 2862 + "node_modules/has-tostringtag": { 2863 + "version": "1.0.2", 2864 + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 2865 + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 2866 + "dev": true, 2867 + "license": "MIT", 2868 + "dependencies": { 2869 + "has-symbols": "^1.0.3" 2870 + }, 2871 + "engines": { 2872 + "node": ">= 0.4" 2873 + }, 2874 + "funding": { 2875 + "url": "https://github.com/sponsors/ljharb" 2876 + } 2877 + }, 2878 + "node_modules/hasown": { 2879 + "version": "2.0.2", 2880 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 2881 + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 2882 + "dev": true, 2883 + "license": "MIT", 2884 + "dependencies": { 2885 + "function-bind": "^1.1.2" 2886 + }, 2887 + "engines": { 2888 + "node": ">= 0.4" 2889 + } 2890 + }, 2891 + "node_modules/hermes-estree": { 2892 + "version": "0.25.1", 2893 + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", 2894 + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", 2895 + "dev": true, 2896 + "license": "MIT" 2897 + }, 2898 + "node_modules/hermes-parser": { 2899 + "version": "0.25.1", 2900 + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", 2901 + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", 2902 + "dev": true, 2903 + "license": "MIT", 2904 + "dependencies": { 2905 + "hermes-estree": "0.25.1" 2906 + } 2907 + }, 2908 + "node_modules/ignore": { 2909 + "version": "5.3.2", 2910 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 2911 + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 2912 + "dev": true, 2913 + "license": "MIT", 2914 + "engines": { 2915 + "node": ">= 4" 2916 + } 2917 + }, 2918 + "node_modules/import-fresh": { 2919 + "version": "3.3.1", 2920 + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", 2921 + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 2922 + "dev": true, 2923 + "license": "MIT", 2924 + "dependencies": { 2925 + "parent-module": "^1.0.0", 2926 + "resolve-from": "^4.0.0" 2927 + }, 2928 + "engines": { 2929 + "node": ">=6" 2930 + }, 2931 + "funding": { 2932 + "url": "https://github.com/sponsors/sindresorhus" 2933 + } 2934 + }, 2935 + "node_modules/imurmurhash": { 2936 + "version": "0.1.4", 2937 + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 2938 + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 2939 + "dev": true, 2940 + "license": "MIT", 2941 + "engines": { 2942 + "node": ">=0.8.19" 2943 + } 2944 + }, 2945 + "node_modules/internal-slot": { 2946 + "version": "1.1.0", 2947 + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", 2948 + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", 2949 + "dev": true, 2950 + "license": "MIT", 2951 + "dependencies": { 2952 + "es-errors": "^1.3.0", 2953 + "hasown": "^2.0.2", 2954 + "side-channel": "^1.1.0" 2955 + }, 2956 + "engines": { 2957 + "node": ">= 0.4" 2958 + } 2959 + }, 2960 + "node_modules/is-array-buffer": { 2961 + "version": "3.0.5", 2962 + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", 2963 + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", 2964 + "dev": true, 2965 + "license": "MIT", 2966 + "dependencies": { 2967 + "call-bind": "^1.0.8", 2968 + "call-bound": "^1.0.3", 2969 + "get-intrinsic": "^1.2.6" 2970 + }, 2971 + "engines": { 2972 + "node": ">= 0.4" 2973 + }, 2974 + "funding": { 2975 + "url": "https://github.com/sponsors/ljharb" 2976 + } 2977 + }, 2978 + "node_modules/is-async-function": { 2979 + "version": "2.1.1", 2980 + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", 2981 + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", 2982 + "dev": true, 2983 + "license": "MIT", 2984 + "dependencies": { 2985 + "async-function": "^1.0.0", 2986 + "call-bound": "^1.0.3", 2987 + "get-proto": "^1.0.1", 2988 + "has-tostringtag": "^1.0.2", 2989 + "safe-regex-test": "^1.1.0" 2990 + }, 2991 + "engines": { 2992 + "node": ">= 0.4" 2993 + }, 2994 + "funding": { 2995 + "url": "https://github.com/sponsors/ljharb" 2996 + } 2997 + }, 2998 + "node_modules/is-bigint": { 2999 + "version": "1.1.0", 3000 + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", 3001 + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", 3002 + "dev": true, 3003 + "license": "MIT", 3004 + "dependencies": { 3005 + "has-bigints": "^1.0.2" 3006 + }, 3007 + "engines": { 3008 + "node": ">= 0.4" 3009 + }, 3010 + "funding": { 3011 + "url": "https://github.com/sponsors/ljharb" 3012 + } 3013 + }, 3014 + "node_modules/is-boolean-object": { 3015 + "version": "1.2.2", 3016 + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", 3017 + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", 3018 + "dev": true, 3019 + "license": "MIT", 3020 + "dependencies": { 3021 + "call-bound": "^1.0.3", 3022 + "has-tostringtag": "^1.0.2" 3023 + }, 3024 + "engines": { 3025 + "node": ">= 0.4" 3026 + }, 3027 + "funding": { 3028 + "url": "https://github.com/sponsors/ljharb" 3029 + } 3030 + }, 3031 + "node_modules/is-callable": { 3032 + "version": "1.2.7", 3033 + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", 3034 + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", 3035 + "dev": true, 3036 + "license": "MIT", 3037 + "engines": { 3038 + "node": ">= 0.4" 3039 + }, 3040 + "funding": { 3041 + "url": "https://github.com/sponsors/ljharb" 3042 + } 3043 + }, 3044 + "node_modules/is-core-module": { 3045 + "version": "2.16.1", 3046 + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", 3047 + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", 3048 + "dev": true, 3049 + "license": "MIT", 3050 + "dependencies": { 3051 + "hasown": "^2.0.2" 3052 + }, 3053 + "engines": { 3054 + "node": ">= 0.4" 3055 + }, 3056 + "funding": { 3057 + "url": "https://github.com/sponsors/ljharb" 3058 + } 3059 + }, 3060 + "node_modules/is-data-view": { 3061 + "version": "1.0.2", 3062 + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", 3063 + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", 3064 + "dev": true, 3065 + "license": "MIT", 3066 + "dependencies": { 3067 + "call-bound": "^1.0.2", 3068 + "get-intrinsic": "^1.2.6", 3069 + "is-typed-array": "^1.1.13" 3070 + }, 3071 + "engines": { 3072 + "node": ">= 0.4" 3073 + }, 3074 + "funding": { 3075 + "url": "https://github.com/sponsors/ljharb" 3076 + } 3077 + }, 3078 + "node_modules/is-date-object": { 3079 + "version": "1.1.0", 3080 + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", 3081 + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", 3082 + "dev": true, 3083 + "license": "MIT", 3084 + "dependencies": { 3085 + "call-bound": "^1.0.2", 3086 + "has-tostringtag": "^1.0.2" 3087 + }, 3088 + "engines": { 3089 + "node": ">= 0.4" 3090 + }, 3091 + "funding": { 3092 + "url": "https://github.com/sponsors/ljharb" 3093 + } 3094 + }, 3095 + "node_modules/is-extglob": { 3096 + "version": "2.1.1", 3097 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 3098 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 3099 + "dev": true, 3100 + "license": "MIT", 3101 + "engines": { 3102 + "node": ">=0.10.0" 3103 + } 3104 + }, 3105 + "node_modules/is-finalizationregistry": { 3106 + "version": "1.1.1", 3107 + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", 3108 + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", 3109 + "dev": true, 3110 + "license": "MIT", 3111 + "dependencies": { 3112 + "call-bound": "^1.0.3" 3113 + }, 3114 + "engines": { 3115 + "node": ">= 0.4" 3116 + }, 3117 + "funding": { 3118 + "url": "https://github.com/sponsors/ljharb" 3119 + } 3120 + }, 3121 + "node_modules/is-generator-function": { 3122 + "version": "1.1.2", 3123 + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", 3124 + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", 3125 + "dev": true, 3126 + "license": "MIT", 3127 + "dependencies": { 3128 + "call-bound": "^1.0.4", 3129 + "generator-function": "^2.0.0", 3130 + "get-proto": "^1.0.1", 3131 + "has-tostringtag": "^1.0.2", 3132 + "safe-regex-test": "^1.1.0" 3133 + }, 3134 + "engines": { 3135 + "node": ">= 0.4" 3136 + }, 3137 + "funding": { 3138 + "url": "https://github.com/sponsors/ljharb" 3139 + } 3140 + }, 3141 + "node_modules/is-glob": { 3142 + "version": "4.0.3", 3143 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 3144 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 3145 + "dev": true, 3146 + "license": "MIT", 3147 + "dependencies": { 3148 + "is-extglob": "^2.1.1" 3149 + }, 3150 + "engines": { 3151 + "node": ">=0.10.0" 3152 + } 3153 + }, 3154 + "node_modules/is-map": { 3155 + "version": "2.0.3", 3156 + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", 3157 + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", 3158 + "dev": true, 3159 + "license": "MIT", 3160 + "engines": { 3161 + "node": ">= 0.4" 3162 + }, 3163 + "funding": { 3164 + "url": "https://github.com/sponsors/ljharb" 3165 + } 3166 + }, 3167 + "node_modules/is-negative-zero": { 3168 + "version": "2.0.3", 3169 + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", 3170 + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", 3171 + "dev": true, 3172 + "license": "MIT", 3173 + "engines": { 3174 + "node": ">= 0.4" 3175 + }, 3176 + "funding": { 3177 + "url": "https://github.com/sponsors/ljharb" 3178 + } 3179 + }, 3180 + "node_modules/is-number-object": { 3181 + "version": "1.1.1", 3182 + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", 3183 + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", 3184 + "dev": true, 3185 + "license": "MIT", 3186 + "dependencies": { 3187 + "call-bound": "^1.0.3", 3188 + "has-tostringtag": "^1.0.2" 3189 + }, 3190 + "engines": { 3191 + "node": ">= 0.4" 3192 + }, 3193 + "funding": { 3194 + "url": "https://github.com/sponsors/ljharb" 3195 + } 3196 + }, 3197 + "node_modules/is-regex": { 3198 + "version": "1.2.1", 3199 + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", 3200 + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", 3201 + "dev": true, 3202 + "license": "MIT", 3203 + "dependencies": { 3204 + "call-bound": "^1.0.2", 3205 + "gopd": "^1.2.0", 3206 + "has-tostringtag": "^1.0.2", 3207 + "hasown": "^2.0.2" 3208 + }, 3209 + "engines": { 3210 + "node": ">= 0.4" 3211 + }, 3212 + "funding": { 3213 + "url": "https://github.com/sponsors/ljharb" 3214 + } 3215 + }, 3216 + "node_modules/is-set": { 3217 + "version": "2.0.3", 3218 + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", 3219 + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", 3220 + "dev": true, 3221 + "license": "MIT", 3222 + "engines": { 3223 + "node": ">= 0.4" 3224 + }, 3225 + "funding": { 3226 + "url": "https://github.com/sponsors/ljharb" 3227 + } 3228 + }, 3229 + "node_modules/is-shared-array-buffer": { 3230 + "version": "1.0.4", 3231 + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", 3232 + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", 3233 + "dev": true, 3234 + "license": "MIT", 3235 + "dependencies": { 3236 + "call-bound": "^1.0.3" 3237 + }, 3238 + "engines": { 3239 + "node": ">= 0.4" 3240 + }, 3241 + "funding": { 3242 + "url": "https://github.com/sponsors/ljharb" 3243 + } 3244 + }, 3245 + "node_modules/is-string": { 3246 + "version": "1.1.1", 3247 + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", 3248 + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", 3249 + "dev": true, 3250 + "license": "MIT", 3251 + "dependencies": { 3252 + "call-bound": "^1.0.3", 3253 + "has-tostringtag": "^1.0.2" 3254 + }, 3255 + "engines": { 3256 + "node": ">= 0.4" 3257 + }, 3258 + "funding": { 3259 + "url": "https://github.com/sponsors/ljharb" 3260 + } 3261 + }, 3262 + "node_modules/is-symbol": { 3263 + "version": "1.1.1", 3264 + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", 3265 + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", 3266 + "dev": true, 3267 + "license": "MIT", 3268 + "dependencies": { 3269 + "call-bound": "^1.0.2", 3270 + "has-symbols": "^1.1.0", 3271 + "safe-regex-test": "^1.1.0" 3272 + }, 3273 + "engines": { 3274 + "node": ">= 0.4" 3275 + }, 3276 + "funding": { 3277 + "url": "https://github.com/sponsors/ljharb" 3278 + } 3279 + }, 3280 + "node_modules/is-typed-array": { 3281 + "version": "1.1.15", 3282 + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", 3283 + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", 3284 + "dev": true, 3285 + "license": "MIT", 3286 + "dependencies": { 3287 + "which-typed-array": "^1.1.16" 3288 + }, 3289 + "engines": { 3290 + "node": ">= 0.4" 3291 + }, 3292 + "funding": { 3293 + "url": "https://github.com/sponsors/ljharb" 3294 + } 3295 + }, 3296 + "node_modules/is-weakmap": { 3297 + "version": "2.0.2", 3298 + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", 3299 + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", 3300 + "dev": true, 3301 + "license": "MIT", 3302 + "engines": { 3303 + "node": ">= 0.4" 3304 + }, 3305 + "funding": { 3306 + "url": "https://github.com/sponsors/ljharb" 3307 + } 3308 + }, 3309 + "node_modules/is-weakref": { 3310 + "version": "1.1.1", 3311 + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", 3312 + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", 3313 + "dev": true, 3314 + "license": "MIT", 3315 + "dependencies": { 3316 + "call-bound": "^1.0.3" 3317 + }, 3318 + "engines": { 3319 + "node": ">= 0.4" 3320 + }, 3321 + "funding": { 3322 + "url": "https://github.com/sponsors/ljharb" 3323 + } 3324 + }, 3325 + "node_modules/is-weakset": { 3326 + "version": "2.0.4", 3327 + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", 3328 + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", 3329 + "dev": true, 3330 + "license": "MIT", 3331 + "dependencies": { 3332 + "call-bound": "^1.0.3", 3333 + "get-intrinsic": "^1.2.6" 3334 + }, 3335 + "engines": { 3336 + "node": ">= 0.4" 3337 + }, 3338 + "funding": { 3339 + "url": "https://github.com/sponsors/ljharb" 3340 + } 3341 + }, 3342 + "node_modules/isarray": { 3343 + "version": "2.0.5", 3344 + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", 3345 + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", 3346 + "dev": true, 3347 + "license": "MIT" 3348 + }, 3349 + "node_modules/isexe": { 3350 + "version": "2.0.0", 3351 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 3352 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 3353 + "dev": true, 3354 + "license": "ISC" 3355 + }, 3356 + "node_modules/iterator.prototype": { 3357 + "version": "1.1.5", 3358 + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", 3359 + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", 3360 + "dev": true, 3361 + "license": "MIT", 3362 + "dependencies": { 3363 + "define-data-property": "^1.1.4", 3364 + "es-object-atoms": "^1.0.0", 3365 + "get-intrinsic": "^1.2.6", 3366 + "get-proto": "^1.0.0", 3367 + "has-symbols": "^1.1.0", 3368 + "set-function-name": "^2.0.2" 3369 + }, 3370 + "engines": { 3371 + "node": ">= 0.4" 3372 + } 3373 + }, 1425 3374 "node_modules/js-tokens": { 1426 3375 "version": "4.0.0", 1427 3376 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 1428 3377 "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 1429 3378 "license": "MIT" 1430 3379 }, 3380 + "node_modules/js-yaml": { 3381 + "version": "4.1.1", 3382 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", 3383 + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", 3384 + "dev": true, 3385 + "license": "MIT", 3386 + "dependencies": { 3387 + "argparse": "^2.0.1" 3388 + }, 3389 + "bin": { 3390 + "js-yaml": "bin/js-yaml.js" 3391 + } 3392 + }, 1431 3393 "node_modules/jsesc": { 1432 3394 "version": "3.1.0", 1433 3395 "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", ··· 1441 3403 "node": ">=6" 1442 3404 } 1443 3405 }, 3406 + "node_modules/json-buffer": { 3407 + "version": "3.0.1", 3408 + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 3409 + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 3410 + "dev": true, 3411 + "license": "MIT" 3412 + }, 3413 + "node_modules/json-schema-traverse": { 3414 + "version": "0.4.1", 3415 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 3416 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 3417 + "dev": true, 3418 + "license": "MIT" 3419 + }, 3420 + "node_modules/json-stable-stringify-without-jsonify": { 3421 + "version": "1.0.1", 3422 + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 3423 + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 3424 + "dev": true, 3425 + "license": "MIT" 3426 + }, 1444 3427 "node_modules/json5": { 1445 3428 "version": "2.2.3", 1446 3429 "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", ··· 1454 3437 "node": ">=6" 1455 3438 } 1456 3439 }, 3440 + "node_modules/jsx-ast-utils": { 3441 + "version": "3.3.5", 3442 + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", 3443 + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", 3444 + "dev": true, 3445 + "license": "MIT", 3446 + "dependencies": { 3447 + "array-includes": "^3.1.6", 3448 + "array.prototype.flat": "^1.3.1", 3449 + "object.assign": "^4.1.4", 3450 + "object.values": "^1.1.6" 3451 + }, 3452 + "engines": { 3453 + "node": ">=4.0" 3454 + } 3455 + }, 3456 + "node_modules/keyv": { 3457 + "version": "4.5.4", 3458 + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 3459 + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 3460 + "dev": true, 3461 + "license": "MIT", 3462 + "dependencies": { 3463 + "json-buffer": "3.0.1" 3464 + } 3465 + }, 3466 + "node_modules/levn": { 3467 + "version": "0.4.1", 3468 + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 3469 + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 3470 + "dev": true, 3471 + "license": "MIT", 3472 + "dependencies": { 3473 + "prelude-ls": "^1.2.1", 3474 + "type-check": "~0.4.0" 3475 + }, 3476 + "engines": { 3477 + "node": ">= 0.8.0" 3478 + } 3479 + }, 3480 + "node_modules/locate-path": { 3481 + "version": "6.0.0", 3482 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 3483 + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 3484 + "dev": true, 3485 + "license": "MIT", 3486 + "dependencies": { 3487 + "p-locate": "^5.0.0" 3488 + }, 3489 + "engines": { 3490 + "node": ">=10" 3491 + }, 3492 + "funding": { 3493 + "url": "https://github.com/sponsors/sindresorhus" 3494 + } 3495 + }, 3496 + "node_modules/lodash.merge": { 3497 + "version": "4.6.2", 3498 + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 3499 + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 3500 + "dev": true, 3501 + "license": "MIT" 3502 + }, 1457 3503 "node_modules/loose-envify": { 1458 3504 "version": "1.4.0", 1459 3505 "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", ··· 1485 3531 "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" 1486 3532 } 1487 3533 }, 3534 + "node_modules/math-intrinsics": { 3535 + "version": "1.1.0", 3536 + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 3537 + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 3538 + "dev": true, 3539 + "license": "MIT", 3540 + "engines": { 3541 + "node": ">= 0.4" 3542 + } 3543 + }, 3544 + "node_modules/minimatch": { 3545 + "version": "3.1.2", 3546 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 3547 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 3548 + "dev": true, 3549 + "license": "ISC", 3550 + "dependencies": { 3551 + "brace-expansion": "^1.1.7" 3552 + }, 3553 + "engines": { 3554 + "node": "*" 3555 + } 3556 + }, 1488 3557 "node_modules/ms": { 1489 3558 "version": "2.1.3", 1490 3559 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", ··· 1511 3580 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1512 3581 } 1513 3582 }, 3583 + "node_modules/natural-compare": { 3584 + "version": "1.4.0", 3585 + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 3586 + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 3587 + "dev": true, 3588 + "license": "MIT" 3589 + }, 1514 3590 "node_modules/node-releases": { 1515 3591 "version": "2.0.27", 1516 3592 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", 1517 3593 "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", 3594 + "dev": true, 3595 + "license": "MIT" 3596 + }, 3597 + "node_modules/object-assign": { 3598 + "version": "4.1.1", 3599 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 3600 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 3601 + "dev": true, 3602 + "license": "MIT", 3603 + "engines": { 3604 + "node": ">=0.10.0" 3605 + } 3606 + }, 3607 + "node_modules/object-inspect": { 3608 + "version": "1.13.4", 3609 + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 3610 + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 3611 + "dev": true, 3612 + "license": "MIT", 3613 + "engines": { 3614 + "node": ">= 0.4" 3615 + }, 3616 + "funding": { 3617 + "url": "https://github.com/sponsors/ljharb" 3618 + } 3619 + }, 3620 + "node_modules/object-keys": { 3621 + "version": "1.1.1", 3622 + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 3623 + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 3624 + "dev": true, 3625 + "license": "MIT", 3626 + "engines": { 3627 + "node": ">= 0.4" 3628 + } 3629 + }, 3630 + "node_modules/object.assign": { 3631 + "version": "4.1.7", 3632 + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", 3633 + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", 3634 + "dev": true, 3635 + "license": "MIT", 3636 + "dependencies": { 3637 + "call-bind": "^1.0.8", 3638 + "call-bound": "^1.0.3", 3639 + "define-properties": "^1.2.1", 3640 + "es-object-atoms": "^1.0.0", 3641 + "has-symbols": "^1.1.0", 3642 + "object-keys": "^1.1.1" 3643 + }, 3644 + "engines": { 3645 + "node": ">= 0.4" 3646 + }, 3647 + "funding": { 3648 + "url": "https://github.com/sponsors/ljharb" 3649 + } 3650 + }, 3651 + "node_modules/object.entries": { 3652 + "version": "1.1.9", 3653 + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", 3654 + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", 3655 + "dev": true, 3656 + "license": "MIT", 3657 + "dependencies": { 3658 + "call-bind": "^1.0.8", 3659 + "call-bound": "^1.0.4", 3660 + "define-properties": "^1.2.1", 3661 + "es-object-atoms": "^1.1.1" 3662 + }, 3663 + "engines": { 3664 + "node": ">= 0.4" 3665 + } 3666 + }, 3667 + "node_modules/object.fromentries": { 3668 + "version": "2.0.8", 3669 + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", 3670 + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", 3671 + "dev": true, 3672 + "license": "MIT", 3673 + "dependencies": { 3674 + "call-bind": "^1.0.7", 3675 + "define-properties": "^1.2.1", 3676 + "es-abstract": "^1.23.2", 3677 + "es-object-atoms": "^1.0.0" 3678 + }, 3679 + "engines": { 3680 + "node": ">= 0.4" 3681 + }, 3682 + "funding": { 3683 + "url": "https://github.com/sponsors/ljharb" 3684 + } 3685 + }, 3686 + "node_modules/object.values": { 3687 + "version": "1.2.1", 3688 + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", 3689 + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", 3690 + "dev": true, 3691 + "license": "MIT", 3692 + "dependencies": { 3693 + "call-bind": "^1.0.8", 3694 + "call-bound": "^1.0.3", 3695 + "define-properties": "^1.2.1", 3696 + "es-object-atoms": "^1.0.0" 3697 + }, 3698 + "engines": { 3699 + "node": ">= 0.4" 3700 + }, 3701 + "funding": { 3702 + "url": "https://github.com/sponsors/ljharb" 3703 + } 3704 + }, 3705 + "node_modules/optionator": { 3706 + "version": "0.9.4", 3707 + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", 3708 + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", 3709 + "dev": true, 3710 + "license": "MIT", 3711 + "dependencies": { 3712 + "deep-is": "^0.1.3", 3713 + "fast-levenshtein": "^2.0.6", 3714 + "levn": "^0.4.1", 3715 + "prelude-ls": "^1.2.1", 3716 + "type-check": "^0.4.0", 3717 + "word-wrap": "^1.2.5" 3718 + }, 3719 + "engines": { 3720 + "node": ">= 0.8.0" 3721 + } 3722 + }, 3723 + "node_modules/own-keys": { 3724 + "version": "1.0.1", 3725 + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", 3726 + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", 3727 + "dev": true, 3728 + "license": "MIT", 3729 + "dependencies": { 3730 + "get-intrinsic": "^1.2.6", 3731 + "object-keys": "^1.1.1", 3732 + "safe-push-apply": "^1.0.0" 3733 + }, 3734 + "engines": { 3735 + "node": ">= 0.4" 3736 + }, 3737 + "funding": { 3738 + "url": "https://github.com/sponsors/ljharb" 3739 + } 3740 + }, 3741 + "node_modules/p-limit": { 3742 + "version": "3.1.0", 3743 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 3744 + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 3745 + "dev": true, 3746 + "license": "MIT", 3747 + "dependencies": { 3748 + "yocto-queue": "^0.1.0" 3749 + }, 3750 + "engines": { 3751 + "node": ">=10" 3752 + }, 3753 + "funding": { 3754 + "url": "https://github.com/sponsors/sindresorhus" 3755 + } 3756 + }, 3757 + "node_modules/p-locate": { 3758 + "version": "5.0.0", 3759 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 3760 + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 3761 + "dev": true, 3762 + "license": "MIT", 3763 + "dependencies": { 3764 + "p-limit": "^3.0.2" 3765 + }, 3766 + "engines": { 3767 + "node": ">=10" 3768 + }, 3769 + "funding": { 3770 + "url": "https://github.com/sponsors/sindresorhus" 3771 + } 3772 + }, 3773 + "node_modules/parent-module": { 3774 + "version": "1.0.1", 3775 + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 3776 + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 3777 + "dev": true, 3778 + "license": "MIT", 3779 + "dependencies": { 3780 + "callsites": "^3.0.0" 3781 + }, 3782 + "engines": { 3783 + "node": ">=6" 3784 + } 3785 + }, 3786 + "node_modules/path-exists": { 3787 + "version": "4.0.0", 3788 + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 3789 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 3790 + "dev": true, 3791 + "license": "MIT", 3792 + "engines": { 3793 + "node": ">=8" 3794 + } 3795 + }, 3796 + "node_modules/path-key": { 3797 + "version": "3.1.1", 3798 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 3799 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 3800 + "dev": true, 3801 + "license": "MIT", 3802 + "engines": { 3803 + "node": ">=8" 3804 + } 3805 + }, 3806 + "node_modules/path-parse": { 3807 + "version": "1.0.7", 3808 + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 3809 + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 1518 3810 "dev": true, 1519 3811 "license": "MIT" 1520 3812 }, ··· 1539 3831 "url": "https://github.com/sponsors/jonschlinkert" 1540 3832 } 1541 3833 }, 3834 + "node_modules/possible-typed-array-names": { 3835 + "version": "1.1.0", 3836 + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", 3837 + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", 3838 + "dev": true, 3839 + "license": "MIT", 3840 + "engines": { 3841 + "node": ">= 0.4" 3842 + } 3843 + }, 1542 3844 "node_modules/postcss": { 1543 3845 "version": "8.5.6", 1544 3846 "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", ··· 1568 3870 "node": "^10 || ^12 || >=14" 1569 3871 } 1570 3872 }, 3873 + "node_modules/prelude-ls": { 3874 + "version": "1.2.1", 3875 + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 3876 + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 3877 + "dev": true, 3878 + "license": "MIT", 3879 + "engines": { 3880 + "node": ">= 0.8.0" 3881 + } 3882 + }, 3883 + "node_modules/prop-types": { 3884 + "version": "15.8.1", 3885 + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", 3886 + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", 3887 + "dev": true, 3888 + "license": "MIT", 3889 + "dependencies": { 3890 + "loose-envify": "^1.4.0", 3891 + "object-assign": "^4.1.1", 3892 + "react-is": "^16.13.1" 3893 + } 3894 + }, 3895 + "node_modules/punycode": { 3896 + "version": "2.3.1", 3897 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 3898 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 3899 + "dev": true, 3900 + "license": "MIT", 3901 + "engines": { 3902 + "node": ">=6" 3903 + } 3904 + }, 1571 3905 "node_modules/react": { 1572 3906 "version": "18.3.1", 1573 3907 "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", ··· 1604 3938 "react": "*" 1605 3939 } 1606 3940 }, 3941 + "node_modules/react-is": { 3942 + "version": "16.13.1", 3943 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 3944 + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", 3945 + "dev": true, 3946 + "license": "MIT" 3947 + }, 1607 3948 "node_modules/react-refresh": { 1608 3949 "version": "0.17.0", 1609 3950 "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", ··· 1615 3956 } 1616 3957 }, 1617 3958 "node_modules/react-router": { 1618 - "version": "6.30.2", 1619 - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", 1620 - "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", 3959 + "version": "6.30.3", 3960 + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", 3961 + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", 1621 3962 "license": "MIT", 1622 3963 "dependencies": { 1623 - "@remix-run/router": "1.23.1" 3964 + "@remix-run/router": "1.23.2" 1624 3965 }, 1625 3966 "engines": { 1626 3967 "node": ">=14.0.0" ··· 1630 3971 } 1631 3972 }, 1632 3973 "node_modules/react-router-dom": { 1633 - "version": "6.30.2", 1634 - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", 1635 - "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", 3974 + "version": "6.30.3", 3975 + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", 3976 + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", 1636 3977 "license": "MIT", 1637 3978 "dependencies": { 1638 - "@remix-run/router": "1.23.1", 1639 - "react-router": "6.30.2" 3979 + "@remix-run/router": "1.23.2", 3980 + "react-router": "6.30.3" 1640 3981 }, 1641 3982 "engines": { 1642 3983 "node": ">=14.0.0" ··· 1646 3987 "react-dom": ">=16.8" 1647 3988 } 1648 3989 }, 3990 + "node_modules/reflect.getprototypeof": { 3991 + "version": "1.0.10", 3992 + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", 3993 + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", 3994 + "dev": true, 3995 + "license": "MIT", 3996 + "dependencies": { 3997 + "call-bind": "^1.0.8", 3998 + "define-properties": "^1.2.1", 3999 + "es-abstract": "^1.23.9", 4000 + "es-errors": "^1.3.0", 4001 + "es-object-atoms": "^1.0.0", 4002 + "get-intrinsic": "^1.2.7", 4003 + "get-proto": "^1.0.1", 4004 + "which-builtin-type": "^1.2.1" 4005 + }, 4006 + "engines": { 4007 + "node": ">= 0.4" 4008 + }, 4009 + "funding": { 4010 + "url": "https://github.com/sponsors/ljharb" 4011 + } 4012 + }, 4013 + "node_modules/regexp.prototype.flags": { 4014 + "version": "1.5.4", 4015 + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", 4016 + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", 4017 + "dev": true, 4018 + "license": "MIT", 4019 + "dependencies": { 4020 + "call-bind": "^1.0.8", 4021 + "define-properties": "^1.2.1", 4022 + "es-errors": "^1.3.0", 4023 + "get-proto": "^1.0.1", 4024 + "gopd": "^1.2.0", 4025 + "set-function-name": "^2.0.2" 4026 + }, 4027 + "engines": { 4028 + "node": ">= 0.4" 4029 + }, 4030 + "funding": { 4031 + "url": "https://github.com/sponsors/ljharb" 4032 + } 4033 + }, 4034 + "node_modules/resolve": { 4035 + "version": "2.0.0-next.5", 4036 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", 4037 + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", 4038 + "dev": true, 4039 + "license": "MIT", 4040 + "dependencies": { 4041 + "is-core-module": "^2.13.0", 4042 + "path-parse": "^1.0.7", 4043 + "supports-preserve-symlinks-flag": "^1.0.0" 4044 + }, 4045 + "bin": { 4046 + "resolve": "bin/resolve" 4047 + }, 4048 + "funding": { 4049 + "url": "https://github.com/sponsors/ljharb" 4050 + } 4051 + }, 4052 + "node_modules/resolve-from": { 4053 + "version": "4.0.0", 4054 + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 4055 + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 4056 + "dev": true, 4057 + "license": "MIT", 4058 + "engines": { 4059 + "node": ">=4" 4060 + } 4061 + }, 1649 4062 "node_modules/rollup": { 1650 4063 "version": "4.54.0", 1651 4064 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", ··· 1688 4101 "fsevents": "~2.3.2" 1689 4102 } 1690 4103 }, 4104 + "node_modules/safe-array-concat": { 4105 + "version": "1.1.3", 4106 + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", 4107 + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", 4108 + "dev": true, 4109 + "license": "MIT", 4110 + "dependencies": { 4111 + "call-bind": "^1.0.8", 4112 + "call-bound": "^1.0.2", 4113 + "get-intrinsic": "^1.2.6", 4114 + "has-symbols": "^1.1.0", 4115 + "isarray": "^2.0.5" 4116 + }, 4117 + "engines": { 4118 + "node": ">=0.4" 4119 + }, 4120 + "funding": { 4121 + "url": "https://github.com/sponsors/ljharb" 4122 + } 4123 + }, 4124 + "node_modules/safe-push-apply": { 4125 + "version": "1.0.0", 4126 + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", 4127 + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", 4128 + "dev": true, 4129 + "license": "MIT", 4130 + "dependencies": { 4131 + "es-errors": "^1.3.0", 4132 + "isarray": "^2.0.5" 4133 + }, 4134 + "engines": { 4135 + "node": ">= 0.4" 4136 + }, 4137 + "funding": { 4138 + "url": "https://github.com/sponsors/ljharb" 4139 + } 4140 + }, 4141 + "node_modules/safe-regex-test": { 4142 + "version": "1.1.0", 4143 + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", 4144 + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", 4145 + "dev": true, 4146 + "license": "MIT", 4147 + "dependencies": { 4148 + "call-bound": "^1.0.2", 4149 + "es-errors": "^1.3.0", 4150 + "is-regex": "^1.2.1" 4151 + }, 4152 + "engines": { 4153 + "node": ">= 0.4" 4154 + }, 4155 + "funding": { 4156 + "url": "https://github.com/sponsors/ljharb" 4157 + } 4158 + }, 1691 4159 "node_modules/scheduler": { 1692 4160 "version": "0.23.2", 1693 4161 "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", ··· 1707 4175 "semver": "bin/semver.js" 1708 4176 } 1709 4177 }, 4178 + "node_modules/set-function-length": { 4179 + "version": "1.2.2", 4180 + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 4181 + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 4182 + "dev": true, 4183 + "license": "MIT", 4184 + "dependencies": { 4185 + "define-data-property": "^1.1.4", 4186 + "es-errors": "^1.3.0", 4187 + "function-bind": "^1.1.2", 4188 + "get-intrinsic": "^1.2.4", 4189 + "gopd": "^1.0.1", 4190 + "has-property-descriptors": "^1.0.2" 4191 + }, 4192 + "engines": { 4193 + "node": ">= 0.4" 4194 + } 4195 + }, 4196 + "node_modules/set-function-name": { 4197 + "version": "2.0.2", 4198 + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", 4199 + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", 4200 + "dev": true, 4201 + "license": "MIT", 4202 + "dependencies": { 4203 + "define-data-property": "^1.1.4", 4204 + "es-errors": "^1.3.0", 4205 + "functions-have-names": "^1.2.3", 4206 + "has-property-descriptors": "^1.0.2" 4207 + }, 4208 + "engines": { 4209 + "node": ">= 0.4" 4210 + } 4211 + }, 4212 + "node_modules/set-proto": { 4213 + "version": "1.0.0", 4214 + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", 4215 + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", 4216 + "dev": true, 4217 + "license": "MIT", 4218 + "dependencies": { 4219 + "dunder-proto": "^1.0.1", 4220 + "es-errors": "^1.3.0", 4221 + "es-object-atoms": "^1.0.0" 4222 + }, 4223 + "engines": { 4224 + "node": ">= 0.4" 4225 + } 4226 + }, 4227 + "node_modules/shebang-command": { 4228 + "version": "2.0.0", 4229 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 4230 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 4231 + "dev": true, 4232 + "license": "MIT", 4233 + "dependencies": { 4234 + "shebang-regex": "^3.0.0" 4235 + }, 4236 + "engines": { 4237 + "node": ">=8" 4238 + } 4239 + }, 4240 + "node_modules/shebang-regex": { 4241 + "version": "3.0.0", 4242 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 4243 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 4244 + "dev": true, 4245 + "license": "MIT", 4246 + "engines": { 4247 + "node": ">=8" 4248 + } 4249 + }, 4250 + "node_modules/side-channel": { 4251 + "version": "1.1.0", 4252 + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 4253 + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 4254 + "dev": true, 4255 + "license": "MIT", 4256 + "dependencies": { 4257 + "es-errors": "^1.3.0", 4258 + "object-inspect": "^1.13.3", 4259 + "side-channel-list": "^1.0.0", 4260 + "side-channel-map": "^1.0.1", 4261 + "side-channel-weakmap": "^1.0.2" 4262 + }, 4263 + "engines": { 4264 + "node": ">= 0.4" 4265 + }, 4266 + "funding": { 4267 + "url": "https://github.com/sponsors/ljharb" 4268 + } 4269 + }, 4270 + "node_modules/side-channel-list": { 4271 + "version": "1.0.0", 4272 + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 4273 + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 4274 + "dev": true, 4275 + "license": "MIT", 4276 + "dependencies": { 4277 + "es-errors": "^1.3.0", 4278 + "object-inspect": "^1.13.3" 4279 + }, 4280 + "engines": { 4281 + "node": ">= 0.4" 4282 + }, 4283 + "funding": { 4284 + "url": "https://github.com/sponsors/ljharb" 4285 + } 4286 + }, 4287 + "node_modules/side-channel-map": { 4288 + "version": "1.0.1", 4289 + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 4290 + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 4291 + "dev": true, 4292 + "license": "MIT", 4293 + "dependencies": { 4294 + "call-bound": "^1.0.2", 4295 + "es-errors": "^1.3.0", 4296 + "get-intrinsic": "^1.2.5", 4297 + "object-inspect": "^1.13.3" 4298 + }, 4299 + "engines": { 4300 + "node": ">= 0.4" 4301 + }, 4302 + "funding": { 4303 + "url": "https://github.com/sponsors/ljharb" 4304 + } 4305 + }, 4306 + "node_modules/side-channel-weakmap": { 4307 + "version": "1.0.2", 4308 + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 4309 + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 4310 + "dev": true, 4311 + "license": "MIT", 4312 + "dependencies": { 4313 + "call-bound": "^1.0.2", 4314 + "es-errors": "^1.3.0", 4315 + "get-intrinsic": "^1.2.5", 4316 + "object-inspect": "^1.13.3", 4317 + "side-channel-map": "^1.0.1" 4318 + }, 4319 + "engines": { 4320 + "node": ">= 0.4" 4321 + }, 4322 + "funding": { 4323 + "url": "https://github.com/sponsors/ljharb" 4324 + } 4325 + }, 1710 4326 "node_modules/source-map-js": { 1711 4327 "version": "1.2.1", 1712 4328 "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", ··· 1717 4333 "node": ">=0.10.0" 1718 4334 } 1719 4335 }, 4336 + "node_modules/stop-iteration-iterator": { 4337 + "version": "1.1.0", 4338 + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", 4339 + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", 4340 + "dev": true, 4341 + "license": "MIT", 4342 + "dependencies": { 4343 + "es-errors": "^1.3.0", 4344 + "internal-slot": "^1.1.0" 4345 + }, 4346 + "engines": { 4347 + "node": ">= 0.4" 4348 + } 4349 + }, 4350 + "node_modules/string.prototype.matchall": { 4351 + "version": "4.0.12", 4352 + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", 4353 + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", 4354 + "dev": true, 4355 + "license": "MIT", 4356 + "dependencies": { 4357 + "call-bind": "^1.0.8", 4358 + "call-bound": "^1.0.3", 4359 + "define-properties": "^1.2.1", 4360 + "es-abstract": "^1.23.6", 4361 + "es-errors": "^1.3.0", 4362 + "es-object-atoms": "^1.0.0", 4363 + "get-intrinsic": "^1.2.6", 4364 + "gopd": "^1.2.0", 4365 + "has-symbols": "^1.1.0", 4366 + "internal-slot": "^1.1.0", 4367 + "regexp.prototype.flags": "^1.5.3", 4368 + "set-function-name": "^2.0.2", 4369 + "side-channel": "^1.1.0" 4370 + }, 4371 + "engines": { 4372 + "node": ">= 0.4" 4373 + }, 4374 + "funding": { 4375 + "url": "https://github.com/sponsors/ljharb" 4376 + } 4377 + }, 4378 + "node_modules/string.prototype.repeat": { 4379 + "version": "1.0.0", 4380 + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", 4381 + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", 4382 + "dev": true, 4383 + "license": "MIT", 4384 + "dependencies": { 4385 + "define-properties": "^1.1.3", 4386 + "es-abstract": "^1.17.5" 4387 + } 4388 + }, 4389 + "node_modules/string.prototype.trim": { 4390 + "version": "1.2.10", 4391 + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", 4392 + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", 4393 + "dev": true, 4394 + "license": "MIT", 4395 + "dependencies": { 4396 + "call-bind": "^1.0.8", 4397 + "call-bound": "^1.0.2", 4398 + "define-data-property": "^1.1.4", 4399 + "define-properties": "^1.2.1", 4400 + "es-abstract": "^1.23.5", 4401 + "es-object-atoms": "^1.0.0", 4402 + "has-property-descriptors": "^1.0.2" 4403 + }, 4404 + "engines": { 4405 + "node": ">= 0.4" 4406 + }, 4407 + "funding": { 4408 + "url": "https://github.com/sponsors/ljharb" 4409 + } 4410 + }, 4411 + "node_modules/string.prototype.trimend": { 4412 + "version": "1.0.9", 4413 + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", 4414 + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", 4415 + "dev": true, 4416 + "license": "MIT", 4417 + "dependencies": { 4418 + "call-bind": "^1.0.8", 4419 + "call-bound": "^1.0.2", 4420 + "define-properties": "^1.2.1", 4421 + "es-object-atoms": "^1.0.0" 4422 + }, 4423 + "engines": { 4424 + "node": ">= 0.4" 4425 + }, 4426 + "funding": { 4427 + "url": "https://github.com/sponsors/ljharb" 4428 + } 4429 + }, 4430 + "node_modules/string.prototype.trimstart": { 4431 + "version": "1.0.8", 4432 + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", 4433 + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", 4434 + "dev": true, 4435 + "license": "MIT", 4436 + "dependencies": { 4437 + "call-bind": "^1.0.7", 4438 + "define-properties": "^1.2.1", 4439 + "es-object-atoms": "^1.0.0" 4440 + }, 4441 + "engines": { 4442 + "node": ">= 0.4" 4443 + }, 4444 + "funding": { 4445 + "url": "https://github.com/sponsors/ljharb" 4446 + } 4447 + }, 4448 + "node_modules/strip-json-comments": { 4449 + "version": "3.1.1", 4450 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 4451 + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 4452 + "dev": true, 4453 + "license": "MIT", 4454 + "engines": { 4455 + "node": ">=8" 4456 + }, 4457 + "funding": { 4458 + "url": "https://github.com/sponsors/sindresorhus" 4459 + } 4460 + }, 4461 + "node_modules/supports-color": { 4462 + "version": "7.2.0", 4463 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 4464 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 4465 + "dev": true, 4466 + "license": "MIT", 4467 + "dependencies": { 4468 + "has-flag": "^4.0.0" 4469 + }, 4470 + "engines": { 4471 + "node": ">=8" 4472 + } 4473 + }, 4474 + "node_modules/supports-preserve-symlinks-flag": { 4475 + "version": "1.0.0", 4476 + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 4477 + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 4478 + "dev": true, 4479 + "license": "MIT", 4480 + "engines": { 4481 + "node": ">= 0.4" 4482 + }, 4483 + "funding": { 4484 + "url": "https://github.com/sponsors/ljharb" 4485 + } 4486 + }, 1720 4487 "node_modules/tinyglobby": { 1721 4488 "version": "0.2.15", 1722 4489 "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", ··· 1734 4501 "url": "https://github.com/sponsors/SuperchupuDev" 1735 4502 } 1736 4503 }, 4504 + "node_modules/type-check": { 4505 + "version": "0.4.0", 4506 + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 4507 + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 4508 + "dev": true, 4509 + "license": "MIT", 4510 + "dependencies": { 4511 + "prelude-ls": "^1.2.1" 4512 + }, 4513 + "engines": { 4514 + "node": ">= 0.8.0" 4515 + } 4516 + }, 4517 + "node_modules/typed-array-buffer": { 4518 + "version": "1.0.3", 4519 + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", 4520 + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", 4521 + "dev": true, 4522 + "license": "MIT", 4523 + "dependencies": { 4524 + "call-bound": "^1.0.3", 4525 + "es-errors": "^1.3.0", 4526 + "is-typed-array": "^1.1.14" 4527 + }, 4528 + "engines": { 4529 + "node": ">= 0.4" 4530 + } 4531 + }, 4532 + "node_modules/typed-array-byte-length": { 4533 + "version": "1.0.3", 4534 + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", 4535 + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", 4536 + "dev": true, 4537 + "license": "MIT", 4538 + "dependencies": { 4539 + "call-bind": "^1.0.8", 4540 + "for-each": "^0.3.3", 4541 + "gopd": "^1.2.0", 4542 + "has-proto": "^1.2.0", 4543 + "is-typed-array": "^1.1.14" 4544 + }, 4545 + "engines": { 4546 + "node": ">= 0.4" 4547 + }, 4548 + "funding": { 4549 + "url": "https://github.com/sponsors/ljharb" 4550 + } 4551 + }, 4552 + "node_modules/typed-array-byte-offset": { 4553 + "version": "1.0.4", 4554 + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", 4555 + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", 4556 + "dev": true, 4557 + "license": "MIT", 4558 + "dependencies": { 4559 + "available-typed-arrays": "^1.0.7", 4560 + "call-bind": "^1.0.8", 4561 + "for-each": "^0.3.3", 4562 + "gopd": "^1.2.0", 4563 + "has-proto": "^1.2.0", 4564 + "is-typed-array": "^1.1.15", 4565 + "reflect.getprototypeof": "^1.0.9" 4566 + }, 4567 + "engines": { 4568 + "node": ">= 0.4" 4569 + }, 4570 + "funding": { 4571 + "url": "https://github.com/sponsors/ljharb" 4572 + } 4573 + }, 4574 + "node_modules/typed-array-length": { 4575 + "version": "1.0.7", 4576 + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", 4577 + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", 4578 + "dev": true, 4579 + "license": "MIT", 4580 + "dependencies": { 4581 + "call-bind": "^1.0.7", 4582 + "for-each": "^0.3.3", 4583 + "gopd": "^1.0.1", 4584 + "is-typed-array": "^1.1.13", 4585 + "possible-typed-array-names": "^1.0.0", 4586 + "reflect.getprototypeof": "^1.0.6" 4587 + }, 4588 + "engines": { 4589 + "node": ">= 0.4" 4590 + }, 4591 + "funding": { 4592 + "url": "https://github.com/sponsors/ljharb" 4593 + } 4594 + }, 4595 + "node_modules/unbox-primitive": { 4596 + "version": "1.1.0", 4597 + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", 4598 + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", 4599 + "dev": true, 4600 + "license": "MIT", 4601 + "dependencies": { 4602 + "call-bound": "^1.0.3", 4603 + "has-bigints": "^1.0.2", 4604 + "has-symbols": "^1.1.0", 4605 + "which-boxed-primitive": "^1.1.1" 4606 + }, 4607 + "engines": { 4608 + "node": ">= 0.4" 4609 + }, 4610 + "funding": { 4611 + "url": "https://github.com/sponsors/ljharb" 4612 + } 4613 + }, 1737 4614 "node_modules/update-browserslist-db": { 1738 4615 "version": "1.2.3", 1739 4616 "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", ··· 1763 4640 }, 1764 4641 "peerDependencies": { 1765 4642 "browserslist": ">= 4.21.0" 4643 + } 4644 + }, 4645 + "node_modules/uri-js": { 4646 + "version": "4.4.1", 4647 + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 4648 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 4649 + "dev": true, 4650 + "license": "BSD-2-Clause", 4651 + "dependencies": { 4652 + "punycode": "^2.1.0" 1766 4653 } 1767 4654 }, 1768 4655 "node_modules/vite": { ··· 1841 4728 } 1842 4729 } 1843 4730 }, 4731 + "node_modules/which": { 4732 + "version": "2.0.2", 4733 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 4734 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 4735 + "dev": true, 4736 + "license": "ISC", 4737 + "dependencies": { 4738 + "isexe": "^2.0.0" 4739 + }, 4740 + "bin": { 4741 + "node-which": "bin/node-which" 4742 + }, 4743 + "engines": { 4744 + "node": ">= 8" 4745 + } 4746 + }, 4747 + "node_modules/which-boxed-primitive": { 4748 + "version": "1.1.1", 4749 + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", 4750 + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", 4751 + "dev": true, 4752 + "license": "MIT", 4753 + "dependencies": { 4754 + "is-bigint": "^1.1.0", 4755 + "is-boolean-object": "^1.2.1", 4756 + "is-number-object": "^1.1.1", 4757 + "is-string": "^1.1.1", 4758 + "is-symbol": "^1.1.1" 4759 + }, 4760 + "engines": { 4761 + "node": ">= 0.4" 4762 + }, 4763 + "funding": { 4764 + "url": "https://github.com/sponsors/ljharb" 4765 + } 4766 + }, 4767 + "node_modules/which-builtin-type": { 4768 + "version": "1.2.1", 4769 + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", 4770 + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", 4771 + "dev": true, 4772 + "license": "MIT", 4773 + "dependencies": { 4774 + "call-bound": "^1.0.2", 4775 + "function.prototype.name": "^1.1.6", 4776 + "has-tostringtag": "^1.0.2", 4777 + "is-async-function": "^2.0.0", 4778 + "is-date-object": "^1.1.0", 4779 + "is-finalizationregistry": "^1.1.0", 4780 + "is-generator-function": "^1.0.10", 4781 + "is-regex": "^1.2.1", 4782 + "is-weakref": "^1.0.2", 4783 + "isarray": "^2.0.5", 4784 + "which-boxed-primitive": "^1.1.0", 4785 + "which-collection": "^1.0.2", 4786 + "which-typed-array": "^1.1.16" 4787 + }, 4788 + "engines": { 4789 + "node": ">= 0.4" 4790 + }, 4791 + "funding": { 4792 + "url": "https://github.com/sponsors/ljharb" 4793 + } 4794 + }, 4795 + "node_modules/which-collection": { 4796 + "version": "1.0.2", 4797 + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", 4798 + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", 4799 + "dev": true, 4800 + "license": "MIT", 4801 + "dependencies": { 4802 + "is-map": "^2.0.3", 4803 + "is-set": "^2.0.3", 4804 + "is-weakmap": "^2.0.2", 4805 + "is-weakset": "^2.0.3" 4806 + }, 4807 + "engines": { 4808 + "node": ">= 0.4" 4809 + }, 4810 + "funding": { 4811 + "url": "https://github.com/sponsors/ljharb" 4812 + } 4813 + }, 4814 + "node_modules/which-typed-array": { 4815 + "version": "1.1.20", 4816 + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", 4817 + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", 4818 + "dev": true, 4819 + "license": "MIT", 4820 + "dependencies": { 4821 + "available-typed-arrays": "^1.0.7", 4822 + "call-bind": "^1.0.8", 4823 + "call-bound": "^1.0.4", 4824 + "for-each": "^0.3.5", 4825 + "get-proto": "^1.0.1", 4826 + "gopd": "^1.2.0", 4827 + "has-tostringtag": "^1.0.2" 4828 + }, 4829 + "engines": { 4830 + "node": ">= 0.4" 4831 + }, 4832 + "funding": { 4833 + "url": "https://github.com/sponsors/ljharb" 4834 + } 4835 + }, 4836 + "node_modules/word-wrap": { 4837 + "version": "1.2.5", 4838 + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 4839 + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 4840 + "dev": true, 4841 + "license": "MIT", 4842 + "engines": { 4843 + "node": ">=0.10.0" 4844 + } 4845 + }, 1844 4846 "node_modules/yallist": { 1845 4847 "version": "3.1.1", 1846 4848 "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 1847 4849 "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 1848 4850 "dev": true, 1849 4851 "license": "ISC" 4852 + }, 4853 + "node_modules/yocto-queue": { 4854 + "version": "0.1.0", 4855 + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 4856 + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 4857 + "dev": true, 4858 + "license": "MIT", 4859 + "engines": { 4860 + "node": ">=10" 4861 + }, 4862 + "funding": { 4863 + "url": "https://github.com/sponsors/sindresorhus" 4864 + } 4865 + }, 4866 + "node_modules/zod": { 4867 + "version": "4.3.5", 4868 + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", 4869 + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", 4870 + "dev": true, 4871 + "license": "MIT", 4872 + "peer": true, 4873 + "funding": { 4874 + "url": "https://github.com/sponsors/colinhacks" 4875 + } 4876 + }, 4877 + "node_modules/zod-validation-error": { 4878 + "version": "4.0.2", 4879 + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", 4880 + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", 4881 + "dev": true, 4882 + "license": "MIT", 4883 + "engines": { 4884 + "node": ">=18.0.0" 4885 + }, 4886 + "peerDependencies": { 4887 + "zod": "^3.25.0 || ^4.0.0" 4888 + } 1850 4889 } 1851 4890 } 1852 4891 }
+7
web/package.json
··· 6 6 "scripts": { 7 7 "dev": "vite", 8 8 "build": "vite build", 9 + "lint": "eslint .", 9 10 "preview": "vite preview" 10 11 }, 11 12 "dependencies": { ··· 16 17 "react-router-dom": "^6.28.0" 17 18 }, 18 19 "devDependencies": { 20 + "@eslint/js": "^9.39.2", 19 21 "@types/react": "^18.3.12", 20 22 "@types/react-dom": "^18.3.1", 21 23 "@vitejs/plugin-react": "^4.3.3", 24 + "eslint": "^9.39.2", 25 + "eslint-plugin-react": "^7.37.5", 26 + "eslint-plugin-react-hooks": "^7.0.1", 27 + "eslint-plugin-react-refresh": "^0.4.26", 28 + "globals": "^17.0.0", 22 29 "vite": "^6.0.3" 23 30 } 24 31 }
+47 -41
web/src/App.jsx
··· 1 1 import { Routes, Route } from "react-router-dom"; 2 2 import { AuthProvider } from "./context/AuthContext"; 3 - import Navbar from "./components/Navbar"; 3 + import Sidebar from "./components/Sidebar"; 4 + import RightSidebar from "./components/RightSidebar"; 5 + import MobileNav from "./components/MobileNav"; 4 6 import Feed from "./pages/Feed"; 5 7 import Url from "./pages/Url"; 6 8 import Profile from "./pages/Profile"; ··· 13 15 import Collections from "./pages/Collections"; 14 16 import CollectionDetail from "./pages/CollectionDetail"; 15 17 import Privacy from "./pages/Privacy"; 18 + import Terms from "./pages/Terms"; 19 + import ScrollToTop from "./components/ScrollToTop"; 16 20 17 21 function AppContent() { 18 22 return ( 19 - <div className="app"> 20 - <Navbar /> 21 - <main className="main-content"> 22 - <Routes> 23 - <Route path="/" element={<Feed />} /> 24 - <Route path="/url" element={<Url />} /> 25 - <Route path="/new" element={<New />} /> 26 - <Route path="/bookmarks" element={<Bookmarks />} /> 27 - <Route path="/highlights" element={<Highlights />} /> 28 - <Route path="/notifications" element={<Notifications />} /> 29 - <Route path="/profile/:handle" element={<Profile />} /> 30 - <Route path="/login" element={<Login />} /> 31 - {} 32 - <Route path="/at/:did/:rkey" element={<AnnotationDetail />} /> 33 - {} 34 - <Route path="/annotation/:uri" element={<AnnotationDetail />} /> 35 - <Route path="/collections" element={<Collections />} /> 36 - <Route path="/collections/:rkey" element={<CollectionDetail />} /> 37 - <Route 38 - path="/:handle/collection/:rkey" 39 - element={<CollectionDetail />} 40 - /> 41 - 42 - <Route 43 - path="/:handle/annotation/:rkey" 44 - element={<AnnotationDetail />} 45 - /> 46 - <Route 47 - path="/:handle/highlight/:rkey" 48 - element={<AnnotationDetail />} 49 - /> 50 - <Route 51 - path="/:handle/bookmark/:rkey" 52 - element={<AnnotationDetail />} 53 - /> 54 - 55 - <Route path="/collection/*" element={<CollectionDetail />} /> 56 - <Route path="/privacy" element={<Privacy />} /> 57 - </Routes> 58 - </main> 23 + <div className="layout"> 24 + <ScrollToTop /> 25 + <Sidebar /> 26 + <div className="main-layout"> 27 + <main className="main-content-wrapper"> 28 + <Routes> 29 + <Route path="/" element={<Feed />} /> 30 + <Route path="/url" element={<Url />} /> 31 + <Route path="/new" element={<New />} /> 32 + <Route path="/bookmarks" element={<Bookmarks />} /> 33 + <Route path="/highlights" element={<Highlights />} /> 34 + <Route path="/notifications" element={<Notifications />} /> 35 + <Route path="/profile/:handle" element={<Profile />} /> 36 + <Route path="/login" element={<Login />} /> 37 + <Route path="/at/:did/:rkey" element={<AnnotationDetail />} /> 38 + <Route path="/annotation/:uri" element={<AnnotationDetail />} /> 39 + <Route path="/collections" element={<Collections />} /> 40 + <Route path="/collections/:rkey" element={<CollectionDetail />} /> 41 + <Route 42 + path="/:handle/collection/:rkey" 43 + element={<CollectionDetail />} 44 + /> 45 + <Route 46 + path="/:handle/annotation/:rkey" 47 + element={<AnnotationDetail />} 48 + /> 49 + <Route 50 + path="/:handle/highlight/:rkey" 51 + element={<AnnotationDetail />} 52 + /> 53 + <Route 54 + path="/:handle/bookmark/:rkey" 55 + element={<AnnotationDetail />} 56 + /> 57 + <Route path="/collection/*" element={<CollectionDetail />} /> 58 + <Route path="/privacy" element={<Privacy />} /> 59 + <Route path="/terms" element={<Terms />} /> 60 + </Routes> 61 + </main> 62 + </div> 63 + <RightSidebar /> 64 + <MobileNav /> 59 65 </div> 60 66 ); 61 67 }
+18
web/src/api/client.js
··· 427 427 body: JSON.stringify({ handle, invite_code: inviteCode }), 428 428 }); 429 429 } 430 + export async function getTrendingTags(limit = 10) { 431 + return request(`${API_BASE}/tags/trending?limit=${limit}`); 432 + } 433 + 434 + export async function getAPIKeys() { 435 + return request(`${API_BASE}/keys`); 436 + } 437 + 438 + export async function createAPIKey(name) { 439 + return request(`${API_BASE}/keys`, { 440 + method: "POST", 441 + body: JSON.stringify({ name }), 442 + }); 443 + } 444 + 445 + export async function deleteAPIKey(id) { 446 + return request(`${API_BASE}/keys/${id}`, { method: "DELETE" }); 447 + }
+154
web/src/assets/tangled.svg
··· 1 + <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 + <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 + <svg 5 + version="1.1" 6 + id="svg1" 7 + width="24.122343" 8 + height="23.274094" 9 + viewBox="0 0 24.122343 23.274094" 10 + sodipodi:docname="tangled_dolly_face_only.svg" 11 + inkscape:export-filename="tangled_logotype_black_on_trans.svg" 12 + inkscape:export-xdpi="96" 13 + inkscape:export-ydpi="96" 14 + inkscape:version="1.4 (e7c3feb100, 2024-10-09)" 15 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 16 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 17 + xmlns="http://www.w3.org/2000/svg" 18 + xmlns:svg="http://www.w3.org/2000/svg"> 19 + <defs 20 + id="defs1"> 21 + <filter 22 + style="color-interpolation-filters:sRGB" 23 + inkscape:menu-tooltip="Fades hue progressively to white" 24 + inkscape:menu="Color" 25 + inkscape:label="Hue to White" 26 + id="filter24" 27 + x="0" 28 + y="0" 29 + width="1" 30 + height="1"> 31 + <feColorMatrix 32 + values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 " 33 + type="matrix" 34 + result="r" 35 + in="SourceGraphic" 36 + id="feColorMatrix17" /> 37 + <feColorMatrix 38 + values="0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 " 39 + type="matrix" 40 + result="g" 41 + in="SourceGraphic" 42 + id="feColorMatrix18" /> 43 + <feColorMatrix 44 + values="0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 1 " 45 + type="matrix" 46 + result="b" 47 + in="SourceGraphic" 48 + id="feColorMatrix19" /> 49 + <feBlend 50 + result="minrg" 51 + in="r" 52 + mode="darken" 53 + in2="g" 54 + id="feBlend19" /> 55 + <feBlend 56 + result="p" 57 + in="minrg" 58 + mode="darken" 59 + in2="b" 60 + id="feBlend20" /> 61 + <feBlend 62 + result="maxrg" 63 + in="r" 64 + mode="lighten" 65 + in2="g" 66 + id="feBlend21" /> 67 + <feBlend 68 + result="q" 69 + in="maxrg" 70 + mode="lighten" 71 + in2="b" 72 + id="feBlend22" /> 73 + <feComponentTransfer 74 + result="q2" 75 + in="q" 76 + id="feComponentTransfer22"> 77 + <feFuncR 78 + slope="0" 79 + type="linear" 80 + id="feFuncR22" /> 81 + </feComponentTransfer> 82 + <feBlend 83 + result="pq" 84 + in="p" 85 + mode="lighten" 86 + in2="q2" 87 + id="feBlend23" /> 88 + <feColorMatrix 89 + values="-1 1 0 0 0 -1 1 0 0 0 -1 1 0 0 0 0 0 0 0 1 " 90 + type="matrix" 91 + result="qminp" 92 + in="pq" 93 + id="feColorMatrix23" /> 94 + <feComposite 95 + k3="1" 96 + operator="arithmetic" 97 + result="qminpc" 98 + in="qminp" 99 + in2="qminp" 100 + id="feComposite23" 101 + k1="0" 102 + k2="0" 103 + k4="0" /> 104 + <feBlend 105 + result="result2" 106 + in2="SourceGraphic" 107 + mode="screen" 108 + id="feBlend24" /> 109 + <feComposite 110 + operator="in" 111 + in="result2" 112 + in2="SourceGraphic" 113 + result="result1" 114 + id="feComposite24" /> 115 + </filter> 116 + </defs> 117 + <sodipodi:namedview 118 + id="namedview1" 119 + pagecolor="#ffffff" 120 + bordercolor="#000000" 121 + borderopacity="0.25" 122 + inkscape:showpageshadow="2" 123 + inkscape:pageopacity="0.0" 124 + inkscape:pagecheckerboard="true" 125 + inkscape:deskcolor="#d5d5d5" 126 + inkscape:zoom="7.0916564" 127 + inkscape:cx="38.84847" 128 + inkscape:cy="31.515909" 129 + inkscape:window-width="1920" 130 + inkscape:window-height="1080" 131 + inkscape:window-x="0" 132 + inkscape:window-y="0" 133 + inkscape:window-maximized="0" 134 + inkscape:current-layer="g1"> 135 + <inkscape:page 136 + x="0" 137 + y="0" 138 + width="24.122343" 139 + height="23.274094" 140 + id="page2" 141 + margin="0" 142 + bleed="0" /> 143 + </sodipodi:namedview> 144 + <g 145 + inkscape:groupmode="layer" 146 + inkscape:label="Image" 147 + id="g1" 148 + transform="translate(-0.4388285,-0.8629527)"> 149 + <path 150 + style="fill:#ffffff;fill-opacity:1;stroke-width:0.111183;filter:url(#filter24)" 151 + d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" 152 + id="path4" /> 153 + </g> 154 + </svg>
+4 -4
web/src/components/AddToCollectionModal.jsx
··· 1 - import { useState, useEffect } from "react"; 1 + import { useState, useEffect, useCallback } from "react"; 2 2 import { X, Plus, Check, Folder } from "lucide-react"; 3 3 import { 4 4 getCollections, ··· 30 30 loadCollections(); 31 31 setError(null); 32 32 } 33 - }, [isOpen, user, annotationUri]); 33 + }, [isOpen, user, annotationUri, loadCollections]); 34 34 35 - const loadCollections = async () => { 35 + const loadCollections = useCallback(async () => { 36 36 try { 37 37 setLoading(true); 38 38 const [data, existingURIs] = await Promise.all([ ··· 49 49 } finally { 50 50 setLoading(false); 51 51 } 52 - }; 52 + }, [user?.did, annotationUri]); 53 53 54 54 const handleAdd = async (collectionUri) => { 55 55 if (addedTo.has(collectionUri)) return;
+37 -49
web/src/components/AnnotationCard.jsx
··· 5 5 import { 6 6 normalizeAnnotation, 7 7 normalizeHighlight, 8 - normalizeBookmark, 9 - deleteAnnotation, 10 8 likeAnnotation, 11 9 unlikeAnnotation, 12 10 getReplies, 13 11 createReply, 14 12 deleteReply, 15 - getLikeCount, 16 13 updateAnnotation, 17 14 updateHighlight, 18 - updateBookmark, 19 15 getEditHistory, 16 + deleteAnnotation, 20 17 } from "../api/client"; 21 18 import { 22 - HeartIcon, 23 - MessageIcon, 24 - TrashIcon, 25 - ExternalLinkIcon, 26 - HighlightIcon, 27 - BookmarkIcon, 28 - } from "./Icons"; 29 - import { Folder, Edit2, Save, X, Clock } from "lucide-react"; 19 + MessageSquare, 20 + Heart, 21 + Trash2, 22 + Folder, 23 + Edit2, 24 + Save, 25 + X, 26 + Clock, 27 + } from "lucide-react"; 28 + import { HighlightIcon, TrashIcon } from "./Icons"; 30 29 import ShareMenu from "./ShareMenu"; 31 30 32 31 function buildTextFragmentUrl(baseUrl, selector) { ··· 90 89 91 90 const [hasEditHistory, setHasEditHistory] = useState(false); 92 91 93 - useEffect(() => {}, []); 92 + useEffect(() => { 93 + if (data.uri && !data.color && !data.description) { 94 + getEditHistory(data.uri) 95 + .then((history) => { 96 + if (history && history.length > 0) { 97 + setHasEditHistory(true); 98 + } 99 + }) 100 + .catch(() => {}); 101 + } 102 + }, [data.uri, data.color, data.description]); 94 103 95 104 const fetchHistory = async () => { 96 105 if (showHistory) { ··· 216 225 } 217 226 }; 218 227 219 - const handleShare = async () => { 220 - const uriParts = data.uri.split("/"); 221 - const did = uriParts[2]; 222 - const rkey = uriParts[uriParts.length - 1]; 223 - const shareUrl = `${window.location.origin}/at/${did}/${rkey}`; 224 - 225 - if (navigator.share) { 226 - try { 227 - await navigator.share({ 228 - title: "Margin Annotation", 229 - text: data.text?.substring(0, 100), 230 - url: shareUrl, 231 - }); 232 - } catch (err) {} 233 - } else { 234 - try { 235 - await navigator.clipboard.writeText(shareUrl); 236 - alert("Link copied!"); 237 - } catch { 238 - prompt("Copy this link:", shareUrl); 239 - } 240 - } 241 - }; 242 - 243 228 const handleDelete = async () => { 244 229 if (!confirm("Delete this annotation? This cannot be undone.")) return; 245 230 try { ··· 324 309 disabled={deleting} 325 310 title="Delete" 326 311 > 327 - <TrashIcon size={16} /> 312 + <Trash2 size={16} /> 328 313 </button> 329 314 </> 330 315 )} ··· 386 371 borderLeftColor: data.color || "var(--accent)", 387 372 }} 388 373 > 389 - <mark>"{highlightedText}"</mark> 374 + <mark>&quot;{highlightedText}&quot;</mark> 390 375 </a> 391 376 )} 392 377 ··· 454 439 className={`annotation-action ${isLiked ? "liked" : ""}`} 455 440 onClick={handleLike} 456 441 > 457 - <HeartIcon filled={isLiked} size={16} /> 442 + <Heart filled={isLiked} size={16} /> 458 443 {likeCount > 0 && <span>{likeCount}</span>} 459 444 </button> 460 445 <button ··· 471 456 setShowReplies(!showReplies); 472 457 }} 473 458 > 474 - <MessageIcon size={16} /> 459 + <MessageSquare size={16} /> 475 460 <span>{replyCount > 0 ? `${replyCount}` : "Reply"}</span> 476 461 </button> 477 462 <ShareMenu ··· 561 546 onChange={(e) => setReplyText(e.target.value)} 562 547 onFocus={(e) => { 563 548 if (!user) { 564 - e.target.blur(); 565 - login(); 549 + e.preventDefault(); 550 + alert("Please sign in to like annotations"); 566 551 } 567 552 }} 568 553 rows={2} ··· 589 574 ); 590 575 } 591 576 592 - export function HighlightCard({ highlight, onDelete, onAddToCollection }) { 577 + export function HighlightCard({ 578 + highlight, 579 + onDelete, 580 + onAddToCollection, 581 + onUpdate, 582 + }) { 593 583 const { user, login } = useAuth(); 594 584 const data = normalizeHighlight(highlight); 595 585 const highlightedText = ··· 609 599 610 600 await updateHighlight(data.uri, editColor, tagList); 611 601 setIsEditing(false); 612 - 613 - if (highlight.color) highlight.color = editColor; 614 - if (highlight.tags) highlight.tags = tagList; 615 - else highlight.value = { ...highlight.value, tags: tagList }; 602 + if (typeof onUpdate === "function") 603 + onUpdate({ ...highlight, color: editColor, tags: tagList }); 616 604 } catch (err) { 617 605 alert("Failed to update: " + err.message); 618 606 } ··· 720 708 borderLeftColor: isEditing ? editColor : data.color || "#f59e0b", 721 709 }} 722 710 > 723 - <mark>"{highlightedText}"</mark> 711 + <mark>&quot;{highlightedText}&quot;</mark> 724 712 </a> 725 713 )} 726 714
+26
web/src/components/AnnotationSkeleton.jsx
··· 1 + export default function AnnotationSkeleton() { 2 + return ( 3 + <div className="skeleton-card"> 4 + <div className="skeleton-header"> 5 + <div className="skeleton skeleton-avatar" /> 6 + <div className="skeleton-meta"> 7 + <div className="skeleton skeleton-name" /> 8 + <div className="skeleton skeleton-handle" /> 9 + </div> 10 + </div> 11 + 12 + <div className="skeleton-content"> 13 + <div className="skeleton skeleton-source" /> 14 + <div className="skeleton skeleton-highlight" /> 15 + <div className="skeleton skeleton-text-1" /> 16 + <div className="skeleton skeleton-text-2" /> 17 + </div> 18 + 19 + <div className="skeleton-actions"> 20 + <div className="skeleton skeleton-action" /> 21 + <div className="skeleton skeleton-action" /> 22 + <div className="skeleton skeleton-action" /> 23 + </div> 24 + </div> 25 + ); 26 + }
+20 -10
web/src/components/BookmarkCard.jsx
··· 9 9 getLikeCount, 10 10 deleteBookmark, 11 11 } from "../api/client"; 12 - import { HeartIcon, TrashIcon, ExternalLinkIcon, BookmarkIcon } from "./Icons"; 12 + import { HeartIcon, TrashIcon, BookmarkIcon } from "./Icons"; 13 13 import { Folder } from "lucide-react"; 14 14 import ShareMenu from "./ShareMenu"; 15 15 16 - export default function BookmarkCard({ bookmark, onAddToCollection }) { 16 + export default function BookmarkCard({ 17 + bookmark, 18 + onAddToCollection, 19 + onDelete, 20 + }) { 17 21 const { user, login } = useAuth(); 18 22 const raw = bookmark; 19 23 const data = ··· 34 38 if (likeRes.count !== undefined) setLikeCount(likeRes.count); 35 39 if (likeRes.liked !== undefined) setIsLiked(likeRes.liked); 36 40 } 37 - } catch (err) { 38 - console.error("Failed to fetch data:", err); 41 + } catch { 42 + /* ignore */ 39 43 } 40 44 } 41 45 if (data.uri) fetchData(); ··· 60 64 const cid = data.cid || ""; 61 65 if (data.uri && cid) await likeAnnotation(data.uri, cid); 62 66 } 63 - } catch (err) { 67 + } catch { 64 68 setIsLiked(!isLiked); 65 69 setLikeCount((prev) => (isLiked ? prev + 1 : prev - 1)); 66 70 } 67 71 }; 68 72 69 73 const handleDelete = async () => { 74 + if (onDelete) { 75 + onDelete(data.uri); 76 + return; 77 + } 78 + 70 79 if (!confirm("Delete this bookmark?")) return; 71 80 try { 72 81 setDeleting(true); 73 82 const parts = data.uri.split("/"); 74 83 const rkey = parts[parts.length - 1]; 75 84 await deleteBookmark(rkey); 76 - if (onDelete) onDelete(data.uri); 77 - else window.location.reload(); 85 + window.location.reload(); 78 86 } catch (err) { 79 87 alert("Failed to delete: " + err.message); 80 88 } finally { ··· 100 108 let domain = ""; 101 109 try { 102 110 if (data.url) domain = new URL(data.url).hostname.replace("www.", ""); 103 - } catch {} 111 + } catch { 112 + /* ignore */ 113 + } 104 114 105 115 const authorDisplayName = data.author?.displayName || data.author?.handle; 106 116 const authorHandle = data.author?.handle; ··· 109 119 const marginProfileUrl = authorDid ? `/profile/${authorDid}` : null; 110 120 111 121 return ( 112 - <article className="card bookmark-card"> 122 + <article className="card annotation-card bookmark-card"> 113 123 <header className="annotation-header"> 114 124 <div className="annotation-header-left"> 115 125 <Link to={marginProfileUrl || "#"} className="annotation-avatar-link"> ··· 150 160 151 161 <div className="annotation-header-right"> 152 162 <div style={{ display: "flex", gap: "4px" }}> 153 - {isOwner && ( 163 + {(isOwner || onDelete) && ( 154 164 <button 155 165 className="annotation-action action-icon-only" 156 166 onClick={handleDelete}
-1
web/src/components/CollectionItemCard.jsx
··· 1 - import React from "react"; 2 1 import { Link } from "react-router-dom"; 3 2 import AnnotationCard, { HighlightCard } from "./AnnotationCard"; 4 3 import BookmarkCard from "./BookmarkCard";
-7
web/src/components/CollectionModal.jsx
··· 12 12 Camera, 13 13 Code, 14 14 Globe, 15 - Lock, 16 15 Flag, 17 16 Tag, 18 17 Box, ··· 21 20 Image, 22 21 Video, 23 22 Mail, 24 - Phone, 25 23 MapPin, 26 24 Calendar, 27 25 Clock, ··· 31 29 Users, 32 30 Home, 33 31 Briefcase, 34 - ShoppingBag, 35 32 Gift, 36 33 Award, 37 34 Target, 38 35 TrendingUp, 39 - BarChart, 40 - PieChart, 41 36 Activity, 42 37 Cpu, 43 38 Database, ··· 46 41 Moon, 47 42 Flame, 48 43 Leaf, 49 - Droplet, 50 - Snowflake, 51 44 } from "lucide-react"; 52 45 import { createCollection, updateCollection } from "../api/client"; 53 46
+1 -1
web/src/components/Composer.jsx
··· 91 91 ร— 92 92 </button> 93 93 <blockquote> 94 - <mark className="quote-exact">"{highlightedText}"</mark> 94 + <mark className="quote-exact">&quot;{highlightedText}&quot;</mark> 95 95 </blockquote> 96 96 </div> 97 97 )}
+61
web/src/components/MobileNav.jsx
··· 1 + import { Link, useLocation } from "react-router-dom"; 2 + import { useAuth } from "../context/AuthContext"; 3 + import { Home, Search, Folder, User, PenSquare } from "lucide-react"; 4 + 5 + export default function MobileNav() { 6 + const { user, isAuthenticated } = useAuth(); 7 + const location = useLocation(); 8 + 9 + const isActive = (path) => { 10 + if (path === "/") return location.pathname === "/"; 11 + return location.pathname.startsWith(path); 12 + }; 13 + 14 + return ( 15 + <nav className="mobile-nav"> 16 + <div className="mobile-nav-inner"> 17 + <Link 18 + to="/" 19 + className={`mobile-nav-item ${isActive("/") ? "active" : ""}`} 20 + > 21 + <Home /> 22 + <span>Home</span> 23 + </Link> 24 + 25 + <Link 26 + to="/url" 27 + className={`mobile-nav-item ${isActive("/url") ? "active" : ""}`} 28 + > 29 + <Search /> 30 + <span>Browse</span> 31 + </Link> 32 + 33 + {isAuthenticated ? ( 34 + <Link to="/new" className="mobile-nav-item mobile-nav-new"> 35 + <PenSquare /> 36 + </Link> 37 + ) : ( 38 + <Link to="/login" className="mobile-nav-item mobile-nav-new"> 39 + <User /> 40 + </Link> 41 + )} 42 + 43 + <Link 44 + to="/collections" 45 + className={`mobile-nav-item ${isActive("/collections") ? "active" : ""}`} 46 + > 47 + <Folder /> 48 + <span>Library</span> 49 + </Link> 50 + 51 + <Link 52 + to={isAuthenticated && user?.did ? `/profile/${user.did}` : "/login"} 53 + className={`mobile-nav-item ${isActive("/profile") ? "active" : ""}`} 54 + > 55 + <User /> 56 + <span>Profile</span> 57 + </Link> 58 + </div> 59 + </nav> 60 + ); 61 + }
-245
web/src/components/Navbar.jsx
··· 1 - import { useState, useRef, useEffect } from "react"; 2 - import { Link, useLocation } from "react-router-dom"; 3 - import { Folder } from "lucide-react"; 4 - import { useAuth } from "../context/AuthContext"; 5 - import { 6 - PenIcon, 7 - BookmarkIcon, 8 - HighlightIcon, 9 - SearchIcon, 10 - LogoutIcon, 11 - BellIcon, 12 - } from "./Icons"; 13 - import { getUnreadNotificationCount } from "../api/client"; 14 - import { SiFirefox, SiGooglechrome } from "react-icons/si"; 15 - import { FaEdge } from "react-icons/fa"; 16 - 17 - import logo from "../assets/logo.svg"; 18 - 19 - const isFirefox = 20 - typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent); 21 - const isEdge = 22 - typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent); 23 - const isChrome = 24 - typeof navigator !== "undefined" && 25 - /Chrome/i.test(navigator.userAgent) && 26 - !isEdge; 27 - 28 - export default function Navbar() { 29 - const { user, isAuthenticated, logout, loading } = useAuth(); 30 - const location = useLocation(); 31 - const [menuOpen, setMenuOpen] = useState(false); 32 - const [unreadCount, setUnreadCount] = useState(0); 33 - const menuRef = useRef(null); 34 - 35 - const isActive = (path) => location.pathname === path; 36 - 37 - useEffect(() => { 38 - if (isAuthenticated) { 39 - getUnreadNotificationCount() 40 - .then((data) => setUnreadCount(data.count || 0)) 41 - .catch(() => {}); 42 - const interval = setInterval(() => { 43 - getUnreadNotificationCount() 44 - .then((data) => setUnreadCount(data.count || 0)) 45 - .catch(() => {}); 46 - }, 60000); 47 - return () => clearInterval(interval); 48 - } 49 - }, [isAuthenticated]); 50 - 51 - useEffect(() => { 52 - const handleClickOutside = (e) => { 53 - if (menuRef.current && !menuRef.current.contains(e.target)) { 54 - setMenuOpen(false); 55 - } 56 - }; 57 - document.addEventListener("mousedown", handleClickOutside); 58 - return () => document.removeEventListener("mousedown", handleClickOutside); 59 - }, []); 60 - 61 - const getInitials = () => { 62 - if (user?.displayName) { 63 - return user.displayName.substring(0, 2).toUpperCase(); 64 - } 65 - if (user?.handle) { 66 - return user.handle.substring(0, 2).toUpperCase(); 67 - } 68 - return "U"; 69 - }; 70 - 71 - return ( 72 - <nav className="navbar"> 73 - <div className="navbar-inner"> 74 - {} 75 - <Link to="/" className="navbar-brand"> 76 - <img src={logo} alt="Margin Logo" className="navbar-logo-img" /> 77 - <span className="navbar-title">Margin</span> 78 - </Link> 79 - 80 - {} 81 - <div className="navbar-center"> 82 - <Link 83 - to="/" 84 - className={`navbar-link ${isActive("/") ? "active" : ""}`} 85 - > 86 - Feed 87 - </Link> 88 - <Link 89 - to="/url" 90 - className={`navbar-link ${isActive("/url") ? "active" : ""}`} 91 - > 92 - <SearchIcon size={16} /> 93 - Browse 94 - </Link> 95 - {isFirefox ? ( 96 - <a 97 - href="https://addons.mozilla.org/en-US/firefox/addon/margin/" 98 - target="_blank" 99 - rel="noopener noreferrer" 100 - className="navbar-link navbar-extension-link" 101 - > 102 - <SiFirefox size={16} /> 103 - Get Extension 104 - </a> 105 - ) : isEdge ? ( 106 - <a 107 - href="https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn" 108 - target="_blank" 109 - rel="noopener noreferrer" 110 - className="navbar-link navbar-extension-link" 111 - > 112 - <FaEdge size={16} /> 113 - Get Extension 114 - </a> 115 - ) : isChrome ? ( 116 - <a 117 - href="https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/" 118 - target="_blank" 119 - rel="noopener noreferrer" 120 - className="navbar-link navbar-extension-link" 121 - > 122 - <SiGooglechrome size={16} /> 123 - Get Extension 124 - </a> 125 - ) : ( 126 - <a 127 - href="https://addons.mozilla.org/en-US/firefox/addon/margin/" 128 - target="_blank" 129 - rel="noopener noreferrer" 130 - className="navbar-link navbar-extension-link" 131 - > 132 - <SiFirefox size={16} /> 133 - Get Extension 134 - </a> 135 - )} 136 - </div> 137 - 138 - {} 139 - <div className="navbar-right"> 140 - {!loading && 141 - (isAuthenticated ? ( 142 - <> 143 - <Link 144 - to="/highlights" 145 - className={`navbar-icon-link ${isActive("/highlights") ? "active" : ""}`} 146 - title="Highlights" 147 - > 148 - <HighlightIcon size={20} /> 149 - </Link> 150 - <Link 151 - to="/bookmarks" 152 - className={`navbar-icon-link ${isActive("/bookmarks") ? "active" : ""}`} 153 - title="Bookmarks" 154 - > 155 - <BookmarkIcon size={20} /> 156 - </Link> 157 - <Link 158 - to="/collections" 159 - className={`navbar-icon-link ${isActive("/collections") ? "active" : ""}`} 160 - title="Collections" 161 - > 162 - <Folder size={20} /> 163 - </Link> 164 - <Link 165 - to="/notifications" 166 - className={`navbar-icon-link notification-link ${isActive("/notifications") ? "active" : ""}`} 167 - title="Notifications" 168 - onClick={() => setUnreadCount(0)} 169 - > 170 - <BellIcon size={20} /> 171 - {unreadCount > 0 && ( 172 - <span className="notification-badge">{unreadCount}</span> 173 - )} 174 - </Link> 175 - <Link 176 - to="/new" 177 - className="navbar-new-btn" 178 - title="New Annotation" 179 - > 180 - <PenIcon size={16} /> 181 - <span>New</span> 182 - </Link> 183 - 184 - {} 185 - <div className="navbar-user-menu" ref={menuRef}> 186 - <button 187 - className="navbar-avatar-btn" 188 - onClick={() => setMenuOpen(!menuOpen)} 189 - title={user?.handle} 190 - > 191 - {user?.avatar ? ( 192 - <img 193 - src={user.avatar} 194 - alt={user.displayName} 195 - className="navbar-avatar-img" 196 - /> 197 - ) : ( 198 - <span className="navbar-avatar-text"> 199 - {getInitials()} 200 - </span> 201 - )} 202 - </button> 203 - 204 - {menuOpen && ( 205 - <div className="navbar-dropdown"> 206 - <div className="navbar-dropdown-header"> 207 - <span className="navbar-dropdown-name"> 208 - {user?.displayName} 209 - </span> 210 - <span className="navbar-dropdown-handle"> 211 - @{user?.handle} 212 - </span> 213 - </div> 214 - <div className="navbar-dropdown-divider" /> 215 - <Link 216 - to={`/profile/${user?.did}`} 217 - className="navbar-dropdown-item" 218 - onClick={() => setMenuOpen(false)} 219 - > 220 - View Profile 221 - </Link> 222 - <button 223 - onClick={() => { 224 - logout(); 225 - setMenuOpen(false); 226 - }} 227 - className="navbar-dropdown-item navbar-dropdown-logout" 228 - > 229 - <LogoutIcon size={16} /> 230 - Sign Out 231 - </button> 232 - </div> 233 - )} 234 - </div> 235 - </> 236 - ) : ( 237 - <Link to="/login" className="navbar-signin"> 238 - Sign In 239 - </Link> 240 - ))} 241 - </div> 242 - </div> 243 - </nav> 244 - ); 245 - }
-1
web/src/components/ReplyList.jsx
··· 1 - import React from "react"; 2 1 import { Link } from "react-router-dom"; 3 2 import { MessageSquare, Trash2, Reply } from "lucide-react"; 4 3
+195
web/src/components/RightSidebar.jsx
··· 1 + import { useState, useEffect } from "react"; 2 + import { Link } from "react-router-dom"; 3 + import { ExternalLink } from "lucide-react"; 4 + import { 5 + SiFirefox, 6 + SiGooglechrome, 7 + SiGithub, 8 + SiBluesky, 9 + SiApple, 10 + SiKofi, 11 + } from "react-icons/si"; 12 + import { FaEdge } from "react-icons/fa"; 13 + import { useAuth } from "../context/AuthContext"; 14 + import { getTrendingTags } from "../api/client"; 15 + 16 + const isFirefox = 17 + typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent); 18 + const isEdge = 19 + typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent); 20 + const isMobileSafari = 21 + typeof navigator !== "undefined" && 22 + /iPhone|iPad|iPod/.test(navigator.userAgent) && 23 + /Safari/.test(navigator.userAgent) && 24 + !/CriOS|FxiOS|OPiOS|EdgiOS/.test(navigator.userAgent); 25 + 26 + function getExtensionInfo() { 27 + if (isMobileSafari) { 28 + return { 29 + url: "https://margin.at/soon", 30 + icon: SiApple, 31 + name: "iOS", 32 + label: "Coming Soon", 33 + }; 34 + } 35 + if (isFirefox) { 36 + return { 37 + url: "https://addons.mozilla.org/en-US/firefox/addon/margin/", 38 + icon: SiFirefox, 39 + name: "Firefox", 40 + label: "Install for Firefox", 41 + }; 42 + } 43 + if (isEdge) { 44 + return { 45 + url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn", 46 + icon: FaEdge, 47 + name: "Edge", 48 + label: "Install for Edge", 49 + }; 50 + } 51 + return { 52 + url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/", 53 + icon: SiGooglechrome, 54 + name: "Chrome", 55 + label: "Install for Chrome", 56 + }; 57 + } 58 + 59 + export default function RightSidebar() { 60 + const { isAuthenticated } = useAuth(); 61 + const ext = getExtensionInfo(); 62 + const ExtIcon = ext.icon; 63 + const [trendingTags, setTrendingTags] = useState([]); 64 + const [loading, setLoading] = useState(true); 65 + 66 + useEffect(() => { 67 + getTrendingTags() 68 + .then((tags) => setTrendingTags(tags)) 69 + .catch((err) => console.error("Failed to fetch trending tags:", err)) 70 + .finally(() => setLoading(false)); 71 + }, []); 72 + 73 + return ( 74 + <aside className="right-sidebar"> 75 + <div className="right-section"> 76 + <h3 className="right-section-title"> 77 + {isMobileSafari ? "Save from Safari" : "Get the Extension"} 78 + </h3> 79 + <p className="right-section-desc"> 80 + {isMobileSafari 81 + ? "Bookmark pages using Safari's share sheet" 82 + : "Annotate, highlight, and bookmark any webpage"} 83 + </p> 84 + <a 85 + href={ext.url} 86 + target="_blank" 87 + rel="noopener noreferrer" 88 + className="right-extension-btn" 89 + > 90 + <ExtIcon size={18} /> 91 + {ext.label} 92 + <ExternalLink size={14} /> 93 + </a> 94 + </div> 95 + 96 + {isAuthenticated ? ( 97 + <div className="right-section"> 98 + <h3 className="right-section-title">Trending Tags</h3> 99 + <div className="right-links"> 100 + {loading ? ( 101 + <span className="right-section-desc">Loading...</span> 102 + ) : trendingTags.length > 0 ? ( 103 + trendingTags.map(({ tag, count }) => ( 104 + <Link 105 + key={tag} 106 + to={`/?tag=${encodeURIComponent(tag)}`} 107 + className="right-link" 108 + > 109 + <span>#{tag}</span> 110 + <span style={{ fontSize: "0.75rem", opacity: 0.6 }}> 111 + {count} 112 + </span> 113 + </Link> 114 + )) 115 + ) : ( 116 + <span className="right-section-desc">No trending tags yet</span> 117 + )} 118 + </div> 119 + </div> 120 + ) : ( 121 + <div className="right-section"> 122 + <h3 className="right-section-title">Explore</h3> 123 + <nav className="right-links"> 124 + <Link to="/url" className="right-link"> 125 + Browse by URL 126 + </Link> 127 + <Link to="/highlights" className="right-link"> 128 + Public Highlights 129 + </Link> 130 + </nav> 131 + </div> 132 + )} 133 + 134 + <div className="right-section"> 135 + <h3 className="right-section-title">Resources</h3> 136 + <nav className="right-links"> 137 + <a 138 + href="https://github.com/margin-at/margin" 139 + target="_blank" 140 + rel="noopener noreferrer" 141 + className="right-link" 142 + > 143 + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 144 + <SiGithub size={16} /> 145 + GitHub 146 + </div> 147 + <ExternalLink size={12} /> 148 + </a> 149 + <a 150 + href="https://tangled.org/margin.at/margin" 151 + target="_blank" 152 + rel="noopener noreferrer" 153 + className="right-link" 154 + > 155 + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 156 + <div className="tangled-icon" /> 157 + Tangled 158 + </div> 159 + <ExternalLink size={12} /> 160 + </a> 161 + <a 162 + href="https://bsky.app/profile/margin.at" 163 + target="_blank" 164 + rel="noopener noreferrer" 165 + className="right-link" 166 + > 167 + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 168 + <SiBluesky size={16} /> 169 + Bluesky 170 + </div> 171 + <ExternalLink size={12} /> 172 + </a> 173 + <a 174 + href="https://ko-fi.com/scan" 175 + target="_blank" 176 + rel="noopener noreferrer" 177 + className="right-link" 178 + > 179 + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 180 + <SiKofi size={16} /> 181 + Donate 182 + </div> 183 + <ExternalLink size={12} /> 184 + </a> 185 + </nav> 186 + </div> 187 + 188 + <div className="right-footer"> 189 + <Link to="/privacy">Privacy</Link> 190 + <span>ยท</span> 191 + <Link to="/terms">Terms</Link> 192 + </div> 193 + </aside> 194 + ); 195 + }
+12
web/src/components/ScrollToTop.jsx
··· 1 + import { useEffect } from "react"; 2 + import { useLocation } from "react-router-dom"; 3 + 4 + export default function ScrollToTop() { 5 + const { pathname } = useLocation(); 6 + 7 + useEffect(() => { 8 + window.scrollTo(0, 0); 9 + }, [pathname]); 10 + 11 + return null; 12 + }
+3 -1
web/src/components/ShareMenu.jsx
··· 171 171 text: text?.substring(0, 100), 172 172 url: shareUrl, 173 173 }); 174 - } catch {} 174 + } catch { 175 + /* ignore */ 176 + } 175 177 } 176 178 setIsOpen(false); 177 179 };
+189
web/src/components/Sidebar.jsx
··· 1 + import { useState, useRef, useEffect } from "react"; 2 + import { Link, useLocation } from "react-router-dom"; 3 + import { useAuth } from "../context/AuthContext"; 4 + import { 5 + Home, 6 + Search, 7 + Folder, 8 + Bell, 9 + PenSquare, 10 + User, 11 + LogOut, 12 + MoreHorizontal, 13 + Highlighter, 14 + Bookmark, 15 + } from "lucide-react"; 16 + import { getUnreadNotificationCount } from "../api/client"; 17 + import logo from "../assets/logo.svg"; 18 + 19 + export default function Sidebar() { 20 + const { user, isAuthenticated, logout, loading } = useAuth(); 21 + const location = useLocation(); 22 + const [menuOpen, setMenuOpen] = useState(false); 23 + const [unreadCount, setUnreadCount] = useState(0); 24 + const menuRef = useRef(null); 25 + 26 + const isActive = (path) => { 27 + if (path === "/") return location.pathname === "/"; 28 + return location.pathname.startsWith(path); 29 + }; 30 + 31 + useEffect(() => { 32 + if (isAuthenticated) { 33 + getUnreadNotificationCount() 34 + .then((data) => setUnreadCount(data.count || 0)) 35 + .catch(() => {}); 36 + const interval = setInterval(() => { 37 + getUnreadNotificationCount() 38 + .then((data) => setUnreadCount(data.count || 0)) 39 + .catch(() => {}); 40 + }, 60000); 41 + return () => clearInterval(interval); 42 + } 43 + }, [isAuthenticated]); 44 + 45 + useEffect(() => { 46 + const handleClickOutside = (e) => { 47 + if (menuRef.current && !menuRef.current.contains(e.target)) { 48 + setMenuOpen(false); 49 + } 50 + }; 51 + document.addEventListener("mousedown", handleClickOutside); 52 + return () => document.removeEventListener("mousedown", handleClickOutside); 53 + }, []); 54 + 55 + const getInitials = () => { 56 + if (user?.displayName) { 57 + return user.displayName.substring(0, 2).toUpperCase(); 58 + } 59 + if (user?.handle) { 60 + return user.handle.substring(0, 2).toUpperCase(); 61 + } 62 + return "U"; 63 + }; 64 + 65 + return ( 66 + <aside className="sidebar"> 67 + <Link to="/" className="sidebar-header"> 68 + <img src={logo} alt="Margin" className="sidebar-logo" /> 69 + <span className="sidebar-brand">Margin</span> 70 + </Link> 71 + 72 + <nav className="sidebar-nav"> 73 + <Link 74 + to="/" 75 + className={`sidebar-link ${isActive("/") ? "active" : ""}`} 76 + > 77 + <Home size={20} /> 78 + <span>Home</span> 79 + </Link> 80 + <Link 81 + to="/url" 82 + className={`sidebar-link ${isActive("/url") ? "active" : ""}`} 83 + > 84 + <Search size={20} /> 85 + <span>Browse</span> 86 + </Link> 87 + 88 + {isAuthenticated && ( 89 + <> 90 + <div className="sidebar-section-title">Library</div> 91 + <Link 92 + to="/highlights" 93 + className={`sidebar-link ${isActive("/highlights") ? "active" : ""}`} 94 + > 95 + <Highlighter size={20} /> 96 + <span>Highlights</span> 97 + </Link> 98 + <Link 99 + to="/bookmarks" 100 + className={`sidebar-link ${isActive("/bookmarks") ? "active" : ""}`} 101 + > 102 + <Bookmark size={20} /> 103 + <span>Bookmarks</span> 104 + </Link> 105 + <Link 106 + to="/collections" 107 + className={`sidebar-link ${isActive("/collections") ? "active" : ""}`} 108 + > 109 + <Folder size={20} /> 110 + <span>Collections</span> 111 + </Link> 112 + <Link 113 + to="/notifications" 114 + className={`sidebar-link ${isActive("/notifications") ? "active" : ""}`} 115 + onClick={() => setUnreadCount(0)} 116 + > 117 + <Bell size={20} /> 118 + <span>Notifications</span> 119 + {unreadCount > 0 && ( 120 + <span className="notification-badge">{unreadCount}</span> 121 + )} 122 + </Link> 123 + </> 124 + )} 125 + </nav> 126 + 127 + {isAuthenticated && ( 128 + <Link to="/new" className="sidebar-new-btn"> 129 + <PenSquare size={18} /> 130 + <span>New</span> 131 + </Link> 132 + )} 133 + 134 + <div className="sidebar-footer" ref={menuRef}> 135 + {!loading && 136 + (isAuthenticated ? ( 137 + <> 138 + <div 139 + className="sidebar-user" 140 + onClick={() => setMenuOpen(!menuOpen)} 141 + > 142 + <div className="sidebar-avatar"> 143 + {user?.avatar ? ( 144 + <img src={user.avatar} alt={user.displayName} /> 145 + ) : ( 146 + <span>{getInitials()}</span> 147 + )} 148 + </div> 149 + <div className="sidebar-user-info"> 150 + <div className="sidebar-user-name"> 151 + {user?.displayName || user?.handle} 152 + </div> 153 + <div className="sidebar-user-handle">@{user?.handle}</div> 154 + </div> 155 + <MoreHorizontal size={18} className="sidebar-user-menu" /> 156 + </div> 157 + 158 + {menuOpen && ( 159 + <div className="sidebar-dropdown"> 160 + <Link 161 + to={`/profile/${user?.did}`} 162 + className="sidebar-dropdown-item" 163 + onClick={() => setMenuOpen(false)} 164 + > 165 + <User size={16} /> 166 + View Profile 167 + </Link> 168 + <button 169 + onClick={() => { 170 + logout(); 171 + setMenuOpen(false); 172 + }} 173 + className="sidebar-dropdown-item danger" 174 + > 175 + <LogOut size={16} /> 176 + Sign Out 177 + </button> 178 + </div> 179 + )} 180 + </> 181 + ) : ( 182 + <Link to="/login" className="sidebar-new-btn" style={{ margin: 0 }}> 183 + Sign In 184 + </Link> 185 + ))} 186 + </div> 187 + </aside> 188 + ); 189 + }
+4 -1
web/src/context/AuthContext.jsx
··· 48 48 const handleLogout = async () => { 49 49 try { 50 50 await logout(); 51 - } catch {} 51 + } catch (e) { 52 + console.warn("Logout failed", e); 53 + } 52 54 setUser(null); 53 55 }; 54 56 ··· 64 66 return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; 65 67 } 66 68 69 + // eslint-disable-next-line react-refresh/only-export-components 67 70 export function useAuth() { 68 71 const context = useContext(AuthContext); 69 72 if (!context) {
+506
web/src/css/annotations.css
··· 1 + .annotation-detail-page { 2 + max-width: 680px; 3 + margin: 0 auto; 4 + padding: 24px 16px; 5 + min-height: 100vh; 6 + } 7 + 8 + .annotation-detail-header { 9 + margin-bottom: 24px; 10 + } 11 + 12 + .back-link { 13 + display: inline-flex; 14 + align-items: center; 15 + color: var(--text-tertiary); 16 + text-decoration: none; 17 + font-size: 0.9rem; 18 + font-weight: 500; 19 + transition: color 0.15s; 20 + } 21 + 22 + .back-link:hover { 23 + color: var(--text-primary); 24 + } 25 + 26 + .replies-section { 27 + margin-top: 32px; 28 + border-top: 1px solid var(--border); 29 + padding-top: 24px; 30 + } 31 + 32 + .replies-title { 33 + display: flex; 34 + align-items: center; 35 + gap: 8px; 36 + font-size: 1.1rem; 37 + font-weight: 600; 38 + color: var(--text-primary); 39 + margin-bottom: 20px; 40 + } 41 + 42 + .annotation-card { 43 + display: flex; 44 + flex-direction: column; 45 + gap: 12px; 46 + padding: 20px 0; 47 + border-bottom: 1px solid var(--border); 48 + transition: background 0.15s ease; 49 + } 50 + 51 + .annotation-card:last-child { 52 + border-bottom: none; 53 + } 54 + 55 + .annotation-header { 56 + display: flex; 57 + justify-content: space-between; 58 + align-items: flex-start; 59 + gap: 12px; 60 + } 61 + 62 + .annotation-header-left { 63 + display: flex; 64 + align-items: center; 65 + gap: 10px; 66 + flex: 1; 67 + min-width: 0; 68 + } 69 + 70 + .annotation-avatar { 71 + width: 36px; 72 + height: 36px; 73 + min-width: 36px; 74 + border-radius: 50%; 75 + background: var(--bg-tertiary); 76 + display: flex; 77 + align-items: center; 78 + justify-content: center; 79 + font-weight: 600; 80 + font-size: 0.85rem; 81 + color: var(--text-secondary); 82 + overflow: hidden; 83 + } 84 + 85 + .annotation-avatar img { 86 + width: 100%; 87 + height: 100%; 88 + object-fit: cover; 89 + } 90 + 91 + .annotation-meta { 92 + display: flex; 93 + flex-direction: column; 94 + justify-content: center; 95 + line-height: 1.3; 96 + } 97 + 98 + .annotation-avatar-link { 99 + text-decoration: none; 100 + border-radius: 50%; 101 + } 102 + 103 + .annotation-author-row { 104 + display: flex; 105 + align-items: baseline; 106 + gap: 6px; 107 + flex-wrap: wrap; 108 + } 109 + 110 + .annotation-author { 111 + font-weight: 600; 112 + color: var(--text-primary); 113 + font-size: 0.9rem; 114 + } 115 + 116 + .annotation-handle { 117 + font-size: 0.85rem; 118 + color: var(--text-tertiary); 119 + text-decoration: none; 120 + } 121 + 122 + .annotation-handle:hover { 123 + color: var(--text-secondary); 124 + } 125 + 126 + .annotation-time { 127 + font-size: 0.75rem; 128 + color: var(--text-tertiary); 129 + } 130 + 131 + .annotation-content { 132 + display: flex; 133 + flex-direction: column; 134 + gap: 10px; 135 + padding-left: 46px; 136 + } 137 + 138 + .annotation-source { 139 + display: inline-flex; 140 + align-items: center; 141 + gap: 6px; 142 + font-size: 0.75rem; 143 + color: var(--text-tertiary); 144 + text-decoration: none; 145 + transition: color 0.15s ease; 146 + max-width: 100%; 147 + overflow: hidden; 148 + text-overflow: ellipsis; 149 + white-space: nowrap; 150 + } 151 + 152 + .annotation-source:hover { 153 + color: var(--text-secondary); 154 + text-decoration: underline; 155 + } 156 + 157 + .annotation-source-title { 158 + color: var(--text-tertiary); 159 + opacity: 0.7; 160 + } 161 + 162 + .annotation-highlight { 163 + display: block; 164 + position: relative; 165 + padding-left: 12px; 166 + margin: 4px 0; 167 + text-decoration: none; 168 + border-left: 2px solid var(--border); 169 + transition: all 0.15s ease; 170 + } 171 + 172 + .annotation-highlight:hover { 173 + border-left-color: var(--text-secondary); 174 + } 175 + 176 + .annotation-highlight mark { 177 + background: transparent; 178 + color: var(--text-primary); 179 + font-style: italic; 180 + font-size: 1rem; 181 + line-height: 1.6; 182 + font-weight: 400; 183 + font-family: var(--font-serif, var(--font-sans)); 184 + display: inline; 185 + overflow-wrap: anywhere; 186 + word-break: break-all; 187 + padding-right: 4px; 188 + } 189 + 190 + .annotation-text { 191 + font-size: 0.95rem; 192 + line-height: 1.6; 193 + color: var(--text-primary); 194 + white-space: pre-wrap; 195 + } 196 + 197 + .annotation-tags { 198 + display: flex; 199 + flex-wrap: wrap; 200 + gap: 6px; 201 + margin-top: 4px; 202 + } 203 + 204 + .annotation-tag { 205 + font-size: 0.8rem; 206 + color: var(--accent); 207 + text-decoration: none; 208 + font-weight: 500; 209 + opacity: 0.9; 210 + transition: opacity 0.15s; 211 + } 212 + 213 + .annotation-tag:hover { 214 + opacity: 1; 215 + text-decoration: underline; 216 + } 217 + 218 + .annotation-actions { 219 + display: flex; 220 + align-items: center; 221 + justify-content: space-between; 222 + margin-top: 4px; 223 + padding-left: 46px; 224 + } 225 + 226 + .annotation-actions-left { 227 + display: flex; 228 + align-items: center; 229 + gap: 16px; 230 + } 231 + 232 + .annotation-action { 233 + display: flex; 234 + align-items: center; 235 + gap: 6px; 236 + color: var(--text-tertiary); 237 + font-size: 0.8rem; 238 + font-weight: 500; 239 + padding: 6px; 240 + margin-left: -6px; 241 + border-radius: var(--radius-sm); 242 + transition: all 0.15s ease; 243 + background: transparent; 244 + cursor: pointer; 245 + border: none; 246 + } 247 + 248 + .annotation-action:hover { 249 + color: var(--text-secondary); 250 + background: var(--bg-tertiary); 251 + } 252 + 253 + .annotation-action.liked { 254 + color: #ef4444; 255 + } 256 + 257 + .annotation-action.liked svg { 258 + fill: #ef4444; 259 + } 260 + 261 + .annotation-action.active { 262 + color: var(--accent); 263 + } 264 + 265 + .action-icon-only { 266 + padding: 6px; 267 + } 268 + 269 + .annotation-header-right { 270 + opacity: 0; 271 + transition: opacity 0.15s; 272 + } 273 + 274 + .annotation-card:hover .annotation-header-right { 275 + opacity: 1; 276 + } 277 + 278 + .inline-replies { 279 + margin-top: 12px; 280 + padding-left: 46px; 281 + } 282 + 283 + .annotation-text, 284 + .reply-text, 285 + .history-content { 286 + overflow-wrap: break-word; 287 + word-break: break-word; 288 + max-width: 100%; 289 + } 290 + 291 + .annotation-highlight mark { 292 + overflow-wrap: break-word; 293 + word-break: break-word; 294 + display: inline; 295 + } 296 + 297 + .annotation-header-left, 298 + .annotation-meta, 299 + .reply-meta { 300 + min-width: 0; 301 + max-width: 100%; 302 + } 303 + 304 + .annotation-author-row, 305 + .reply-author { 306 + max-width: 100%; 307 + } 308 + 309 + .annotation-source { 310 + max-width: 100%; 311 + } 312 + 313 + @media (max-width: 768px) { 314 + .annotation-content, 315 + .annotation-actions, 316 + .inline-replies { 317 + padding-left: 0; 318 + } 319 + 320 + .annotation-header-right { 321 + opacity: 1; 322 + } 323 + } 324 + 325 + .replies-list-threaded { 326 + margin-top: 16px; 327 + display: flex; 328 + flex-direction: column; 329 + } 330 + 331 + .reply-card-threaded { 332 + position: relative; 333 + padding-left: 0; 334 + transition: background 0.15s; 335 + } 336 + 337 + .reply-header { 338 + display: flex; 339 + align-items: center; 340 + gap: 10px; 341 + margin-bottom: 6px; 342 + } 343 + 344 + .reply-avatar { 345 + width: 28px; 346 + height: 28px; 347 + border-radius: 50%; 348 + background: var(--bg-tertiary); 349 + overflow: hidden; 350 + flex-shrink: 0; 351 + display: flex; 352 + align-items: center; 353 + justify-content: center; 354 + } 355 + 356 + .reply-avatar img { 357 + width: 100%; 358 + height: 100%; 359 + object-fit: cover; 360 + } 361 + 362 + .reply-avatar span { 363 + font-size: 0.7rem; 364 + font-weight: 600; 365 + color: var(--text-secondary); 366 + } 367 + 368 + .reply-meta { 369 + display: flex; 370 + align-items: baseline; 371 + gap: 6px; 372 + flex: 1; 373 + min-width: 0; 374 + } 375 + 376 + .reply-author { 377 + font-weight: 600; 378 + font-size: 0.85rem; 379 + color: var(--text-primary); 380 + white-space: nowrap; 381 + overflow: hidden; 382 + text-overflow: ellipsis; 383 + } 384 + 385 + .reply-handle { 386 + font-size: 0.8rem; 387 + color: var(--text-tertiary); 388 + text-decoration: none; 389 + white-space: nowrap; 390 + overflow: hidden; 391 + text-overflow: ellipsis; 392 + } 393 + 394 + .reply-time { 395 + font-size: 0.75rem; 396 + color: var(--text-tertiary); 397 + white-space: nowrap; 398 + } 399 + 400 + .reply-dot { 401 + color: var(--text-tertiary); 402 + font-size: 0.7rem; 403 + } 404 + 405 + .reply-text { 406 + font-size: 0.9rem; 407 + line-height: 1.5; 408 + color: var(--text-primary); 409 + margin: 0; 410 + padding-left: 38px; 411 + } 412 + 413 + .reply-actions { 414 + display: flex; 415 + align-items: center; 416 + gap: 4px; 417 + opacity: 0; 418 + transition: opacity 0.15s; 419 + } 420 + 421 + .reply-card-threaded:hover .reply-actions { 422 + opacity: 1; 423 + } 424 + 425 + .reply-action-btn { 426 + background: none; 427 + border: none; 428 + padding: 4px; 429 + color: var(--text-tertiary); 430 + cursor: pointer; 431 + border-radius: 4px; 432 + display: flex; 433 + align-items: center; 434 + justify-content: center; 435 + } 436 + 437 + .reply-action-btn:hover { 438 + background: var(--bg-tertiary); 439 + color: var(--text-secondary); 440 + } 441 + 442 + .reply-action-delete:hover { 443 + color: #ef4444; 444 + background: rgba(239, 68, 68, 0.1); 445 + } 446 + 447 + .reply-form { 448 + border: 1px solid var(--border); 449 + border-radius: var(--radius-md); 450 + padding: 16px; 451 + background: var(--bg-secondary); 452 + margin-bottom: 24px; 453 + } 454 + 455 + .replying-to-banner { 456 + display: flex; 457 + justify-content: space-between; 458 + align-items: center; 459 + background: var(--bg-tertiary); 460 + padding: 8px 12px; 461 + border-radius: var(--radius-sm); 462 + margin-bottom: 12px; 463 + font-size: 0.85rem; 464 + color: var(--text-secondary); 465 + } 466 + 467 + .cancel-reply { 468 + background: none; 469 + border: none; 470 + color: var(--text-tertiary); 471 + cursor: pointer; 472 + font-size: 1.2rem; 473 + padding: 0 4px; 474 + line-height: 1; 475 + } 476 + 477 + .cancel-reply:hover { 478 + color: var(--text-primary); 479 + } 480 + 481 + .reply-input { 482 + width: 100%; 483 + background: var(--bg-primary); 484 + border: 1px solid var(--border); 485 + border-radius: var(--radius-sm); 486 + padding: 12px; 487 + color: var(--text-primary); 488 + font-family: inherit; 489 + font-size: 0.95rem; 490 + resize: vertical; 491 + min-height: 80px; 492 + transition: border-color 0.15s; 493 + display: block; 494 + box-sizing: border-box; 495 + } 496 + 497 + .reply-input:focus { 498 + outline: none; 499 + border-color: var(--accent); 500 + } 501 + 502 + .reply-form-actions { 503 + display: flex; 504 + justify-content: flex-end; 505 + margin-top: 12px; 506 + }
+142
web/src/css/base.css
··· 1 + :root { 2 + --bg-primary: #09090b; 3 + --bg-secondary: #0f0f12; 4 + --bg-tertiary: #18181b; 5 + --bg-card: #09090b; 6 + --bg-elevated: #18181b; 7 + --text-primary: #e4e4e7; 8 + --text-secondary: #a1a1aa; 9 + --text-tertiary: #71717a; 10 + --border: #27272a; 11 + --border-hover: #3f3f46; 12 + --accent: #6366f1; 13 + --accent-hover: #4f46e5; 14 + --accent-subtle: rgba(99, 102, 241, 0.1); 15 + --accent-text: #818cf8; 16 + --success: #10b981; 17 + --error: #ef4444; 18 + --warning: #f59e0b; 19 + --info: #3b82f6; 20 + --radius-sm: 4px; 21 + --radius-md: 6px; 22 + --radius-lg: 8px; 23 + --radius-full: 9999px; 24 + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 25 + --shadow-md: 26 + 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 27 + --shadow-lg: 28 + 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 29 + --font-sans: 30 + "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; 31 + --font-mono: 32 + "JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace; 33 + } 34 + 35 + * { 36 + margin: 0; 37 + padding: 0; 38 + box-sizing: border-box; 39 + } 40 + 41 + html { 42 + font-size: 16px; 43 + -webkit-text-size-adjust: 100%; 44 + overflow-x: hidden; 45 + } 46 + 47 + body { 48 + font-family: var(--font-sans); 49 + background: var(--bg-primary); 50 + color: var(--text-primary); 51 + line-height: 1.5; 52 + min-height: 100vh; 53 + -webkit-font-smoothing: antialiased; 54 + -moz-osx-font-smoothing: grayscale; 55 + overflow-x: hidden; 56 + max-width: 100vw; 57 + } 58 + 59 + a { 60 + color: inherit; 61 + text-decoration: none; 62 + transition: color 0.15s ease; 63 + } 64 + 65 + h1, 66 + h2, 67 + h3, 68 + h4, 69 + h5, 70 + h6 { 71 + font-weight: 600; 72 + line-height: 1.25; 73 + letter-spacing: -0.025em; 74 + color: var(--text-primary); 75 + } 76 + 77 + p { 78 + color: var(--text-secondary); 79 + } 80 + 81 + button { 82 + font-family: inherit; 83 + cursor: pointer; 84 + border: none; 85 + background: none; 86 + } 87 + 88 + input, 89 + textarea, 90 + select { 91 + font-family: inherit; 92 + font-size: inherit; 93 + color: var(--text-primary); 94 + } 95 + 96 + ::selection { 97 + background: var(--accent-subtle); 98 + color: var(--accent-text); 99 + } 100 + 101 + .text-sm { 102 + font-size: 0.875rem; 103 + } 104 + 105 + .text-xs { 106 + font-size: 0.75rem; 107 + } 108 + 109 + .font-medium { 110 + font-weight: 500; 111 + } 112 + 113 + .font-semibold { 114 + font-weight: 600; 115 + } 116 + 117 + .text-muted { 118 + color: var(--text-secondary); 119 + } 120 + 121 + .text-faint { 122 + color: var(--text-tertiary); 123 + } 124 + 125 + ::-webkit-scrollbar { 126 + width: 10px; 127 + height: 10px; 128 + } 129 + 130 + ::-webkit-scrollbar-track { 131 + background: transparent; 132 + } 133 + 134 + ::-webkit-scrollbar-thumb { 135 + background: var(--border); 136 + border-radius: 5px; 137 + border: 2px solid var(--bg-primary); 138 + } 139 + 140 + ::-webkit-scrollbar-thumb:hover { 141 + background: var(--border-hover); 142 + }
+129
web/src/css/buttons.css
··· 1 + .btn { 2 + display: inline-flex; 3 + align-items: center; 4 + justify-content: center; 5 + gap: 8px; 6 + padding: 10px 20px; 7 + font-size: 0.9rem; 8 + font-weight: 500; 9 + border-radius: var(--radius-md); 10 + transition: all 0.15s ease; 11 + white-space: pre; 12 + } 13 + 14 + .btn-primary { 15 + background: var(--accent); 16 + color: white; 17 + } 18 + 19 + .btn-primary:hover { 20 + background: var(--accent-hover); 21 + transform: translateY(-1px); 22 + box-shadow: var(--shadow-md); 23 + } 24 + 25 + .btn-secondary { 26 + background: var(--bg-tertiary); 27 + color: var(--text-primary); 28 + border: 1px solid var(--border); 29 + } 30 + 31 + .btn-secondary:hover { 32 + background: var(--bg-hover); 33 + border-color: var(--border-hover); 34 + } 35 + 36 + .btn-ghost { 37 + color: var(--text-secondary); 38 + padding: 8px 12px; 39 + } 40 + 41 + .btn-ghost:hover { 42 + color: var(--text-primary); 43 + background: var(--bg-tertiary); 44 + } 45 + 46 + .btn-bluesky { 47 + background: #0085ff; 48 + color: white; 49 + display: flex; 50 + align-items: center; 51 + justify-content: center; 52 + gap: 10px; 53 + transition: 54 + background 0.2s, 55 + transform 0.2s; 56 + } 57 + 58 + .btn-bluesky:hover { 59 + background: #0070dd; 60 + transform: translateY(-1px); 61 + } 62 + 63 + .btn-sm { 64 + padding: 6px 12px; 65 + font-size: 0.85rem; 66 + } 67 + 68 + .btn-text { 69 + background: none; 70 + border: none; 71 + color: var(--text-secondary); 72 + font-size: 0.9rem; 73 + padding: 8px 12px; 74 + cursor: pointer; 75 + transition: color 0.15s; 76 + } 77 + 78 + .btn-text:hover { 79 + color: var(--text-primary); 80 + } 81 + 82 + .btn-block { 83 + width: 100%; 84 + text-align: left; 85 + padding: 8px 12px; 86 + color: var(--text-secondary); 87 + background: var(--bg-tertiary); 88 + border-radius: var(--radius-md); 89 + margin-top: 8px; 90 + font-size: 0.9rem; 91 + cursor: pointer; 92 + transition: all 0.2s; 93 + } 94 + 95 + .btn-block:hover { 96 + background: var(--border); 97 + color: var(--text-primary); 98 + } 99 + 100 + .btn-icon-danger { 101 + padding: 8px; 102 + background: var(--error); 103 + color: white; 104 + border: none; 105 + border-radius: var(--radius-md); 106 + cursor: pointer; 107 + box-shadow: var(--shadow-md); 108 + transition: all 0.15s ease; 109 + display: flex; 110 + align-items: center; 111 + justify-content: center; 112 + } 113 + 114 + .btn-icon-danger:hover { 115 + background: #dc2626; 116 + transform: scale(1.05); 117 + } 118 + 119 + .action-buttons { 120 + display: flex; 121 + gap: 8px; 122 + flex-wrap: wrap; 123 + } 124 + 125 + .action-buttons-end { 126 + display: flex; 127 + justify-content: flex-end; 128 + gap: 8px; 129 + }
+326
web/src/css/collections.css
··· 1 + .collections-list { 2 + display: flex; 3 + flex-direction: column; 4 + gap: 2px; 5 + background: var(--bg-card); 6 + border: 1px solid var(--border); 7 + border-radius: var(--radius-lg); 8 + overflow: hidden; 9 + } 10 + 11 + .collection-row { 12 + display: flex; 13 + align-items: center; 14 + background: var(--bg-card); 15 + transition: background 0.15s ease; 16 + } 17 + 18 + .collection-row:not(:last-child) { 19 + border-bottom: 1px solid var(--border); 20 + } 21 + 22 + .collection-row:hover { 23 + background: var(--bg-secondary); 24 + } 25 + 26 + .collection-row-content { 27 + flex: 1; 28 + display: flex; 29 + align-items: center; 30 + gap: 16px; 31 + padding: 16px 20px; 32 + text-decoration: none; 33 + min-width: 0; 34 + } 35 + 36 + .collection-row-icon { 37 + width: 44px; 38 + height: 44px; 39 + min-width: 44px; 40 + display: flex; 41 + align-items: center; 42 + justify-content: center; 43 + background: linear-gradient( 44 + 135deg, 45 + rgba(79, 70, 229, 0.1), 46 + rgba(168, 85, 247, 0.15) 47 + ); 48 + color: var(--accent); 49 + border-radius: var(--radius-md); 50 + transition: all 0.2s ease; 51 + } 52 + 53 + .collection-row:hover .collection-row-icon { 54 + background: linear-gradient( 55 + 135deg, 56 + rgba(79, 70, 229, 0.15), 57 + rgba(168, 85, 247, 0.2) 58 + ); 59 + transform: scale(1.05); 60 + } 61 + 62 + .collection-row-info { 63 + flex: 1; 64 + min-width: 0; 65 + } 66 + 67 + .collection-row-name { 68 + font-size: 1rem; 69 + font-weight: 600; 70 + color: var(--text-primary); 71 + margin: 0 0 2px 0; 72 + white-space: nowrap; 73 + overflow: hidden; 74 + text-overflow: ellipsis; 75 + } 76 + 77 + .collection-row:hover .collection-row-name { 78 + color: var(--accent); 79 + } 80 + 81 + .collection-row-desc { 82 + font-size: 0.85rem; 83 + color: var(--text-secondary); 84 + margin: 0; 85 + white-space: nowrap; 86 + overflow: hidden; 87 + text-overflow: ellipsis; 88 + } 89 + 90 + .collection-row-arrow { 91 + color: var(--text-tertiary); 92 + opacity: 0; 93 + transition: all 0.2s ease; 94 + } 95 + 96 + .collection-row:hover .collection-row-arrow { 97 + opacity: 1; 98 + color: var(--accent); 99 + transform: translateX(2px); 100 + } 101 + 102 + .collection-row-edit { 103 + padding: 10px; 104 + margin-right: 12px; 105 + color: var(--text-tertiary); 106 + background: none; 107 + border: none; 108 + border-radius: var(--radius-sm); 109 + cursor: pointer; 110 + opacity: 0; 111 + transition: all 0.15s ease; 112 + } 113 + 114 + .collection-row:hover .collection-row-edit { 115 + opacity: 1; 116 + } 117 + 118 + .collection-row-edit:hover { 119 + color: var(--text-primary); 120 + background: var(--bg-tertiary); 121 + } 122 + 123 + .collection-detail-header { 124 + display: flex; 125 + gap: 20px; 126 + padding: 24px; 127 + background: var(--bg-card); 128 + border: 1px solid var(--border); 129 + border-radius: var(--radius-lg); 130 + margin-bottom: 32px; 131 + position: relative; 132 + } 133 + 134 + .collection-detail-icon { 135 + width: 56px; 136 + height: 56px; 137 + min-width: 56px; 138 + display: flex; 139 + align-items: center; 140 + justify-content: center; 141 + background: linear-gradient( 142 + 135deg, 143 + rgba(79, 70, 229, 0.1), 144 + rgba(168, 85, 247, 0.1) 145 + ); 146 + color: var(--accent); 147 + border-radius: var(--radius-md); 148 + } 149 + 150 + .collection-detail-info { 151 + flex: 1; 152 + min-width: 0; 153 + } 154 + 155 + .collection-detail-visibility { 156 + display: flex; 157 + align-items: center; 158 + gap: 6px; 159 + font-size: 0.8rem; 160 + font-weight: 600; 161 + color: var(--accent); 162 + text-transform: capitalize; 163 + margin-bottom: 8px; 164 + } 165 + 166 + .collection-detail-title { 167 + font-size: 1.5rem; 168 + font-weight: 700; 169 + color: var(--text-primary); 170 + margin-bottom: 8px; 171 + line-height: 1.3; 172 + } 173 + 174 + @media (max-width: 600px) { 175 + .collection-detail-header { 176 + flex-direction: column; 177 + padding: 16px; 178 + gap: 16px; 179 + } 180 + 181 + .collection-detail-actions { 182 + position: static; 183 + margin-top: -8px; 184 + justify-content: flex-end; 185 + } 186 + } 187 + 188 + .collection-detail-desc { 189 + color: var(--text-secondary); 190 + font-size: 1rem; 191 + line-height: 1.5; 192 + margin-bottom: 12px; 193 + max-width: 600px; 194 + overflow-wrap: break-word; 195 + word-break: break-word; 196 + } 197 + 198 + .collection-detail-stats { 199 + display: flex; 200 + align-items: center; 201 + gap: 8px; 202 + font-size: 0.85rem; 203 + color: var(--text-tertiary); 204 + } 205 + 206 + .collection-detail-actions { 207 + position: absolute; 208 + top: 20px; 209 + right: 20px; 210 + display: flex; 211 + align-items: center; 212 + gap: 8px; 213 + } 214 + 215 + .collection-detail-actions .share-menu-container { 216 + display: flex; 217 + align-items: center; 218 + } 219 + 220 + .collection-detail-actions .annotation-action { 221 + padding: 10px; 222 + color: var(--text-tertiary); 223 + background: none; 224 + border: none; 225 + border-radius: var(--radius-sm); 226 + cursor: pointer; 227 + transition: all 0.15s ease; 228 + } 229 + 230 + .collection-detail-actions .annotation-action:hover { 231 + color: var(--accent); 232 + background: var(--bg-tertiary); 233 + } 234 + 235 + .collection-detail-edit, 236 + .collection-detail-delete { 237 + padding: 10px; 238 + color: var(--text-tertiary); 239 + background: none; 240 + border: none; 241 + border-radius: var(--radius-sm); 242 + cursor: pointer; 243 + transition: all 0.15s ease; 244 + } 245 + 246 + .collection-detail-edit:hover { 247 + color: var(--accent); 248 + background: var(--bg-tertiary); 249 + } 250 + 251 + .collection-detail-delete:hover { 252 + color: var(--error); 253 + background: rgba(239, 68, 68, 0.1); 254 + } 255 + 256 + .collection-item-wrapper { 257 + position: relative; 258 + } 259 + 260 + .collection-item-remove { 261 + position: absolute; 262 + top: 12px; 263 + left: -40px; 264 + z-index: 10; 265 + padding: 8px; 266 + background: var(--bg-card); 267 + border: 1px solid var(--border); 268 + border-radius: var(--radius-sm); 269 + color: var(--text-tertiary); 270 + cursor: pointer; 271 + opacity: 0; 272 + transition: all 0.15s ease; 273 + } 274 + 275 + .collection-item-wrapper:hover .collection-item-remove { 276 + opacity: 1; 277 + } 278 + 279 + .collection-item-remove:hover { 280 + color: var(--error); 281 + border-color: var(--error); 282 + background: rgba(239, 68, 68, 0.05); 283 + } 284 + 285 + .collection-list-item { 286 + width: 100%; 287 + text-align: left; 288 + padding: 12px 16px; 289 + border-radius: var(--radius-md); 290 + background: var(--bg-primary); 291 + border: 1px solid transparent; 292 + color: var(--text-primary); 293 + transition: all 0.15s ease; 294 + display: flex; 295 + align-items: center; 296 + justify-content: space-between; 297 + cursor: pointer; 298 + } 299 + 300 + .collection-list-item:hover { 301 + background: var(--bg-hover); 302 + border-color: var(--border); 303 + } 304 + 305 + .collection-list-item:hover .collection-list-item-icon { 306 + opacity: 1; 307 + } 308 + 309 + .collection-list-item:disabled { 310 + opacity: 0.6; 311 + cursor: not-allowed; 312 + } 313 + 314 + .item-delete-overlay { 315 + position: absolute; 316 + top: 16px; 317 + right: 16px; 318 + z-index: 10; 319 + opacity: 0; 320 + transition: opacity 0.15s ease; 321 + } 322 + 323 + .card:hover .item-delete-overlay, 324 + div:hover > .item-delete-overlay { 325 + opacity: 1; 326 + }
+141
web/src/css/feed.css
··· 1 + .feed { 2 + display: flex; 3 + flex-direction: column; 4 + gap: 16px; 5 + } 6 + 7 + .feed-header { 8 + display: flex; 9 + align-items: center; 10 + justify-content: space-between; 11 + margin-bottom: 8px; 12 + } 13 + 14 + .feed-title { 15 + font-size: 1.5rem; 16 + font-weight: 700; 17 + } 18 + 19 + .feed-filters { 20 + display: flex; 21 + gap: 8px; 22 + margin-bottom: 24px; 23 + padding: 4px; 24 + background: var(--bg-tertiary); 25 + border-radius: var(--radius-lg); 26 + width: fit-content; 27 + max-width: 100%; 28 + flex-wrap: wrap; 29 + } 30 + 31 + .filter-tab { 32 + padding: 8px 16px; 33 + font-size: 0.9rem; 34 + font-weight: 500; 35 + color: var(--text-secondary); 36 + background: transparent; 37 + border: none; 38 + border-radius: var(--radius-md); 39 + cursor: pointer; 40 + transition: all 0.15s ease; 41 + } 42 + 43 + .filter-tab:hover { 44 + color: var(--text-primary); 45 + background: var(--bg-hover); 46 + } 47 + 48 + .filter-tab.active { 49 + color: var(--text-primary); 50 + background: var(--bg-card); 51 + box-shadow: var(--shadow-sm); 52 + } 53 + 54 + .page-header { 55 + margin-bottom: 32px; 56 + } 57 + 58 + .page-title { 59 + font-size: 2rem; 60 + font-weight: 700; 61 + margin-bottom: 8px; 62 + } 63 + 64 + .page-description { 65 + color: var(--text-secondary); 66 + font-size: 1.1rem; 67 + } 68 + 69 + .url-input-wrapper { 70 + margin-bottom: 24px; 71 + } 72 + 73 + .url-input-container { 74 + display: flex; 75 + gap: 12px; 76 + } 77 + 78 + .url-input { 79 + width: 100%; 80 + padding: 16px; 81 + background: var(--bg-secondary); 82 + border: 1px solid var(--border); 83 + border-radius: var(--radius-md); 84 + color: var(--text-primary); 85 + font-size: 1.1rem; 86 + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 87 + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 88 + } 89 + 90 + .url-input:focus { 91 + outline: none; 92 + border-color: var(--accent); 93 + box-shadow: 0 0 0 4px var(--accent-subtle); 94 + background: var(--bg-primary); 95 + } 96 + 97 + .url-input::placeholder { 98 + color: var(--text-tertiary); 99 + } 100 + 101 + .url-results-header { 102 + display: flex; 103 + align-items: center; 104 + justify-content: space-between; 105 + margin-bottom: 16px; 106 + flex-wrap: wrap; 107 + gap: 12px; 108 + } 109 + 110 + .back-link { 111 + display: inline-flex; 112 + align-items: center; 113 + gap: 8px; 114 + color: var(--text-secondary); 115 + font-size: 0.9rem; 116 + text-decoration: none; 117 + margin-bottom: 24px; 118 + transition: color 0.15s; 119 + } 120 + 121 + .back-link:hover { 122 + color: var(--accent); 123 + } 124 + 125 + .new-page { 126 + max-width: 600px; 127 + margin: 0 auto; 128 + display: flex; 129 + flex-direction: column; 130 + gap: 32px; 131 + } 132 + 133 + @media (max-width: 640px) { 134 + .main-content { 135 + padding: 16px 12px; 136 + } 137 + 138 + .page-title { 139 + font-size: 1.5rem; 140 + } 141 + }
+513
web/src/css/layout.css
··· 1 + .layout { 2 + display: flex; 3 + min-height: 100vh; 4 + background: var(--bg-primary); 5 + } 6 + 7 + .sidebar { 8 + position: fixed; 9 + left: 0; 10 + top: 0; 11 + bottom: 0; 12 + width: 240px; 13 + background: var(--bg-primary); 14 + border-right: 1px solid var(--border); 15 + display: flex; 16 + flex-direction: column; 17 + z-index: 50; 18 + padding-bottom: 20px; 19 + } 20 + 21 + .sidebar-header { 22 + height: 64px; 23 + display: flex; 24 + align-items: center; 25 + padding: 0 20px; 26 + margin-bottom: 12px; 27 + text-decoration: none; 28 + color: var(--text-primary); 29 + } 30 + 31 + .sidebar-logo { 32 + width: 24px; 33 + height: 24px; 34 + object-fit: contain; 35 + margin-right: 12px; 36 + } 37 + 38 + .sidebar-brand { 39 + font-size: 1rem; 40 + font-weight: 600; 41 + color: var(--text-primary); 42 + letter-spacing: -0.01em; 43 + } 44 + 45 + .sidebar-nav { 46 + flex: 1; 47 + display: flex; 48 + flex-direction: column; 49 + gap: 4px; 50 + padding: 0 12px; 51 + overflow-y: auto; 52 + } 53 + 54 + .sidebar-link { 55 + display: flex; 56 + align-items: center; 57 + gap: 12px; 58 + padding: 8px 12px; 59 + border-radius: var(--radius-md); 60 + color: var(--text-secondary); 61 + text-decoration: none; 62 + font-size: 0.9rem; 63 + font-weight: 500; 64 + transition: all 0.15s ease; 65 + } 66 + 67 + .sidebar-link:hover { 68 + background: var(--bg-tertiary); 69 + color: var(--text-primary); 70 + } 71 + 72 + .sidebar-link.active { 73 + background: var(--bg-tertiary); 74 + color: var(--text-primary); 75 + } 76 + 77 + .sidebar-link svg { 78 + width: 18px; 79 + height: 18px; 80 + color: var(--text-tertiary); 81 + transition: color 0.15s ease; 82 + } 83 + 84 + .sidebar-link:hover svg, 85 + .sidebar-link.active svg { 86 + color: var(--text-primary); 87 + } 88 + 89 + .sidebar-section-title { 90 + padding: 24px 12px 8px; 91 + font-size: 0.75rem; 92 + font-weight: 600; 93 + color: var(--text-tertiary); 94 + text-transform: uppercase; 95 + letter-spacing: 0.05em; 96 + } 97 + 98 + .notification-badge { 99 + background: var(--accent); 100 + color: white; 101 + font-size: 0.7rem; 102 + font-weight: 600; 103 + padding: 0 6px; 104 + height: 18px; 105 + border-radius: 99px; 106 + display: flex; 107 + align-items: center; 108 + justify-content: center; 109 + margin-left: auto; 110 + } 111 + 112 + .sidebar-new-btn { 113 + display: flex; 114 + align-items: center; 115 + gap: 10px; 116 + margin: 0 12px 16px; 117 + padding: 10px 16px; 118 + background: var(--text-primary); 119 + color: var(--bg-primary); 120 + border-radius: var(--radius-md); 121 + font-size: 0.9rem; 122 + font-weight: 600; 123 + text-decoration: none; 124 + transition: opacity 0.15s; 125 + justify-content: center; 126 + } 127 + 128 + .sidebar-new-btn:hover { 129 + opacity: 0.9; 130 + } 131 + 132 + .sidebar-footer { 133 + padding: 0 12px; 134 + margin-top: auto; 135 + } 136 + 137 + .sidebar-user { 138 + display: flex; 139 + align-items: center; 140 + gap: 10px; 141 + padding: 8px 12px; 142 + border-radius: var(--radius-md); 143 + cursor: pointer; 144 + transition: background 0.15s ease; 145 + } 146 + 147 + .sidebar-user:hover, 148 + .sidebar-user.active { 149 + background: var(--bg-tertiary); 150 + } 151 + 152 + .sidebar-avatar { 153 + width: 32px; 154 + height: 32px; 155 + border-radius: 50%; 156 + background: var(--bg-tertiary); 157 + display: flex; 158 + align-items: center; 159 + justify-content: center; 160 + color: var(--text-secondary); 161 + font-size: 0.8rem; 162 + font-weight: 500; 163 + overflow: hidden; 164 + flex-shrink: 0; 165 + border: 1px solid var(--border); 166 + } 167 + 168 + .sidebar-avatar img { 169 + width: 100%; 170 + height: 100%; 171 + object-fit: cover; 172 + } 173 + 174 + .sidebar-user-info { 175 + flex: 1; 176 + min-width: 0; 177 + display: flex; 178 + flex-direction: column; 179 + } 180 + 181 + .sidebar-user-name { 182 + font-size: 0.85rem; 183 + font-weight: 500; 184 + color: var(--text-primary); 185 + } 186 + 187 + .sidebar-user-handle { 188 + font-size: 0.75rem; 189 + color: var(--text-tertiary); 190 + } 191 + 192 + .sidebar-dropdown { 193 + position: absolute; 194 + bottom: 74px; 195 + left: 12px; 196 + width: 216px; 197 + background: var(--bg-card); 198 + border: 1px solid var(--border); 199 + border-radius: var(--radius-md); 200 + box-shadow: var(--shadow-lg); 201 + padding: 4px; 202 + z-index: 1000; 203 + overflow: hidden; 204 + animation: scaleIn 0.1s ease-out; 205 + transform-origin: bottom center; 206 + } 207 + 208 + @keyframes scaleIn { 209 + from { 210 + opacity: 0; 211 + transform: scale(0.95); 212 + } 213 + 214 + to { 215 + opacity: 1; 216 + transform: scale(1); 217 + } 218 + } 219 + 220 + .sidebar-dropdown-item { 221 + display: flex; 222 + align-items: center; 223 + gap: 10px; 224 + width: 100%; 225 + padding: 8px 12px; 226 + font-size: 0.85rem; 227 + color: var(--text-secondary); 228 + text-decoration: none; 229 + background: transparent; 230 + cursor: pointer; 231 + border-radius: var(--radius-sm); 232 + transition: all 0.15s; 233 + border: none; 234 + } 235 + 236 + .sidebar-dropdown-item:hover { 237 + background: var(--bg-tertiary); 238 + color: var(--text-primary); 239 + } 240 + 241 + .sidebar-dropdown-item.danger:hover { 242 + background: rgba(239, 68, 68, 0.1); 243 + color: var(--error); 244 + } 245 + 246 + .main-layout { 247 + flex: 1; 248 + margin-left: 240px; 249 + margin-right: 280px; 250 + min-height: 100vh; 251 + } 252 + 253 + .main-content-wrapper { 254 + max-width: 640px; 255 + margin: 0 auto; 256 + padding: 40px 24px; 257 + } 258 + 259 + .right-sidebar { 260 + position: fixed; 261 + right: 0; 262 + top: 0; 263 + bottom: 0; 264 + width: 280px; 265 + background: var(--bg-primary); 266 + border-left: 1px solid var(--border); 267 + padding: 32px 24px; 268 + overflow-y: auto; 269 + display: flex; 270 + flex-direction: column; 271 + gap: 32px; 272 + } 273 + 274 + .right-section { 275 + display: flex; 276 + flex-direction: column; 277 + gap: 12px; 278 + } 279 + 280 + .right-section-title { 281 + font-size: 0.75rem; 282 + font-weight: 600; 283 + color: var(--text-primary); 284 + margin-bottom: 4px; 285 + } 286 + 287 + .right-section-desc { 288 + font-size: 0.85rem; 289 + line-height: 1.5; 290 + color: var(--text-secondary); 291 + } 292 + 293 + .right-extension-btn { 294 + display: inline-flex; 295 + align-items: center; 296 + gap: 8px; 297 + padding: 8px 12px; 298 + background: var(--bg-primary); 299 + border: 1px solid var(--border); 300 + border-radius: var(--radius-md); 301 + color: var(--text-primary); 302 + font-size: 0.85rem; 303 + font-weight: 500; 304 + text-decoration: none; 305 + transition: all 0.15s ease; 306 + width: fit-content; 307 + } 308 + 309 + .right-extension-btn:hover { 310 + border-color: var(--text-tertiary); 311 + background: var(--bg-tertiary); 312 + } 313 + 314 + .right-links { 315 + display: flex; 316 + flex-direction: column; 317 + gap: 4px; 318 + } 319 + 320 + .right-link { 321 + display: flex; 322 + align-items: center; 323 + justify-content: space-between; 324 + padding: 6px 0; 325 + color: var(--text-secondary); 326 + font-size: 0.9rem; 327 + transition: color 0.15s; 328 + text-decoration: none; 329 + } 330 + 331 + .right-link:hover { 332 + color: var(--text-primary); 333 + } 334 + 335 + .right-link svg { 336 + width: 16px; 337 + height: 16px; 338 + color: var(--text-tertiary); 339 + transition: all 0.15s; 340 + } 341 + 342 + .right-link:hover svg { 343 + color: var(--text-secondary); 344 + } 345 + 346 + .tangled-icon { 347 + width: 16px; 348 + height: 16px; 349 + background-color: var(--text-tertiary); 350 + -webkit-mask: url("../assets/tangled.svg") no-repeat center / contain; 351 + mask: url("../assets/tangled.svg") no-repeat center / contain; 352 + transition: background-color 0.15s; 353 + } 354 + 355 + .right-link:hover .tangled-icon { 356 + background-color: var(--text-secondary); 357 + } 358 + 359 + .right-footer { 360 + margin-top: auto; 361 + display: flex; 362 + flex-wrap: wrap; 363 + gap: 12px; 364 + font-size: 0.75rem; 365 + color: var(--text-tertiary); 366 + } 367 + 368 + .right-footer a { 369 + color: var(--text-tertiary); 370 + } 371 + 372 + .right-footer a:hover { 373 + color: var(--text-secondary); 374 + } 375 + 376 + .mobile-nav { 377 + display: none; 378 + position: fixed; 379 + bottom: 0; 380 + left: 0; 381 + right: 0; 382 + background: rgba(9, 9, 11, 0.9); 383 + backdrop-filter: blur(12px); 384 + -webkit-backdrop-filter: blur(12px); 385 + border-top: 1px solid var(--border); 386 + padding: 8px 16px; 387 + padding-bottom: calc(8px + env(safe-area-inset-bottom, 0)); 388 + z-index: 100; 389 + } 390 + 391 + .mobile-nav-inner { 392 + display: flex; 393 + justify-content: space-between; 394 + align-items: center; 395 + } 396 + 397 + .mobile-nav-item { 398 + display: flex; 399 + flex-direction: column; 400 + align-items: center; 401 + justify-content: center; 402 + gap: 4px; 403 + color: var(--text-tertiary); 404 + text-decoration: none; 405 + font-size: 0.65rem; 406 + font-weight: 500; 407 + width: 60px; 408 + transition: color 0.15s; 409 + } 410 + 411 + .mobile-nav-item.active { 412 + color: var(--text-primary); 413 + } 414 + 415 + .mobile-nav-item svg { 416 + width: 24px; 417 + height: 24px; 418 + } 419 + 420 + .mobile-nav-new { 421 + width: 48px; 422 + height: 36px; 423 + border-radius: var(--radius-md); 424 + background: var(--text-primary); 425 + color: var(--bg-primary); 426 + display: flex; 427 + align-items: center; 428 + justify-content: center; 429 + } 430 + 431 + .mobile-nav-new svg { 432 + width: 20px; 433 + height: 20px; 434 + } 435 + 436 + @media (max-width: 1200px) { 437 + .right-sidebar { 438 + display: none; 439 + } 440 + 441 + .main-layout { 442 + margin-right: 0; 443 + } 444 + } 445 + 446 + @media (max-width: 768px) { 447 + .sidebar { 448 + display: none; 449 + } 450 + 451 + .main-layout { 452 + margin-left: 0; 453 + padding-bottom: 80px; 454 + width: 100%; 455 + min-width: 0; 456 + } 457 + 458 + .main-content-wrapper { 459 + padding: 20px 16px; 460 + max-width: 100%; 461 + width: 100%; 462 + overflow-x: hidden; 463 + min-width: 0; 464 + } 465 + 466 + .mobile-nav { 467 + display: block; 468 + max-width: 100vw; 469 + } 470 + 471 + .card, 472 + .annotation-card, 473 + .collection-card, 474 + .profile-header, 475 + .api-keys-section { 476 + overflow-x: hidden; 477 + max-width: 100%; 478 + } 479 + 480 + code { 481 + word-break: break-all; 482 + overflow-wrap: break-word; 483 + } 484 + 485 + pre { 486 + overflow-x: auto; 487 + max-width: 100%; 488 + } 489 + 490 + input, 491 + textarea { 492 + max-width: 100%; 493 + } 494 + 495 + .flex-row, 496 + [style*="display: flex"][style*="gap"] { 497 + flex-wrap: wrap; 498 + } 499 + 500 + .static-page { 501 + overflow-x: hidden; 502 + } 503 + 504 + .static-page ol, 505 + .static-page ul { 506 + padding-left: 1.25rem; 507 + } 508 + 509 + .static-page code { 510 + font-size: 0.75rem; 511 + word-break: break-all; 512 + } 513 + }
+317
web/src/css/login.css
··· 1 + .login-page { 2 + display: flex; 3 + flex-direction: column; 4 + align-items: center; 5 + justify-content: center; 6 + min-height: 70vh; 7 + padding: 60px 20px; 8 + width: 100%; 9 + max-width: 500px; 10 + margin: 0 auto; 11 + } 12 + 13 + @media (max-width: 600px) { 14 + .login-page { 15 + padding: 40px 16px; 16 + } 17 + 18 + .login-at-logo { 19 + font-size: 4rem; 20 + } 21 + 22 + .login-brand-name { 23 + font-size: 1.25rem; 24 + } 25 + 26 + .login-brand-icon { 27 + width: 40px; 28 + height: 40px; 29 + font-size: 1.5rem; 30 + } 31 + } 32 + 33 + .login-at-logo { 34 + font-size: 5rem; 35 + font-weight: 800; 36 + color: var(--accent); 37 + margin-bottom: 24px; 38 + line-height: 1; 39 + } 40 + 41 + .login-logo-img { 42 + width: 80px; 43 + height: 80px; 44 + margin-bottom: 24px; 45 + object-fit: contain; 46 + } 47 + 48 + .login-heading { 49 + font-size: 1.5rem; 50 + font-weight: 600; 51 + margin-bottom: 32px; 52 + display: flex; 53 + align-items: center; 54 + gap: 10px; 55 + text-align: center; 56 + line-height: 1.4; 57 + } 58 + 59 + .login-help-btn { 60 + background: none; 61 + border: none; 62 + color: var(--text-tertiary); 63 + cursor: pointer; 64 + padding: 4px; 65 + display: flex; 66 + align-items: center; 67 + transition: color 0.15s; 68 + flex-shrink: 0; 69 + } 70 + 71 + .login-help-btn:hover { 72 + color: var(--accent); 73 + } 74 + 75 + .login-help-text { 76 + background: var(--bg-elevated); 77 + border: 1px solid var(--border); 78 + border-radius: var(--radius-md); 79 + padding: 16px 20px; 80 + margin-bottom: 24px; 81 + font-size: 0.95rem; 82 + color: var(--text-secondary); 83 + line-height: 1.6; 84 + text-align: center; 85 + } 86 + 87 + .login-help-text code { 88 + background: var(--bg-tertiary); 89 + padding: 2px 8px; 90 + border-radius: var(--radius-sm); 91 + font-size: 0.9rem; 92 + } 93 + 94 + .login-form { 95 + display: flex; 96 + flex-direction: column; 97 + gap: 16px; 98 + width: 100%; 99 + } 100 + 101 + .login-input-wrapper { 102 + position: relative; 103 + } 104 + 105 + .login-input { 106 + width: 100%; 107 + padding: 14px 16px; 108 + background: var(--bg-elevated); 109 + border: 1px solid var(--border); 110 + border-radius: var(--radius-md); 111 + color: var(--text-primary); 112 + font-size: 1rem; 113 + transition: 114 + border-color 0.15s, 115 + box-shadow 0.15s; 116 + } 117 + 118 + .login-input:focus { 119 + outline: none; 120 + border-color: var(--accent); 121 + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); 122 + } 123 + 124 + .login-input::placeholder { 125 + color: var(--text-tertiary); 126 + } 127 + 128 + .login-suggestions { 129 + position: absolute; 130 + top: calc(100% + 4px); 131 + left: 0; 132 + right: 0; 133 + background: var(--bg-card); 134 + border: 1px solid var(--border); 135 + border-radius: var(--radius-md); 136 + box-shadow: var(--shadow-lg); 137 + overflow: hidden; 138 + z-index: 100; 139 + } 140 + 141 + .login-suggestion { 142 + display: flex; 143 + align-items: center; 144 + gap: 12px; 145 + width: 100%; 146 + padding: 12px 16px; 147 + background: transparent; 148 + border: none; 149 + cursor: pointer; 150 + text-align: left; 151 + transition: background 0.1s; 152 + } 153 + 154 + .login-suggestion:hover, 155 + .login-suggestion.selected { 156 + background: var(--bg-elevated); 157 + } 158 + 159 + .login-suggestion-avatar { 160 + width: 40px; 161 + height: 40px; 162 + border-radius: var(--radius-full); 163 + background: linear-gradient(135deg, var(--accent), #a855f7); 164 + display: flex; 165 + align-items: center; 166 + justify-content: center; 167 + flex-shrink: 0; 168 + overflow: hidden; 169 + font-size: 0.875rem; 170 + font-weight: 600; 171 + color: white; 172 + } 173 + 174 + .login-suggestion-avatar img { 175 + width: 100%; 176 + height: 100%; 177 + object-fit: cover; 178 + } 179 + 180 + .login-suggestion-info { 181 + display: flex; 182 + flex-direction: column; 183 + min-width: 0; 184 + } 185 + 186 + .login-suggestion-name { 187 + font-weight: 600; 188 + color: var(--text-primary); 189 + white-space: nowrap; 190 + overflow: hidden; 191 + text-overflow: ellipsis; 192 + } 193 + 194 + .login-suggestion-handle { 195 + font-size: 0.875rem; 196 + color: var(--text-secondary); 197 + white-space: nowrap; 198 + overflow: hidden; 199 + text-overflow: ellipsis; 200 + } 201 + 202 + .login-error { 203 + padding: 12px 16px; 204 + background: rgba(239, 68, 68, 0.1); 205 + border: 1px solid rgba(239, 68, 68, 0.3); 206 + border-radius: var(--radius-md); 207 + color: #ef4444; 208 + font-size: 0.875rem; 209 + } 210 + 211 + .login-legal { 212 + font-size: 0.75rem; 213 + color: var(--text-tertiary); 214 + line-height: 1.5; 215 + margin-top: 16px; 216 + } 217 + 218 + .login-brand { 219 + display: flex; 220 + align-items: center; 221 + justify-content: center; 222 + gap: 12px; 223 + margin-bottom: 24px; 224 + } 225 + 226 + .login-brand-icon { 227 + width: 48px; 228 + height: 48px; 229 + background: linear-gradient(135deg, var(--accent), #a855f7); 230 + border-radius: var(--radius-lg); 231 + display: flex; 232 + align-items: center; 233 + justify-content: center; 234 + font-size: 1.75rem; 235 + font-weight: 800; 236 + color: white; 237 + } 238 + 239 + .login-brand-name { 240 + font-size: 1.75rem; 241 + font-weight: 700; 242 + } 243 + 244 + .login-avatar { 245 + width: 72px; 246 + height: 72px; 247 + border-radius: var(--radius-full); 248 + background: linear-gradient(135deg, var(--accent), #a855f7); 249 + display: flex; 250 + align-items: center; 251 + justify-content: center; 252 + margin: 0 auto 16px; 253 + font-weight: 700; 254 + font-size: 1.5rem; 255 + color: white; 256 + overflow: hidden; 257 + } 258 + 259 + .login-avatar img { 260 + width: 100%; 261 + height: 100%; 262 + object-fit: cover; 263 + } 264 + 265 + .login-avatar-large { 266 + width: 100px; 267 + height: 100px; 268 + border-radius: var(--radius-full); 269 + background: linear-gradient(135deg, var(--accent), #a855f7); 270 + display: flex; 271 + align-items: center; 272 + justify-content: center; 273 + margin-bottom: 20px; 274 + font-weight: 700; 275 + font-size: 2rem; 276 + color: white; 277 + overflow: hidden; 278 + } 279 + 280 + .login-avatar-large img { 281 + width: 100%; 282 + height: 100%; 283 + object-fit: cover; 284 + } 285 + 286 + .login-welcome { 287 + font-size: 1.5rem; 288 + font-weight: 600; 289 + margin-bottom: 32px; 290 + text-align: center; 291 + } 292 + 293 + .login-welcome-name { 294 + font-size: 1.25rem; 295 + font-weight: 600; 296 + margin-bottom: 24px; 297 + } 298 + 299 + .login-actions { 300 + display: flex; 301 + flex-direction: column; 302 + gap: 12px; 303 + width: 100%; 304 + } 305 + 306 + .login-btn { 307 + width: 100%; 308 + padding: 14px 24px; 309 + font-size: 1rem; 310 + font-weight: 600; 311 + } 312 + 313 + .login-submit { 314 + padding: 18px 32px; 315 + font-size: 1.1rem; 316 + font-weight: 600; 317 + }
+262
web/src/css/modals.css
··· 1 + .modal-overlay { 2 + position: fixed; 3 + inset: 0; 4 + background: rgba(0, 0, 0, 0.5); 5 + display: flex; 6 + align-items: center; 7 + justify-content: center; 8 + padding: 16px; 9 + z-index: 50; 10 + animation: fadeIn 0.2s ease-out; 11 + } 12 + 13 + .modal-container { 14 + background: var(--bg-secondary); 15 + border-radius: var(--radius-lg); 16 + width: 100%; 17 + max-width: 28rem; 18 + border: 1px solid var(--border); 19 + box-shadow: var(--shadow-lg); 20 + animation: zoomIn 0.2s ease-out; 21 + } 22 + 23 + .modal-header { 24 + display: flex; 25 + align-items: center; 26 + justify-content: space-between; 27 + padding: 16px; 28 + border-bottom: 1px solid var(--border); 29 + } 30 + 31 + .modal-title { 32 + font-size: 1.25rem; 33 + font-weight: 700; 34 + color: var(--text-primary); 35 + } 36 + 37 + .modal-close-btn { 38 + padding: 8px; 39 + color: var(--text-tertiary); 40 + border-radius: var(--radius-md); 41 + transition: color 0.15s; 42 + } 43 + 44 + .modal-close-btn:hover { 45 + color: var(--text-primary); 46 + background: var(--bg-hover); 47 + } 48 + 49 + .modal-form { 50 + padding: 16px; 51 + display: flex; 52 + flex-direction: column; 53 + gap: 16px; 54 + } 55 + 56 + .icon-picker-tabs { 57 + display: flex; 58 + gap: 4px; 59 + margin-bottom: 12px; 60 + } 61 + 62 + .icon-picker-tab { 63 + flex: 1; 64 + padding: 8px 12px; 65 + background: var(--bg-primary); 66 + border: 1px solid var(--border); 67 + border-radius: var(--radius-md); 68 + color: var(--text-secondary); 69 + font-size: 0.85rem; 70 + font-weight: 500; 71 + cursor: pointer; 72 + transition: all 0.15s ease; 73 + } 74 + 75 + .icon-picker-tab:hover { 76 + background: var(--bg-tertiary); 77 + } 78 + 79 + .icon-picker-tab.active { 80 + background: var(--accent); 81 + border-color: var(--accent); 82 + color: white; 83 + } 84 + 85 + .emoji-picker-wrapper { 86 + display: flex; 87 + flex-direction: column; 88 + gap: 10px; 89 + } 90 + 91 + .emoji-custom-input input { 92 + width: 100%; 93 + } 94 + 95 + .emoji-picker, 96 + .icon-picker { 97 + display: flex; 98 + flex-wrap: wrap; 99 + gap: 4px; 100 + max-height: 120px; 101 + overflow-y: auto; 102 + padding: 8px; 103 + background: var(--bg-primary); 104 + border: 1px solid var(--border); 105 + border-radius: var(--radius-md); 106 + } 107 + 108 + .emoji-option, 109 + .icon-option { 110 + width: 36px; 111 + height: 36px; 112 + display: flex; 113 + align-items: center; 114 + justify-content: center; 115 + font-size: 1.2rem; 116 + background: transparent; 117 + border: 2px solid transparent; 118 + border-radius: var(--radius-sm); 119 + cursor: pointer; 120 + transition: all 0.15s ease; 121 + color: var(--text-secondary); 122 + } 123 + 124 + .emoji-option:hover, 125 + .icon-option:hover { 126 + background: var(--bg-tertiary); 127 + transform: scale(1.1); 128 + color: var(--text-primary); 129 + } 130 + 131 + .emoji-option.selected, 132 + .icon-option.selected { 133 + border-color: var(--accent); 134 + background: var(--accent-subtle); 135 + color: var(--accent); 136 + } 137 + 138 + .modal-actions { 139 + display: flex; 140 + justify-content: flex-end; 141 + gap: 12px; 142 + padding-top: 8px; 143 + } 144 + 145 + @keyframes fadeIn { 146 + from { 147 + opacity: 0; 148 + } 149 + 150 + to { 151 + opacity: 1; 152 + } 153 + } 154 + 155 + @keyframes zoomIn { 156 + from { 157 + opacity: 0; 158 + transform: scale(0.95); 159 + } 160 + 161 + to { 162 + opacity: 1; 163 + transform: scale(1); 164 + } 165 + } 166 + 167 + .form-group { 168 + margin-bottom: 0; 169 + } 170 + 171 + .form-label { 172 + display: block; 173 + font-size: 0.85rem; 174 + font-weight: 600; 175 + color: var(--text-secondary); 176 + margin-bottom: 6px; 177 + } 178 + 179 + .form-input, 180 + .form-textarea, 181 + .form-select { 182 + width: 100%; 183 + padding: 8px 12px; 184 + background: var(--bg-primary); 185 + border: 1px solid var(--border); 186 + border-radius: var(--radius-md); 187 + color: var(--text-primary); 188 + transition: all 0.15s; 189 + } 190 + 191 + .form-input:focus, 192 + .form-textarea:focus, 193 + .form-select:focus { 194 + outline: none; 195 + border-color: var(--accent); 196 + box-shadow: 0 0 0 2px var(--accent-subtle); 197 + } 198 + 199 + .form-textarea { 200 + resize: none; 201 + } 202 + 203 + .input { 204 + width: 100%; 205 + padding: 12px 14px; 206 + font-size: 0.95rem; 207 + color: var(--text-primary); 208 + background: var(--bg-secondary); 209 + border: 1px solid var(--border); 210 + border-radius: var(--radius-md); 211 + outline: none; 212 + transition: all 0.15s ease; 213 + } 214 + 215 + .input:focus { 216 + border-color: var(--accent); 217 + box-shadow: 0 0 0 3px var(--accent-subtle); 218 + } 219 + 220 + .input::placeholder { 221 + color: var(--text-tertiary); 222 + } 223 + 224 + .color-input-container { 225 + display: flex; 226 + align-items: center; 227 + gap: 12px; 228 + background: var(--bg-tertiary); 229 + padding: 8px 12px; 230 + border-radius: var(--radius-md); 231 + border: 1px solid var(--border); 232 + width: fit-content; 233 + } 234 + 235 + .color-input-wrapper { 236 + position: relative; 237 + width: 32px; 238 + height: 32px; 239 + border-radius: var(--radius-full); 240 + overflow: hidden; 241 + border: 2px solid var(--border); 242 + cursor: pointer; 243 + transition: transform 0.1s; 244 + } 245 + 246 + .color-input-wrapper:hover { 247 + transform: scale(1.1); 248 + border-color: var(--accent); 249 + } 250 + 251 + .color-input-wrapper input[type="color"] { 252 + position: absolute; 253 + top: -50%; 254 + left: -50%; 255 + width: 200%; 256 + height: 200%; 257 + padding: 0; 258 + margin: 0; 259 + border: none; 260 + cursor: pointer; 261 + opacity: 0; 262 + }
+67
web/src/css/notifications.css
··· 1 + .notifications-page { 2 + max-width: 680px; 3 + margin: 0 auto; 4 + } 5 + 6 + .notifications-list { 7 + display: flex; 8 + flex-direction: column; 9 + gap: 12px; 10 + } 11 + 12 + .notification-item { 13 + display: flex; 14 + gap: 16px; 15 + align-items: flex-start; 16 + text-decoration: none; 17 + color: inherit; 18 + } 19 + 20 + .notification-item:hover { 21 + background: var(--bg-hover); 22 + } 23 + 24 + .notification-icon { 25 + width: 36px; 26 + height: 36px; 27 + border-radius: var(--radius-full); 28 + display: flex; 29 + align-items: center; 30 + justify-content: center; 31 + background: var(--bg-tertiary); 32 + color: var(--text-secondary); 33 + flex-shrink: 0; 34 + } 35 + 36 + .notification-icon[data-type="like"] { 37 + color: #ef4444; 38 + background: rgba(239, 68, 68, 0.1); 39 + } 40 + 41 + .notification-icon[data-type="reply"] { 42 + color: #3b82f6; 43 + background: rgba(59, 130, 246, 0.1); 44 + } 45 + 46 + .notification-content { 47 + flex: 1; 48 + min-width: 0; 49 + } 50 + 51 + .notification-text { 52 + font-size: 0.95rem; 53 + margin-bottom: 4px; 54 + line-height: 1.4; 55 + color: var(--text-primary); 56 + overflow-wrap: break-word; 57 + word-break: break-word; 58 + } 59 + 60 + .notification-text strong { 61 + font-weight: 600; 62 + } 63 + 64 + .notification-time { 65 + font-size: 0.85rem; 66 + color: var(--text-tertiary); 67 + }
+257
web/src/css/profile.css
··· 1 + .profile-header { 2 + display: flex; 3 + align-items: center; 4 + gap: 24px; 5 + margin-bottom: 32px; 6 + padding-bottom: 24px; 7 + border-bottom: 1px solid var(--border); 8 + } 9 + 10 + .profile-avatar { 11 + width: 80px; 12 + height: 80px; 13 + min-width: 80px; 14 + border-radius: 50%; 15 + background: var(--bg-tertiary); 16 + display: flex; 17 + align-items: center; 18 + justify-content: center; 19 + font-weight: 600; 20 + font-size: 2rem; 21 + color: var(--text-secondary); 22 + overflow: hidden; 23 + border: 1px solid var(--border); 24 + } 25 + 26 + .profile-avatar img { 27 + width: 100%; 28 + height: 100%; 29 + object-fit: cover; 30 + } 31 + 32 + .profile-avatar-link { 33 + text-decoration: none; 34 + } 35 + 36 + .profile-info { 37 + flex: 1; 38 + display: flex; 39 + flex-direction: column; 40 + gap: 4px; 41 + } 42 + 43 + .profile-name { 44 + font-size: 1.5rem; 45 + font-weight: 700; 46 + color: var(--text-primary); 47 + line-height: 1.2; 48 + overflow-wrap: break-word; 49 + word-break: break-word; 50 + } 51 + 52 + .profile-handle-row { 53 + display: flex; 54 + align-items: center; 55 + gap: 12px; 56 + margin-top: 4px; 57 + flex-wrap: wrap; 58 + } 59 + 60 + .profile-handle-link { 61 + color: var(--text-tertiary); 62 + text-decoration: none; 63 + font-size: 1rem; 64 + transition: color 0.15s; 65 + overflow-wrap: break-word; 66 + word-break: break-all; 67 + } 68 + 69 + .profile-handle-link:hover { 70 + color: var(--text-secondary); 71 + } 72 + 73 + .profile-bluesky-link { 74 + display: inline-flex; 75 + align-items: center; 76 + gap: 6px; 77 + color: #3b82f6; 78 + text-decoration: none; 79 + font-size: 0.85rem; 80 + font-weight: 500; 81 + padding: 2px 8px; 82 + border-radius: var(--radius-sm); 83 + background: rgba(59, 130, 246, 0.1); 84 + transition: all 0.15s ease; 85 + width: fit-content; 86 + } 87 + 88 + .profile-bluesky-link:hover { 89 + background: rgba(59, 130, 246, 0.15); 90 + } 91 + 92 + .profile-stats { 93 + display: flex; 94 + gap: 24px; 95 + margin-top: 12px; 96 + } 97 + 98 + .profile-stat { 99 + color: var(--text-tertiary); 100 + font-size: 0.9rem; 101 + } 102 + 103 + .profile-stat strong { 104 + color: var(--text-primary); 105 + font-weight: 600; 106 + } 107 + 108 + .profile-tabs { 109 + display: flex; 110 + gap: 24px; 111 + margin-bottom: 24px; 112 + border-bottom: 1px solid var(--border); 113 + flex-wrap: wrap; 114 + row-gap: 8px; 115 + } 116 + 117 + .profile-tab { 118 + padding: 12px 0; 119 + font-size: 0.95rem; 120 + font-weight: 500; 121 + color: var(--text-tertiary); 122 + background: transparent; 123 + border: none; 124 + cursor: pointer; 125 + transition: all 0.15s ease; 126 + position: relative; 127 + } 128 + 129 + .profile-tab:hover { 130 + color: var(--text-primary); 131 + } 132 + 133 + .profile-tab.active { 134 + color: var(--text-primary); 135 + } 136 + 137 + .profile-tab.active::after { 138 + content: ""; 139 + position: absolute; 140 + bottom: -1px; 141 + left: 0; 142 + right: 0; 143 + height: 2px; 144 + background: var(--text-primary); 145 + } 146 + 147 + .profile-badge-wrapper { 148 + display: inline-flex; 149 + align-items: center; 150 + } 151 + 152 + .profile-badge-clickable { 153 + position: relative; 154 + display: inline-flex; 155 + align-items: center; 156 + cursor: pointer; 157 + margin-left: 8px; 158 + } 159 + 160 + .badge-info-popover { 161 + position: absolute; 162 + top: calc(100% + 8px); 163 + left: 50%; 164 + transform: translateX(-50%); 165 + padding: 16px; 166 + background: var(--bg-elevated); 167 + border: 1px solid var(--border); 168 + border-radius: var(--radius-md); 169 + box-shadow: var(--shadow-lg); 170 + font-size: 0.85rem; 171 + white-space: nowrap; 172 + z-index: 100; 173 + min-width: 200px; 174 + } 175 + 176 + .badge-info-title { 177 + font-weight: 600; 178 + color: var(--text-primary); 179 + margin-bottom: 8px; 180 + } 181 + 182 + .verifier-link { 183 + display: flex; 184 + align-items: center; 185 + gap: 8px; 186 + padding: 8px; 187 + background: var(--bg-tertiary); 188 + border-radius: var(--radius-sm); 189 + text-decoration: none; 190 + transition: background 0.15s ease; 191 + } 192 + 193 + .verifier-link:hover { 194 + background: var(--bg-hover); 195 + } 196 + 197 + .verifier-avatar { 198 + width: 24px; 199 + height: 24px; 200 + border-radius: 50%; 201 + object-fit: cover; 202 + } 203 + 204 + .verifier-name { 205 + color: var(--text-primary); 206 + font-size: 0.85rem; 207 + font-weight: 500; 208 + } 209 + 210 + .profile-suspended { 211 + display: flex; 212 + flex-direction: column; 213 + align-items: center; 214 + justify-content: center; 215 + padding: 60px 20px; 216 + text-align: center; 217 + background: var(--bg-secondary); 218 + border-radius: var(--radius-lg); 219 + margin-top: 20px; 220 + border: 1px solid var(--border); 221 + } 222 + 223 + .suspended-icon { 224 + font-size: 40px; 225 + margin-bottom: 16px; 226 + color: var(--text-tertiary); 227 + } 228 + 229 + .profile-suspended h2 { 230 + color: var(--text-primary); 231 + margin-bottom: 8px; 232 + font-size: 1.25rem; 233 + } 234 + 235 + @media (max-width: 640px) { 236 + .profile-header { 237 + flex-direction: column; 238 + text-align: center; 239 + } 240 + 241 + .profile-info { 242 + align-items: center; 243 + } 244 + 245 + .profile-handle-row { 246 + justify-content: center; 247 + } 248 + 249 + .profile-stats { 250 + justify-content: center; 251 + } 252 + 253 + .profile-tabs { 254 + justify-content: center; 255 + gap: 16px; 256 + } 257 + }
+106
web/src/css/skeleton.css
··· 1 + @keyframes shimmer { 2 + 0% { 3 + background-position: -200% 0; 4 + } 5 + 6 + 100% { 7 + background-position: 200% 0; 8 + } 9 + } 10 + 11 + .skeleton { 12 + background: linear-gradient( 13 + 90deg, 14 + var(--bg-tertiary) 25%, 15 + var(--bg-secondary) 50%, 16 + var(--bg-tertiary) 75% 17 + ); 18 + background-size: 200% 100%; 19 + animation: shimmer 1.5s infinite; 20 + border-radius: var(--radius-sm); 21 + } 22 + 23 + .skeleton-card { 24 + padding: 24px 0; 25 + border-bottom: 1px solid var(--border); 26 + display: flex; 27 + flex-direction: column; 28 + gap: 16px; 29 + } 30 + 31 + .skeleton-header { 32 + display: flex; 33 + align-items: center; 34 + gap: 12px; 35 + } 36 + 37 + .skeleton-avatar { 38 + width: 36px; 39 + height: 36px; 40 + border-radius: 50%; 41 + } 42 + 43 + .skeleton-meta { 44 + display: flex; 45 + flex-direction: column; 46 + gap: 6px; 47 + } 48 + 49 + .skeleton-name { 50 + width: 120px; 51 + height: 14px; 52 + } 53 + 54 + .skeleton-handle { 55 + width: 80px; 56 + height: 12px; 57 + } 58 + 59 + .skeleton-content { 60 + display: flex; 61 + flex-direction: column; 62 + gap: 12px; 63 + padding-left: 48px; 64 + } 65 + 66 + .skeleton-source { 67 + width: 180px; 68 + height: 24px; 69 + border-radius: var(--radius-full); 70 + } 71 + 72 + .skeleton-highlight { 73 + width: 100%; 74 + height: 60px; 75 + border-left: 2px solid var(--border); 76 + } 77 + 78 + .skeleton-text-1 { 79 + width: 90%; 80 + height: 14px; 81 + } 82 + 83 + .skeleton-text-2 { 84 + width: 60%; 85 + height: 14px; 86 + } 87 + 88 + .skeleton-actions { 89 + display: flex; 90 + gap: 24px; 91 + padding-left: 48px; 92 + margin-top: 4px; 93 + } 94 + 95 + .skeleton-action { 96 + width: 24px; 97 + height: 24px; 98 + border-radius: var(--radius-sm); 99 + } 100 + 101 + @media (max-width: 600px) { 102 + .skeleton-content, 103 + .skeleton-actions { 104 + padding-left: 0; 105 + } 106 + }
+749
web/src/css/utilities.css
··· 1 + .legal-content { 2 + max-width: 800px; 3 + margin: 0 auto; 4 + padding: 20px; 5 + } 6 + 7 + .legal-content h1 { 8 + font-size: 2rem; 9 + margin-bottom: 8px; 10 + color: var(--text-primary); 11 + } 12 + 13 + .legal-content h2 { 14 + font-size: 1.4rem; 15 + margin-top: 32px; 16 + margin-bottom: 12px; 17 + color: var(--text-primary); 18 + } 19 + 20 + .legal-content h3 { 21 + font-size: 1.1rem; 22 + margin-top: 20px; 23 + margin-bottom: 8px; 24 + color: var(--text-primary); 25 + } 26 + 27 + .legal-content p { 28 + color: var(--text-secondary); 29 + line-height: 1.7; 30 + margin-bottom: 12px; 31 + } 32 + 33 + .legal-content ul { 34 + color: var(--text-secondary); 35 + line-height: 1.7; 36 + margin-left: 24px; 37 + margin-bottom: 12px; 38 + } 39 + 40 + .legal-content li { 41 + margin-bottom: 6px; 42 + } 43 + 44 + .legal-content a { 45 + color: var(--accent); 46 + text-decoration: none; 47 + } 48 + 49 + .legal-content a:hover { 50 + text-decoration: underline; 51 + } 52 + 53 + .legal-content section { 54 + margin-bottom: 24px; 55 + } 56 + 57 + .text-secondary { 58 + color: var(--text-secondary); 59 + } 60 + 61 + .text-error { 62 + color: var(--error); 63 + } 64 + 65 + .text-center { 66 + text-align: center; 67 + } 68 + 69 + .flex { 70 + display: flex; 71 + } 72 + 73 + .items-center { 74 + align-items: center; 75 + } 76 + 77 + .justify-center { 78 + justify-content: center; 79 + } 80 + 81 + .justify-end { 82 + justify-content: flex-end; 83 + } 84 + 85 + .gap-2 { 86 + gap: 8px; 87 + } 88 + 89 + .gap-3 { 90 + gap: 12px; 91 + } 92 + 93 + .mt-3 { 94 + margin-top: 12px; 95 + } 96 + 97 + .mb-6 { 98 + margin-bottom: 24px; 99 + } 100 + 101 + .composer { 102 + margin-bottom: 24px; 103 + } 104 + 105 + .composer-header { 106 + display: flex; 107 + justify-content: space-between; 108 + align-items: center; 109 + margin-bottom: 12px; 110 + } 111 + 112 + .composer-title { 113 + font-size: 1.1rem; 114 + font-weight: 600; 115 + color: var(--text-primary); 116 + margin: 0; 117 + } 118 + 119 + .composer-input { 120 + width: 100%; 121 + min-height: 120px; 122 + padding: 16px; 123 + background: var(--bg-secondary); 124 + border: 1px solid var(--border); 125 + border-radius: var(--radius-md); 126 + color: var(--text-primary); 127 + font-size: 1rem; 128 + resize: vertical; 129 + transition: all 0.15s ease; 130 + } 131 + 132 + .composer-input:focus { 133 + outline: none; 134 + border-color: var(--accent); 135 + box-shadow: 0 0 0 3px var(--accent-subtle); 136 + } 137 + 138 + .composer-footer { 139 + display: flex; 140 + justify-content: space-between; 141 + align-items: center; 142 + margin-top: 12px; 143 + } 144 + 145 + .composer-actions { 146 + display: flex; 147 + justify-content: flex-end; 148 + gap: 8px; 149 + } 150 + 151 + .composer-count { 152 + font-size: 0.85rem; 153 + color: var(--text-tertiary); 154 + } 155 + 156 + .composer-count.warning { 157 + color: var(--warning); 158 + } 159 + 160 + .composer-count.error { 161 + color: var(--error); 162 + } 163 + 164 + .composer-char-count.warning { 165 + color: var(--warning); 166 + } 167 + 168 + .composer-char-count.error { 169 + color: var(--error); 170 + } 171 + 172 + .composer-add-quote { 173 + width: 100%; 174 + padding: 12px 16px; 175 + margin-bottom: 12px; 176 + background: var(--bg-tertiary); 177 + border: 1px dashed var(--border); 178 + border-radius: var(--radius-md); 179 + color: var(--text-secondary); 180 + font-size: 0.9rem; 181 + cursor: pointer; 182 + transition: all 0.15s ease; 183 + } 184 + 185 + .composer-add-quote:hover { 186 + border-color: var(--accent); 187 + color: var(--accent); 188 + background: var(--accent-subtle); 189 + } 190 + 191 + .composer-quote-input-wrapper { 192 + margin-bottom: 12px; 193 + } 194 + 195 + .composer-quote-input { 196 + width: 100%; 197 + padding: 12px 16px; 198 + background: linear-gradient( 199 + 135deg, 200 + rgba(79, 70, 229, 0.05), 201 + rgba(168, 85, 247, 0.05) 202 + ); 203 + border: 1px solid var(--border); 204 + border-left: 3px solid var(--accent); 205 + border-radius: 0 var(--radius-md) var(--radius-md) 0; 206 + color: var(--text-primary); 207 + font-size: 0.95rem; 208 + font-style: italic; 209 + resize: vertical; 210 + font-family: inherit; 211 + transition: all 0.15s ease; 212 + } 213 + 214 + .composer-quote-input:focus { 215 + outline: none; 216 + border-color: var(--accent); 217 + } 218 + 219 + .composer-quote-input::placeholder { 220 + color: var(--text-tertiary); 221 + font-style: italic; 222 + } 223 + 224 + .composer-quote-remove-btn { 225 + margin-top: 8px; 226 + padding: 6px 12px; 227 + background: none; 228 + border: none; 229 + color: var(--text-tertiary); 230 + font-size: 0.85rem; 231 + cursor: pointer; 232 + } 233 + 234 + .composer-quote-remove-btn:hover { 235 + color: var(--error); 236 + } 237 + 238 + .composer-error { 239 + margin-top: 12px; 240 + padding: 12px; 241 + background: rgba(239, 68, 68, 0.1); 242 + border: 1px solid rgba(239, 68, 68, 0.3); 243 + border-radius: var(--radius-md); 244 + color: var(--error); 245 + font-size: 0.9rem; 246 + } 247 + 248 + .composer-url { 249 + font-size: 0.85rem; 250 + color: var(--text-secondary); 251 + word-break: break-all; 252 + } 253 + 254 + .composer-quote { 255 + position: relative; 256 + padding: 12px 16px; 257 + padding-right: 36px; 258 + background: var(--bg-secondary); 259 + border-left: 3px solid var(--accent); 260 + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; 261 + margin-bottom: 16px; 262 + font-style: italic; 263 + color: var(--text-secondary); 264 + overflow-wrap: break-word; 265 + word-break: break-word; 266 + max-width: 100%; 267 + } 268 + 269 + .composer-quote-remove { 270 + position: absolute; 271 + top: 8px; 272 + right: 8px; 273 + width: 24px; 274 + height: 24px; 275 + border-radius: var(--radius-full); 276 + background: var(--bg-tertiary); 277 + color: var(--text-secondary); 278 + font-size: 1rem; 279 + display: flex; 280 + align-items: center; 281 + justify-content: center; 282 + } 283 + 284 + .composer-quote-remove:hover { 285 + background: var(--bg-hover); 286 + color: var(--text-primary); 287 + } 288 + 289 + .composer-tags { 290 + flex: 1; 291 + } 292 + 293 + .composer-meta-row { 294 + display: flex; 295 + gap: 12px; 296 + margin-top: 12px; 297 + align-items: flex-start; 298 + } 299 + 300 + .composer-labels-wrapper { 301 + position: relative; 302 + } 303 + 304 + .composer-labels-btn { 305 + display: flex; 306 + align-items: center; 307 + justify-content: center; 308 + width: 42px; 309 + height: 42px; 310 + background: var(--bg-secondary); 311 + border: 1px solid var(--border); 312 + border-radius: var(--radius-md); 313 + cursor: pointer; 314 + color: var(--text-tertiary); 315 + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 316 + position: relative; 317 + } 318 + 319 + .composer-labels-btn:hover { 320 + color: var(--text-primary); 321 + background: var(--bg-hover); 322 + border-color: var(--text-tertiary); 323 + } 324 + 325 + .composer-labels-btn.active { 326 + color: var(--accent); 327 + background: var(--accent-subtle); 328 + border-color: var(--accent); 329 + } 330 + 331 + .composer-labels-badge { 332 + position: absolute; 333 + top: -4px; 334 + right: -4px; 335 + background: var(--error); 336 + color: white; 337 + font-size: 0.7rem; 338 + width: 18px; 339 + height: 18px; 340 + border-radius: 50%; 341 + display: flex; 342 + align-items: center; 343 + justify-content: center; 344 + font-weight: bold; 345 + border: 2px solid var(--bg-primary); 346 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 347 + } 348 + 349 + .composer-labels-picker { 350 + position: absolute; 351 + bottom: 100%; 352 + right: 0; 353 + margin-bottom: 12px; 354 + background: var(--bg-elevated); 355 + border: 1px solid var(--border); 356 + border-radius: var(--radius-md); 357 + padding: 8px 0; 358 + min-width: 200px; 359 + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); 360 + z-index: 50; 361 + animation: scaleIn 0.2s ease-out forwards; 362 + transform-origin: bottom right; 363 + } 364 + 365 + @keyframes scaleIn { 366 + from { 367 + opacity: 0; 368 + transform: scale(0.95) translateY(5px); 369 + } 370 + 371 + to { 372 + opacity: 1; 373 + transform: scale(1) translateY(0); 374 + } 375 + } 376 + 377 + .picker-header { 378 + font-size: 0.75rem; 379 + font-weight: 600; 380 + color: var(--text-tertiary); 381 + text-transform: uppercase; 382 + letter-spacing: 0.05em; 383 + margin-bottom: 4px; 384 + padding: 4px 12px 8px; 385 + border-bottom: 1px solid var(--border); 386 + } 387 + 388 + .picker-item { 389 + display: flex; 390 + align-items: center; 391 + gap: 10px; 392 + padding: 10px 14px; 393 + cursor: pointer; 394 + color: var(--text-secondary); 395 + font-size: 0.9rem; 396 + transition: all 0.15s ease; 397 + user-select: none; 398 + } 399 + 400 + .picker-item:hover { 401 + background: var(--bg-hover); 402 + color: var(--text-primary); 403 + } 404 + 405 + .picker-checkbox-wrapper { 406 + position: relative; 407 + width: 18px; 408 + height: 18px; 409 + display: flex; 410 + align-items: center; 411 + justify-content: center; 412 + } 413 + 414 + .picker-checkbox-wrapper input { 415 + position: absolute; 416 + opacity: 0; 417 + width: 100%; 418 + height: 100%; 419 + cursor: pointer; 420 + z-index: 10; 421 + } 422 + 423 + .picker-checkbox-custom { 424 + width: 18px; 425 + height: 18px; 426 + border: 2px solid var(--text-tertiary); 427 + border-radius: 4px; 428 + display: flex; 429 + align-items: center; 430 + justify-content: center; 431 + background: transparent; 432 + transition: all 0.2s ease; 433 + color: white; 434 + } 435 + 436 + .picker-item:hover .picker-checkbox-custom { 437 + border-color: var(--text-secondary); 438 + } 439 + 440 + .picker-checkbox-wrapper input:checked + .picker-checkbox-custom { 441 + background: var(--accent); 442 + border-color: var(--accent); 443 + color: white; 444 + } 445 + 446 + .composer-tags-input { 447 + width: 100%; 448 + padding: 12px 16px; 449 + background: var(--bg-secondary); 450 + border: 1px solid var(--border); 451 + border-radius: var(--radius-md); 452 + color: var(--text-primary); 453 + font-size: 0.95rem; 454 + transition: all 0.15s ease; 455 + } 456 + 457 + .composer-tags-input:focus { 458 + outline: none; 459 + border-color: var(--accent); 460 + box-shadow: 0 0 0 3px var(--accent-subtle); 461 + } 462 + 463 + .composer-tags-input::placeholder { 464 + color: var(--text-tertiary); 465 + } 466 + 467 + .history-panel { 468 + background: var(--bg-tertiary); 469 + border: 1px solid var(--border); 470 + border-radius: var(--radius-md); 471 + padding: 1rem; 472 + margin-bottom: 1rem; 473 + font-size: 0.9rem; 474 + animation: fadeIn 0.2s ease-out; 475 + } 476 + 477 + .history-header { 478 + display: flex; 479 + justify-content: space-between; 480 + align-items: center; 481 + margin-bottom: 1rem; 482 + padding-bottom: 0.5rem; 483 + border-bottom: 1px solid var(--border); 484 + } 485 + 486 + .history-title { 487 + font-weight: 600; 488 + text-transform: uppercase; 489 + letter-spacing: 0.05em; 490 + font-size: 0.75rem; 491 + color: var(--text-secondary); 492 + } 493 + 494 + .history-list { 495 + list-style: none; 496 + display: flex; 497 + flex-direction: column; 498 + gap: 1rem; 499 + } 500 + 501 + .history-item { 502 + position: relative; 503 + padding-left: 1rem; 504 + border-left: 2px solid var(--border); 505 + } 506 + 507 + .history-date { 508 + font-size: 0.75rem; 509 + color: var(--text-tertiary); 510 + margin-bottom: 0.25rem; 511 + } 512 + 513 + .history-content { 514 + color: var(--text-secondary); 515 + white-space: pre-wrap; 516 + } 517 + 518 + .history-close-btn { 519 + color: var(--text-tertiary); 520 + padding: 4px; 521 + border-radius: var(--radius-sm); 522 + transition: all 0.2s; 523 + display: flex; 524 + align-items: center; 525 + justify-content: center; 526 + } 527 + 528 + .history-close-btn:hover { 529 + background: var(--bg-hover); 530 + color: var(--text-primary); 531 + } 532 + 533 + .history-status { 534 + text-align: center; 535 + color: var(--text-tertiary); 536 + font-style: italic; 537 + padding: 1rem; 538 + } 539 + 540 + .share-menu-container { 541 + position: relative; 542 + } 543 + 544 + .share-menu { 545 + position: absolute; 546 + top: 100%; 547 + right: 0; 548 + margin-top: 8px; 549 + background: var(--bg-primary); 550 + border: 1px solid var(--border); 551 + border-radius: var(--radius-lg); 552 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); 553 + min-width: 180px; 554 + padding: 8px 0; 555 + z-index: 100; 556 + animation: fadeInUp 0.15s ease; 557 + } 558 + 559 + @keyframes fadeInUp { 560 + from { 561 + opacity: 0; 562 + transform: translateY(-8px); 563 + } 564 + 565 + to { 566 + opacity: 1; 567 + transform: translateY(0); 568 + } 569 + } 570 + 571 + .share-menu-section { 572 + display: flex; 573 + flex-direction: column; 574 + } 575 + 576 + .share-menu-label { 577 + padding: 4px 12px 8px; 578 + font-size: 0.7rem; 579 + font-weight: 600; 580 + text-transform: uppercase; 581 + letter-spacing: 0.05em; 582 + color: var(--text-tertiary); 583 + } 584 + 585 + .share-menu-item { 586 + display: flex; 587 + align-items: center; 588 + gap: 10px; 589 + padding: 10px 14px; 590 + background: none; 591 + border: none; 592 + width: 100%; 593 + text-align: left; 594 + font-size: 0.9rem; 595 + color: var(--text-primary); 596 + cursor: pointer; 597 + transition: all 0.1s ease; 598 + } 599 + 600 + .share-menu-item:hover { 601 + background: var(--bg-tertiary); 602 + } 603 + 604 + .share-menu-icon { 605 + font-size: 1.1rem; 606 + width: 24px; 607 + text-align: center; 608 + } 609 + 610 + .share-menu-divider { 611 + height: 1px; 612 + background: var(--border); 613 + margin: 6px 0; 614 + } 615 + 616 + .bookmark-card { 617 + display: flex; 618 + flex-direction: column; 619 + gap: 16px; 620 + } 621 + 622 + .bookmark-preview { 623 + display: flex; 624 + flex-direction: column; 625 + background: var(--bg-secondary); 626 + border: 1px solid var(--border); 627 + border-radius: var(--radius-md); 628 + overflow: hidden; 629 + text-decoration: none; 630 + transition: all 0.2s ease; 631 + position: relative; 632 + } 633 + 634 + .bookmark-preview:hover { 635 + border-color: var(--accent); 636 + box-shadow: var(--shadow-sm); 637 + transform: translateY(-1px); 638 + } 639 + 640 + .bookmark-preview::before { 641 + content: ""; 642 + position: absolute; 643 + left: 0; 644 + top: 0; 645 + bottom: 0; 646 + width: 4px; 647 + background: var(--accent); 648 + opacity: 0.7; 649 + } 650 + 651 + .bookmark-preview-content { 652 + padding: 16px 20px; 653 + display: flex; 654 + flex-direction: column; 655 + gap: 8px; 656 + } 657 + 658 + .bookmark-preview-header { 659 + display: flex; 660 + align-items: center; 661 + gap: 8px; 662 + margin-bottom: 4px; 663 + } 664 + 665 + .bookmark-preview-site { 666 + display: flex; 667 + align-items: center; 668 + gap: 6px; 669 + font-size: 0.75rem; 670 + font-weight: 600; 671 + color: var(--accent); 672 + text-transform: uppercase; 673 + letter-spacing: 0.03em; 674 + } 675 + 676 + .bookmark-preview-title { 677 + font-size: 1rem; 678 + font-weight: 600; 679 + line-height: 1.4; 680 + color: var(--text-primary); 681 + margin: 0; 682 + display: -webkit-box; 683 + -webkit-line-clamp: 2; 684 + line-clamp: 2; 685 + -webkit-box-orient: vertical; 686 + overflow: hidden; 687 + } 688 + 689 + .bookmark-preview-desc { 690 + font-size: 0.875rem; 691 + color: var(--text-secondary); 692 + line-height: 1.5; 693 + margin: 0; 694 + display: -webkit-box; 695 + -webkit-line-clamp: 2; 696 + line-clamp: 2; 697 + -webkit-box-orient: vertical; 698 + overflow: hidden; 699 + } 700 + 701 + .bookmark-preview-arrow { 702 + display: flex; 703 + align-items: center; 704 + justify-content: center; 705 + color: var(--text-tertiary); 706 + padding: 0 4px; 707 + transition: all 0.2s ease; 708 + } 709 + 710 + .bookmark-preview:hover .bookmark-preview-arrow { 711 + color: var(--accent); 712 + transform: translateX(2px); 713 + } 714 + 715 + .bookmark-description { 716 + font-size: 0.9rem; 717 + color: var(--text-secondary); 718 + margin: 0; 719 + line-height: 1.5; 720 + } 721 + 722 + .bookmark-meta { 723 + display: flex; 724 + align-items: center; 725 + gap: 12px; 726 + margin-top: 12px; 727 + font-size: 0.85rem; 728 + color: var(--text-tertiary); 729 + } 730 + 731 + .bookmark-time { 732 + color: var(--text-tertiary); 733 + } 734 + 735 + .bookmark-preview { 736 + max-width: 100%; 737 + width: 100%; 738 + box-sizing: border-box; 739 + } 740 + 741 + @media (max-width: 600px) { 742 + .bookmark-preview-content { 743 + padding: 12px 14px; 744 + } 745 + 746 + .legal-content { 747 + padding: 16px; 748 + } 749 + }
+13 -3424
web/src/index.css
··· 1 - :root { 2 - --bg-primary: #0c0a14; 3 - --bg-secondary: #110e1c; 4 - --bg-tertiary: #1a1528; 5 - --bg-card: #14111f; 6 - --bg-hover: #1e1932; 7 - --bg-elevated: #1a1528; 8 - 9 - --text-primary: #f4f0ff; 10 - --text-secondary: #a89ec8; 11 - --text-tertiary: #6b5f8a; 12 - 13 - --accent: #a855f7; 14 - --accent-hover: #c084fc; 15 - --accent-subtle: rgba(168, 85, 247, 0.15); 16 - 17 - --border: #2d2640; 18 - --border-hover: #3d3560; 19 - 20 - --success: #22c55e; 21 - --error: #ef4444; 22 - --warning: #f59e0b; 23 - 24 - --radius-sm: 6px; 25 - --radius-md: 10px; 26 - --radius-lg: 16px; 27 - --radius-full: 9999px; 28 - 29 - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 30 - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 31 - --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.5), 0 0 40px rgba(168, 85, 247, 0.1); 32 - --shadow-glow: 0 0 20px rgba(168, 85, 247, 0.3); 33 - 34 - --font-sans: 35 - "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; 36 - } 37 - 38 - * { 39 - margin: 0; 40 - padding: 0; 41 - box-sizing: border-box; 42 - } 43 - 44 - html { 45 - font-size: 16px; 46 - } 47 - 48 - body { 49 - font-family: var(--font-sans); 50 - background: var(--bg-primary); 51 - color: var(--text-primary); 52 - line-height: 1.6; 53 - min-height: 100vh; 54 - -webkit-font-smoothing: antialiased; 55 - -moz-osx-font-smoothing: grayscale; 56 - } 57 - 58 - a { 59 - color: var(--accent); 60 - text-decoration: none; 61 - transition: color 0.15s ease; 62 - } 63 - 64 - a:hover { 65 - color: var(--accent-hover); 66 - } 67 - 68 - button { 69 - font-family: inherit; 70 - cursor: pointer; 71 - border: none; 72 - background: none; 73 - } 74 - 75 - input, 76 - textarea { 77 - font-family: inherit; 78 - font-size: inherit; 79 - } 80 - 81 - .app { 82 - min-height: 100vh; 83 - display: flex; 84 - flex-direction: column; 85 - } 86 - 87 - .main-content { 88 - flex: 1; 89 - max-width: 680px; 90 - width: 100%; 91 - margin: 0 auto; 92 - padding: 24px 16px; 93 - } 94 - 95 - .btn { 96 - display: inline-flex; 97 - align-items: center; 98 - justify-content: center; 99 - gap: 8px; 100 - padding: 10px 20px; 101 - font-size: 0.9rem; 102 - font-weight: 500; 103 - border-radius: var(--radius-md); 104 - transition: all 0.15s ease; 105 - } 106 - 107 - .btn-primary { 108 - background: var(--accent); 109 - color: white; 110 - } 111 - 112 - .btn-primary:hover { 113 - background: var(--accent-hover); 114 - transform: translateY(-1px); 115 - box-shadow: var(--shadow-md); 116 - } 117 - 118 - .btn-secondary { 119 - background: var(--bg-tertiary); 120 - color: var(--text-primary); 121 - border: 1px solid var(--border); 122 - } 123 - 124 - .btn-secondary:hover { 125 - background: var(--bg-hover); 126 - border-color: var(--border-hover); 127 - } 128 - 129 - .btn-ghost { 130 - color: var(--text-secondary); 131 - padding: 8px 12px; 132 - } 133 - 134 - .btn-ghost:hover { 135 - color: var(--text-primary); 136 - background: var(--bg-tertiary); 137 - } 138 - 139 - .card { 140 - background: var(--bg-card); 141 - border: 1px solid var(--border); 142 - border-radius: var(--radius-lg); 143 - padding: 24px; 144 - transition: all 0.2s ease; 145 - position: relative; 146 - } 147 - 148 - .card:hover { 149 - border-color: var(--border-hover); 150 - box-shadow: var(--shadow-md); 151 - transform: translateY(-1px); 152 - } 153 - 154 - .annotation-card { 155 - display: flex; 156 - flex-direction: column; 157 - gap: 16px; 158 - } 159 - 160 - .annotation-header { 161 - display: flex; 162 - justify-content: space-between; 163 - align-items: flex-start; 164 - gap: 12px; 165 - } 166 - 167 - .annotation-header-left { 168 - display: flex; 169 - align-items: center; 170 - gap: 12px; 171 - flex: 1; 172 - min-width: 0; 173 - } 174 - 175 - .annotation-avatar { 176 - width: 40px; 177 - height: 40px; 178 - min-width: 40px; 179 - border-radius: var(--radius-full); 180 - background: linear-gradient(135deg, var(--accent), #a855f7); 181 - display: flex; 182 - align-items: center; 183 - justify-content: center; 184 - font-weight: 600; 185 - font-size: 0.95rem; 186 - color: white; 187 - overflow: hidden; 188 - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 189 - } 190 - 191 - .annotation-avatar img { 192 - width: 100%; 193 - height: 100%; 194 - object-fit: cover; 195 - } 196 - 197 - .annotation-meta { 198 - display: flex; 199 - flex-direction: column; 200 - justify-content: center; 201 - line-height: 1.3; 202 - } 203 - 204 - .annotation-avatar-link { 205 - text-decoration: none; 206 - border-radius: var(--radius-full); 207 - transition: transform 0.15s ease; 208 - } 209 - 210 - .annotation-avatar-link:hover { 211 - transform: scale(1.05); 212 - } 213 - 214 - .annotation-author-row { 215 - display: flex; 216 - align-items: center; 217 - gap: 6px; 218 - flex-wrap: wrap; 219 - } 220 - 221 - .annotation-author { 222 - font-weight: 600; 223 - color: var(--text-primary); 224 - font-size: 0.95rem; 225 - } 226 - 227 - .annotation-handle { 228 - font-size: 0.85rem; 229 - color: var(--text-tertiary); 230 - text-decoration: none; 231 - display: flex; 232 - align-items: center; 233 - gap: 3px; 234 - } 235 - 236 - .annotation-handle:hover { 237 - color: var(--accent); 238 - } 239 - 240 - .annotation-time { 241 - font-size: 0.8rem; 242 - color: var(--text-tertiary); 243 - } 244 - 245 - .annotation-content { 246 - display: flex; 247 - flex-direction: column; 248 - gap: 12px; 249 - } 250 - 251 - .annotation-source { 252 - display: inline-flex; 253 - align-items: center; 254 - gap: 6px; 255 - font-size: 0.8rem; 256 - color: var(--text-tertiary); 257 - text-decoration: none; 258 - padding: 4px 10px; 259 - background: var(--bg-tertiary); 260 - border-radius: var(--radius-full); 261 - width: fit-content; 262 - transition: all 0.15s ease; 263 - max-width: 100%; 264 - overflow: hidden; 265 - text-overflow: ellipsis; 266 - white-space: nowrap; 267 - } 268 - 269 - .annotation-source:hover { 270 - color: var(--text-primary); 271 - background: var(--bg-hover); 272 - } 273 - 274 - .annotation-source-title { 275 - color: var(--text-secondary); 276 - opacity: 0.8; 277 - } 278 - 279 - .annotation-highlight { 280 - display: block; 281 - position: relative; 282 - padding: 16px 20px; 283 - background: linear-gradient( 284 - 135deg, 285 - rgba(79, 70, 229, 0.03), 286 - rgba(168, 85, 247, 0.03) 287 - ); 288 - border-left: 3px solid var(--accent); 289 - border-radius: 4px var(--radius-md) var(--radius-md) 4px; 290 - text-decoration: none; 291 - transition: all 0.2s ease; 292 - margin: 4px 0; 293 - } 294 - 295 - .annotation-highlight:hover { 296 - background: linear-gradient( 297 - 135deg, 298 - rgba(79, 70, 229, 0.08), 299 - rgba(168, 85, 247, 0.08) 300 - ); 301 - transform: translateX(2px); 302 - } 303 - 304 - .annotation-highlight mark { 305 - background: transparent; 306 - color: var(--text-primary); 307 - font-style: italic; 308 - font-size: 1.05rem; 309 - line-height: 1.6; 310 - font-weight: 400; 311 - display: inline; 312 - } 313 - 314 - .annotation-text { 315 - font-size: 1rem; 316 - line-height: 1.65; 317 - color: var(--text-primary); 318 - white-space: pre-wrap; 319 - } 320 - 321 - .annotation-actions { 322 - display: flex; 323 - align-items: center; 324 - justify-content: space-between; 325 - padding-top: 16px; 326 - margin-top: 8px; 327 - border-top: 1px solid rgba(255, 255, 255, 0.03); 328 - } 329 - 330 - .annotation-actions-left { 331 - display: flex; 332 - align-items: center; 333 - gap: 8px; 334 - } 335 - 336 - .annotation-action { 337 - display: flex; 338 - align-items: center; 339 - gap: 6px; 340 - color: var(--text-tertiary); 341 - font-size: 0.85rem; 342 - font-weight: 500; 343 - padding: 6px 10px; 344 - border-radius: var(--radius-md); 345 - transition: all 0.2s ease; 346 - background: transparent; 347 - cursor: pointer; 348 - } 349 - 350 - .annotation-action:hover { 351 - color: var(--text-secondary); 352 - background: var(--bg-elevated); 353 - } 354 - 355 - .annotation-action.liked { 356 - color: #ef4444; 357 - background: rgba(239, 68, 68, 0.05); 358 - } 359 - 360 - .annotation-action.liked:hover { 361 - background: rgba(239, 68, 68, 0.1); 362 - } 363 - 364 - .annotation-action.active { 365 - color: var(--accent); 366 - background: var(--accent-subtle); 367 - } 368 - 369 - .action-icon-only { 370 - padding: 8px; 371 - } 372 - 373 - .annotation-delete { 374 - background: none; 375 - border: none; 376 - cursor: pointer; 377 - padding: 8px; 378 - font-size: 1rem; 379 - color: var(--text-tertiary); 380 - transition: all 0.2s ease; 381 - border-radius: var(--radius-md); 382 - opacity: 0.6; 383 - } 384 - 385 - .annotation-delete:hover { 386 - color: var(--error); 387 - background: rgba(239, 68, 68, 0.1); 388 - opacity: 1; 389 - } 390 - 391 - .annotation-delete:disabled { 392 - cursor: not-allowed; 393 - opacity: 0.3; 394 - } 395 - 396 - .share-menu-container { 397 - position: relative; 398 - } 399 - 400 - .share-menu { 401 - position: absolute; 402 - top: 100%; 403 - right: 0; 404 - margin-top: 8px; 405 - background: var(--bg-primary); 406 - border: 1px solid var(--border); 407 - border-radius: var(--radius-lg); 408 - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); 409 - min-width: 180px; 410 - padding: 8px 0; 411 - z-index: 100; 412 - animation: fadeInUp 0.15s ease; 413 - } 414 - 415 - @keyframes fadeInUp { 416 - from { 417 - opacity: 0; 418 - transform: translateY(-8px); 419 - } 420 - 421 - to { 422 - opacity: 1; 423 - transform: translateY(0); 424 - } 425 - } 426 - 427 - .share-menu-section { 428 - display: flex; 429 - flex-direction: column; 430 - } 431 - 432 - .share-menu-label { 433 - padding: 4px 12px 8px; 434 - font-size: 0.7rem; 435 - font-weight: 600; 436 - text-transform: uppercase; 437 - letter-spacing: 0.05em; 438 - color: var(--text-tertiary); 439 - } 440 - 441 - .share-menu-item { 442 - display: flex; 443 - align-items: center; 444 - gap: 10px; 445 - padding: 10px 14px; 446 - background: none; 447 - border: none; 448 - width: 100%; 449 - text-align: left; 450 - font-size: 0.9rem; 451 - color: var(--text-primary); 452 - cursor: pointer; 453 - transition: all 0.1s ease; 454 - } 455 - 456 - .share-menu-item:hover { 457 - background: var(--bg-tertiary); 458 - } 459 - 460 - .share-menu-icon { 461 - font-size: 1.1rem; 462 - width: 24px; 463 - text-align: center; 464 - } 465 - 466 - .share-menu-divider { 467 - height: 1px; 468 - background: var(--border); 469 - margin: 6px 0; 470 - } 471 - 472 - .feed { 473 - display: flex; 474 - flex-direction: column; 475 - gap: 16px; 476 - } 477 - 478 - .feed-header { 479 - display: flex; 480 - align-items: center; 481 - justify-content: space-between; 482 - margin-bottom: 8px; 483 - } 484 - 485 - .feed-title { 486 - font-size: 1.5rem; 487 - font-weight: 700; 488 - } 489 - 490 - .page-header { 491 - margin-bottom: 32px; 492 - } 493 - 494 - .page-title { 495 - font-size: 2rem; 496 - font-weight: 700; 497 - margin-bottom: 8px; 498 - } 499 - 500 - .page-description { 501 - color: var(--text-secondary); 502 - font-size: 1.1rem; 503 - } 504 - 505 - .url-input-wrapper { 506 - margin-bottom: 32px; 507 - } 508 - 509 - .url-input-container { 510 - display: flex; 511 - gap: 12px; 512 - } 513 - 514 - .url-input { 515 - flex: 1; 516 - padding: 14px 18px; 517 - background: var(--bg-secondary); 518 - border: 1px solid var(--border); 519 - border-radius: var(--radius-md); 520 - color: var(--text-primary); 521 - font-size: 1rem; 522 - transition: all 0.15s ease; 523 - } 524 - 525 - .url-input:focus { 526 - outline: none; 527 - border-color: var(--accent); 528 - box-shadow: 0 0 0 3px var(--accent-subtle); 529 - } 530 - 531 - .url-input::placeholder { 532 - color: var(--text-tertiary); 533 - } 534 - 535 - .empty-state { 536 - text-align: center; 537 - padding: 60px 20px; 538 - color: var(--text-secondary); 539 - } 540 - 541 - .empty-state-icon { 542 - font-size: 3rem; 543 - margin-bottom: 16px; 544 - opacity: 0.5; 545 - } 546 - 547 - .empty-state-title { 548 - font-size: 1.25rem; 549 - font-weight: 600; 550 - color: var(--text-primary); 551 - margin-bottom: 8px; 552 - } 553 - 554 - .empty-state-text { 555 - font-size: 1rem; 556 - max-width: 400px; 557 - margin: 0 auto; 558 - } 559 - 560 - .feed-filters { 561 - display: flex; 562 - gap: 8px; 563 - margin-bottom: 24px; 564 - padding: 4px; 565 - background: var(--bg-tertiary); 566 - border-radius: var(--radius-lg); 567 - width: fit-content; 568 - } 569 - 570 - .login-page { 571 - display: flex; 572 - flex-direction: column; 573 - align-items: center; 574 - justify-content: center; 575 - min-height: 70vh; 576 - padding: 60px 20px; 577 - width: 100%; 578 - max-width: 500px; 579 - margin: 0 auto; 580 - } 581 - 582 - .login-at-logo { 583 - font-size: 5rem; 584 - font-weight: 800; 585 - color: var(--accent); 586 - margin-bottom: 24px; 587 - line-height: 1; 588 - } 589 - 590 - .login-heading { 591 - font-size: 1.5rem; 592 - font-weight: 600; 593 - margin-bottom: 32px; 594 - display: flex; 595 - align-items: center; 596 - gap: 10px; 597 - text-align: center; 598 - line-height: 1.4; 599 - } 600 - 601 - .login-help-btn { 602 - background: none; 603 - border: none; 604 - color: var(--text-tertiary); 605 - cursor: pointer; 606 - padding: 4px; 607 - display: flex; 608 - align-items: center; 609 - transition: color 0.15s; 610 - flex-shrink: 0; 611 - } 612 - 613 - .login-help-btn:hover { 614 - color: var(--accent); 615 - } 616 - 617 - .login-help-text { 618 - background: var(--bg-elevated); 619 - border: 1px solid var(--border); 620 - border-radius: var(--radius-md); 621 - padding: 16px 20px; 622 - margin-bottom: 24px; 623 - font-size: 0.95rem; 624 - color: var(--text-secondary); 625 - line-height: 1.6; 626 - text-align: center; 627 - } 628 - 629 - .login-help-text code { 630 - background: var(--bg-tertiary); 631 - padding: 2px 8px; 632 - border-radius: var(--radius-sm); 633 - font-size: 0.9rem; 634 - } 635 - 636 - .login-form { 637 - display: flex; 638 - flex-direction: column; 639 - gap: 20px; 640 - width: 100%; 641 - } 642 - 643 - .login-input-wrapper { 644 - position: relative; 645 - } 646 - 647 - .login-input { 648 - width: 100%; 649 - padding: 18px 20px; 650 - background: var(--bg-elevated); 651 - border: 2px solid var(--border); 652 - border-radius: var(--radius-lg); 653 - color: var(--text-primary); 654 - font-size: 1.1rem; 655 - transition: 656 - border-color 0.15s, 657 - box-shadow 0.15s; 658 - } 659 - 660 - .login-input:focus { 661 - outline: none; 662 - border-color: var(--accent); 663 - box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15); 664 - } 665 - 666 - .login-input::placeholder { 667 - color: var(--text-tertiary); 668 - } 669 - 670 - .login-suggestions { 671 - position: absolute; 672 - top: calc(100% + 8px); 673 - left: 0; 674 - right: 0; 675 - background: var(--bg-card); 676 - border: 1px solid var(--border); 677 - border-radius: var(--radius-lg); 678 - box-shadow: var(--shadow-lg); 679 - overflow: hidden; 680 - z-index: 100; 681 - } 682 - 683 - .login-suggestion { 684 - display: flex; 685 - align-items: center; 686 - gap: 14px; 687 - width: 100%; 688 - padding: 14px 18px; 689 - background: transparent; 690 - border: none; 691 - cursor: pointer; 692 - text-align: left; 693 - color: var(--text-primary); 694 - transition: background 0.1s; 695 - } 696 - 697 - .login-suggestion:hover, 698 - .login-suggestion.selected { 699 - background: var(--bg-elevated); 700 - } 701 - 702 - .login-suggestion-avatar { 703 - width: 44px; 704 - height: 44px; 705 - border-radius: var(--radius-full); 706 - background: linear-gradient(135deg, var(--accent), #a855f7); 707 - display: flex; 708 - align-items: center; 709 - justify-content: center; 710 - flex-shrink: 0; 711 - overflow: hidden; 712 - font-size: 0.9rem; 713 - font-weight: 600; 714 - color: white; 715 - } 716 - 717 - .login-suggestion-avatar img { 718 - width: 100%; 719 - height: 100%; 720 - object-fit: cover; 721 - } 722 - 723 - .login-suggestion-info { 724 - display: flex; 725 - flex-direction: column; 726 - gap: 2px; 727 - min-width: 0; 728 - } 729 - 730 - .login-suggestion-name { 731 - font-weight: 600; 732 - font-size: 1rem; 733 - color: var(--text-primary); 734 - white-space: nowrap; 735 - overflow: hidden; 736 - text-overflow: ellipsis; 737 - } 738 - 739 - .login-suggestion-handle { 740 - font-size: 0.9rem; 741 - color: var(--text-secondary); 742 - white-space: nowrap; 743 - overflow: hidden; 744 - text-overflow: ellipsis; 745 - } 746 - 747 - .login-error { 748 - padding: 12px 16px; 749 - background: rgba(239, 68, 68, 0.1); 750 - border: 1px solid rgba(239, 68, 68, 0.3); 751 - border-radius: var(--radius-md); 752 - color: #ef4444; 753 - font-size: 0.9rem; 754 - text-align: center; 755 - } 756 - 757 - .login-submit { 758 - padding: 18px 32px; 759 - font-size: 1.1rem; 760 - font-weight: 600; 761 - } 762 - 763 - .login-avatar-large { 764 - width: 100px; 765 - height: 100px; 766 - border-radius: var(--radius-full); 767 - background: linear-gradient(135deg, var(--accent), #a855f7); 768 - display: flex; 769 - align-items: center; 770 - justify-content: center; 771 - margin-bottom: 20px; 772 - font-weight: 700; 773 - font-size: 2rem; 774 - color: white; 775 - overflow: hidden; 776 - } 777 - 778 - .login-avatar-large img { 779 - width: 100%; 780 - height: 100%; 781 - object-fit: cover; 782 - } 783 - 784 - .login-welcome { 785 - font-size: 1.5rem; 786 - font-weight: 600; 787 - margin-bottom: 32px; 788 - text-align: center; 789 - } 790 - 791 - .login-actions { 792 - display: flex; 793 - flex-direction: column; 794 - gap: 12px; 795 - width: 100%; 796 - } 797 - 798 - .login-avatar { 799 - width: 72px; 800 - height: 72px; 801 - border-radius: var(--radius-full); 802 - background: linear-gradient(135deg, var(--accent), #a855f7); 803 - display: flex; 804 - align-items: center; 805 - justify-content: center; 806 - margin: 0 auto 16px; 807 - font-weight: 700; 808 - font-size: 1.5rem; 809 - color: white; 810 - overflow: hidden; 811 - } 812 - 813 - .login-avatar img { 814 - width: 100%; 815 - height: 100%; 816 - object-fit: cover; 817 - } 818 - 819 - .login-welcome-name { 820 - font-size: 1.25rem; 821 - font-weight: 600; 822 - margin-bottom: 24px; 823 - } 824 - 825 - .login-actions { 826 - display: flex; 827 - flex-direction: column; 828 - gap: 12px; 829 - } 830 - 831 - .btn-bluesky { 832 - background: #0085ff; 833 - color: white; 834 - display: flex; 835 - align-items: center; 836 - justify-content: center; 837 - gap: 10px; 838 - transition: 839 - background 0.2s, 840 - transform 0.2s; 841 - } 842 - 843 - .btn-bluesky:hover { 844 - background: #0070dd; 845 - transform: translateY(-1px); 846 - } 847 - 848 - .login-btn { 849 - width: 100%; 850 - padding: 14px 24px; 851 - font-size: 1rem; 852 - font-weight: 600; 853 - } 854 - 855 - .login-brand { 856 - display: flex; 857 - align-items: center; 858 - justify-content: center; 859 - gap: 12px; 860 - margin-bottom: 24px; 861 - } 862 - 863 - .login-brand-icon { 864 - width: 48px; 865 - height: 48px; 866 - background: linear-gradient(135deg, var(--accent), #a855f7); 867 - border-radius: var(--radius-lg); 868 - display: flex; 869 - align-items: center; 870 - justify-content: center; 871 - font-size: 1.75rem; 872 - font-weight: 800; 873 - color: white; 874 - } 875 - 876 - .login-brand-name { 877 - font-size: 1.75rem; 878 - font-weight: 700; 879 - } 880 - 881 - .login-form { 882 - display: flex; 883 - flex-direction: column; 884 - gap: 16px; 885 - } 886 - 887 - .login-input-wrapper { 888 - position: relative; 889 - } 890 - 891 - .login-input { 892 - width: 100%; 893 - padding: 14px 16px; 894 - background: var(--bg-elevated); 895 - border: 1px solid var(--border); 896 - border-radius: var(--radius-md); 897 - color: var(--text-primary); 898 - font-size: 1rem; 899 - transition: 900 - border-color 0.15s, 901 - box-shadow 0.15s; 902 - } 903 - 904 - .login-input:focus { 905 - outline: none; 906 - border-color: var(--accent); 907 - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); 908 - } 909 - 910 - .login-input::placeholder { 911 - color: var(--text-tertiary); 912 - } 913 - 914 - .login-suggestions { 915 - position: absolute; 916 - top: calc(100% + 4px); 917 - left: 0; 918 - right: 0; 919 - background: var(--bg-card); 920 - border: 1px solid var(--border); 921 - border-radius: var(--radius-md); 922 - box-shadow: var(--shadow-lg); 923 - overflow: hidden; 924 - z-index: 100; 925 - } 926 - 927 - .login-suggestion { 928 - display: flex; 929 - align-items: center; 930 - gap: 12px; 931 - width: 100%; 932 - padding: 12px 16px; 933 - background: transparent; 934 - border: none; 935 - cursor: pointer; 936 - text-align: left; 937 - transition: background 0.1s; 938 - } 939 - 940 - .login-suggestion:hover, 941 - .login-suggestion.selected { 942 - background: var(--bg-elevated); 943 - } 944 - 945 - .login-suggestion-avatar { 946 - width: 40px; 947 - height: 40px; 948 - border-radius: var(--radius-full); 949 - background: linear-gradient(135deg, var(--accent), #a855f7); 950 - display: flex; 951 - align-items: center; 952 - justify-content: center; 953 - flex-shrink: 0; 954 - overflow: hidden; 955 - font-size: 0.875rem; 956 - font-weight: 600; 957 - color: white; 958 - } 959 - 960 - .login-suggestion-avatar img { 961 - width: 100%; 962 - height: 100%; 963 - object-fit: cover; 964 - } 965 - 966 - .login-suggestion-info { 967 - display: flex; 968 - flex-direction: column; 969 - min-width: 0; 970 - } 971 - 972 - .login-suggestion-name { 973 - font-weight: 600; 974 - color: var(--text-primary); 975 - white-space: nowrap; 976 - overflow: hidden; 977 - text-overflow: ellipsis; 978 - } 979 - 980 - .login-suggestion-handle { 981 - font-size: 0.875rem; 982 - color: var(--text-secondary); 983 - white-space: nowrap; 984 - overflow: hidden; 985 - text-overflow: ellipsis; 986 - } 987 - 988 - .login-error { 989 - padding: 12px 16px; 990 - background: rgba(239, 68, 68, 0.1); 991 - border: 1px solid rgba(239, 68, 68, 0.3); 992 - border-radius: var(--radius-md); 993 - color: #ef4444; 994 - font-size: 0.875rem; 995 - } 996 - 997 - .login-legal { 998 - font-size: 0.75rem; 999 - color: var(--text-tertiary); 1000 - line-height: 1.5; 1001 - margin-top: 16px; 1002 - } 1003 - 1004 - .profile-header { 1005 - display: flex; 1006 - align-items: center; 1007 - gap: 20px; 1008 - margin-bottom: 32px; 1009 - padding-bottom: 24px; 1010 - border-bottom: 1px solid var(--border); 1011 - } 1012 - 1013 - .profile-avatar { 1014 - width: 80px; 1015 - height: 80px; 1016 - min-width: 80px; 1017 - border-radius: var(--radius-full); 1018 - background: linear-gradient(135deg, var(--accent), #a855f7); 1019 - display: flex; 1020 - align-items: center; 1021 - justify-content: center; 1022 - font-weight: 700; 1023 - font-size: 2rem; 1024 - color: white; 1025 - overflow: hidden; 1026 - } 1027 - 1028 - .profile-avatar img { 1029 - width: 100%; 1030 - height: 100%; 1031 - object-fit: cover; 1032 - } 1033 - 1034 - .profile-avatar-link { 1035 - text-decoration: none; 1036 - } 1037 - 1038 - .profile-info { 1039 - flex: 1; 1040 - } 1041 - 1042 - .profile-name { 1043 - font-size: 1.5rem; 1044 - font-weight: 700; 1045 - } 1046 - 1047 - .profile-handle-link { 1048 - color: var(--text-secondary); 1049 - text-decoration: none; 1050 - } 1051 - 1052 - .profile-handle-link:hover { 1053 - color: var(--accent); 1054 - text-decoration: underline; 1055 - } 1056 - 1057 - .profile-bluesky-link { 1058 - display: inline-flex; 1059 - align-items: center; 1060 - gap: 6px; 1061 - color: #0085ff; 1062 - text-decoration: none; 1063 - font-size: 0.95rem; 1064 - padding: 4px 10px; 1065 - border-radius: var(--radius-md); 1066 - background: rgba(0, 133, 255, 0.1); 1067 - transition: all 0.15s ease; 1068 - } 1069 - 1070 - .profile-bluesky-link:hover { 1071 - background: rgba(0, 133, 255, 0.2); 1072 - color: #0070dd; 1073 - } 1074 - 1075 - .profile-stats { 1076 - display: flex; 1077 - gap: 24px; 1078 - margin-top: 8px; 1079 - } 1080 - 1081 - .profile-stat { 1082 - color: var(--text-secondary); 1083 - font-size: 0.9rem; 1084 - } 1085 - 1086 - .profile-stat strong { 1087 - color: var(--text-primary); 1088 - } 1089 - 1090 - .profile-tabs { 1091 - display: flex; 1092 - gap: 0; 1093 - margin-bottom: 24px; 1094 - border-bottom: 1px solid var(--border); 1095 - } 1096 - 1097 - .profile-tab { 1098 - padding: 12px 20px; 1099 - font-size: 0.9rem; 1100 - font-weight: 500; 1101 - color: var(--text-secondary); 1102 - background: transparent; 1103 - border: none; 1104 - border-bottom: 2px solid transparent; 1105 - cursor: pointer; 1106 - transition: all 0.15s ease; 1107 - margin-bottom: -1px; 1108 - } 1109 - 1110 - .profile-tab:hover { 1111 - color: var(--text-primary); 1112 - background: var(--bg-tertiary); 1113 - } 1114 - 1115 - .profile-tab.active { 1116 - color: var(--accent); 1117 - border-bottom-color: var(--accent); 1118 - } 1119 - 1120 - .bookmark-description { 1121 - font-size: 0.9rem; 1122 - color: var(--text-secondary); 1123 - margin: 0; 1124 - line-height: 1.5; 1125 - } 1126 - 1127 - .bookmark-meta { 1128 - display: flex; 1129 - align-items: center; 1130 - gap: 12px; 1131 - margin-top: 12px; 1132 - font-size: 0.85rem; 1133 - color: var(--text-tertiary); 1134 - } 1135 - 1136 - .bookmark-time { 1137 - color: var(--text-tertiary); 1138 - } 1139 - 1140 - .composer { 1141 - margin-bottom: 24px; 1142 - } 1143 - 1144 - .composer-textarea { 1145 - width: 100%; 1146 - min-height: 120px; 1147 - padding: 16px; 1148 - background: var(--bg-secondary); 1149 - border: 1px solid var(--border); 1150 - border-radius: var(--radius-md); 1151 - color: var(--text-primary); 1152 - font-size: 1rem; 1153 - resize: vertical; 1154 - transition: all 0.15s ease; 1155 - } 1156 - 1157 - .composer-textarea:focus { 1158 - outline: none; 1159 - border-color: var(--accent); 1160 - box-shadow: 0 0 0 3px var(--accent-subtle); 1161 - } 1162 - 1163 - .composer-footer { 1164 - display: flex; 1165 - justify-content: space-between; 1166 - align-items: center; 1167 - margin-top: 12px; 1168 - } 1169 - 1170 - .composer-char-count { 1171 - font-size: 0.85rem; 1172 - color: var(--text-tertiary); 1173 - } 1174 - 1175 - .composer-char-count.warning { 1176 - color: var(--warning); 1177 - } 1178 - 1179 - .composer-char-count.error { 1180 - color: var(--error); 1181 - } 1182 - 1183 - .composer-add-quote { 1184 - width: 100%; 1185 - padding: 12px 16px; 1186 - margin-bottom: 12px; 1187 - background: var(--bg-tertiary); 1188 - border: 1px dashed var(--border); 1189 - border-radius: var(--radius-md); 1190 - color: var(--text-secondary); 1191 - font-size: 0.9rem; 1192 - cursor: pointer; 1193 - transition: all 0.15s ease; 1194 - } 1195 - 1196 - .composer-add-quote:hover { 1197 - border-color: var(--accent); 1198 - color: var(--accent); 1199 - background: var(--accent-subtle); 1200 - } 1201 - 1202 - .composer-quote-input-wrapper { 1203 - margin-bottom: 12px; 1204 - } 1205 - 1206 - .composer-quote-input { 1207 - width: 100%; 1208 - padding: 12px 16px; 1209 - background: linear-gradient( 1210 - 135deg, 1211 - rgba(79, 70, 229, 0.05), 1212 - rgba(168, 85, 247, 0.05) 1213 - ); 1214 - border: 1px solid var(--border); 1215 - border-left: 3px solid var(--accent); 1216 - border-radius: 0 var(--radius-md) var(--radius-md) 0; 1217 - color: var(--text-primary); 1218 - font-size: 0.95rem; 1219 - font-style: italic; 1220 - resize: vertical; 1221 - font-family: inherit; 1222 - transition: all 0.15s ease; 1223 - } 1224 - 1225 - .composer-quote-input:focus { 1226 - outline: none; 1227 - border-color: var(--accent); 1228 - } 1229 - 1230 - .composer-quote-input::placeholder { 1231 - color: var(--text-tertiary); 1232 - font-style: italic; 1233 - } 1234 - 1235 - .composer-quote-remove-btn { 1236 - margin-top: 8px; 1237 - padding: 6px 12px; 1238 - background: none; 1239 - border: none; 1240 - color: var(--text-tertiary); 1241 - font-size: 0.85rem; 1242 - cursor: pointer; 1243 - } 1244 - 1245 - .composer-quote-remove-btn:hover { 1246 - color: var(--error); 1247 - } 1248 - 1249 - @keyframes shimmer { 1250 - 0% { 1251 - background-position: -200% 0; 1252 - } 1253 - 1254 - 100% { 1255 - background-position: 200% 0; 1256 - } 1257 - } 1258 - 1259 - .skeleton { 1260 - background: linear-gradient( 1261 - 90deg, 1262 - var(--bg-tertiary) 25%, 1263 - var(--bg-hover) 50%, 1264 - var(--bg-tertiary) 75% 1265 - ); 1266 - background-size: 200% 100%; 1267 - animation: shimmer 1.5s infinite; 1268 - border-radius: var(--radius-sm); 1269 - } 1270 - 1271 - .skeleton-text { 1272 - height: 1em; 1273 - margin-bottom: 8px; 1274 - } 1275 - 1276 - .skeleton-text:last-child { 1277 - width: 60%; 1278 - } 1279 - 1280 - @media (max-width: 640px) { 1281 - .main-content { 1282 - padding: 16px 12px; 1283 - } 1284 - 1285 - .navbar-inner { 1286 - padding: 0 16px; 1287 - } 1288 - 1289 - .page-title { 1290 - font-size: 1.5rem; 1291 - } 1292 - 1293 - .url-input-container { 1294 - flex-direction: column; 1295 - } 1296 - 1297 - .profile-header { 1298 - flex-direction: column; 1299 - text-align: center; 1300 - } 1301 - 1302 - .profile-stats { 1303 - justify-content: center; 1304 - } 1305 - } 1306 - 1307 - .main { 1308 - flex: 1; 1309 - width: 100%; 1310 - } 1311 - 1312 - .page-container { 1313 - max-width: 680px; 1314 - margin: 0 auto; 1315 - padding: 24px 16px; 1316 - } 1317 - 1318 - .navbar-logo { 1319 - width: 32px; 1320 - height: 32px; 1321 - background: linear-gradient(135deg, var(--accent), #8b5cf6); 1322 - border-radius: var(--radius-sm); 1323 - display: flex; 1324 - align-items: center; 1325 - justify-content: center; 1326 - font-weight: 700; 1327 - font-size: 1rem; 1328 - color: white; 1329 - } 1330 - 1331 - .navbar-user { 1332 - display: flex; 1333 - align-items: center; 1334 - gap: 8px; 1335 - } 1336 - 1337 - .navbar-avatar { 1338 - width: 36px; 1339 - height: 36px; 1340 - border-radius: var(--radius-full); 1341 - background: linear-gradient(135deg, var(--accent), #a855f7); 1342 - display: flex; 1343 - align-items: center; 1344 - justify-content: center; 1345 - font-weight: 600; 1346 - font-size: 0.85rem; 1347 - color: white; 1348 - text-decoration: none; 1349 - } 1350 - 1351 - .btn-sm { 1352 - padding: 6px 12px; 1353 - font-size: 0.85rem; 1354 - } 1355 - 1356 - .composer-url { 1357 - font-size: 0.85rem; 1358 - color: var(--text-secondary); 1359 - word-break: break-all; 1360 - } 1361 - 1362 - .composer-quote { 1363 - position: relative; 1364 - padding: 12px 16px; 1365 - padding-right: 36px; 1366 - background: var(--bg-secondary); 1367 - border-left: 3px solid var(--accent); 1368 - border-radius: 0 var(--radius-sm) var(--radius-sm) 0; 1369 - margin-bottom: 16px; 1370 - font-style: italic; 1371 - color: var(--text-secondary); 1372 - } 1373 - 1374 - .composer-quote-remove { 1375 - position: absolute; 1376 - top: 8px; 1377 - right: 8px; 1378 - width: 24px; 1379 - height: 24px; 1380 - border-radius: var(--radius-full); 1381 - background: var(--bg-tertiary); 1382 - color: var(--text-secondary); 1383 - font-size: 1rem; 1384 - display: flex; 1385 - align-items: center; 1386 - justify-content: center; 1387 - } 1388 - 1389 - .composer-quote-remove:hover { 1390 - background: var(--bg-hover); 1391 - color: var(--text-primary); 1392 - } 1393 - 1394 - .composer-input { 1395 - width: 100%; 1396 - min-height: 120px; 1397 - padding: 16px; 1398 - background: var(--bg-secondary); 1399 - border: 1px solid var(--border); 1400 - border-radius: var(--radius-md); 1401 - color: var(--text-primary); 1402 - font-size: 1rem; 1403 - resize: vertical; 1404 - transition: all 0.15s ease; 1405 - } 1406 - 1407 - .composer-input:focus { 1408 - outline: none; 1409 - border-color: var(--accent); 1410 - box-shadow: 0 0 0 3px var(--accent-subtle); 1411 - } 1412 - 1413 - .composer-input::placeholder { 1414 - color: var(--text-tertiary); 1415 - } 1416 - 1417 - .composer-tags { 1418 - margin-top: 12px; 1419 - } 1420 - 1421 - .composer-tags-input { 1422 - width: 100%; 1423 - padding: 12px 16px; 1424 - background: var(--bg-secondary); 1425 - border: 1px solid var(--border); 1426 - border-radius: var(--radius-md); 1427 - color: var(--text-primary); 1428 - font-size: 0.95rem; 1429 - transition: all 0.15s ease; 1430 - } 1431 - 1432 - .composer-tags-input:focus { 1433 - outline: none; 1434 - border-color: var(--accent); 1435 - box-shadow: 0 0 0 3px var(--accent-subtle); 1436 - } 1437 - 1438 - .composer-tags-input::placeholder { 1439 - color: var(--text-tertiary); 1440 - } 1441 - 1442 - .composer-footer { 1443 - display: flex; 1444 - justify-content: space-between; 1445 - align-items: center; 1446 - margin-top: 12px; 1447 - } 1448 - 1449 - .composer-count { 1450 - font-size: 0.85rem; 1451 - color: var(--text-tertiary); 1452 - } 1453 - 1454 - .composer-actions { 1455 - display: flex; 1456 - gap: 8px; 1457 - } 1458 - 1459 - .composer-error { 1460 - margin-top: 12px; 1461 - padding: 12px; 1462 - background: rgba(239, 68, 68, 0.1); 1463 - border: 1px solid rgba(239, 68, 68, 0.3); 1464 - border-radius: var(--radius-md); 1465 - color: var(--error); 1466 - font-size: 0.9rem; 1467 - } 1468 - 1469 - .annotation-tags { 1470 - display: flex; 1471 - flex-wrap: wrap; 1472 - gap: 6px; 1473 - margin-top: 12px; 1474 - margin-bottom: 8px; 1475 - } 1476 - 1477 - .annotation-tag { 1478 - display: inline-flex; 1479 - align-items: center; 1480 - padding: 4px 10px; 1481 - background: var(--bg-tertiary); 1482 - color: var(--text-secondary); 1483 - font-size: 0.8rem; 1484 - font-weight: 500; 1485 - border-radius: var(--radius-full); 1486 - transition: all 0.15s ease; 1487 - border: 1px solid transparent; 1488 - text-decoration: none; 1489 - } 1490 - 1491 - .annotation-tag:hover { 1492 - background: var(--bg-hover); 1493 - color: var(--text-primary); 1494 - border-color: var(--border); 1495 - transform: translateY(-1px); 1496 - } 1497 - 1498 - .url-input-wrapper { 1499 - margin-bottom: 24px; 1500 - } 1501 - 1502 - .url-input { 1503 - width: 100%; 1504 - padding: 16px; 1505 - background: var(--bg-secondary); 1506 - border: 1px solid var(--border); 1507 - border-radius: var(--radius-md); 1508 - color: var(--text-primary); 1509 - font-size: 1.1rem; 1510 - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 1511 - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 1512 - } 1513 - 1514 - .url-input:focus { 1515 - outline: none; 1516 - border-color: var(--accent); 1517 - box-shadow: 0 0 0 4px var(--accent-subtle); 1518 - background: var(--bg-primary); 1519 - } 1520 - 1521 - .url-input::placeholder { 1522 - color: var(--text-tertiary); 1523 - } 1524 - 1525 - .annotation-detail-page { 1526 - max-width: 680px; 1527 - margin: 0 auto; 1528 - padding: 24px 16px; 1529 - } 1530 - 1531 - .annotation-detail-header { 1532 - margin-bottom: 24px; 1533 - } 1534 - 1535 - .back-link { 1536 - color: var(--text-secondary); 1537 - text-decoration: none; 1538 - font-size: 0.9rem; 1539 - } 1540 - 1541 - .back-link:hover { 1542 - color: var(--accent); 1543 - } 1544 - 1545 - .replies-section { 1546 - margin-top: 32px; 1547 - } 1548 - 1549 - .replies-title { 1550 - font-size: 1.1rem; 1551 - font-weight: 600; 1552 - margin-bottom: 16px; 1553 - color: var(--text-primary); 1554 - } 1555 - 1556 - .reply-form { 1557 - margin-bottom: 24px; 1558 - } 1559 - 1560 - .reply-input { 1561 - width: 100%; 1562 - padding: 12px; 1563 - border: 1px solid var(--border); 1564 - border-radius: var(--radius-md); 1565 - font-size: 0.95rem; 1566 - resize: vertical; 1567 - margin-bottom: 12px; 1568 - font-family: inherit; 1569 - } 1570 - 1571 - .reply-input:focus { 1572 - outline: none; 1573 - border-color: var(--accent); 1574 - box-shadow: 0 0 0 3px var(--accent-subtle); 1575 - } 1576 - 1577 - .replies-list { 1578 - display: flex; 1579 - flex-direction: column; 1580 - gap: 12px; 1581 - } 1582 - 1583 - .reply-card { 1584 - padding: 16px; 1585 - background: var(--bg-secondary); 1586 - border-radius: var(--radius-md); 1587 - border: 1px solid var(--border); 1588 - } 1589 - 1590 - .reply-header { 1591 - display: flex; 1592 - align-items: center; 1593 - gap: 12px; 1594 - margin-bottom: 12px; 1595 - } 1596 - 1597 - .reply-avatar-link { 1598 - text-decoration: none; 1599 - } 1600 - 1601 - .reply-avatar { 1602 - width: 36px; 1603 - height: 36px; 1604 - min-width: 36px; 1605 - border-radius: var(--radius-full); 1606 - background: linear-gradient(135deg, var(--accent), #a855f7); 1607 - display: flex; 1608 - align-items: center; 1609 - justify-content: center; 1610 - font-weight: 600; 1611 - font-size: 0.85rem; 1612 - color: white; 1613 - overflow: hidden; 1614 - } 1615 - 1616 - .reply-avatar img { 1617 - width: 100%; 1618 - height: 100%; 1619 - object-fit: cover; 1620 - } 1621 - 1622 - .reply-meta { 1623 - flex: 1; 1624 - min-width: 0; 1625 - } 1626 - 1627 - .reply-author { 1628 - font-weight: 600; 1629 - color: var(--text-primary); 1630 - } 1631 - 1632 - .reply-handle { 1633 - font-size: 0.85rem; 1634 - color: var(--text-tertiary); 1635 - text-decoration: none; 1636 - margin-left: 6px; 1637 - } 1638 - 1639 - .reply-handle:hover { 1640 - color: var(--accent); 1641 - text-decoration: underline; 1642 - } 1643 - 1644 - .reply-time { 1645 - font-size: 0.85rem; 1646 - color: var(--text-tertiary); 1647 - white-space: nowrap; 1648 - } 1649 - 1650 - .reply-text { 1651 - color: var(--text-primary); 1652 - line-height: 1.5; 1653 - margin: 0; 1654 - } 1655 - 1656 - .replies-title { 1657 - display: flex; 1658 - align-items: center; 1659 - gap: 8px; 1660 - } 1661 - 1662 - .replies-title svg { 1663 - color: var(--accent); 1664 - } 1665 - 1666 - .replies-list-threaded { 1667 - display: flex; 1668 - flex-direction: column; 1669 - gap: 8px; 1670 - } 1671 - 1672 - .reply-card-threaded { 1673 - padding: 16px; 1674 - transition: background 0.15s ease; 1675 - } 1676 - 1677 - .reply-card-threaded .reply-header { 1678 - margin-bottom: 8px; 1679 - } 1680 - 1681 - .reply-card-threaded .reply-meta { 1682 - display: flex; 1683 - align-items: center; 1684 - gap: 6px; 1685 - flex-wrap: wrap; 1686 - } 1687 - 1688 - .reply-dot { 1689 - color: var(--text-tertiary); 1690 - font-size: 0.75rem; 1691 - } 1692 - 1693 - .reply-actions { 1694 - display: flex; 1695 - gap: 4px; 1696 - margin-left: auto; 1697 - } 1698 - 1699 - .reply-action-btn { 1700 - background: none; 1701 - border: none; 1702 - padding: 4px 8px; 1703 - color: var(--text-tertiary); 1704 - cursor: pointer; 1705 - border-radius: var(--radius-sm); 1706 - transition: all 0.15s ease; 1707 - display: flex; 1708 - align-items: center; 1709 - justify-content: center; 1710 - } 1711 - 1712 - .reply-action-btn:hover { 1713 - color: var(--accent); 1714 - background: var(--accent-subtle); 1715 - } 1716 - 1717 - .reply-action-delete:hover { 1718 - color: var(--error); 1719 - background: rgba(239, 68, 68, 0.1); 1720 - } 1721 - 1722 - .replying-to-banner { 1723 - display: flex; 1724 - align-items: center; 1725 - justify-content: space-between; 1726 - padding: 8px 12px; 1727 - margin-bottom: 12px; 1728 - background: var(--accent-subtle); 1729 - border-radius: var(--radius-sm); 1730 - font-size: 0.85rem; 1731 - color: var(--text-secondary); 1732 - } 1733 - 1734 - .cancel-reply { 1735 - background: none; 1736 - border: none; 1737 - font-size: 1.2rem; 1738 - color: var(--text-tertiary); 1739 - cursor: pointer; 1740 - padding: 0 4px; 1741 - line-height: 1; 1742 - } 1743 - 1744 - .cancel-reply:hover { 1745 - color: var(--text-primary); 1746 - } 1747 - 1748 - .reply-form.card { 1749 - padding: 16px; 1750 - margin-bottom: 16px; 1751 - } 1752 - 1753 - .reply-form-actions { 1754 - display: flex; 1755 - justify-content: flex-end; 1756 - } 1757 - 1758 - .inline-replies { 1759 - margin-top: 16px; 1760 - padding-top: 16px; 1761 - border-top: 1px solid var(--border); 1762 - display: flex; 1763 - flex-direction: column; 1764 - gap: 16px; 1765 - } 1766 - 1767 - .main-reply-composer { 1768 - margin-top: 16px; 1769 - background: var(--bg-secondary); 1770 - padding: 12px; 1771 - border-radius: var(--radius-md); 1772 - } 1773 - 1774 - .reply-input { 1775 - width: 100%; 1776 - min-height: 80px; 1777 - padding: 12px; 1778 - border: 1px solid var(--border); 1779 - border-radius: var(--radius-md); 1780 - background: var(--bg-card); 1781 - color: var(--text-primary); 1782 - font-family: inherit; 1783 - font-size: 0.95rem; 1784 - resize: vertical; 1785 - display: block; 1786 - } 1787 - 1788 - .reply-input:focus { 1789 - border-color: var(--accent); 1790 - outline: none; 1791 - } 1792 - 1793 - .reply-input.small { 1794 - min-height: 60px; 1795 - font-size: 0.9rem; 1796 - margin-bottom: 8px; 1797 - } 1798 - 1799 - .composer-actions { 1800 - display: flex; 1801 - justify-content: flex-end; 1802 - } 1803 - 1804 - .btn-block { 1805 - width: 100%; 1806 - text-align: left; 1807 - padding: 8px 12px; 1808 - color: var(--text-secondary); 1809 - background: var(--bg-tertiary); 1810 - border-radius: var(--radius-md); 1811 - margin-top: 8px; 1812 - font-size: 0.9rem; 1813 - cursor: pointer; 1814 - transition: all 0.2s; 1815 - } 1816 - 1817 - .btn-block:hover { 1818 - background: var(--border); 1819 - color: var(--text-primary); 1820 - } 1821 - 1822 - .annotation-action.active { 1823 - color: var(--accent); 1824 - } 1825 - 1826 - .new-page { 1827 - max-width: 600px; 1828 - margin: 0 auto; 1829 - display: flex; 1830 - flex-direction: column; 1831 - gap: 32px; 1832 - } 1833 - 1834 - .loading-spinner { 1835 - width: 32px; 1836 - height: 32px; 1837 - border: 3px solid var(--border); 1838 - border-top-color: var(--accent); 1839 - border-radius: 50%; 1840 - animation: spin 0.8s linear infinite; 1841 - margin: 60px auto; 1842 - } 1843 - 1844 - @keyframes spin { 1845 - to { 1846 - transform: rotate(360deg); 1847 - } 1848 - } 1849 - 1850 - .navbar { 1851 - position: sticky; 1852 - top: 0; 1853 - z-index: 1000; 1854 - background: rgba(12, 10, 20, 0.95); 1855 - backdrop-filter: blur(12px); 1856 - -webkit-backdrop-filter: blur(12px); 1857 - border-bottom: 1px solid var(--border); 1858 - } 1859 - 1860 - .navbar-inner { 1861 - max-width: 1200px; 1862 - margin: 0 auto; 1863 - padding: 12px 24px; 1864 - display: flex; 1865 - align-items: center; 1866 - justify-content: space-between; 1867 - gap: 24px; 1868 - } 1869 - 1870 - .navbar-brand { 1871 - display: flex; 1872 - align-items: center; 1873 - gap: 10px; 1874 - text-decoration: none; 1875 - flex-shrink: 0; 1876 - } 1877 - 1878 - .navbar-logo { 1879 - width: 32px; 1880 - height: 32px; 1881 - background: linear-gradient(135deg, var(--accent), #8b5cf6); 1882 - border-radius: 8px; 1883 - display: flex; 1884 - align-items: center; 1885 - justify-content: center; 1886 - font-weight: 700; 1887 - font-size: 1rem; 1888 - color: white; 1889 - } 1890 - 1891 - .navbar-title { 1892 - font-weight: 700; 1893 - font-size: 1.25rem; 1894 - color: var(--text-primary); 1895 - } 1896 - 1897 - .navbar-center { 1898 - display: flex; 1899 - align-items: center; 1900 - gap: 8px; 1901 - background: var(--bg-tertiary); 1902 - padding: 4px; 1903 - border-radius: var(--radius-lg); 1904 - } 1905 - 1906 - .navbar-link { 1907 - display: flex; 1908 - align-items: center; 1909 - gap: 6px; 1910 - padding: 8px 16px; 1911 - font-size: 0.9rem; 1912 - font-weight: 500; 1913 - color: var(--text-secondary); 1914 - text-decoration: none; 1915 - border-radius: var(--radius-md); 1916 - transition: all 0.15s ease; 1917 - } 1918 - 1919 - .navbar-link:hover { 1920 - color: var(--text-primary); 1921 - background: var(--bg-hover); 1922 - } 1923 - 1924 - .navbar-link.active { 1925 - color: var(--text-primary); 1926 - background: var(--bg-card); 1927 - box-shadow: var(--shadow-sm); 1928 - } 1929 - 1930 - .navbar-right { 1931 - display: flex; 1932 - align-items: center; 1933 - gap: 12px; 1934 - flex-shrink: 0; 1935 - } 1936 - 1937 - .navbar-icon-link { 1938 - display: flex; 1939 - align-items: center; 1940 - justify-content: center; 1941 - width: 36px; 1942 - height: 36px; 1943 - color: var(--text-tertiary); 1944 - border-radius: var(--radius-md); 1945 - transition: all 0.15s ease; 1946 - } 1947 - 1948 - .navbar-icon-link:hover { 1949 - color: var(--text-primary); 1950 - background: var(--bg-tertiary); 1951 - } 1952 - 1953 - .navbar-icon-link.active { 1954 - color: var(--accent); 1955 - background: var(--accent-subtle); 1956 - } 1957 - 1958 - .navbar-new-btn { 1959 - display: flex; 1960 - align-items: center; 1961 - gap: 6px; 1962 - padding: 8px 14px; 1963 - background: linear-gradient(135deg, var(--accent), #8b5cf6); 1964 - color: white; 1965 - font-size: 0.85rem; 1966 - font-weight: 600; 1967 - text-decoration: none; 1968 - border-radius: var(--radius-full); 1969 - transition: all 0.2s ease; 1970 - } 1971 - 1972 - .navbar-new-btn:hover { 1973 - transform: translateY(-1px); 1974 - box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); 1975 - color: white; 1976 - } 1977 - 1978 - .navbar-user-section { 1979 - display: flex; 1980 - align-items: center; 1981 - gap: 4px; 1982 - } 1983 - 1984 - .navbar-avatar { 1985 - width: 32px; 1986 - height: 32px; 1987 - border-radius: var(--radius-full); 1988 - background: linear-gradient(135deg, var(--accent), #a855f7); 1989 - display: flex; 1990 - align-items: center; 1991 - justify-content: center; 1992 - font-weight: 600; 1993 - font-size: 0.75rem; 1994 - color: white; 1995 - text-decoration: none; 1996 - transition: transform 0.15s ease; 1997 - } 1998 - 1999 - .navbar-avatar:hover { 2000 - transform: scale(1.05); 2001 - } 2002 - 2003 - .navbar-logout { 2004 - width: 24px; 2005 - height: 24px; 2006 - border: none; 2007 - background: transparent; 2008 - color: var(--text-tertiary); 2009 - font-size: 1.25rem; 2010 - cursor: pointer; 2011 - border-radius: var(--radius-sm); 2012 - transition: all 0.15s ease; 2013 - display: flex; 2014 - align-items: center; 2015 - justify-content: center; 2016 - } 2017 - 2018 - .navbar-logout:hover { 2019 - color: var(--error); 2020 - background: rgba(239, 68, 68, 0.1); 2021 - } 2022 - 2023 - .navbar-signin { 2024 - padding: 8px 16px; 2025 - background: var(--accent); 2026 - color: white; 2027 - font-size: 0.9rem; 2028 - font-weight: 500; 2029 - text-decoration: none; 2030 - border-radius: var(--radius-full); 2031 - transition: all 0.15s ease; 2032 - } 2033 - 2034 - .navbar-signin:hover { 2035 - background: var(--accent-hover); 2036 - color: white; 2037 - } 2038 - 2039 - .navbar-user-menu { 2040 - position: relative; 2041 - } 2042 - 2043 - .navbar-avatar-btn { 2044 - width: 36px; 2045 - height: 36px; 2046 - border-radius: var(--radius-full); 2047 - background: linear-gradient(135deg, var(--accent), #a855f7); 2048 - border: none; 2049 - cursor: pointer; 2050 - overflow: hidden; 2051 - display: flex; 2052 - align-items: center; 2053 - justify-content: center; 2054 - transition: 2055 - transform 0.15s ease, 2056 - box-shadow 0.15s ease; 2057 - } 2058 - 2059 - .navbar-avatar-btn:hover { 2060 - transform: scale(1.05); 2061 - box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); 2062 - } 2063 - 2064 - .navbar-avatar-img { 2065 - width: 100%; 2066 - height: 100%; 2067 - object-fit: cover; 2068 - } 2069 - 2070 - .navbar-avatar-text { 2071 - font-weight: 600; 2072 - font-size: 0.75rem; 2073 - color: white; 2074 - } 2075 - 2076 - .navbar-dropdown { 2077 - position: absolute; 2078 - top: calc(100% + 8px); 2079 - right: 0; 2080 - min-width: 200px; 2081 - background: var(--bg-card); 2082 - border: 1px solid var(--border); 2083 - border-radius: var(--radius-lg); 2084 - box-shadow: var(--shadow-lg); 2085 - overflow: hidden; 2086 - z-index: 1001; 2087 - animation: dropdownFade 0.15s ease; 2088 - } 2089 - 2090 - @keyframes dropdownFade { 2091 - from { 2092 - opacity: 0; 2093 - transform: translateY(-8px); 2094 - } 2095 - 2096 - to { 2097 - opacity: 1; 2098 - transform: translateY(0); 2099 - } 2100 - } 2101 - 2102 - .navbar-dropdown-header { 2103 - padding: 12px 16px; 2104 - background: var(--bg-secondary); 2105 - } 2106 - 2107 - .navbar-dropdown-name { 2108 - display: block; 2109 - font-weight: 600; 2110 - color: var(--text-primary); 2111 - font-size: 0.9rem; 2112 - } 2113 - 2114 - .navbar-dropdown-handle { 2115 - display: block; 2116 - color: var(--text-tertiary); 2117 - font-size: 0.8rem; 2118 - margin-top: 2px; 2119 - } 2120 - 2121 - .navbar-dropdown-divider { 2122 - height: 1px; 2123 - background: var(--border); 2124 - } 2125 - 2126 - .navbar-dropdown-item { 2127 - display: flex; 2128 - align-items: center; 2129 - gap: 10px; 2130 - width: 100%; 2131 - padding: 12px 16px; 2132 - font-size: 0.9rem; 2133 - color: var(--text-primary); 2134 - text-decoration: none; 2135 - background: none; 2136 - border: none; 2137 - cursor: pointer; 2138 - transition: background 0.15s ease; 2139 - text-align: left; 2140 - } 2141 - 2142 - .navbar-dropdown-item:hover { 2143 - background: var(--bg-tertiary); 2144 - } 2145 - 2146 - .navbar-dropdown-logout { 2147 - color: var(--error); 2148 - border-top: 1px solid var(--border); 2149 - } 2150 - 2151 - .navbar-dropdown-logout:hover { 2152 - background: rgba(239, 68, 68, 0.1); 2153 - } 2154 - 2155 - @media (max-width: 768px) { 2156 - .navbar-inner { 2157 - padding: 10px 16px; 2158 - } 2159 - 2160 - .navbar-title { 2161 - display: none; 2162 - } 2163 - 2164 - .navbar-center { 2165 - display: none; 2166 - } 2167 - 2168 - .navbar-new-btn span { 2169 - display: none; 2170 - } 2171 - 2172 - .navbar-new-btn { 2173 - width: 36px; 2174 - height: 36px; 2175 - padding: 0; 2176 - justify-content: center; 2177 - } 2178 - } 2179 - 2180 - .collections-list { 2181 - display: flex; 2182 - flex-direction: column; 2183 - gap: 2px; 2184 - background: var(--bg-card); 2185 - border: 1px solid var(--border); 2186 - border-radius: var(--radius-lg); 2187 - overflow: hidden; 2188 - } 2189 - 2190 - .collection-row { 2191 - display: flex; 2192 - align-items: center; 2193 - background: var(--bg-card); 2194 - transition: background 0.15s ease; 2195 - } 2196 - 2197 - .collection-row:not(:last-child) { 2198 - border-bottom: 1px solid var(--border); 2199 - } 2200 - 2201 - .collection-row:hover { 2202 - background: var(--bg-secondary); 2203 - } 2204 - 2205 - .collection-row-content { 2206 - flex: 1; 2207 - display: flex; 2208 - align-items: center; 2209 - gap: 16px; 2210 - padding: 16px 20px; 2211 - text-decoration: none; 2212 - min-width: 0; 2213 - } 2214 - 2215 - .collection-row-icon { 2216 - width: 44px; 2217 - height: 44px; 2218 - min-width: 44px; 2219 - display: flex; 2220 - align-items: center; 2221 - justify-content: center; 2222 - background: linear-gradient( 2223 - 135deg, 2224 - rgba(79, 70, 229, 0.1), 2225 - rgba(168, 85, 247, 0.15) 2226 - ); 2227 - color: var(--accent); 2228 - border-radius: var(--radius-md); 2229 - transition: all 0.2s ease; 2230 - } 2231 - 2232 - .collection-row:hover .collection-row-icon { 2233 - background: linear-gradient( 2234 - 135deg, 2235 - rgba(79, 70, 229, 0.15), 2236 - rgba(168, 85, 247, 0.2) 2237 - ); 2238 - transform: scale(1.05); 2239 - } 2240 - 2241 - .collection-row-info { 2242 - flex: 1; 2243 - min-width: 0; 2244 - } 2245 - 2246 - .collection-row-name { 2247 - font-size: 1rem; 2248 - font-weight: 600; 2249 - color: var(--text-primary); 2250 - margin: 0 0 2px 0; 2251 - white-space: nowrap; 2252 - overflow: hidden; 2253 - text-overflow: ellipsis; 2254 - } 2255 - 2256 - .collection-row:hover .collection-row-name { 2257 - color: var(--accent); 2258 - } 2259 - 2260 - .collection-row-desc { 2261 - font-size: 0.85rem; 2262 - color: var(--text-secondary); 2263 - margin: 0; 2264 - white-space: nowrap; 2265 - overflow: hidden; 2266 - text-overflow: ellipsis; 2267 - } 2268 - 2269 - .collection-row-arrow { 2270 - color: var(--text-tertiary); 2271 - opacity: 0; 2272 - transition: all 0.2s ease; 2273 - } 2274 - 2275 - .collection-row:hover .collection-row-arrow { 2276 - opacity: 1; 2277 - color: var(--accent); 2278 - transform: translateX(2px); 2279 - } 2280 - 2281 - .collection-row-edit { 2282 - padding: 10px; 2283 - margin-right: 12px; 2284 - color: var(--text-tertiary); 2285 - background: none; 2286 - border: none; 2287 - border-radius: var(--radius-sm); 2288 - cursor: pointer; 2289 - opacity: 0; 2290 - transition: all 0.15s ease; 2291 - } 2292 - 2293 - .collection-row:hover .collection-row-edit { 2294 - opacity: 1; 2295 - } 2296 - 2297 - .collection-row-edit:hover { 2298 - color: var(--text-primary); 2299 - background: var(--bg-tertiary); 2300 - } 2301 - 2302 - .back-link { 2303 - display: inline-flex; 2304 - align-items: center; 2305 - gap: 6px; 2306 - color: var(--text-tertiary); 2307 - font-size: 0.9rem; 2308 - font-weight: 500; 2309 - text-decoration: none; 2310 - margin-bottom: 24px; 2311 - transition: color 0.15s ease; 2312 - } 2313 - 2314 - .back-link:hover { 2315 - color: var(--accent); 2316 - } 2317 - 2318 - .collection-detail-header { 2319 - display: flex; 2320 - gap: 20px; 2321 - padding: 24px; 2322 - background: var(--bg-card); 2323 - border: 1px solid var(--border); 2324 - border-radius: var(--radius-lg); 2325 - margin-bottom: 32px; 2326 - position: relative; 2327 - } 2328 - 2329 - .collection-detail-icon { 2330 - width: 56px; 2331 - height: 56px; 2332 - min-width: 56px; 2333 - display: flex; 2334 - align-items: center; 2335 - justify-content: center; 2336 - background: linear-gradient( 2337 - 135deg, 2338 - rgba(79, 70, 229, 0.1), 2339 - rgba(168, 85, 247, 0.1) 2340 - ); 2341 - color: var(--accent); 2342 - border-radius: var(--radius-md); 2343 - } 2344 - 2345 - .collection-detail-info { 2346 - flex: 1; 2347 - min-width: 0; 2348 - } 2349 - 2350 - .collection-detail-visibility { 2351 - display: flex; 2352 - align-items: center; 2353 - gap: 6px; 2354 - font-size: 0.8rem; 2355 - font-weight: 600; 2356 - color: var(--accent); 2357 - text-transform: capitalize; 2358 - margin-bottom: 8px; 2359 - } 2360 - 2361 - .collection-detail-title { 2362 - font-size: 1.5rem; 2363 - font-weight: 700; 2364 - color: var(--text-primary); 2365 - margin-bottom: 8px; 2366 - line-height: 1.3; 2367 - } 2368 - 2369 - .collection-detail-desc { 2370 - color: var(--text-secondary); 2371 - font-size: 1rem; 2372 - line-height: 1.5; 2373 - margin-bottom: 12px; 2374 - max-width: 600px; 2375 - } 2376 - 2377 - .collection-detail-stats { 2378 - display: flex; 2379 - align-items: center; 2380 - gap: 8px; 2381 - font-size: 0.85rem; 2382 - color: var(--text-tertiary); 2383 - } 2384 - 2385 - .collection-detail-actions { 2386 - position: absolute; 2387 - top: 20px; 2388 - right: 20px; 2389 - display: flex; 2390 - align-items: center; 2391 - gap: 8px; 2392 - } 2393 - 2394 - .collection-detail-actions .share-menu-container { 2395 - display: flex; 2396 - align-items: center; 2397 - } 2398 - 2399 - .collection-detail-actions .annotation-action { 2400 - padding: 10px; 2401 - color: var(--text-tertiary); 2402 - background: none; 2403 - border: none; 2404 - border-radius: var(--radius-sm); 2405 - cursor: pointer; 2406 - transition: all 0.15s ease; 2407 - } 2408 - 2409 - .collection-detail-actions .annotation-action:hover { 2410 - color: var(--accent); 2411 - background: var(--bg-tertiary); 2412 - } 2413 - 2414 - .collection-detail-edit, 2415 - .collection-detail-delete { 2416 - padding: 10px; 2417 - color: var(--text-tertiary); 2418 - background: none; 2419 - border: none; 2420 - border-radius: var(--radius-sm); 2421 - cursor: pointer; 2422 - transition: all 0.15s ease; 2423 - } 2424 - 2425 - .collection-detail-edit:hover { 2426 - color: var(--accent); 2427 - background: var(--bg-tertiary); 2428 - } 2429 - 2430 - .collection-detail-delete:hover { 2431 - color: var(--error); 2432 - background: rgba(239, 68, 68, 0.1); 2433 - } 2434 - 2435 - .collection-item-wrapper { 2436 - position: relative; 2437 - } 2438 - 2439 - .collection-item-remove { 2440 - position: absolute; 2441 - top: 12px; 2442 - left: -40px; 2443 - z-index: 10; 2444 - padding: 8px; 2445 - background: var(--bg-card); 2446 - border: 1px solid var(--border); 2447 - border-radius: var(--radius-sm); 2448 - color: var(--text-tertiary); 2449 - cursor: pointer; 2450 - opacity: 0; 2451 - transition: all 0.15s ease; 2452 - } 2453 - 2454 - .collection-item-wrapper:hover .collection-item-remove { 2455 - opacity: 1; 2456 - } 2457 - 2458 - .collection-item-remove:hover { 2459 - color: var(--error); 2460 - border-color: var(--error); 2461 - background: rgba(239, 68, 68, 0.05); 2462 - } 2463 - 2464 - .modal-overlay { 2465 - position: fixed; 2466 - inset: 0; 2467 - background: rgba(0, 0, 0, 0.5); 2468 - display: flex; 2469 - align-items: center; 2470 - justify-content: center; 2471 - padding: 16px; 2472 - z-index: 50; 2473 - animation: fadeIn 0.2s ease-out; 2474 - } 2475 - 2476 - .modal-container { 2477 - background: var(--bg-secondary); 2478 - border-radius: var(--radius-lg); 2479 - width: 100%; 2480 - max-width: 28rem; 2481 - border: 1px solid var(--border); 2482 - box-shadow: var(--shadow-lg); 2483 - animation: zoomIn 0.2s ease-out; 2484 - } 2485 - 2486 - .modal-header { 2487 - display: flex; 2488 - align-items: center; 2489 - justify-content: space-between; 2490 - padding: 16px; 2491 - border-bottom: 1px solid var(--border); 2492 - } 2493 - 2494 - .modal-title { 2495 - font-size: 1.25rem; 2496 - font-weight: 700; 2497 - color: var(--text-primary); 2498 - } 2499 - 2500 - .modal-close-btn { 2501 - padding: 8px; 2502 - color: var(--text-tertiary); 2503 - border-radius: var(--radius-md); 2504 - transition: color 0.15s; 2505 - } 2506 - 2507 - .modal-close-btn:hover { 2508 - color: var(--text-primary); 2509 - background: var(--bg-hover); 2510 - } 2511 - 2512 - .modal-form { 2513 - padding: 16px; 2514 - display: flex; 2515 - flex-direction: column; 2516 - gap: 16px; 2517 - } 2518 - 2519 - .icon-picker-tabs { 2520 - display: flex; 2521 - gap: 4px; 2522 - margin-bottom: 12px; 2523 - } 2524 - 2525 - .icon-picker-tab { 2526 - flex: 1; 2527 - padding: 8px 12px; 2528 - background: var(--bg-primary); 2529 - border: 1px solid var(--border); 2530 - border-radius: var(--radius-md); 2531 - color: var(--text-secondary); 2532 - font-size: 0.85rem; 2533 - font-weight: 500; 2534 - cursor: pointer; 2535 - transition: all 0.15s ease; 2536 - } 2537 - 2538 - .icon-picker-tab:hover { 2539 - background: var(--bg-tertiary); 2540 - } 2541 - 2542 - .icon-picker-tab.active { 2543 - background: var(--accent); 2544 - border-color: var(--accent); 2545 - color: white; 2546 - } 2547 - 2548 - .emoji-picker-wrapper { 2549 - display: flex; 2550 - flex-direction: column; 2551 - gap: 10px; 2552 - } 2553 - 2554 - .emoji-custom-input input { 2555 - width: 100%; 2556 - } 2557 - 2558 - .emoji-picker, 2559 - .icon-picker { 2560 - display: flex; 2561 - flex-wrap: wrap; 2562 - gap: 4px; 2563 - max-height: 120px; 2564 - overflow-y: auto; 2565 - padding: 8px; 2566 - background: var(--bg-primary); 2567 - border: 1px solid var(--border); 2568 - border-radius: var(--radius-md); 2569 - } 2570 - 2571 - .emoji-option, 2572 - .icon-option { 2573 - width: 36px; 2574 - height: 36px; 2575 - display: flex; 2576 - align-items: center; 2577 - justify-content: center; 2578 - font-size: 1.2rem; 2579 - background: transparent; 2580 - border: 2px solid transparent; 2581 - border-radius: var(--radius-sm); 2582 - cursor: pointer; 2583 - transition: all 0.15s ease; 2584 - color: var(--text-secondary); 2585 - } 2586 - 2587 - .emoji-option:hover, 2588 - .icon-option:hover { 2589 - background: var(--bg-tertiary); 2590 - transform: scale(1.1); 2591 - color: var(--text-primary); 2592 - } 2593 - 2594 - .emoji-option.selected, 2595 - .icon-option.selected { 2596 - border-color: var(--accent); 2597 - background: var(--accent-subtle); 2598 - color: var(--accent); 2599 - } 2600 - 2601 - .form-group { 2602 - margin-bottom: 0; 2603 - } 2604 - 2605 - .form-label { 2606 - display: block; 2607 - font-size: 0.875rem; 2608 - font-weight: 500; 2609 - color: var(--text-secondary); 2610 - margin-bottom: 4px; 2611 - } 2612 - 2613 - .form-input, 2614 - .form-textarea, 2615 - .form-select { 2616 - width: 100%; 2617 - padding: 8px 12px; 2618 - background: var(--bg-primary); 2619 - border: 1px solid var(--border); 2620 - border-radius: var(--radius-md); 2621 - color: var(--text-primary); 2622 - transition: all 0.15s; 2623 - } 2624 - 2625 - .form-input:focus, 2626 - .form-textarea:focus, 2627 - .form-select:focus { 2628 - outline: none; 2629 - border-color: var(--accent); 2630 - box-shadow: 0 0 0 2px var(--accent-subtle); 2631 - } 2632 - 2633 - .form-textarea { 2634 - resize: none; 2635 - } 2636 - 2637 - .modal-actions { 2638 - display: flex; 2639 - justify-content: flex-end; 2640 - gap: 12px; 2641 - padding-top: 8px; 2642 - } 2643 - 2644 - @keyframes fadeIn { 2645 - from { 2646 - opacity: 0; 2647 - } 2648 - 2649 - to { 2650 - opacity: 1; 2651 - } 2652 - } 2653 - 2654 - @keyframes zoomIn { 2655 - from { 2656 - opacity: 0; 2657 - transform: scale(0.95); 2658 - } 2659 - 2660 - to { 2661 - opacity: 1; 2662 - transform: scale(1); 2663 - } 2664 - } 2665 - 2666 - .annotation-detail-page { 2667 - max-width: 680px; 2668 - margin: 0 auto; 2669 - padding: 24px 16px; 2670 - } 2671 - 2672 - .annotation-detail-header { 2673 - margin-bottom: 24px; 2674 - } 2675 - 2676 - .back-link { 2677 - display: inline-flex; 2678 - align-items: center; 2679 - gap: 8px; 2680 - color: var(--text-secondary); 2681 - font-size: 0.9rem; 2682 - transition: color 0.15s; 2683 - } 2684 - 2685 - .back-link:hover { 2686 - color: var(--text-primary); 2687 - } 2688 - 2689 - .text-secondary { 2690 - color: var(--text-secondary); 2691 - } 2692 - 2693 - .text-error { 2694 - color: var(--error); 2695 - } 2696 - 2697 - .text-center { 2698 - text-align: center; 2699 - } 2700 - 2701 - .flex { 2702 - display: flex; 2703 - } 2704 - 2705 - .items-center { 2706 - align-items: center; 2707 - } 2708 - 2709 - .justify-center { 2710 - justify-content: center; 2711 - } 2712 - 2713 - .justify-end { 2714 - justify-content: flex-end; 2715 - } 2716 - 2717 - .gap-2 { 2718 - gap: 8px; 2719 - } 2720 - 2721 - .gap-3 { 2722 - gap: 12px; 2723 - } 2724 - 2725 - .mt-3 { 2726 - margin-top: 12px; 2727 - } 2728 - 2729 - .mb-6 { 2730 - margin-bottom: 24px; 2731 - } 2732 - 2733 - .btn-text { 2734 - background: none; 2735 - border: none; 2736 - color: var(--text-secondary); 2737 - font-size: 0.9rem; 2738 - padding: 8px 12px; 2739 - cursor: pointer; 2740 - transition: color 0.15s; 2741 - } 2742 - 2743 - .btn-text:hover { 2744 - color: var(--text-primary); 2745 - } 2746 - 2747 - .btn-sm { 2748 - padding: 6px 12px; 2749 - font-size: 0.85rem; 2750 - } 2751 - 2752 - .annotation-edit-btn { 2753 - background: none; 2754 - border: none; 2755 - cursor: pointer; 2756 - padding: 6px 8px; 2757 - color: var(--text-tertiary); 2758 - border-radius: var(--radius-sm); 2759 - transition: all 0.15s ease; 2760 - } 2761 - 2762 - .annotation-edit-btn:hover { 2763 - color: var(--accent); 2764 - background: var(--accent-subtle); 2765 - } 2766 - 2767 - .spinner { 2768 - width: 32px; 2769 - height: 32px; 2770 - border: 3px solid var(--border); 2771 - border-top-color: var(--accent); 2772 - border-radius: 50%; 2773 - animation: spin 0.8s linear infinite; 2774 - } 2775 - 2776 - .spinner-sm { 2777 - width: 16px; 2778 - height: 16px; 2779 - border-width: 2px; 2780 - } 2781 - 2782 - @keyframes spin { 2783 - to { 2784 - transform: rotate(360deg); 2785 - } 2786 - } 2787 - 2788 - .collection-list-item { 2789 - width: 100%; 2790 - text-align: left; 2791 - padding: 12px 16px; 2792 - border-radius: var(--radius-md); 2793 - background: var(--bg-primary); 2794 - border: 1px solid transparent; 2795 - color: var(--text-primary); 2796 - transition: all 0.15s ease; 2797 - display: flex; 2798 - align-items: center; 2799 - justify-content: space-between; 2800 - cursor: pointer; 2801 - } 2802 - 2803 - .collection-list-item:hover { 2804 - background: var(--bg-hover); 2805 - border-color: var(--border); 2806 - } 2807 - 2808 - .collection-list-item:hover .collection-list-item-icon { 2809 - opacity: 1; 2810 - } 2811 - 2812 - .collection-list-item:disabled { 2813 - opacity: 0.6; 2814 - cursor: not-allowed; 2815 - } 2816 - 2817 - .item-delete-overlay { 2818 - position: absolute; 2819 - top: 16px; 2820 - right: 16px; 2821 - z-index: 10; 2822 - opacity: 0; 2823 - transition: opacity 0.15s ease; 2824 - } 2825 - 2826 - .card:hover .item-delete-overlay, 2827 - div:hover > .item-delete-overlay { 2828 - opacity: 1; 2829 - } 2830 - 2831 - .btn-icon-danger { 2832 - padding: 8px; 2833 - background: var(--error); 2834 - color: white; 2835 - border: none; 2836 - border-radius: var(--radius-md); 2837 - cursor: pointer; 2838 - box-shadow: var(--shadow-md); 2839 - transition: all 0.15s ease; 2840 - display: flex; 2841 - align-items: center; 2842 - justify-content: center; 2843 - } 2844 - 2845 - .btn-icon-danger:hover { 2846 - background: #dc2626; 2847 - transform: scale(1.05); 2848 - } 2849 - 2850 - .action-buttons { 2851 - display: flex; 2852 - gap: 8px; 2853 - } 2854 - 2855 - .action-buttons-end { 2856 - display: flex; 2857 - justify-content: flex-end; 2858 - gap: 8px; 2859 - } 2860 - 2861 - .filter-tab { 2862 - padding: 8px 16px; 2863 - font-size: 0.9rem; 2864 - font-weight: 500; 2865 - color: var(--text-secondary); 2866 - background: transparent; 2867 - border: none; 2868 - border-radius: var(--radius-md); 2869 - cursor: pointer; 2870 - transition: all 0.15s ease; 2871 - } 2872 - 2873 - .filter-tab:hover { 2874 - color: var(--text-primary); 2875 - background: var(--bg-hover); 2876 - } 2877 - 2878 - .filter-tab.active { 2879 - color: var(--text-primary); 2880 - background: var(--bg-card); 2881 - box-shadow: var(--shadow-sm); 2882 - } 2883 - 2884 - .inline-reply { 2885 - padding: 12px 16px; 2886 - border-bottom: 1px solid var(--border); 2887 - } 2888 - 2889 - .inline-reply:last-child { 2890 - border-bottom: none; 2891 - } 2892 - 2893 - .inline-reply-avatar { 2894 - width: 28px; 2895 - height: 28px; 2896 - min-width: 28px; 2897 - border-radius: var(--radius-full); 2898 - background: linear-gradient(135deg, var(--accent), #a855f7); 2899 - display: flex; 2900 - align-items: center; 2901 - justify-content: center; 2902 - font-weight: 600; 2903 - font-size: 0.7rem; 2904 - color: white; 2905 - overflow: hidden; 2906 - } 2907 - 2908 - .inline-reply-avatar img, 2909 - .inline-reply-avatar-placeholder { 2910 - width: 100%; 2911 - height: 100%; 2912 - object-fit: cover; 2913 - } 2914 - 2915 - .inline-reply-avatar-placeholder { 2916 - display: flex; 2917 - align-items: center; 2918 - justify-content: center; 2919 - font-weight: 600; 2920 - font-size: 0.7rem; 2921 - color: white; 2922 - } 2923 - 2924 - .inline-reply-content { 2925 - flex: 1; 2926 - min-width: 0; 2927 - } 2928 - 2929 - .inline-reply-header { 2930 - display: flex; 2931 - align-items: center; 2932 - gap: 8px; 2933 - margin-bottom: 4px; 2934 - } 2935 - 2936 - .inline-reply-author { 2937 - font-weight: 600; 2938 - font-size: 0.85rem; 2939 - color: var(--text-primary); 2940 - } 2941 - 2942 - .inline-reply-handle { 2943 - color: var(--text-tertiary); 2944 - font-size: 0.8rem; 2945 - text-decoration: none; 2946 - } 2947 - 2948 - .inline-reply-time { 2949 - color: var(--text-tertiary); 2950 - font-size: 0.75rem; 2951 - margin-left: auto; 2952 - } 2953 - 2954 - .inline-reply-text { 2955 - font-size: 0.9rem; 2956 - color: var(--text-primary); 2957 - line-height: 1.5; 2958 - } 2959 - 2960 - .inline-reply-action { 2961 - display: flex; 2962 - align-items: center; 2963 - gap: 4px; 2964 - padding: 4px 8px; 2965 - font-size: 0.8rem; 2966 - color: var(--text-tertiary); 2967 - background: none; 2968 - border: none; 2969 - border-radius: var(--radius-sm); 2970 - cursor: pointer; 2971 - transition: all 0.15s ease; 2972 - } 2973 - 2974 - .inline-reply-action:hover { 2975 - color: var(--text-secondary); 2976 - background: var(--bg-hover); 2977 - } 2978 - 2979 - .inline-reply-composer { 2980 - display: flex; 2981 - align-items: flex-start; 2982 - gap: 12px; 2983 - padding: 12px 16px; 2984 - } 2985 - 2986 - .history-panel { 2987 - background: var(--bg-tertiary); 2988 - border: 1px solid var(--border); 2989 - border-radius: var(--radius-md); 2990 - padding: 1rem; 2991 - margin-bottom: 1rem; 2992 - font-size: 0.9rem; 2993 - animation: fadeIn 0.2s ease-out; 2994 - } 2995 - 2996 - .history-header { 2997 - display: flex; 2998 - justify-content: space-between; 2999 - align-items: center; 3000 - margin-bottom: 1rem; 3001 - padding-bottom: 0.5rem; 3002 - border-bottom: 1px solid var(--border); 3003 - } 3004 - 3005 - .history-title { 3006 - font-weight: 600; 3007 - text-transform: uppercase; 3008 - letter-spacing: 0.05em; 3009 - font-size: 0.75rem; 3010 - color: var(--text-secondary); 3011 - } 3012 - 3013 - .history-list { 3014 - list-style: none; 3015 - display: flex; 3016 - flex-direction: column; 3017 - gap: 1rem; 3018 - } 3019 - 3020 - .history-item { 3021 - position: relative; 3022 - padding-left: 1rem; 3023 - border-left: 2px solid var(--border); 3024 - } 3025 - 3026 - .history-date { 3027 - font-size: 0.75rem; 3028 - color: var(--text-tertiary); 3029 - margin-bottom: 0.25rem; 3030 - } 3031 - 3032 - .history-content { 3033 - color: var(--text-secondary); 3034 - white-space: pre-wrap; 3035 - } 3036 - 3037 - .history-close-btn { 3038 - color: var(--text-tertiary); 3039 - padding: 4px; 3040 - border-radius: var(--radius-sm); 3041 - transition: all 0.2s; 3042 - display: flex; 3043 - align-items: center; 3044 - justify-content: center; 3045 - } 3046 - 3047 - .history-close-btn:hover { 3048 - background: var(--bg-hover); 3049 - color: var(--text-primary); 3050 - } 3051 - 3052 - .history-status { 3053 - text-align: center; 3054 - color: var(--text-tertiary); 3055 - font-style: italic; 3056 - padding: 1rem; 3057 - } 3058 - 3059 - .form-label { 3060 - display: block; 3061 - font-size: 0.85rem; 3062 - font-weight: 600; 3063 - color: var(--text-secondary); 3064 - margin-bottom: 6px; 3065 - } 3066 - 3067 - .color-input-container { 3068 - display: flex; 3069 - align-items: center; 3070 - gap: 12px; 3071 - background: var(--bg-tertiary); 3072 - padding: 8px 12px; 3073 - border-radius: var(--radius-md); 3074 - border: 1px solid var(--border); 3075 - width: fit-content; 3076 - } 3077 - 3078 - .color-input-wrapper { 3079 - position: relative; 3080 - width: 32px; 3081 - height: 32px; 3082 - border-radius: var(--radius-full); 3083 - overflow: hidden; 3084 - border: 2px solid var(--border); 3085 - cursor: pointer; 3086 - transition: transform 0.1s; 3087 - } 3088 - 3089 - .color-input-wrapper:hover { 3090 - transform: scale(1.1); 3091 - border-color: var(--accent); 3092 - } 3093 - 3094 - .color-input-wrapper input[type="color"] { 3095 - position: absolute; 3096 - top: -50%; 3097 - left: -50%; 3098 - width: 200%; 3099 - height: 200%; 3100 - padding: 0; 3101 - margin: 0; 3102 - border: none; 3103 - cursor: pointer; 3104 - opacity: 0; 3105 - } 3106 - 3107 - .bookmark-card { 3108 - display: flex; 3109 - flex-direction: column; 3110 - gap: 16px; 3111 - } 3112 - 3113 - .bookmark-preview { 3114 - display: flex; 3115 - flex-direction: column; 3116 - background: var(--bg-secondary); 3117 - border: 1px solid var(--border); 3118 - border-radius: var(--radius-md); 3119 - overflow: hidden; 3120 - text-decoration: none; 3121 - transition: all 0.2s ease; 3122 - position: relative; 3123 - } 3124 - 3125 - .bookmark-preview:hover { 3126 - border-color: var(--accent); 3127 - box-shadow: var(--shadow-sm); 3128 - transform: translateY(-1px); 3129 - } 3130 - 3131 - .bookmark-preview::before { 3132 - content: ""; 3133 - position: absolute; 3134 - left: 0; 3135 - top: 0; 3136 - bottom: 0; 3137 - width: 4px; 3138 - background: var(--accent); 3139 - opacity: 0.7; 3140 - } 3141 - 3142 - .bookmark-preview-content { 3143 - padding: 16px 20px; 3144 - display: flex; 3145 - flex-direction: column; 3146 - gap: 8px; 3147 - } 3148 - 3149 - .bookmark-preview-header { 3150 - display: flex; 3151 - align-items: center; 3152 - gap: 8px; 3153 - margin-bottom: 4px; 3154 - } 3155 - 3156 - .bookmark-preview-site { 3157 - font-size: 0.75rem; 3158 - color: var(--accent); 3159 - text-transform: uppercase; 3160 - letter-spacing: 0.05em; 3161 - font-weight: 700; 3162 - display: flex; 3163 - align-items: center; 3164 - gap: 6px; 3165 - } 3166 - 3167 - .bookmark-preview-title { 3168 - font-size: 1.15rem; 3169 - font-weight: 700; 3170 - color: var(--text-primary); 3171 - line-height: 1.4; 3172 - } 3173 - 3174 - .bookmark-preview-desc { 3175 - font-size: 0.95rem; 3176 - color: var(--text-secondary); 3177 - line-height: 1.6; 3178 - } 3179 - 3180 - .bookmark-preview-arrow { 3181 - display: none; 3182 - } 3183 - 3184 - .bookmark-preview:hover { 3185 - background: var(--bg-tertiary); 3186 - border-color: var(--accent-subtle); 3187 - transform: translateY(-1px); 3188 - } 3189 - 3190 - .bookmark-preview-content { 3191 - flex: 1; 3192 - min-width: 0; 3193 - display: flex; 3194 - flex-direction: column; 3195 - gap: 6px; 3196 - } 3197 - 3198 - .bookmark-preview-site { 3199 - display: flex; 3200 - align-items: center; 3201 - gap: 6px; 3202 - font-size: 0.75rem; 3203 - font-weight: 600; 3204 - color: var(--accent); 3205 - text-transform: uppercase; 3206 - letter-spacing: 0.03em; 3207 - } 3208 - 3209 - .bookmark-preview-title { 3210 - font-size: 1rem; 3211 - font-weight: 600; 3212 - line-height: 1.4; 3213 - color: var(--text-primary); 3214 - margin: 0; 3215 - display: -webkit-box; 3216 - -webkit-line-clamp: 2; 3217 - line-clamp: 2; 3218 - -webkit-box-orient: vertical; 3219 - overflow: hidden; 3220 - } 3221 - 3222 - .bookmark-preview-desc { 3223 - font-size: 0.875rem; 3224 - color: var(--text-secondary); 3225 - line-height: 1.5; 3226 - margin: 0; 3227 - display: -webkit-box; 3228 - -webkit-line-clamp: 2; 3229 - line-clamp: 2; 3230 - -webkit-box-orient: vertical; 3231 - overflow: hidden; 3232 - } 3233 - 3234 - .bookmark-preview-arrow { 3235 - display: flex; 3236 - align-items: center; 3237 - justify-content: center; 3238 - color: var(--text-tertiary); 3239 - padding: 0 4px; 3240 - transition: all 0.2s ease; 3241 - } 3242 - 3243 - .bookmark-preview:hover .bookmark-preview-arrow { 3244 - color: var(--accent); 3245 - transform: translateX(2px); 3246 - } 3247 - 3248 - .navbar-logo-img { 3249 - width: 24px; 3250 - height: 24px; 3251 - object-fit: contain; 3252 - } 3253 - 3254 - .login-logo-img { 3255 - width: 80px; 3256 - height: 80px; 3257 - margin-bottom: 24px; 3258 - object-fit: contain; 3259 - } 3260 - 3261 - .legal-content { 3262 - max-width: 800px; 3263 - margin: 0 auto; 3264 - padding: 20px; 3265 - } 3266 - 3267 - .legal-content h1 { 3268 - font-size: 2rem; 3269 - margin-bottom: 8px; 3270 - color: var(--text-primary); 3271 - } 3272 - 3273 - .legal-content h2 { 3274 - font-size: 1.4rem; 3275 - margin-top: 32px; 3276 - margin-bottom: 12px; 3277 - color: var(--text-primary); 3278 - } 3279 - 3280 - .legal-content h3 { 3281 - font-size: 1.1rem; 3282 - margin-top: 20px; 3283 - margin-bottom: 8px; 3284 - color: var(--text-primary); 3285 - } 3286 - 3287 - .legal-content p { 3288 - color: var(--text-secondary); 3289 - line-height: 1.7; 3290 - margin-bottom: 12px; 3291 - } 3292 - 3293 - .legal-content ul { 3294 - color: var(--text-secondary); 3295 - line-height: 1.7; 3296 - margin-left: 24px; 3297 - margin-bottom: 12px; 3298 - } 3299 - 3300 - .legal-content li { 3301 - margin-bottom: 6px; 3302 - } 3303 - 3304 - .legal-content a { 3305 - color: var(--accent); 3306 - text-decoration: none; 3307 - } 3308 - 3309 - .legal-content a:hover { 3310 - text-decoration: underline; 3311 - } 3312 - 3313 - .legal-content section { 3314 - margin-bottom: 24px; 3315 - } 3316 - 3317 - .input { 3318 - width: 100%; 3319 - padding: 12px 14px; 3320 - font-size: 0.95rem; 3321 - color: var(--text-primary); 3322 - background: var(--bg-secondary); 3323 - border: 1px solid var(--border); 3324 - border-radius: var(--radius-md); 3325 - outline: none; 3326 - transition: all 0.15s ease; 3327 - } 3328 - 3329 - .input:focus { 3330 - border-color: var(--accent); 3331 - box-shadow: 0 0 0 3px var(--accent-subtle); 3332 - } 3333 - 3334 - .input::placeholder { 3335 - color: var(--text-tertiary); 3336 - } 3337 - 3338 - .notifications-page { 3339 - max-width: 680px; 3340 - margin: 0 auto; 3341 - } 3342 - 3343 - .notifications-list { 3344 - display: flex; 3345 - flex-direction: column; 3346 - gap: 12px; 3347 - } 3348 - 3349 - .notification-item { 3350 - display: flex; 3351 - gap: 16px; 3352 - align-items: flex-start; 3353 - text-decoration: none; 3354 - color: inherit; 3355 - } 3356 - 3357 - .notification-item:hover { 3358 - background: var(--bg-hover); 3359 - } 3360 - 3361 - .notification-icon { 3362 - width: 36px; 3363 - height: 36px; 3364 - border-radius: var(--radius-full); 3365 - display: flex; 3366 - align-items: center; 3367 - justify-content: center; 3368 - background: var(--bg-tertiary); 3369 - color: var(--text-secondary); 3370 - flex-shrink: 0; 3371 - } 3372 - 3373 - .notification-icon[data-type="like"] { 3374 - color: #ef4444; 3375 - background: rgba(239, 68, 68, 0.1); 3376 - } 3377 - 3378 - .notification-icon[data-type="reply"] { 3379 - color: #3b82f6; 3380 - background: rgba(59, 130, 246, 0.1); 3381 - } 3382 - 3383 - .notification-content { 3384 - flex: 1; 3385 - min-width: 0; 3386 - } 3387 - 3388 - .notification-text { 3389 - font-size: 0.95rem; 3390 - margin-bottom: 4px; 3391 - line-height: 1.4; 3392 - color: var(--text-primary); 3393 - } 3394 - 3395 - .notification-text strong { 3396 - font-weight: 600; 3397 - } 3398 - 3399 - .notification-time { 3400 - font-size: 0.85rem; 3401 - color: var(--text-tertiary); 3402 - } 3403 - 3404 - .notification-link { 3405 - position: relative; 3406 - } 3407 - 3408 - .notification-badge { 3409 - position: absolute; 3410 - top: -2px; 3411 - right: -2px; 3412 - background: var(--error); 3413 - color: white; 3414 - font-size: 0.7rem; 3415 - font-weight: 700; 3416 - min-width: 16px; 3417 - height: 16px; 3418 - border-radius: var(--radius-full); 3419 - display: flex; 3420 - align-items: center; 3421 - justify-content: center; 3422 - padding: 0 4px; 3423 - border: 2px solid var(--bg-primary); 3424 - } 1 + @import "./css/layout.css"; 2 + @import "./css/base.css"; 3 + @import "./css/buttons.css"; 4 + @import "./css/buttons.css"; 5 + @import "./css/feed.css"; 6 + @import "./css/profile.css"; 7 + @import "./css/login.css"; 8 + @import "./css/annotations.css"; 9 + @import "./css/collections.css"; 10 + @import "./css/modals.css"; 11 + @import "./css/notifications.css"; 12 + @import "./css/skeleton.css"; 13 + @import "./css/utilities.css";
+7 -7
web/src/pages/Bookmarks.jsx
··· 1 - import { useState, useEffect } from "react"; 1 + import { useState, useEffect, useCallback } from "react"; 2 2 import { Link } from "react-router-dom"; 3 3 import { Plus } from "lucide-react"; 4 4 import { useAuth } from "../context/AuthContext"; ··· 22 22 const [submitting, setSubmitting] = useState(false); 23 23 const [fetchingTitle, setFetchingTitle] = useState(false); 24 24 25 - const loadBookmarks = async () => { 25 + const loadBookmarks = useCallback(async () => { 26 26 if (!user?.did) return; 27 27 28 28 try { ··· 35 35 } finally { 36 36 setLoadingBookmarks(false); 37 37 } 38 - }; 38 + }, [user]); 39 39 40 40 useEffect(() => { 41 41 if (isAuthenticated && user) { 42 42 loadBookmarks(); 43 43 } 44 - }, [isAuthenticated, user]); 44 + }, [isAuthenticated, user, loadBookmarks]); 45 45 46 46 const handleDelete = async (uri) => { 47 47 if (!confirm("Delete this bookmark?")) return; ··· 133 133 > 134 134 <div> 135 135 <h1 className="page-title">My Bookmarks</h1> 136 - <p className="page-description">Pages you've saved for later</p> 136 + <p className="page-description">Pages you&apos;ve saved for later</p> 137 137 </div> 138 138 <button 139 139 onClick={() => setShowAddForm(!showAddForm)} ··· 274 274 </div> 275 275 <h3 className="empty-state-title">No bookmarks yet</h3> 276 276 <p className="empty-state-text"> 277 - Click "Add Bookmark" above to save a page, or use the browser 278 - extension. 277 + Click &quot;Add Bookmark&quot; above to save a page, or use the 278 + browser extension. 279 279 </p> 280 280 </div> 281 281 ) : (
+4 -4
web/src/pages/CollectionDetail.jsx
··· 1 - import { useState, useEffect } from "react"; 1 + import { useState, useEffect, useCallback } from "react"; 2 2 import { useParams, useNavigate, Link, useLocation } from "react-router-dom"; 3 3 import { ArrowLeft, Edit2, Trash2, Plus } from "lucide-react"; 4 4 import { ··· 34 34 user?.did && 35 35 (collection?.creator?.did === user.did || paramAuthorDid === user.did); 36 36 37 - const fetchContext = async () => { 37 + const fetchContext = useCallback(async () => { 38 38 try { 39 39 setLoading(true); 40 40 ··· 96 96 } finally { 97 97 setLoading(false); 98 98 } 99 - }; 99 + }, [paramAuthorDid, user, handle, rkey, wildcardPath]); 100 100 101 101 useEffect(() => { 102 102 fetchContext(); 103 - }, [rkey, wildcardPath, handle, paramAuthorDid, user?.did]); 103 + }, [fetchContext]); 104 104 105 105 const handleEditSuccess = () => { 106 106 fetchContext();
+5 -6
web/src/pages/Collections.jsx
··· 1 - import { useState, useEffect } from "react"; 2 - import { Link } from "react-router-dom"; 3 - import { Folder, Plus, Edit2, ChevronRight } from "lucide-react"; 1 + import { useState, useEffect, useCallback } from "react"; 2 + import { Folder, Plus } from "lucide-react"; 4 3 import { getCollections } from "../api/client"; 5 4 import { useAuth } from "../context/AuthContext"; 6 5 import CollectionModal from "../components/CollectionModal"; ··· 14 13 const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); 15 14 const [editingCollection, setEditingCollection] = useState(null); 16 15 17 - const fetchCollections = async () => { 16 + const fetchCollections = useCallback(async () => { 18 17 try { 19 18 setLoading(true); 20 19 const data = await getCollections(user.did); ··· 25 24 } finally { 26 25 setLoading(false); 27 26 } 28 - }; 27 + }, [user]); 29 28 30 29 useEffect(() => { 31 30 if (user) { 32 31 fetchCollections(); 33 32 } 34 - }, [user]); 33 + }, [user, fetchCollections]); 35 34 36 35 const handleCreateSuccess = () => { 37 36 fetchCollections();
+90 -95
web/src/pages/Feed.jsx
··· 3 3 import AnnotationCard, { HighlightCard } from "../components/AnnotationCard"; 4 4 import BookmarkCard from "../components/BookmarkCard"; 5 5 import CollectionItemCard from "../components/CollectionItemCard"; 6 + import AnnotationSkeleton from "../components/AnnotationSkeleton"; 6 7 import { getAnnotationFeed, deleteHighlight } from "../api/client"; 7 8 import { AlertIcon, InboxIcon } from "../components/Icons"; 8 9 import { useAuth } from "../context/AuthContext"; ··· 151 152 </button> 152 153 </div> 153 154 154 - {loading && ( 155 + {loading ? ( 155 156 <div className="feed"> 156 - {[1, 2, 3].map((i) => ( 157 - <div key={i} className="card"> 158 - <div 159 - className="skeleton skeleton-text" 160 - style={{ width: "40%" }} 161 - /> 162 - <div className="skeleton skeleton-text" /> 163 - <div className="skeleton skeleton-text" /> 164 - <div 165 - className="skeleton skeleton-text" 166 - style={{ width: "60%" }} 167 - /> 168 - </div> 157 + {[1, 2, 3, 4, 5].map((i) => ( 158 + <AnnotationSkeleton key={i} /> 169 159 ))} 170 160 </div> 171 - )} 172 - 173 - {error && ( 174 - <div className="empty-state"> 175 - <div className="empty-state-icon"> 176 - <AlertIcon size={32} /> 177 - </div> 178 - <h3 className="empty-state-title">Something went wrong</h3> 179 - <p className="empty-state-text">{error}</p> 180 - </div> 181 - )} 161 + ) : ( 162 + <> 163 + {error && ( 164 + <div className="empty-state"> 165 + <div className="empty-state-icon"> 166 + <AlertIcon size={32} /> 167 + </div> 168 + <h3 className="empty-state-title">Something went wrong</h3> 169 + <p className="empty-state-text">{error}</p> 170 + </div> 171 + )} 182 172 183 - {!loading && !error && filteredAnnotations.length === 0 && ( 184 - <div className="empty-state"> 185 - <div className="empty-state-icon"> 186 - <InboxIcon size={32} /> 187 - </div> 188 - <h3 className="empty-state-title">No items yet</h3> 189 - <p className="empty-state-text"> 190 - {filter === "all" 191 - ? "Be the first to annotate something!" 192 - : `No ${filter} items found.`} 193 - </p> 194 - </div> 195 - )} 173 + {!error && filteredAnnotations.length === 0 && ( 174 + <div className="empty-state"> 175 + <div className="empty-state-icon"> 176 + <InboxIcon size={32} /> 177 + </div> 178 + <h3 className="empty-state-title">No items yet</h3> 179 + <p className="empty-state-text"> 180 + {filter === "all" 181 + ? "Be the first to annotate something!" 182 + : `No ${filter} items found.`} 183 + </p> 184 + </div> 185 + )} 196 186 197 - {!loading && !error && filteredAnnotations.length > 0 && ( 198 - <div className="feed"> 199 - {filteredAnnotations.map((item) => { 200 - if (item.type === "CollectionItem") { 201 - return <CollectionItemCard key={item.id} item={item} />; 202 - } 203 - if ( 204 - item.type === "Highlight" || 205 - item.motivation === "highlighting" 206 - ) { 207 - return ( 208 - <HighlightCard 209 - key={item.id} 210 - highlight={item} 211 - onDelete={async (uri) => { 212 - const rkey = uri.split("/").pop(); 213 - await deleteHighlight(rkey); 214 - setAnnotations((prev) => 215 - prev.filter((a) => a.id !== item.id), 216 - ); 217 - }} 218 - onAddToCollection={() => 219 - setCollectionModalState({ 220 - isOpen: true, 221 - uri: item.uri || item.id, 222 - }) 223 - } 224 - /> 225 - ); 226 - } 227 - if (item.type === "Bookmark" || item.motivation === "bookmarking") { 228 - return ( 229 - <BookmarkCard 230 - key={item.id} 231 - bookmark={item} 232 - onAddToCollection={() => 233 - setCollectionModalState({ 234 - isOpen: true, 235 - uri: item.uri || item.id, 236 - }) 237 - } 238 - /> 239 - ); 240 - } 241 - return ( 242 - <AnnotationCard 243 - key={item.id} 244 - annotation={item} 245 - onAddToCollection={() => 246 - setCollectionModalState({ 247 - isOpen: true, 248 - uri: item.uri || item.id, 249 - }) 187 + {!error && filteredAnnotations.length > 0 && ( 188 + <div className="feed"> 189 + {filteredAnnotations.map((item) => { 190 + if (item.type === "CollectionItem") { 191 + return <CollectionItemCard key={item.id} item={item} />; 250 192 } 251 - /> 252 - ); 253 - })} 254 - </div> 193 + if ( 194 + item.type === "Highlight" || 195 + item.motivation === "highlighting" 196 + ) { 197 + return ( 198 + <HighlightCard 199 + key={item.id} 200 + highlight={item} 201 + onDelete={async (uri) => { 202 + const rkey = uri.split("/").pop(); 203 + await deleteHighlight(rkey); 204 + setAnnotations((prev) => 205 + prev.filter((a) => a.id !== item.id), 206 + ); 207 + }} 208 + onAddToCollection={() => 209 + setCollectionModalState({ 210 + isOpen: true, 211 + uri: item.uri || item.id, 212 + }) 213 + } 214 + /> 215 + ); 216 + } 217 + if ( 218 + item.type === "Bookmark" || 219 + item.motivation === "bookmarking" 220 + ) { 221 + return ( 222 + <BookmarkCard 223 + key={item.id} 224 + bookmark={item} 225 + onAddToCollection={() => 226 + setCollectionModalState({ 227 + isOpen: true, 228 + uri: item.uri || item.id, 229 + }) 230 + } 231 + /> 232 + ); 233 + } 234 + return ( 235 + <AnnotationCard 236 + key={item.id} 237 + annotation={item} 238 + onAddToCollection={() => 239 + setCollectionModalState({ 240 + isOpen: true, 241 + uri: item.uri || item.id, 242 + }) 243 + } 244 + /> 245 + ); 246 + })} 247 + </div> 248 + )} 249 + </> 255 250 )} 256 251 257 252 {collectionModalState.isOpen && (
+1 -1
web/src/pages/Highlights.jsx
··· 77 77 <div className="page-header"> 78 78 <h1 className="page-title">My Highlights</h1> 79 79 <p className="page-description"> 80 - Text you've highlighted across the web 80 + Text you&apos;ve highlighted across the web 81 81 </p> 82 82 </div> 83 83
+24 -22
web/src/pages/Login.jsx
··· 23 23 const isSelectionRef = useRef(false); 24 24 25 25 useEffect(() => { 26 - if (handle.length < 3) { 27 - setSuggestions([]); 28 - setShowSuggestions(false); 29 - return; 30 - } 26 + if (handle.length >= 3) { 27 + if (isSelectionRef.current) { 28 + isSelectionRef.current = false; 29 + return; 30 + } 31 31 32 - if (isSelectionRef.current) { 33 - isSelectionRef.current = false; 34 - return; 32 + const timer = setTimeout(async () => { 33 + try { 34 + const data = await searchActors(handle); 35 + setSuggestions(data.actors || []); 36 + setShowSuggestions(true); 37 + setSelectedIndex(-1); 38 + } catch (e) { 39 + console.error("Search failed:", e); 40 + } 41 + }, 300); 42 + return () => clearTimeout(timer); 35 43 } 36 - 37 - const timer = setTimeout(async () => { 38 - try { 39 - const data = await searchActors(handle); 40 - setSuggestions(data.actors || []); 41 - setShowSuggestions(true); 42 - setSelectedIndex(-1); 43 - } catch (e) { 44 - console.error("Search failed:", e); 45 - } 46 - }, 300); 47 - 48 - return () => clearTimeout(timer); 49 44 }, [handle]); 50 45 51 46 useEffect(() => { ··· 178 173 className="login-input" 179 174 placeholder="yourname.bsky.social" 180 175 value={handle} 181 - onChange={(e) => setHandle(e.target.value)} 176 + onChange={(e) => { 177 + const val = e.target.value; 178 + setHandle(val); 179 + if (val.length < 3) { 180 + setSuggestions([]); 181 + setShowSuggestions(false); 182 + } 183 + }} 182 184 onKeyDown={handleKeyDown} 183 185 onFocus={() => 184 186 handle.length >= 3 &&
+2 -1
web/src/pages/Notifications.jsx
··· 156 156 <BellIcon size={48} /> 157 157 <h3>No notifications yet</h3> 158 158 <p> 159 - When someone likes or replies to your content, you'll see it here 159 + When someone likes or replies to your content, you&apos;ll see it 160 + here 160 161 </p> 161 162 </div> 162 163 )}
+7 -7
web/src/pages/Privacy.jsx
··· 16 16 <section> 17 17 <h2>Overview</h2> 18 18 <p> 19 - Margin ("we", "our", or "us") is a web annotation tool that lets you 20 - highlight, annotate, and bookmark any webpage. Your data is stored 21 - on the decentralized AT Protocol network, giving you ownership and 22 - control over your content. 19 + Margin (&quot;we&quot;, &quot;our&quot;, or &quot;us&quot;) is a web 20 + annotation tool that lets you highlight, annotate, and bookmark any 21 + webpage. Your data is stored on the decentralized AT Protocol 22 + network, giving you ownership and control over your content. 23 23 </p> 24 24 </section> 25 25 ··· 111 111 <strong>Cookies:</strong> To maintain your logged-in session 112 112 </li> 113 113 <li> 114 - <strong>Tabs:</strong> To know which page you're viewing 114 + <strong>Tabs:</strong> To know which page you&apos;re viewing 115 115 </li> 116 116 </ul> 117 117 </section> ··· 121 121 <p>You can:</p> 122 122 <ul> 123 123 <li> 124 - Delete any annotation, highlight, or bookmark you've created 124 + Delete any annotation, highlight, or bookmark you&apos;ve created 125 125 </li> 126 126 <li>Delete your collections</li> 127 127 <li>Export your data from your PDS</li> 128 - <li>Revoke the extension's access at any time</li> 128 + <li>Revoke the extension&apos;s access at any time</li> 129 129 </ul> 130 130 </section> 131 131
+250 -4
web/src/pages/Profile.jsx
··· 7 7 getUserHighlights, 8 8 getUserBookmarks, 9 9 getCollections, 10 + getAPIKeys, 11 + createAPIKey, 12 + deleteAPIKey, 10 13 } from "../api/client"; 14 + import { useAuth } from "../context/AuthContext"; 11 15 import CollectionIcon from "../components/CollectionIcon"; 12 16 import CollectionRow from "../components/CollectionRow"; 13 17 import { ··· 17 21 BlueskyIcon, 18 22 } from "../components/Icons"; 19 23 24 + function KeyIcon({ size = 16 }) { 25 + return ( 26 + <svg 27 + width={size} 28 + height={size} 29 + viewBox="0 0 24 24" 30 + fill="none" 31 + stroke="currentColor" 32 + strokeWidth="2" 33 + strokeLinecap="round" 34 + strokeLinejoin="round" 35 + > 36 + <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" /> 37 + </svg> 38 + ); 39 + } 40 + 20 41 export default function Profile() { 21 42 const { handle } = useParams(); 43 + const { user } = useAuth(); 22 44 const [activeTab, setActiveTab] = useState("annotations"); 23 45 const [profile, setProfile] = useState(null); 24 46 const [annotations, setAnnotations] = useState([]); 25 47 const [highlights, setHighlights] = useState([]); 26 48 const [bookmarks, setBookmarks] = useState([]); 27 49 const [collections, setCollections] = useState([]); 50 + const [apiKeys, setApiKeys] = useState([]); 51 + const [newKeyName, setNewKeyName] = useState(""); 52 + const [newKey, setNewKey] = useState(null); 53 + const [keysLoading, setKeysLoading] = useState(false); 28 54 const [loading, setLoading] = useState(true); 29 55 const [error, setError] = useState(null); 56 + 57 + const isOwnProfile = user && (user.did === handle || user.handle === handle); 30 58 31 59 useEffect(() => { 32 60 async function fetchProfile() { ··· 62 90 fetchProfile(); 63 91 }, [handle]); 64 92 93 + useEffect(() => { 94 + if (isOwnProfile && activeTab === "apikeys") { 95 + loadAPIKeys(); 96 + } 97 + }, [isOwnProfile, activeTab]); 98 + 99 + const loadAPIKeys = async () => { 100 + setKeysLoading(true); 101 + try { 102 + const data = await getAPIKeys(); 103 + setApiKeys(data.keys || []); 104 + } catch { 105 + setApiKeys([]); 106 + } finally { 107 + setKeysLoading(false); 108 + } 109 + }; 110 + 111 + const handleCreateKey = async () => { 112 + if (!newKeyName.trim()) return; 113 + try { 114 + const data = await createAPIKey(newKeyName.trim()); 115 + setNewKey(data.key); 116 + setNewKeyName(""); 117 + loadAPIKeys(); 118 + } catch (err) { 119 + alert("Failed to create key: " + err.message); 120 + } 121 + }; 122 + 123 + const handleDeleteKey = async (id) => { 124 + if (!confirm("Delete this API key? This cannot be undone.")) return; 125 + try { 126 + await deleteAPIKey(id); 127 + loadAPIKeys(); 128 + } catch (err) { 129 + alert("Failed to delete key: " + err.message); 130 + } 131 + }; 132 + 65 133 const displayName = profile?.displayName || profile?.handle || handle; 66 134 const displayHandle = 67 135 profile?.handle || (handle?.startsWith("did:") ? null : handle); ··· 89 157 </div> 90 158 <h3 className="empty-state-title">No annotations</h3> 91 159 <p className="empty-state-text"> 92 - This user hasn't posted any annotations. 160 + This user hasn&apos;t posted any annotations. 93 161 </p> 94 162 </div> 95 163 ); ··· 108 176 </div> 109 177 <h3 className="empty-state-title">No highlights</h3> 110 178 <p className="empty-state-text"> 111 - This user hasn't saved any highlights. 179 + This user hasn&apos;t saved any highlights. 112 180 </p> 113 181 </div> 114 182 ); ··· 125 193 </div> 126 194 <h3 className="empty-state-title">No bookmarks</h3> 127 195 <p className="empty-state-text"> 128 - This user hasn't bookmarked any pages. 196 + This user hasn&apos;t bookmarked any pages. 129 197 </p> 130 198 </div> 131 199 ); ··· 142 210 </div> 143 211 <h3 className="empty-state-title">No collections</h3> 144 212 <p className="empty-state-text"> 145 - This user hasn't created any collections. 213 + This user hasn&apos;t created any collections. 146 214 </p> 147 215 </div> 148 216 ); ··· 155 223 </div> 156 224 ); 157 225 } 226 + 227 + if (activeTab === "apikeys" && isOwnProfile) { 228 + return ( 229 + <div className="api-keys-section"> 230 + <div className="card" style={{ marginBottom: "1rem" }}> 231 + <h3 style={{ marginBottom: "0.5rem" }}>Create API Key</h3> 232 + <p 233 + style={{ 234 + color: "var(--text-muted)", 235 + marginBottom: "1rem", 236 + fontSize: "0.875rem", 237 + }} 238 + > 239 + Use API keys to create bookmarks from iOS Shortcuts or other 240 + tools. 241 + </p> 242 + <div style={{ display: "flex", gap: "0.5rem" }}> 243 + <input 244 + type="text" 245 + value={newKeyName} 246 + onChange={(e) => setNewKeyName(e.target.value)} 247 + placeholder="Key name (e.g., iOS Shortcut)" 248 + className="input" 249 + style={{ flex: 1 }} 250 + /> 251 + <button className="btn btn-primary" onClick={handleCreateKey}> 252 + Generate 253 + </button> 254 + </div> 255 + {newKey && ( 256 + <div 257 + style={{ 258 + marginTop: "1rem", 259 + padding: "1rem", 260 + background: "var(--bg-secondary)", 261 + borderRadius: "8px", 262 + }} 263 + > 264 + <p 265 + style={{ 266 + color: "var(--text-success)", 267 + fontWeight: 500, 268 + marginBottom: "0.5rem", 269 + }} 270 + > 271 + โœ“ Key created! Copy it now, you won&apos;t see it again. 272 + </p> 273 + <code 274 + style={{ 275 + display: "block", 276 + padding: "0.75rem", 277 + background: "var(--bg-tertiary)", 278 + borderRadius: "4px", 279 + wordBreak: "break-all", 280 + fontSize: "0.8rem", 281 + }} 282 + > 283 + {newKey} 284 + </code> 285 + <button 286 + className="btn btn-secondary" 287 + style={{ marginTop: "0.5rem" }} 288 + onClick={() => { 289 + navigator.clipboard.writeText(newKey); 290 + alert("Copied!"); 291 + }} 292 + > 293 + Copy to clipboard 294 + </button> 295 + </div> 296 + )} 297 + </div> 298 + 299 + {keysLoading ? ( 300 + <div className="card"> 301 + <div className="skeleton skeleton-text" /> 302 + </div> 303 + ) : apiKeys.length === 0 ? ( 304 + <div className="empty-state"> 305 + <div className="empty-state-icon"> 306 + <KeyIcon size={32} /> 307 + </div> 308 + <h3 className="empty-state-title">No API keys</h3> 309 + <p className="empty-state-text"> 310 + Create a key to use with iOS Shortcuts. 311 + </p> 312 + </div> 313 + ) : ( 314 + <div className="card"> 315 + <h3 style={{ marginBottom: "1rem" }}>Your API Keys</h3> 316 + {apiKeys.map((key) => ( 317 + <div 318 + key={key.id} 319 + style={{ 320 + display: "flex", 321 + justifyContent: "space-between", 322 + alignItems: "center", 323 + padding: "0.75rem 0", 324 + borderBottom: "1px solid var(--border-color)", 325 + }} 326 + > 327 + <div> 328 + <strong>{key.name}</strong> 329 + <div 330 + style={{ 331 + fontSize: "0.75rem", 332 + color: "var(--text-muted)", 333 + }} 334 + > 335 + Created {new Date(key.createdAt).toLocaleDateString()} 336 + {key.lastUsedAt && 337 + ` โ€ข Last used ${new Date(key.lastUsedAt).toLocaleDateString()}`} 338 + </div> 339 + </div> 340 + <button 341 + className="btn btn-sm" 342 + style={{ 343 + fontSize: "0.75rem", 344 + padding: "0.25rem 0.5rem", 345 + color: "#ef4444", 346 + border: "1px solid #ef4444", 347 + }} 348 + onClick={() => handleDeleteKey(key.id)} 349 + > 350 + Revoke 351 + </button> 352 + </div> 353 + ))} 354 + </div> 355 + )} 356 + 357 + <div className="card" style={{ marginTop: "1rem" }}> 358 + <h3 style={{ marginBottom: "0.5rem" }}>iOS Shortcut</h3> 359 + <p 360 + style={{ 361 + color: "var(--text-muted)", 362 + marginBottom: "1rem", 363 + fontSize: "0.875rem", 364 + }} 365 + > 366 + Save bookmarks from Safari&apos;s share sheet. 367 + </p> 368 + <a 369 + href="#" 370 + className="btn btn-primary" 371 + style={{ 372 + display: "inline-flex", 373 + alignItems: "center", 374 + gap: "0.5rem", 375 + opacity: 0.5, 376 + pointerEvents: "none", 377 + cursor: "default", 378 + }} 379 + onClick={(e) => e.preventDefault()} 380 + > 381 + <AppleIcon size={16} /> Coming Soon 382 + </a> 383 + </div> 384 + </div> 385 + ); 386 + } 158 387 }; 159 388 160 389 const bskyProfileUrl = displayHandle ··· 230 459 > 231 460 Collections ({collections.length}) 232 461 </button> 462 + 463 + {isOwnProfile && ( 464 + <button 465 + className={`profile-tab ${activeTab === "apikeys" ? "active" : ""}`} 466 + onClick={() => setActiveTab("apikeys")} 467 + > 468 + <KeyIcon size={14} /> API Keys 469 + </button> 470 + )} 233 471 </div> 234 472 235 473 {loading && ( ··· 262 500 </div> 263 501 ); 264 502 } 503 + 504 + function AppleIcon({ size = 16 }) { 505 + return ( 506 + <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor"> 507 + <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.81-.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" /> 508 + </svg> 509 + ); 510 + }
+81
web/src/pages/Terms.jsx
··· 1 + import { ArrowLeft } from "lucide-react"; 2 + import { Link } from "react-router-dom"; 3 + 4 + export default function Terms() { 5 + return ( 6 + <div className="feed-page"> 7 + <Link to="/" className="back-link"> 8 + <ArrowLeft size={18} /> 9 + <span>Home</span> 10 + </Link> 11 + 12 + <div className="legal-content"> 13 + <h1>Terms of Service</h1> 14 + <p className="text-secondary">Last updated: January 17, 2026</p> 15 + 16 + <section> 17 + <h2>Overview</h2> 18 + <p> 19 + Margin is an open-source project. By using our service, you agree to 20 + these terms (&quot;Terms&quot;). If you do not agree to these Terms, 21 + please do not use the Service. 22 + </p> 23 + </section> 24 + 25 + <section> 26 + <h2>Open Source</h2> 27 + <p> 28 + Margin is open source software. The code is available publicly and 29 + is provided &quot;as is&quot;, without warranty of any kind, express 30 + or implied. 31 + </p> 32 + </section> 33 + 34 + <section> 35 + <h2>User Conduct</h2> 36 + <p> 37 + You are responsible for your use of the Service and for any content 38 + you provide, including compliance with applicable laws, rules, and 39 + regulations. 40 + </p> 41 + <p> 42 + We reserve the right to remove any content that violates these 43 + terms, including but not limited to: 44 + </p> 45 + <ul> 46 + <li>Illegal content</li> 47 + <li>Harassment or hate speech</li> 48 + <li>Spam or malicious content</li> 49 + </ul> 50 + </section> 51 + 52 + <section> 53 + <h2>Decentralized Nature</h2> 54 + <p> 55 + Margin interacts with the AT Protocol network. We do not control the 56 + network itself or the data stored on your Personal Data Server 57 + (PDS). Please refer to the terms of your PDS provider for data 58 + storage policies. 59 + </p> 60 + </section> 61 + 62 + <section> 63 + <h2>Disclaimer</h2> 64 + <p> 65 + THE SERVICE IS PROVIDED &quot;AS IS&quot; AND &quot;AS 66 + AVAILABLE&quot;. WE DISCLAIM ALL CONDITIONS, REPRESENTATIONS AND 67 + WARRANTIES NOT EXPRESSLY SET OUT IN THESE TERMS. 68 + </p> 69 + </section> 70 + 71 + <section> 72 + <h2>Contact</h2> 73 + <p> 74 + For questions about these Terms, please contact us at{" "} 75 + <a href="mailto:hello@margin.at">hello@margin.at</a> 76 + </p> 77 + </section> 78 + </div> 79 + </div> 80 + ); 81 + }