+1
-1
.github/workflows/docker-publish.yml
+1
-1
.github/workflows/docker-publish.yml
+13
-3
.github/workflows/release-extension.yml
+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
README.md
+2
backend/cmd/server/main.go
+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
+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
+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
+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
+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
+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
+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
+1
-1
backend/internal/api/hydration.go
+10
-4
backend/internal/api/token_refresh.go
+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
+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
+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
+
}
+236
-84
backend/internal/firehose/ingester.go
+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
+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
+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
+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
+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
+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
+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
+14
extension/package.json
+86
-20
extension/popup/popup.css
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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>"{highlightedText}"</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>"{highlightedText}"</mark>
724
712
</a>
725
713
)}
726
714
+26
web/src/components/AnnotationSkeleton.jsx
+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
+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
web/src/components/CollectionItemCard.jsx
-7
web/src/components/CollectionModal.jsx
-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
+1
-1
web/src/components/Composer.jsx
-1
web/src/components/ReplyList.jsx
-1
web/src/components/ReplyList.jsx
+195
web/src/components/RightSidebar.jsx
+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
+12
web/src/components/ScrollToTop.jsx
+189
web/src/components/Sidebar.jsx
+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
+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
+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
+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
+
}
+326
web/src/css/collections.css
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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'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 "Add Bookmark" 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
+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
+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
+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
+1
-1
web/src/pages/Highlights.jsx
+24
-22
web/src/pages/Login.jsx
+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
+2
-1
web/src/pages/Notifications.jsx
+7
-7
web/src/pages/Privacy.jsx
+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 ("we", "our", or "us") 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'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'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's access at any time</li>
129
129
</ul>
130
130
</section>
131
131
+250
-4
web/src/pages/Profile.jsx
+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'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'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'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'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'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'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
+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 ("Terms"). 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 "as is", 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 "AS IS" AND "AS
66
+
AVAILABLE". 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
+
}