+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
+3
-5
README.md
+3
-5
README.md
···
1
1
# Margin
2
2
3
-
*Write in the margins of the web*
3
+
_Write in the margins of the web_
4
4
5
5
A web comments layer built on [AT Protocol](https://atproto.com) that lets you annotate any URL on the internet.
6
6
7
7
## Project Structure
8
8
9
9
```
10
-
project-agua/
10
+
margin/
11
11
โโโ lexicons/ # AT Protocol lexicon schemas
12
12
โ โโโ at/margin/
13
13
โ โโโ annotation.json
···
40
40
41
41
Server runs on http://localhost:8080
42
42
43
-
Server runs on http://localhost:8080
44
-
45
43
### Docker (Recommended)
46
44
47
45
Run the full stack (Backend + Postgres) with Docker:
48
46
49
47
```bash
50
-
docker-compose up -d --build
48
+
docker compose up -d --build
51
49
```
52
50
53
51
### Web App
+8
backend/cmd/server/main.go
+8
backend/cmd/server/main.go
···
97
97
r.Get("/og-image", ogHandler.HandleOGImage)
98
98
r.Get("/annotation/{did}/{rkey}", ogHandler.HandleAnnotationPage)
99
99
r.Get("/at/{did}/{rkey}", ogHandler.HandleAnnotationPage)
100
+
r.Get("/{handle}/annotation/{rkey}", ogHandler.HandleAnnotationPage)
101
+
r.Get("/{handle}/highlight/{rkey}", ogHandler.HandleAnnotationPage)
102
+
r.Get("/{handle}/bookmark/{rkey}", ogHandler.HandleAnnotationPage)
103
+
104
+
r.Get("/api/tags/trending", handler.HandleGetTrendingTags)
105
+
106
+
r.Get("/collection/{uri}", ogHandler.HandleCollectionPage)
107
+
r.Get("/{handle}/collection/{rkey}", ogHandler.HandleCollectionPage)
100
108
101
109
staticDir := getEnv("STATIC_DIR", "../web/dist")
102
110
serveStatic(r, staticDir)
+45
-24
backend/internal/api/annotations.go
+45
-24
backend/internal/api/annotations.go
···
47
47
return
48
48
}
49
49
50
-
if req.URL == "" || req.Text == "" {
51
-
http.Error(w, "URL and text are required", http.StatusBadRequest)
50
+
if req.URL == "" {
51
+
http.Error(w, "URL is required", http.StatusBadRequest)
52
+
return
53
+
}
54
+
55
+
if req.Text == "" && req.Selector == nil && len(req.Tags) == 0 {
56
+
http.Error(w, "Must provide text, selector, or tags", http.StatusBadRequest)
52
57
return
53
58
}
54
59
···
67
72
}
68
73
69
74
record := xrpc.NewAnnotationRecordWithMotivation(req.URL, urlHash, req.Text, req.Selector, req.Title, motivation)
75
+
if len(req.Tags) > 0 {
76
+
record.Tags = req.Tags
77
+
}
70
78
71
79
var result *xrpc.CreateRecordOutput
72
80
err = s.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error {
···
93
101
selectorJSONPtr = &selectorStr
94
102
}
95
103
104
+
var tagsJSONPtr *string
105
+
if len(req.Tags) > 0 {
106
+
tagsBytes, _ := json.Marshal(req.Tags)
107
+
tagsStr := string(tagsBytes)
108
+
tagsJSONPtr = &tagsStr
109
+
}
110
+
96
111
cid := result.CID
97
112
did := session.DID
98
113
annotation := &db.Annotation{
···
105
120
TargetHash: urlHash,
106
121
TargetTitle: targetTitlePtr,
107
122
SelectorJSON: selectorJSONPtr,
123
+
TagsJSON: tagsJSONPtr,
108
124
CreatedAt: time.Now(),
109
125
IndexedAt: time.Now(),
110
126
}
···
203
219
}
204
220
rkey := parts[2]
205
221
206
-
var selector interface{} = nil
207
-
if annotation.SelectorJSON != nil && *annotation.SelectorJSON != "" {
208
-
json.Unmarshal([]byte(*annotation.SelectorJSON), &selector)
209
-
}
210
-
211
222
tagsJSON := ""
212
223
if len(req.Tags) > 0 {
213
224
tagsBytes, _ := json.Marshal(req.Tags)
214
225
tagsJSON = string(tagsBytes)
215
226
}
216
227
217
-
record := map[string]interface{}{
218
-
"$type": xrpc.CollectionAnnotation,
219
-
"text": req.Text,
220
-
"url": annotation.TargetSource,
221
-
"createdAt": annotation.CreatedAt.Format(time.RFC3339),
222
-
}
223
-
if selector != nil {
224
-
record["selector"] = selector
225
-
}
226
-
if len(req.Tags) > 0 {
227
-
record["tags"] = req.Tags
228
-
}
229
-
if annotation.TargetTitle != nil {
230
-
record["title"] = *annotation.TargetTitle
231
-
}
232
-
233
228
if annotation.BodyValue != nil {
234
229
previousContent := *annotation.BodyValue
235
230
s.db.SaveEditHistory(uri, "annotation", previousContent, annotation.CID)
···
237
232
238
233
var result *xrpc.PutRecordOutput
239
234
err = s.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error {
235
+
existing, getErr := client.GetRecord(r.Context(), did, xrpc.CollectionAnnotation, rkey)
236
+
if getErr != nil {
237
+
return fmt.Errorf("failed to fetch existing record: %w", getErr)
238
+
}
239
+
240
+
var record map[string]interface{}
241
+
if err := json.Unmarshal(existing.Value, &record); err != nil {
242
+
return fmt.Errorf("failed to parse existing record: %w", err)
243
+
}
244
+
245
+
record["text"] = req.Text
246
+
if req.Tags != nil {
247
+
record["tags"] = req.Tags
248
+
} else {
249
+
delete(record, "tags")
250
+
}
251
+
240
252
var updateErr error
241
253
result, updateErr = client.PutRecord(r.Context(), did, xrpc.CollectionAnnotation, rkey, record)
242
254
if updateErr != nil {
···
498
510
Title string `json:"title,omitempty"`
499
511
Selector interface{} `json:"selector"`
500
512
Color string `json:"color,omitempty"`
513
+
Tags []string `json:"tags,omitempty"`
501
514
}
502
515
503
516
func (s *AnnotationService) CreateHighlight(w http.ResponseWriter, r *http.Request) {
···
519
532
}
520
533
521
534
urlHash := db.HashURL(req.URL)
522
-
record := xrpc.NewHighlightRecord(req.URL, urlHash, req.Selector, req.Color)
535
+
record := xrpc.NewHighlightRecord(req.URL, urlHash, req.Selector, req.Color, req.Tags)
523
536
524
537
var result *xrpc.CreateRecordOutput
525
538
err = s.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error {
···
549
562
colorPtr = &req.Color
550
563
}
551
564
565
+
var tagsJSONPtr *string
566
+
if len(req.Tags) > 0 {
567
+
tagsBytes, _ := json.Marshal(req.Tags)
568
+
tagsStr := string(tagsBytes)
569
+
tagsJSONPtr = &tagsStr
570
+
}
571
+
552
572
cid := result.CID
553
573
highlight := &db.Highlight{
554
574
URI: result.URI,
···
558
578
TargetTitle: titlePtr,
559
579
SelectorJSON: selectorJSONPtr,
560
580
Color: colorPtr,
581
+
TagsJSON: tagsJSONPtr,
561
582
CreatedAt: time.Now(),
562
583
IndexedAt: time.Now(),
563
584
CID: &cid,
+35
-5
backend/internal/api/collections.go
+35
-5
backend/internal/api/collections.go
···
213
213
return
214
214
}
215
215
216
+
profiles := fetchProfilesForDIDs([]string{authorDID})
217
+
creator := profiles[authorDID]
218
+
219
+
apiCollections := make([]APICollection, len(collections))
220
+
for i, c := range collections {
221
+
icon := ""
222
+
if c.Icon != nil {
223
+
icon = *c.Icon
224
+
}
225
+
desc := ""
226
+
if c.Description != nil {
227
+
desc = *c.Description
228
+
}
229
+
apiCollections[i] = APICollection{
230
+
URI: c.URI,
231
+
Name: c.Name,
232
+
Description: desc,
233
+
Icon: icon,
234
+
Creator: creator,
235
+
CreatedAt: c.CreatedAt,
236
+
IndexedAt: c.IndexedAt,
237
+
}
238
+
}
239
+
216
240
w.Header().Set("Content-Type", "application/json")
217
241
json.NewEncoder(w).Encode(map[string]interface{}{
218
242
"@context": "http://www.w3.org/ns/anno.jsonld",
219
243
"type": "Collection",
220
-
"items": collections,
221
-
"totalItems": len(collections),
244
+
"items": apiCollections,
245
+
"totalItems": len(apiCollections),
222
246
})
223
247
}
224
248
···
254
278
255
279
enrichedItems := make([]EnrichedCollectionItem, 0, len(items))
256
280
281
+
session, err := s.refresher.GetSessionWithAutoRefresh(r)
282
+
viewerDID := ""
283
+
if err == nil {
284
+
viewerDID = session.DID
285
+
}
286
+
257
287
for _, item := range items {
258
288
enriched := EnrichedCollectionItem{
259
289
URI: item.URI,
···
266
296
if strings.Contains(item.AnnotationURI, "at.margin.annotation") {
267
297
enriched.Type = "annotation"
268
298
if a, err := s.db.GetAnnotationByURI(item.AnnotationURI); err == nil {
269
-
hydrated, _ := hydrateAnnotations([]db.Annotation{*a})
299
+
hydrated, _ := hydrateAnnotations(s.db, []db.Annotation{*a}, viewerDID)
270
300
if len(hydrated) > 0 {
271
301
enriched.Annotation = &hydrated[0]
272
302
}
···
274
304
} else if strings.Contains(item.AnnotationURI, "at.margin.highlight") {
275
305
enriched.Type = "highlight"
276
306
if h, err := s.db.GetHighlightByURI(item.AnnotationURI); err == nil {
277
-
hydrated, _ := hydrateHighlights([]db.Highlight{*h})
307
+
hydrated, _ := hydrateHighlights(s.db, []db.Highlight{*h}, viewerDID)
278
308
if len(hydrated) > 0 {
279
309
enriched.Highlight = &hydrated[0]
280
310
}
···
282
312
} else if strings.Contains(item.AnnotationURI, "at.margin.bookmark") {
283
313
enriched.Type = "bookmark"
284
314
if b, err := s.db.GetBookmarkByURI(item.AnnotationURI); err == nil {
285
-
hydrated, _ := hydrateBookmarks([]db.Bookmark{*b})
315
+
hydrated, _ := hydrateBookmarks(s.db, []db.Bookmark{*b}, viewerDID)
286
316
if len(hydrated) > 0 {
287
317
enriched.Bookmark = &hydrated[0]
288
318
}
+119
-40
backend/internal/api/handler.go
+119
-40
backend/internal/api/handler.go
···
81
81
limit := parseIntParam(r, "limit", 50)
82
82
offset := parseIntParam(r, "offset", 0)
83
83
motivation := r.URL.Query().Get("motivation")
84
+
tag := r.URL.Query().Get("tag")
84
85
85
86
var annotations []db.Annotation
86
87
var err error
···
90
91
annotations, err = h.db.GetAnnotationsByTargetHash(urlHash, limit, offset)
91
92
} else if motivation != "" {
92
93
annotations, err = h.db.GetAnnotationsByMotivation(motivation, limit, offset)
94
+
} else if tag != "" {
95
+
annotations, err = h.db.GetAnnotationsByTag(tag, limit, offset)
93
96
} else {
94
97
annotations, err = h.db.GetRecentAnnotations(limit, offset)
95
98
}
···
99
102
return
100
103
}
101
104
102
-
enriched, _ := hydrateAnnotations(annotations)
105
+
enriched, _ := hydrateAnnotations(h.db, annotations, h.getViewerDID(r))
103
106
104
107
w.Header().Set("Content-Type", "application/json")
105
108
json.NewEncoder(w).Encode(map[string]interface{}{
···
112
115
113
116
func (h *Handler) GetFeed(w http.ResponseWriter, r *http.Request) {
114
117
limit := parseIntParam(r, "limit", 50)
115
-
116
-
annotations, _ := h.db.GetRecentAnnotations(limit, 0)
117
-
highlights, _ := h.db.GetRecentHighlights(limit, 0)
118
-
bookmarks, _ := h.db.GetRecentBookmarks(limit, 0)
118
+
tag := r.URL.Query().Get("tag")
119
+
creator := r.URL.Query().Get("creator")
119
120
120
-
authAnnos, _ := hydrateAnnotations(annotations)
121
-
authHighs, _ := hydrateHighlights(highlights)
122
-
authBooks, _ := hydrateBookmarks(bookmarks)
121
+
var annotations []db.Annotation
122
+
var highlights []db.Highlight
123
+
var bookmarks []db.Bookmark
124
+
var collectionItems []db.CollectionItem
125
+
var err error
123
126
124
-
collectionItems, err := h.db.GetRecentCollectionItems(limit, 0)
125
-
if err != nil {
126
-
log.Printf("Error fetching collection items: %v\n", err)
127
+
if tag != "" {
128
+
if creator != "" {
129
+
annotations, _ = h.db.GetAnnotationsByTagAndAuthor(tag, creator, limit, 0)
130
+
highlights, _ = h.db.GetHighlightsByTagAndAuthor(tag, creator, limit, 0)
131
+
bookmarks, _ = h.db.GetBookmarksByTagAndAuthor(tag, creator, limit, 0)
132
+
collectionItems = []db.CollectionItem{}
133
+
} else {
134
+
annotations, _ = h.db.GetAnnotationsByTag(tag, limit, 0)
135
+
highlights, _ = h.db.GetHighlightsByTag(tag, limit, 0)
136
+
bookmarks, _ = h.db.GetBookmarksByTag(tag, limit, 0)
137
+
collectionItems = []db.CollectionItem{}
138
+
}
139
+
} else if creator != "" {
140
+
annotations, _ = h.db.GetAnnotationsByAuthor(creator, limit, 0)
141
+
highlights, _ = h.db.GetHighlightsByAuthor(creator, limit, 0)
142
+
bookmarks, _ = h.db.GetBookmarksByAuthor(creator, limit, 0)
143
+
collectionItems = []db.CollectionItem{}
144
+
} else {
145
+
annotations, _ = h.db.GetRecentAnnotations(limit, 0)
146
+
highlights, _ = h.db.GetRecentHighlights(limit, 0)
147
+
bookmarks, _ = h.db.GetRecentBookmarks(limit, 0)
148
+
collectionItems, err = h.db.GetRecentCollectionItems(limit, 0)
149
+
if err != nil {
150
+
log.Printf("Error fetching collection items: %v\n", err)
151
+
}
127
152
}
128
-
// log.Printf("Fetched %d collection items\n", len(collectionItems))
129
-
authCollectionItems, _ := hydrateCollectionItems(h.db, collectionItems)
130
-
// log.Printf("Hydrated %d collection items\n", len(authCollectionItems))
153
+
154
+
viewerDID := h.getViewerDID(r)
155
+
authAnnos, _ := hydrateAnnotations(h.db, annotations, viewerDID)
156
+
authHighs, _ := hydrateHighlights(h.db, highlights, viewerDID)
157
+
authBooks, _ := hydrateBookmarks(h.db, bookmarks, viewerDID)
158
+
159
+
authCollectionItems, _ := hydrateCollectionItems(h.db, collectionItems, viewerDID)
131
160
132
161
var feed []interface{}
133
162
for _, a := range authAnnos {
···
188
217
return
189
218
}
190
219
191
-
annotation, err := h.db.GetAnnotationByURI(uri)
192
-
if err != nil {
193
-
http.Error(w, "Annotation not found", http.StatusNotFound)
194
-
return
220
+
serveResponse := func(data interface{}, context string) {
221
+
w.Header().Set("Content-Type", "application/json")
222
+
response := map[string]interface{}{
223
+
"@context": context,
224
+
}
225
+
jsonData, _ := json.Marshal(data)
226
+
json.Unmarshal(jsonData, &response)
227
+
json.NewEncoder(w).Encode(response)
228
+
}
229
+
230
+
if annotation, err := h.db.GetAnnotationByURI(uri); err == nil {
231
+
if enriched, _ := hydrateAnnotations(h.db, []db.Annotation{*annotation}, h.getViewerDID(r)); len(enriched) > 0 {
232
+
serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld")
233
+
return
234
+
}
235
+
}
236
+
237
+
if highlight, err := h.db.GetHighlightByURI(uri); err == nil {
238
+
if enriched, _ := hydrateHighlights(h.db, []db.Highlight{*highlight}, h.getViewerDID(r)); len(enriched) > 0 {
239
+
serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld")
240
+
return
241
+
}
242
+
}
243
+
244
+
if strings.Contains(uri, "at.margin.annotation") {
245
+
highlightURI := strings.Replace(uri, "at.margin.annotation", "at.margin.highlight", 1)
246
+
if highlight, err := h.db.GetHighlightByURI(highlightURI); err == nil {
247
+
if enriched, _ := hydrateHighlights(h.db, []db.Highlight{*highlight}, h.getViewerDID(r)); len(enriched) > 0 {
248
+
serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld")
249
+
return
250
+
}
251
+
}
195
252
}
196
253
197
-
enriched, _ := hydrateAnnotations([]db.Annotation{*annotation})
198
-
if len(enriched) == 0 {
199
-
http.Error(w, "Annotation not found", http.StatusNotFound)
200
-
return
254
+
if bookmark, err := h.db.GetBookmarkByURI(uri); err == nil {
255
+
if enriched, _ := hydrateBookmarks(h.db, []db.Bookmark{*bookmark}, h.getViewerDID(r)); len(enriched) > 0 {
256
+
serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld")
257
+
return
258
+
}
201
259
}
202
260
203
-
w.Header().Set("Content-Type", "application/json")
204
-
response := map[string]interface{}{
205
-
"@context": "http://www.w3.org/ns/anno.jsonld",
261
+
if strings.Contains(uri, "at.margin.annotation") {
262
+
bookmarkURI := strings.Replace(uri, "at.margin.annotation", "at.margin.bookmark", 1)
263
+
if bookmark, err := h.db.GetBookmarkByURI(bookmarkURI); err == nil {
264
+
if enriched, _ := hydrateBookmarks(h.db, []db.Bookmark{*bookmark}, h.getViewerDID(r)); len(enriched) > 0 {
265
+
serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld")
266
+
return
267
+
}
268
+
}
206
269
}
207
-
annJSON, _ := json.Marshal(enriched[0])
208
-
json.Unmarshal(annJSON, &response)
270
+
271
+
http.Error(w, "Annotation, Highlight, or Bookmark not found", http.StatusNotFound)
209
272
210
-
json.NewEncoder(w).Encode(response)
211
273
}
212
274
213
275
func (h *Handler) GetByTarget(w http.ResponseWriter, r *http.Request) {
···
228
290
annotations, _ := h.db.GetAnnotationsByTargetHash(urlHash, limit, offset)
229
291
highlights, _ := h.db.GetHighlightsByTargetHash(urlHash, limit, offset)
230
292
231
-
enrichedAnnotations, _ := hydrateAnnotations(annotations)
232
-
enrichedHighlights, _ := hydrateHighlights(highlights)
293
+
enrichedAnnotations, _ := hydrateAnnotations(h.db, annotations, h.getViewerDID(r))
294
+
enrichedHighlights, _ := hydrateHighlights(h.db, highlights, h.getViewerDID(r))
233
295
234
296
w.Header().Set("Content-Type", "application/json")
235
297
json.NewEncoder(w).Encode(map[string]interface{}{
···
243
305
244
306
func (h *Handler) GetHighlights(w http.ResponseWriter, r *http.Request) {
245
307
did := r.URL.Query().Get("creator")
308
+
tag := r.URL.Query().Get("tag")
246
309
limit := parseIntParam(r, "limit", 50)
247
310
offset := parseIntParam(r, "offset", 0)
248
311
249
-
if did == "" {
250
-
http.Error(w, "creator parameter required", http.StatusBadRequest)
251
-
return
312
+
var highlights []db.Highlight
313
+
var err error
314
+
315
+
if did != "" {
316
+
highlights, err = h.db.GetHighlightsByAuthor(did, limit, offset)
317
+
} else if tag != "" {
318
+
highlights, err = h.db.GetHighlightsByTag(tag, limit, offset)
319
+
} else {
320
+
highlights, err = h.db.GetRecentHighlights(limit, offset)
252
321
}
253
322
254
-
highlights, err := h.db.GetHighlightsByAuthor(did, limit, offset)
255
323
if err != nil {
256
324
http.Error(w, err.Error(), http.StatusInternalServerError)
257
325
return
258
326
}
259
327
260
-
enriched, _ := hydrateHighlights(highlights)
328
+
enriched, _ := hydrateHighlights(h.db, highlights, h.getViewerDID(r))
261
329
262
330
w.Header().Set("Content-Type", "application/json")
263
331
json.NewEncoder(w).Encode(map[string]interface{}{
···
284
352
return
285
353
}
286
354
287
-
enriched, _ := hydrateBookmarks(bookmarks)
355
+
enriched, _ := hydrateBookmarks(h.db, bookmarks, h.getViewerDID(r))
288
356
289
357
w.Header().Set("Content-Type", "application/json")
290
358
json.NewEncoder(w).Encode(map[string]interface{}{
···
309
377
return
310
378
}
311
379
312
-
enriched, _ := hydrateAnnotations(annotations)
380
+
enriched, _ := hydrateAnnotations(h.db, annotations, h.getViewerDID(r))
313
381
314
382
w.Header().Set("Content-Type", "application/json")
315
383
json.NewEncoder(w).Encode(map[string]interface{}{
···
335
403
return
336
404
}
337
405
338
-
enriched, _ := hydrateHighlights(highlights)
406
+
enriched, _ := hydrateHighlights(h.db, highlights, h.getViewerDID(r))
339
407
340
408
w.Header().Set("Content-Type", "application/json")
341
409
json.NewEncoder(w).Encode(map[string]interface{}{
···
361
429
return
362
430
}
363
431
364
-
enriched, _ := hydrateBookmarks(bookmarks)
432
+
enriched, _ := hydrateBookmarks(h.db, bookmarks, h.getViewerDID(r))
365
433
366
434
w.Header().Set("Content-Type", "application/json")
367
435
json.NewEncoder(w).Encode(map[string]interface{}{
···
515
583
return
516
584
}
517
585
518
-
enriched, err := hydrateNotifications(notifications)
586
+
enriched, err := hydrateNotifications(h.db, notifications)
519
587
if err != nil {
520
588
log.Printf("Failed to hydrate notifications: %v\n", err)
521
589
}
···
560
628
w.Header().Set("Content-Type", "application/json")
561
629
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
562
630
}
631
+
func (h *Handler) getViewerDID(r *http.Request) string {
632
+
cookie, err := r.Cookie("margin_session")
633
+
if err != nil {
634
+
return ""
635
+
}
636
+
did, _, _, _, _, err := h.db.GetSession(cookie.Value)
637
+
if err != nil {
638
+
return ""
639
+
}
640
+
return did
641
+
}
+131
-50
backend/internal/api/hydration.go
+131
-50
backend/internal/api/hydration.go
···
50
50
}
51
51
52
52
type APIAnnotation struct {
53
-
ID string `json:"id"`
54
-
CID string `json:"cid"`
55
-
Type string `json:"type"`
56
-
Motivation string `json:"motivation,omitempty"`
57
-
Author Author `json:"creator"`
58
-
Body *APIBody `json:"body,omitempty"`
59
-
Target APITarget `json:"target"`
60
-
Tags []string `json:"tags,omitempty"`
61
-
Generator *APIGenerator `json:"generator,omitempty"`
62
-
CreatedAt time.Time `json:"created"`
63
-
IndexedAt time.Time `json:"indexed"`
53
+
ID string `json:"id"`
54
+
CID string `json:"cid"`
55
+
Type string `json:"type"`
56
+
Motivation string `json:"motivation,omitempty"`
57
+
Author Author `json:"creator"`
58
+
Body *APIBody `json:"body,omitempty"`
59
+
Target APITarget `json:"target"`
60
+
Tags []string `json:"tags,omitempty"`
61
+
Generator *APIGenerator `json:"generator,omitempty"`
62
+
CreatedAt time.Time `json:"created"`
63
+
IndexedAt time.Time `json:"indexed"`
64
+
LikeCount int `json:"likeCount"`
65
+
ReplyCount int `json:"replyCount"`
66
+
ViewerHasLiked bool `json:"viewerHasLiked"`
64
67
}
65
68
66
69
type APIHighlight struct {
67
-
ID string `json:"id"`
68
-
Type string `json:"type"`
69
-
Author Author `json:"creator"`
70
-
Target APITarget `json:"target"`
71
-
Color string `json:"color,omitempty"`
72
-
Tags []string `json:"tags,omitempty"`
73
-
CreatedAt time.Time `json:"created"`
74
-
CID string `json:"cid,omitempty"`
70
+
ID string `json:"id"`
71
+
Type string `json:"type"`
72
+
Author Author `json:"creator"`
73
+
Target APITarget `json:"target"`
74
+
Color string `json:"color,omitempty"`
75
+
Tags []string `json:"tags,omitempty"`
76
+
CreatedAt time.Time `json:"created"`
77
+
CID string `json:"cid,omitempty"`
78
+
LikeCount int `json:"likeCount"`
79
+
ReplyCount int `json:"replyCount"`
80
+
ViewerHasLiked bool `json:"viewerHasLiked"`
75
81
}
76
82
77
83
type APIBookmark struct {
78
-
ID string `json:"id"`
79
-
Type string `json:"type"`
80
-
Author Author `json:"creator"`
81
-
Source string `json:"source"`
82
-
Title string `json:"title,omitempty"`
83
-
Description string `json:"description,omitempty"`
84
-
Tags []string `json:"tags,omitempty"`
85
-
CreatedAt time.Time `json:"created"`
86
-
CID string `json:"cid,omitempty"`
84
+
ID string `json:"id"`
85
+
Type string `json:"type"`
86
+
Author Author `json:"creator"`
87
+
Source string `json:"source"`
88
+
Title string `json:"title,omitempty"`
89
+
Description string `json:"description,omitempty"`
90
+
Tags []string `json:"tags,omitempty"`
91
+
CreatedAt time.Time `json:"created"`
92
+
CID string `json:"cid,omitempty"`
93
+
LikeCount int `json:"likeCount"`
94
+
ReplyCount int `json:"replyCount"`
95
+
ViewerHasLiked bool `json:"viewerHasLiked"`
87
96
}
88
97
89
98
type APIReply struct {
···
99
108
}
100
109
101
110
type APICollection struct {
102
-
URI string `json:"uri"`
103
-
Name string `json:"name"`
104
-
Icon string `json:"icon,omitempty"`
111
+
URI string `json:"uri"`
112
+
Name string `json:"name"`
113
+
Description string `json:"description,omitempty"`
114
+
Icon string `json:"icon,omitempty"`
115
+
Creator Author `json:"creator"`
116
+
CreatedAt time.Time `json:"createdAt"`
117
+
IndexedAt time.Time `json:"indexedAt"`
105
118
}
106
119
107
120
type APICollectionItem struct {
···
118
131
}
119
132
120
133
type APINotification struct {
121
-
ID int `json:"id"`
122
-
Recipient Author `json:"recipient"`
123
-
Actor Author `json:"actor"`
124
-
Type string `json:"type"`
125
-
SubjectURI string `json:"subjectUri"`
126
-
CreatedAt time.Time `json:"createdAt"`
127
-
ReadAt *time.Time `json:"readAt,omitempty"`
134
+
ID int `json:"id"`
135
+
Recipient Author `json:"recipient"`
136
+
Actor Author `json:"actor"`
137
+
Type string `json:"type"`
138
+
SubjectURI string `json:"subjectUri"`
139
+
Subject interface{} `json:"subject,omitempty"`
140
+
CreatedAt time.Time `json:"createdAt"`
141
+
ReadAt *time.Time `json:"readAt,omitempty"`
128
142
}
129
143
130
-
func hydrateAnnotations(annotations []db.Annotation) ([]APIAnnotation, error) {
144
+
func hydrateAnnotations(database *db.DB, annotations []db.Annotation, viewerDID string) ([]APIAnnotation, error) {
131
145
if len(annotations) == 0 {
132
146
return []APIAnnotation{}, nil
133
147
}
···
192
206
CreatedAt: a.CreatedAt,
193
207
IndexedAt: a.IndexedAt,
194
208
}
209
+
210
+
if database != nil {
211
+
result[i].LikeCount, _ = database.GetLikeCount(a.URI)
212
+
result[i].ReplyCount, _ = database.GetReplyCount(a.URI)
213
+
if viewerDID != "" {
214
+
if _, err := database.GetLikeByUserAndSubject(viewerDID, a.URI); err == nil {
215
+
result[i].ViewerHasLiked = true
216
+
}
217
+
}
218
+
}
195
219
}
196
220
197
221
return result, nil
198
222
}
199
223
200
-
func hydrateHighlights(highlights []db.Highlight) ([]APIHighlight, error) {
224
+
func hydrateHighlights(database *db.DB, highlights []db.Highlight, viewerDID string) ([]APIHighlight, error) {
201
225
if len(highlights) == 0 {
202
226
return []APIHighlight{}, nil
203
227
}
···
245
269
Tags: tags,
246
270
CreatedAt: h.CreatedAt,
247
271
CID: cid,
272
+
}
273
+
274
+
if database != nil {
275
+
result[i].LikeCount, _ = database.GetLikeCount(h.URI)
276
+
result[i].ReplyCount, _ = database.GetReplyCount(h.URI)
277
+
if viewerDID != "" {
278
+
if _, err := database.GetLikeByUserAndSubject(viewerDID, h.URI); err == nil {
279
+
result[i].ViewerHasLiked = true
280
+
}
281
+
}
248
282
}
249
283
}
250
284
251
285
return result, nil
252
286
}
253
287
254
-
func hydrateBookmarks(bookmarks []db.Bookmark) ([]APIBookmark, error) {
288
+
func hydrateBookmarks(database *db.DB, bookmarks []db.Bookmark, viewerDID string) ([]APIBookmark, error) {
255
289
if len(bookmarks) == 0 {
256
290
return []APIBookmark{}, nil
257
291
}
···
290
324
Tags: tags,
291
325
CreatedAt: b.CreatedAt,
292
326
CID: cid,
327
+
}
328
+
if database != nil {
329
+
result[i].LikeCount, _ = database.GetLikeCount(b.URI)
330
+
result[i].ReplyCount, _ = database.GetReplyCount(b.URI)
331
+
if viewerDID != "" {
332
+
if _, err := database.GetLikeByUserAndSubject(viewerDID, b.URI); err == nil {
333
+
result[i].ViewerHasLiked = true
334
+
}
335
+
}
293
336
}
294
337
}
295
338
···
434
477
return result, nil
435
478
}
436
479
437
-
func hydrateCollectionItems(database *db.DB, items []db.CollectionItem) ([]APICollectionItem, error) {
480
+
func hydrateCollectionItems(database *db.DB, items []db.CollectionItem, viewerDID string) ([]APICollectionItem, error) {
438
481
if len(items) == 0 {
439
482
return []APICollectionItem{}, nil
440
483
}
···
457
500
if coll.Icon != nil {
458
501
icon = *coll.Icon
459
502
}
503
+
desc := ""
504
+
if coll.Description != nil {
505
+
desc = *coll.Description
506
+
}
460
507
apiItem.Collection = &APICollection{
461
-
URI: coll.URI,
462
-
Name: coll.Name,
463
-
Icon: icon,
508
+
URI: coll.URI,
509
+
Name: coll.Name,
510
+
Description: desc,
511
+
Icon: icon,
512
+
Creator: profiles[coll.AuthorDID],
513
+
CreatedAt: coll.CreatedAt,
514
+
IndexedAt: coll.IndexedAt,
464
515
}
465
516
}
466
517
467
518
if strings.Contains(item.AnnotationURI, "at.margin.annotation") {
468
519
if a, err := database.GetAnnotationByURI(item.AnnotationURI); err == nil {
469
-
hydrated, _ := hydrateAnnotations([]db.Annotation{*a})
520
+
hydrated, _ := hydrateAnnotations(database, []db.Annotation{*a}, viewerDID)
470
521
if len(hydrated) > 0 {
471
522
apiItem.Annotation = &hydrated[0]
472
523
}
473
524
}
474
525
} else if strings.Contains(item.AnnotationURI, "at.margin.highlight") {
475
526
if h, err := database.GetHighlightByURI(item.AnnotationURI); err == nil {
476
-
hydrated, _ := hydrateHighlights([]db.Highlight{*h})
527
+
hydrated, _ := hydrateHighlights(database, []db.Highlight{*h}, viewerDID)
477
528
if len(hydrated) > 0 {
478
529
apiItem.Highlight = &hydrated[0]
479
530
}
480
531
}
481
532
} else if strings.Contains(item.AnnotationURI, "at.margin.bookmark") {
482
533
if b, err := database.GetBookmarkByURI(item.AnnotationURI); err == nil {
483
-
hydrated, _ := hydrateBookmarks([]db.Bookmark{*b})
534
+
hydrated, _ := hydrateBookmarks(database, []db.Bookmark{*b}, viewerDID)
484
535
if len(hydrated) > 0 {
485
536
apiItem.Bookmark = &hydrated[0]
486
537
} else {
487
538
log.Printf("Failed to hydrate bookmark %s: empty hydration result\n", item.AnnotationURI)
488
539
}
489
540
} else {
490
-
log.Printf("GetBookmarkByURI failed for %s: %v\n", item.AnnotationURI, err)
491
541
}
492
542
} else {
493
543
log.Printf("Unknown item type for URI: %s\n", item.AnnotationURI)
···
498
548
return result, nil
499
549
}
500
550
501
-
func hydrateNotifications(notifications []db.Notification) ([]APINotification, error) {
551
+
func hydrateNotifications(database *db.DB, notifications []db.Notification) ([]APINotification, error) {
502
552
if len(notifications) == 0 {
503
553
return []APINotification{}, nil
504
554
}
···
518
568
519
569
profiles := fetchProfilesForDIDs(dids)
520
570
571
+
replyURIs := make([]string, 0)
572
+
for _, n := range notifications {
573
+
if n.Type == "reply" {
574
+
replyURIs = append(replyURIs, n.SubjectURI)
575
+
}
576
+
}
577
+
578
+
replyMap := make(map[string]APIReply)
579
+
if len(replyURIs) > 0 {
580
+
var replies []db.Reply
581
+
for _, uri := range replyURIs {
582
+
r, err := database.GetReplyByURI(uri)
583
+
if err == nil {
584
+
replies = append(replies, *r)
585
+
}
586
+
}
587
+
588
+
hydratedReplies, _ := hydrateReplies(replies)
589
+
for _, r := range hydratedReplies {
590
+
replyMap[r.ID] = r
591
+
}
592
+
}
593
+
521
594
result := make([]APINotification, len(notifications))
522
595
for i, n := range notifications {
596
+
var subject interface{}
597
+
if n.Type == "reply" {
598
+
if val, ok := replyMap[n.SubjectURI]; ok {
599
+
subject = val
600
+
}
601
+
}
602
+
523
603
result[i] = APINotification{
524
604
ID: n.ID,
525
605
Recipient: profiles[n.RecipientDID],
526
606
Actor: profiles[n.ActorDID],
527
607
Type: n.Type,
528
608
SubjectURI: n.SubjectURI,
609
+
Subject: subject,
529
610
CreatedAt: n.CreatedAt,
530
611
ReadAt: n.ReadAt,
531
612
}
+691
-60
backend/internal/api/og.go
+691
-60
backend/internal/api/og.go
···
15
15
"net/http"
16
16
"net/url"
17
17
"os"
18
-
"regexp"
19
18
"strings"
20
19
21
20
"golang.org/x/image/font"
···
101
100
"Bluesky",
102
101
}
103
102
103
+
var lucideToEmoji = map[string]string{
104
+
"folder": "๐",
105
+
"star": "โญ",
106
+
"heart": "โค๏ธ",
107
+
"bookmark": "๐",
108
+
"lightbulb": "๐ก",
109
+
"zap": "โก",
110
+
"coffee": "โ",
111
+
"music": "๐ต",
112
+
"camera": "๐ท",
113
+
"code": "๐ป",
114
+
"globe": "๐",
115
+
"flag": "๐ฉ",
116
+
"tag": "๐ท๏ธ",
117
+
"box": "๐ฆ",
118
+
"archive": "๐๏ธ",
119
+
"file": "๐",
120
+
"image": "๐ผ๏ธ",
121
+
"video": "๐ฌ",
122
+
"mail": "โ๏ธ",
123
+
"pin": "๐",
124
+
"calendar": "๐
",
125
+
"clock": "๐",
126
+
"search": "๐",
127
+
"settings": "โ๏ธ",
128
+
"user": "๐ค",
129
+
"users": "๐ฅ",
130
+
"home": "๐ ",
131
+
"briefcase": "๐ผ",
132
+
"gift": "๐",
133
+
"award": "๐",
134
+
"target": "๐ฏ",
135
+
"trending": "๐",
136
+
"activity": "๐",
137
+
"cpu": "๐ฒ",
138
+
"database": "๐๏ธ",
139
+
"cloud": "โ๏ธ",
140
+
"sun": "โ๏ธ",
141
+
"moon": "๐",
142
+
"flame": "๐ฅ",
143
+
"leaf": "๐",
144
+
}
145
+
146
+
func iconToEmoji(icon string) string {
147
+
if strings.HasPrefix(icon, "icon:") {
148
+
name := strings.TrimPrefix(icon, "icon:")
149
+
if emoji, ok := lucideToEmoji[name]; ok {
150
+
return emoji
151
+
}
152
+
return "๐"
153
+
}
154
+
return icon
155
+
}
156
+
104
157
func isCrawler(userAgent string) bool {
105
158
ua := strings.ToLower(userAgent)
106
159
for _, bot := range crawlerUserAgents {
···
111
164
return false
112
165
}
113
166
167
+
func (h *OGHandler) resolveHandle(handle string) (string, error) {
168
+
resp, err := http.Get(fmt.Sprintf("https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=%s", url.QueryEscape(handle)))
169
+
if err == nil && resp.StatusCode == http.StatusOK {
170
+
var result struct {
171
+
Did string `json:"did"`
172
+
}
173
+
if err := json.NewDecoder(resp.Body).Decode(&result); err == nil && result.Did != "" {
174
+
return result.Did, nil
175
+
}
176
+
}
177
+
defer resp.Body.Close()
178
+
179
+
return "", fmt.Errorf("failed to resolve handle")
180
+
}
181
+
114
182
func (h *OGHandler) HandleAnnotationPage(w http.ResponseWriter, r *http.Request) {
115
183
path := r.URL.Path
184
+
var did, rkey, collectionType string
116
185
117
-
var annotationMatch = regexp.MustCompile(`^/at/([^/]+)/([^/]+)$`)
118
-
matches := annotationMatch.FindStringSubmatch(path)
186
+
parts := strings.Split(strings.Trim(path, "/"), "/")
187
+
if len(parts) >= 2 {
188
+
firstPart, _ := url.QueryUnescape(parts[0])
189
+
190
+
if firstPart == "at" || firstPart == "annotation" {
191
+
if len(parts) >= 3 {
192
+
did, _ = url.QueryUnescape(parts[1])
193
+
rkey = parts[2]
194
+
}
195
+
} else {
196
+
if len(parts) >= 3 {
197
+
var err error
198
+
did, err = h.resolveHandle(firstPart)
199
+
if err != nil {
200
+
h.serveIndexHTML(w, r)
201
+
return
202
+
}
119
203
120
-
if len(matches) != 3 {
204
+
switch parts[1] {
205
+
case "highlight":
206
+
collectionType = "at.margin.highlight"
207
+
case "bookmark":
208
+
collectionType = "at.margin.bookmark"
209
+
case "annotation":
210
+
collectionType = "at.margin.annotation"
211
+
}
212
+
rkey = parts[2]
213
+
}
214
+
}
215
+
}
216
+
217
+
if did == "" || rkey == "" {
121
218
h.serveIndexHTML(w, r)
122
219
return
123
220
}
124
221
125
-
did, _ := url.QueryUnescape(matches[1])
126
-
rkey := matches[2]
127
-
128
222
if !isCrawler(r.UserAgent()) {
129
223
h.serveIndexHTML(w, r)
130
224
return
131
225
}
132
226
133
-
uri := fmt.Sprintf("at://%s/at.margin.annotation/%s", did, rkey)
134
-
annotation, err := h.db.GetAnnotationByURI(uri)
135
-
if err == nil && annotation != nil {
136
-
h.serveAnnotationOG(w, annotation)
137
-
return
227
+
if collectionType != "" {
228
+
uri := fmt.Sprintf("at://%s/%s/%s", did, collectionType, rkey)
229
+
if h.tryServeType(w, uri, collectionType) {
230
+
return
231
+
}
232
+
} else {
233
+
types := []string{
234
+
"at.margin.annotation",
235
+
"at.margin.bookmark",
236
+
"at.margin.highlight",
237
+
}
238
+
for _, t := range types {
239
+
uri := fmt.Sprintf("at://%s/%s/%s", did, t, rkey)
240
+
if h.tryServeType(w, uri, t) {
241
+
return
242
+
}
243
+
}
244
+
245
+
colURI := fmt.Sprintf("at://%s/at.margin.collection/%s", did, rkey)
246
+
if h.tryServeType(w, colURI, "at.margin.collection") {
247
+
return
248
+
}
138
249
}
139
250
140
-
bookmarkURI := fmt.Sprintf("at://%s/at.margin.bookmark/%s", did, rkey)
141
-
bookmark, err := h.db.GetBookmarkByURI(bookmarkURI)
142
-
if err == nil && bookmark != nil {
143
-
h.serveBookmarkOG(w, bookmark)
251
+
h.serveIndexHTML(w, r)
252
+
}
253
+
254
+
func (h *OGHandler) tryServeType(w http.ResponseWriter, uri, colType string) bool {
255
+
switch colType {
256
+
case "at.margin.annotation":
257
+
if item, err := h.db.GetAnnotationByURI(uri); err == nil && item != nil {
258
+
h.serveAnnotationOG(w, item)
259
+
return true
260
+
}
261
+
case "at.margin.highlight":
262
+
if item, err := h.db.GetHighlightByURI(uri); err == nil && item != nil {
263
+
h.serveHighlightOG(w, item)
264
+
return true
265
+
}
266
+
case "at.margin.bookmark":
267
+
if item, err := h.db.GetBookmarkByURI(uri); err == nil && item != nil {
268
+
h.serveBookmarkOG(w, item)
269
+
return true
270
+
}
271
+
case "at.margin.collection":
272
+
if item, err := h.db.GetCollectionByURI(uri); err == nil && item != nil {
273
+
h.serveCollectionOG(w, item)
274
+
return true
275
+
}
276
+
}
277
+
return false
278
+
}
279
+
280
+
func (h *OGHandler) HandleCollectionPage(w http.ResponseWriter, r *http.Request) {
281
+
path := r.URL.Path
282
+
var did, rkey string
283
+
284
+
if strings.Contains(path, "/collection/") {
285
+
parts := strings.Split(strings.Trim(path, "/"), "/")
286
+
if len(parts) == 3 && parts[1] == "collection" {
287
+
handle, _ := url.QueryUnescape(parts[0])
288
+
rkey = parts[2]
289
+
var err error
290
+
did, err = h.resolveHandle(handle)
291
+
if err != nil {
292
+
h.serveIndexHTML(w, r)
293
+
return
294
+
}
295
+
} else if strings.HasPrefix(path, "/collection/") {
296
+
uriParam := strings.TrimPrefix(path, "/collection/")
297
+
if uriParam != "" {
298
+
uri, err := url.QueryUnescape(uriParam)
299
+
if err == nil {
300
+
parts := strings.Split(uri, "/")
301
+
if len(parts) >= 3 && strings.HasPrefix(uri, "at://") {
302
+
did = parts[2]
303
+
rkey = parts[len(parts)-1]
304
+
}
305
+
}
306
+
}
307
+
}
308
+
}
309
+
310
+
if did == "" && rkey == "" {
311
+
h.serveIndexHTML(w, r)
144
312
return
313
+
} else if did != "" && rkey != "" {
314
+
uri := fmt.Sprintf("at://%s/at.margin.collection/%s", did, rkey)
315
+
316
+
if !isCrawler(r.UserAgent()) {
317
+
h.serveIndexHTML(w, r)
318
+
return
319
+
}
320
+
321
+
collection, err := h.db.GetCollectionByURI(uri)
322
+
if err == nil && collection != nil {
323
+
h.serveCollectionOG(w, collection)
324
+
return
325
+
}
145
326
}
146
327
147
328
h.serveIndexHTML(w, r)
···
232
413
w.Write([]byte(htmlContent))
233
414
}
234
415
416
+
func (h *OGHandler) serveHighlightOG(w http.ResponseWriter, highlight *db.Highlight) {
417
+
title := "Highlight on Margin"
418
+
description := ""
419
+
420
+
if highlight.SelectorJSON != nil && *highlight.SelectorJSON != "" {
421
+
var selector struct {
422
+
Exact string `json:"exact"`
423
+
}
424
+
if err := json.Unmarshal([]byte(*highlight.SelectorJSON), &selector); err == nil && selector.Exact != "" {
425
+
description = fmt.Sprintf("\"%s\"", selector.Exact)
426
+
if len(description) > 200 {
427
+
description = description[:197] + "...\""
428
+
}
429
+
}
430
+
}
431
+
432
+
if highlight.TargetTitle != nil && *highlight.TargetTitle != "" {
433
+
title = fmt.Sprintf("Highlight on: %s", *highlight.TargetTitle)
434
+
if len(title) > 60 {
435
+
title = title[:57] + "..."
436
+
}
437
+
}
438
+
439
+
sourceDomain := ""
440
+
if highlight.TargetSource != "" {
441
+
if parsed, err := url.Parse(highlight.TargetSource); err == nil {
442
+
sourceDomain = parsed.Host
443
+
}
444
+
}
445
+
446
+
authorHandle := highlight.AuthorDID
447
+
profiles := fetchProfilesForDIDs([]string{highlight.AuthorDID})
448
+
if profile, ok := profiles[highlight.AuthorDID]; ok && profile.Handle != "" {
449
+
authorHandle = "@" + profile.Handle
450
+
}
451
+
452
+
if description == "" {
453
+
description = fmt.Sprintf("A highlight by %s", authorHandle)
454
+
if sourceDomain != "" {
455
+
description += fmt.Sprintf(" on %s", sourceDomain)
456
+
}
457
+
}
458
+
459
+
pageURL := fmt.Sprintf("%s/at/%s", h.baseURL, url.PathEscape(highlight.URI[5:]))
460
+
ogImageURL := fmt.Sprintf("%s/og-image?uri=%s", h.baseURL, url.QueryEscape(highlight.URI))
461
+
462
+
htmlContent := fmt.Sprintf(`<!DOCTYPE html>
463
+
<html lang="en">
464
+
<head>
465
+
<meta charset="UTF-8">
466
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
467
+
<title>%s - Margin</title>
468
+
<meta name="description" content="%s">
469
+
470
+
<!-- Open Graph -->
471
+
<meta property="og:type" content="article">
472
+
<meta property="og:title" content="%s">
473
+
<meta property="og:description" content="%s">
474
+
<meta property="og:url" content="%s">
475
+
<meta property="og:image" content="%s">
476
+
<meta property="og:image:width" content="1200">
477
+
<meta property="og:image:height" content="630">
478
+
<meta property="og:site_name" content="Margin">
479
+
480
+
<!-- Twitter Card -->
481
+
<meta name="twitter:card" content="summary_large_image">
482
+
<meta name="twitter:title" content="%s">
483
+
<meta name="twitter:description" content="%s">
484
+
<meta name="twitter:image" content="%s">
485
+
486
+
<!-- Author -->
487
+
<meta property="article:author" content="%s">
488
+
489
+
<meta http-equiv="refresh" content="0; url=%s">
490
+
</head>
491
+
<body>
492
+
<p>Redirecting to <a href="%s">%s</a>...</p>
493
+
</body>
494
+
</html>`,
495
+
html.EscapeString(title),
496
+
html.EscapeString(description),
497
+
html.EscapeString(title),
498
+
html.EscapeString(description),
499
+
html.EscapeString(pageURL),
500
+
html.EscapeString(ogImageURL),
501
+
html.EscapeString(title),
502
+
html.EscapeString(description),
503
+
html.EscapeString(ogImageURL),
504
+
html.EscapeString(authorHandle),
505
+
html.EscapeString(pageURL),
506
+
html.EscapeString(pageURL),
507
+
html.EscapeString(title),
508
+
)
509
+
510
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
511
+
w.Write([]byte(htmlContent))
512
+
}
513
+
514
+
func (h *OGHandler) serveCollectionOG(w http.ResponseWriter, collection *db.Collection) {
515
+
icon := "๐"
516
+
if collection.Icon != nil && *collection.Icon != "" {
517
+
icon = iconToEmoji(*collection.Icon)
518
+
}
519
+
520
+
title := fmt.Sprintf("%s %s", icon, collection.Name)
521
+
description := ""
522
+
if collection.Description != nil && *collection.Description != "" {
523
+
description = *collection.Description
524
+
if len(description) > 200 {
525
+
description = description[:197] + "..."
526
+
}
527
+
}
528
+
529
+
authorHandle := collection.AuthorDID
530
+
var avatarURL string
531
+
profiles := fetchProfilesForDIDs([]string{collection.AuthorDID})
532
+
if profile, ok := profiles[collection.AuthorDID]; ok {
533
+
if profile.Handle != "" {
534
+
authorHandle = "@" + profile.Handle
535
+
}
536
+
if profile.Avatar != "" {
537
+
avatarURL = profile.Avatar
538
+
}
539
+
}
540
+
541
+
if description == "" {
542
+
description = fmt.Sprintf("A collection by %s", authorHandle)
543
+
} else {
544
+
description = fmt.Sprintf("By %s โข %s", authorHandle, description)
545
+
}
546
+
547
+
pageURL := fmt.Sprintf("%s/collection/%s", h.baseURL, url.PathEscape(collection.URI))
548
+
ogImageURL := fmt.Sprintf("%s/og-image?uri=%s", h.baseURL, url.QueryEscape(collection.URI))
549
+
550
+
_ = avatarURL
551
+
552
+
htmlContent := fmt.Sprintf(`<!DOCTYPE html>
553
+
<html lang="en">
554
+
<head>
555
+
<meta charset="UTF-8">
556
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
557
+
<title>%s - Margin</title>
558
+
<meta name="description" content="%s">
559
+
560
+
<!-- Open Graph -->
561
+
<meta property="og:type" content="article">
562
+
<meta property="og:title" content="%s">
563
+
<meta property="og:description" content="%s">
564
+
<meta property="og:url" content="%s">
565
+
<meta property="og:image" content="%s">
566
+
<meta property="og:image:width" content="1200">
567
+
<meta property="og:image:height" content="630">
568
+
<meta property="og:site_name" content="Margin">
569
+
570
+
<!-- Twitter Card -->
571
+
<meta name="twitter:card" content="summary_large_image">
572
+
<meta name="twitter:title" content="%s">
573
+
<meta name="twitter:description" content="%s">
574
+
<meta name="twitter:image" content="%s">
575
+
576
+
<!-- Author -->
577
+
<meta property="article:author" content="%s">
578
+
579
+
<meta http-equiv="refresh" content="0; url=%s">
580
+
</head>
581
+
<body>
582
+
<p>Redirecting to <a href="%s">%s</a>...</p>
583
+
</body>
584
+
</html>`,
585
+
html.EscapeString(title),
586
+
html.EscapeString(description),
587
+
html.EscapeString(title),
588
+
html.EscapeString(description),
589
+
html.EscapeString(pageURL),
590
+
html.EscapeString(ogImageURL),
591
+
html.EscapeString(title),
592
+
html.EscapeString(description),
593
+
html.EscapeString(ogImageURL),
594
+
html.EscapeString(authorHandle),
595
+
html.EscapeString(pageURL),
596
+
html.EscapeString(pageURL),
597
+
html.EscapeString(title),
598
+
)
599
+
600
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
601
+
w.Write([]byte(htmlContent))
602
+
}
603
+
235
604
func (h *OGHandler) serveAnnotationOG(w http.ResponseWriter, annotation *db.Annotation) {
236
605
title := "Annotation on Margin"
237
606
description := ""
···
417
786
}
418
787
}
419
788
} else {
420
-
http.Error(w, "Record not found", http.StatusNotFound)
421
-
return
789
+
highlight, err := h.db.GetHighlightByURI(uri)
790
+
if err == nil && highlight != nil {
791
+
authorHandle = highlight.AuthorDID
792
+
profiles := fetchProfilesForDIDs([]string{highlight.AuthorDID})
793
+
if profile, ok := profiles[highlight.AuthorDID]; ok {
794
+
if profile.Handle != "" {
795
+
authorHandle = "@" + profile.Handle
796
+
}
797
+
if profile.Avatar != "" {
798
+
avatarURL = profile.Avatar
799
+
}
800
+
}
801
+
802
+
targetTitle := ""
803
+
if highlight.TargetTitle != nil {
804
+
targetTitle = *highlight.TargetTitle
805
+
}
806
+
807
+
if highlight.SelectorJSON != nil && *highlight.SelectorJSON != "" {
808
+
var selector struct {
809
+
Exact string `json:"exact"`
810
+
}
811
+
if err := json.Unmarshal([]byte(*highlight.SelectorJSON), &selector); err == nil && selector.Exact != "" {
812
+
quote = selector.Exact
813
+
}
814
+
}
815
+
816
+
if highlight.TargetSource != "" {
817
+
if parsed, err := url.Parse(highlight.TargetSource); err == nil {
818
+
sourceDomain = parsed.Host
819
+
}
820
+
}
821
+
822
+
img := generateHighlightOGImagePNG(authorHandle, targetTitle, quote, sourceDomain, avatarURL)
823
+
824
+
w.Header().Set("Content-Type", "image/png")
825
+
w.Header().Set("Cache-Control", "public, max-age=86400")
826
+
png.Encode(w, img)
827
+
return
828
+
} else {
829
+
collection, err := h.db.GetCollectionByURI(uri)
830
+
if err == nil && collection != nil {
831
+
authorHandle = collection.AuthorDID
832
+
profiles := fetchProfilesForDIDs([]string{collection.AuthorDID})
833
+
if profile, ok := profiles[collection.AuthorDID]; ok {
834
+
if profile.Handle != "" {
835
+
authorHandle = "@" + profile.Handle
836
+
}
837
+
if profile.Avatar != "" {
838
+
avatarURL = profile.Avatar
839
+
}
840
+
}
841
+
842
+
icon := "๐"
843
+
if collection.Icon != nil && *collection.Icon != "" {
844
+
icon = iconToEmoji(*collection.Icon)
845
+
}
846
+
847
+
description := ""
848
+
if collection.Description != nil && *collection.Description != "" {
849
+
description = *collection.Description
850
+
}
851
+
852
+
img := generateCollectionOGImagePNG(authorHandle, collection.Name, description, icon, avatarURL)
853
+
854
+
w.Header().Set("Content-Type", "image/png")
855
+
w.Header().Set("Cache-Control", "public, max-age=86400")
856
+
png.Encode(w, img)
857
+
return
858
+
} else {
859
+
http.Error(w, "Record not found", http.StatusNotFound)
860
+
return
861
+
}
862
+
}
422
863
}
423
864
}
424
865
···
432
873
func generateOGImagePNG(author, text, quote, source, avatarURL string) image.Image {
433
874
width := 1200
434
875
height := 630
435
-
padding := 120
876
+
padding := 100
436
877
437
878
bgPrimary := color.RGBA{12, 10, 20, 255}
438
879
accent := color.RGBA{168, 85, 247, 255}
439
880
textPrimary := color.RGBA{244, 240, 255, 255}
440
881
textSecondary := color.RGBA{168, 158, 200, 255}
441
-
textTertiary := color.RGBA{107, 95, 138, 255}
442
882
border := color.RGBA{45, 38, 64, 255}
443
883
444
884
img := image.NewRGBA(image.Rect(0, 0, width, height))
445
885
446
886
draw.Draw(img, img.Bounds(), &image.Uniform{bgPrimary}, image.Point{}, draw.Src)
447
-
draw.Draw(img, image.Rect(0, 0, width, 6), &image.Uniform{accent}, image.Point{}, draw.Src)
448
-
449
-
if logoImage != nil {
450
-
logoHeight := 50
451
-
logoWidth := int(float64(logoImage.Bounds().Dx()) * (float64(logoHeight) / float64(logoImage.Bounds().Dy())))
452
-
drawScaledImage(img, logoImage, padding, 80, logoWidth, logoHeight)
453
-
} else {
454
-
drawText(img, "Margin", padding, 120, accent, 36, true)
455
-
}
887
+
draw.Draw(img, image.Rect(0, 0, width, 12), &image.Uniform{accent}, image.Point{}, draw.Src)
456
888
457
-
avatarSize := 80
889
+
avatarSize := 64
458
890
avatarX := padding
459
-
avatarY := 180
891
+
avatarY := padding
892
+
460
893
avatarImg := fetchAvatarImage(avatarURL)
461
894
if avatarImg != nil {
462
895
drawCircularAvatar(img, avatarImg, avatarX, avatarY, avatarSize)
463
896
} else {
464
897
drawDefaultAvatar(img, author, avatarX, avatarY, avatarSize, accent)
465
898
}
466
-
467
-
handleX := avatarX + avatarSize + 24
468
-
drawText(img, author, handleX, avatarY+50, textSecondary, 24, false)
469
-
470
-
yPos := 280
471
-
draw.Draw(img, image.Rect(padding, yPos, width-padding, yPos+1), &image.Uniform{border}, image.Point{}, draw.Src)
472
-
yPos += 40
899
+
drawText(img, author, avatarX+avatarSize+24, avatarY+42, textSecondary, 28, false)
473
900
474
901
contentWidth := width - (padding * 2)
902
+
yPos := 220
475
903
476
-
if quote != "" {
477
-
if len(quote) > 100 {
478
-
quote = quote[:97] + "..."
479
-
}
904
+
if text != "" {
905
+
textLen := len(text)
906
+
textSize := 32.0
907
+
textLineHeight := 42
908
+
maxTextLines := 5
480
909
481
-
lines := wrapTextToWidth(quote, contentWidth-30, 24)
482
-
numLines := min(len(lines), 2)
483
-
barHeight := numLines*32 + 10
910
+
if textLen > 200 {
911
+
textSize = 28.0
912
+
textLineHeight = 36
913
+
maxTextLines = 6
914
+
}
484
915
485
-
draw.Draw(img, image.Rect(padding, yPos, padding+6, yPos+barHeight), &image.Uniform{accent}, image.Point{}, draw.Src)
916
+
lines := wrapTextToWidth(text, contentWidth, int(textSize))
917
+
numLines := min(len(lines), maxTextLines)
486
918
487
-
for i, line := range lines {
488
-
if i >= 2 {
489
-
break
919
+
for i := 0; i < numLines; i++ {
920
+
line := lines[i]
921
+
if i == numLines-1 && len(lines) > numLines {
922
+
line += "..."
490
923
}
491
-
drawText(img, "\""+line+"\"", padding+24, yPos+28+(i*32), textTertiary, 24, true)
924
+
drawText(img, line, padding, yPos+(i*textLineHeight), textPrimary, textSize, false)
492
925
}
493
-
yPos += 30 + (numLines * 32) + 30
926
+
yPos += (numLines * textLineHeight) + 40
494
927
}
495
928
496
-
if text != "" {
497
-
if len(text) > 300 {
498
-
text = text[:297] + "..."
929
+
if quote != "" {
930
+
quoteLen := len(quote)
931
+
quoteSize := 24.0
932
+
quoteLineHeight := 32
933
+
maxQuoteLines := 3
934
+
935
+
if quoteLen > 150 {
936
+
quoteSize = 20.0
937
+
quoteLineHeight = 28
938
+
maxQuoteLines = 4
499
939
}
500
-
lines := wrapTextToWidth(text, contentWidth, 32)
501
-
for i, line := range lines {
502
-
if i >= 6 {
503
-
break
940
+
941
+
lines := wrapTextToWidth(quote, contentWidth-30, int(quoteSize))
942
+
numLines := min(len(lines), maxQuoteLines)
943
+
barHeight := numLines * quoteLineHeight
944
+
945
+
draw.Draw(img, image.Rect(padding, yPos, padding+6, yPos+barHeight), &image.Uniform{accent}, image.Point{}, draw.Src)
946
+
947
+
for i := 0; i < numLines; i++ {
948
+
line := lines[i]
949
+
if i == numLines-1 && len(lines) > numLines {
950
+
line += "..."
504
951
}
505
-
drawText(img, line, padding, yPos+(i*42), textPrimary, 32, false)
952
+
drawText(img, line, padding+24, yPos+24+(i*quoteLineHeight), textSecondary, quoteSize, true)
506
953
}
954
+
yPos += barHeight + 40
507
955
}
508
956
509
-
drawText(img, source, padding, 580, textTertiary, 20, false)
957
+
draw.Draw(img, image.Rect(padding, yPos, width-padding, yPos+1), &image.Uniform{border}, image.Point{}, draw.Src)
958
+
yPos += 40
959
+
drawText(img, source, padding, yPos+32, textSecondary, 24, false)
510
960
511
961
return img
512
962
}
···
662
1112
}
663
1113
return lines
664
1114
}
1115
+
1116
+
func generateCollectionOGImagePNG(author, collectionName, description, icon, avatarURL string) image.Image {
1117
+
width := 1200
1118
+
height := 630
1119
+
padding := 120
1120
+
1121
+
bgPrimary := color.RGBA{12, 10, 20, 255}
1122
+
accent := color.RGBA{168, 85, 247, 255}
1123
+
textPrimary := color.RGBA{244, 240, 255, 255}
1124
+
textSecondary := color.RGBA{168, 158, 200, 255}
1125
+
textTertiary := color.RGBA{107, 95, 138, 255}
1126
+
border := color.RGBA{45, 38, 64, 255}
1127
+
1128
+
img := image.NewRGBA(image.Rect(0, 0, width, height))
1129
+
1130
+
draw.Draw(img, img.Bounds(), &image.Uniform{bgPrimary}, image.Point{}, draw.Src)
1131
+
draw.Draw(img, image.Rect(0, 0, width, 12), &image.Uniform{accent}, image.Point{}, draw.Src)
1132
+
1133
+
iconY := 120
1134
+
var iconWidth int
1135
+
if icon != "" {
1136
+
emojiImg := fetchTwemojiImage(icon)
1137
+
if emojiImg != nil {
1138
+
iconSize := 96
1139
+
drawScaledImage(img, emojiImg, padding, iconY, iconSize, iconSize)
1140
+
iconWidth = iconSize + 32
1141
+
} else {
1142
+
drawText(img, icon, padding, iconY+70, textPrimary, 80, true)
1143
+
iconWidth = 100
1144
+
}
1145
+
}
1146
+
1147
+
drawText(img, collectionName, padding+iconWidth, iconY+65, textPrimary, 64, true)
1148
+
1149
+
yPos := 280
1150
+
contentWidth := width - (padding * 2)
1151
+
1152
+
if description != "" {
1153
+
if len(description) > 200 {
1154
+
description = description[:197] + "..."
1155
+
}
1156
+
lines := wrapTextToWidth(description, contentWidth, 32)
1157
+
for i, line := range lines {
1158
+
if i >= 4 {
1159
+
break
1160
+
}
1161
+
drawText(img, line, padding, yPos+(i*42), textSecondary, 32, false)
1162
+
}
1163
+
} else {
1164
+
drawText(img, "A collection on Margin", padding, yPos, textTertiary, 32, false)
1165
+
}
1166
+
1167
+
yPos = 480
1168
+
draw.Draw(img, image.Rect(padding, yPos, width-padding, yPos+1), &image.Uniform{border}, image.Point{}, draw.Src)
1169
+
1170
+
avatarSize := 64
1171
+
avatarX := padding
1172
+
avatarY := yPos + 40
1173
+
1174
+
avatarImg := fetchAvatarImage(avatarURL)
1175
+
if avatarImg != nil {
1176
+
drawCircularAvatar(img, avatarImg, avatarX, avatarY, avatarSize)
1177
+
} else {
1178
+
drawDefaultAvatar(img, author, avatarX, avatarY, avatarSize, accent)
1179
+
}
1180
+
1181
+
handleX := avatarX + avatarSize + 24
1182
+
drawText(img, author, handleX, avatarY+42, textTertiary, 28, false)
1183
+
1184
+
return img
1185
+
}
1186
+
1187
+
func fetchTwemojiImage(emoji string) image.Image {
1188
+
var codes []string
1189
+
for _, r := range emoji {
1190
+
codes = append(codes, fmt.Sprintf("%x", r))
1191
+
}
1192
+
hexCode := strings.Join(codes, "-")
1193
+
1194
+
url := fmt.Sprintf("https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/%s.png", hexCode)
1195
+
1196
+
resp, err := http.Get(url)
1197
+
if err != nil || resp.StatusCode != 200 {
1198
+
if strings.Contains(hexCode, "-fe0f") {
1199
+
simpleHex := strings.ReplaceAll(hexCode, "-fe0f", "")
1200
+
url = fmt.Sprintf("https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/%s.png", simpleHex)
1201
+
resp, err = http.Get(url)
1202
+
if err != nil || resp.StatusCode != 200 {
1203
+
return nil
1204
+
}
1205
+
} else {
1206
+
return nil
1207
+
}
1208
+
}
1209
+
defer resp.Body.Close()
1210
+
1211
+
img, _, err := image.Decode(resp.Body)
1212
+
if err != nil {
1213
+
return nil
1214
+
}
1215
+
return img
1216
+
}
1217
+
1218
+
func generateHighlightOGImagePNG(author, pageTitle, quote, source, avatarURL string) image.Image {
1219
+
width := 1200
1220
+
height := 630
1221
+
padding := 100
1222
+
1223
+
bgPrimary := color.RGBA{12, 10, 20, 255}
1224
+
accent := color.RGBA{250, 204, 21, 255}
1225
+
textPrimary := color.RGBA{244, 240, 255, 255}
1226
+
textSecondary := color.RGBA{168, 158, 200, 255}
1227
+
border := color.RGBA{45, 38, 64, 255}
1228
+
1229
+
img := image.NewRGBA(image.Rect(0, 0, width, height))
1230
+
1231
+
draw.Draw(img, img.Bounds(), &image.Uniform{bgPrimary}, image.Point{}, draw.Src)
1232
+
draw.Draw(img, image.Rect(0, 0, width, 12), &image.Uniform{accent}, image.Point{}, draw.Src)
1233
+
1234
+
avatarSize := 64
1235
+
avatarX := padding
1236
+
avatarY := padding
1237
+
1238
+
avatarImg := fetchAvatarImage(avatarURL)
1239
+
if avatarImg != nil {
1240
+
drawCircularAvatar(img, avatarImg, avatarX, avatarY, avatarSize)
1241
+
} else {
1242
+
drawDefaultAvatar(img, author, avatarX, avatarY, avatarSize, accent)
1243
+
}
1244
+
drawText(img, author, avatarX+avatarSize+24, avatarY+42, textSecondary, 28, false)
1245
+
1246
+
contentWidth := width - (padding * 2)
1247
+
yPos := 220
1248
+
if quote != "" {
1249
+
quoteLen := len(quote)
1250
+
fontSize := 42.0
1251
+
lineHeight := 56
1252
+
maxLines := 4
1253
+
1254
+
if quoteLen > 200 {
1255
+
fontSize = 32.0
1256
+
lineHeight = 44
1257
+
maxLines = 6
1258
+
} else if quoteLen > 100 {
1259
+
fontSize = 36.0
1260
+
lineHeight = 48
1261
+
maxLines = 5
1262
+
}
1263
+
1264
+
lines := wrapTextToWidth(quote, contentWidth-40, int(fontSize))
1265
+
numLines := min(len(lines), maxLines)
1266
+
barHeight := numLines * lineHeight
1267
+
1268
+
draw.Draw(img, image.Rect(padding, yPos, padding+8, yPos+barHeight), &image.Uniform{accent}, image.Point{}, draw.Src)
1269
+
1270
+
for i := 0; i < numLines; i++ {
1271
+
line := lines[i]
1272
+
if i == numLines-1 && len(lines) > numLines {
1273
+
line += "..."
1274
+
}
1275
+
drawText(img, line, padding+40, yPos+42+(i*lineHeight), textPrimary, fontSize, false)
1276
+
}
1277
+
yPos += barHeight + 40
1278
+
}
1279
+
1280
+
draw.Draw(img, image.Rect(padding, yPos, width-padding, yPos+1), &image.Uniform{border}, image.Point{}, draw.Src)
1281
+
yPos += 40
1282
+
1283
+
if pageTitle != "" {
1284
+
if len(pageTitle) > 60 {
1285
+
pageTitle = pageTitle[:57] + "..."
1286
+
}
1287
+
drawText(img, pageTitle, padding, yPos+32, textSecondary, 32, true)
1288
+
}
1289
+
1290
+
if source != "" {
1291
+
drawText(img, source, padding, yPos+80, textSecondary, 24, false)
1292
+
}
1293
+
1294
+
return img
1295
+
}
+143
backend/internal/db/queries.go
+143
backend/internal/db/queries.go
···
104
104
return scanAnnotations(rows)
105
105
}
106
106
107
+
func (db *DB) GetAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
108
+
pattern := "%\"" + tag + "\"%"
109
+
rows, err := db.Query(db.Rebind(`
110
+
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
111
+
FROM annotations
112
+
WHERE tags_json LIKE ?
113
+
ORDER BY created_at DESC
114
+
LIMIT ? OFFSET ?
115
+
`), pattern, limit, offset)
116
+
if err != nil {
117
+
return nil, err
118
+
}
119
+
defer rows.Close()
120
+
121
+
return scanAnnotations(rows)
122
+
}
123
+
107
124
func (db *DB) DeleteAnnotation(uri string) error {
108
125
_, err := db.Exec(db.Rebind(`DELETE FROM annotations WHERE uri = ?`), uri)
109
126
return err
···
242
259
return highlights, nil
243
260
}
244
261
262
+
func (db *DB) GetHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
263
+
pattern := "%\"" + tag + "\"%"
264
+
rows, err := db.Query(db.Rebind(`
265
+
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
266
+
FROM highlights
267
+
WHERE tags_json LIKE ?
268
+
ORDER BY created_at DESC
269
+
LIMIT ? OFFSET ?
270
+
`), pattern, limit, offset)
271
+
if err != nil {
272
+
return nil, err
273
+
}
274
+
defer rows.Close()
275
+
276
+
var highlights []Highlight
277
+
for rows.Next() {
278
+
var h Highlight
279
+
if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
280
+
return nil, err
281
+
}
282
+
highlights = append(highlights, h)
283
+
}
284
+
return highlights, nil
285
+
}
286
+
245
287
func (db *DB) GetRecentBookmarks(limit, offset int) ([]Bookmark, error) {
246
288
rows, err := db.Query(db.Rebind(`
247
289
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
···
265
307
return bookmarks, nil
266
308
}
267
309
310
+
func (db *DB) GetBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
311
+
pattern := "%\"" + tag + "\"%"
312
+
rows, err := db.Query(db.Rebind(`
313
+
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
314
+
FROM bookmarks
315
+
WHERE tags_json LIKE ?
316
+
ORDER BY created_at DESC
317
+
LIMIT ? OFFSET ?
318
+
`), pattern, limit, offset)
319
+
if err != nil {
320
+
return nil, err
321
+
}
322
+
defer rows.Close()
323
+
324
+
var bookmarks []Bookmark
325
+
for rows.Next() {
326
+
var b Bookmark
327
+
if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
328
+
return nil, err
329
+
}
330
+
bookmarks = append(bookmarks, b)
331
+
}
332
+
return bookmarks, nil
333
+
}
334
+
335
+
func (db *DB) GetAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) {
336
+
pattern := "%\"" + tag + "\"%"
337
+
rows, err := db.Query(db.Rebind(`
338
+
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
339
+
FROM annotations
340
+
WHERE author_did = ? AND tags_json LIKE ?
341
+
ORDER BY created_at DESC
342
+
LIMIT ? OFFSET ?
343
+
`), authorDID, pattern, limit, offset)
344
+
if err != nil {
345
+
return nil, err
346
+
}
347
+
defer rows.Close()
348
+
349
+
return scanAnnotations(rows)
350
+
}
351
+
352
+
func (db *DB) GetHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) {
353
+
pattern := "%\"" + tag + "\"%"
354
+
rows, err := db.Query(db.Rebind(`
355
+
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
356
+
FROM highlights
357
+
WHERE author_did = ? AND tags_json LIKE ?
358
+
ORDER BY created_at DESC
359
+
LIMIT ? OFFSET ?
360
+
`), authorDID, pattern, limit, offset)
361
+
if err != nil {
362
+
return nil, err
363
+
}
364
+
defer rows.Close()
365
+
366
+
var highlights []Highlight
367
+
for rows.Next() {
368
+
var h Highlight
369
+
if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
370
+
return nil, err
371
+
}
372
+
highlights = append(highlights, h)
373
+
}
374
+
return highlights, nil
375
+
}
376
+
377
+
func (db *DB) GetBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) {
378
+
pattern := "%\"" + tag + "\"%"
379
+
rows, err := db.Query(db.Rebind(`
380
+
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
381
+
FROM bookmarks
382
+
WHERE author_did = ? AND tags_json LIKE ?
383
+
ORDER BY created_at DESC
384
+
LIMIT ? OFFSET ?
385
+
`), authorDID, pattern, limit, offset)
386
+
if err != nil {
387
+
return nil, err
388
+
}
389
+
defer rows.Close()
390
+
391
+
var bookmarks []Bookmark
392
+
for rows.Next() {
393
+
var b Bookmark
394
+
if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
395
+
return nil, err
396
+
}
397
+
bookmarks = append(bookmarks, b)
398
+
}
399
+
return bookmarks, nil
400
+
}
401
+
268
402
func (db *DB) GetHighlightsByTargetHash(targetHash string, limit, offset int) ([]Highlight, error) {
269
403
rows, err := db.Query(db.Rebind(`
270
404
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
···
500
634
return count, err
501
635
}
502
636
637
+
func (db *DB) GetReplyCount(rootURI string) (int, error) {
638
+
var count int
639
+
err := db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM replies WHERE root_uri = ?`), rootURI).Scan(&count)
640
+
return count, err
641
+
}
642
+
503
643
func (db *DB) GetLikeByUserAndSubject(userDID, subjectURI string) (*Like, error) {
504
644
var like Like
505
645
err := db.QueryRow(db.Rebind(`
···
685
825
}
686
826
687
827
normalized := strings.ToLower(parsed.Host) + parsed.Path
828
+
if parsed.RawQuery != "" {
829
+
normalized += "?" + parsed.RawQuery
830
+
}
688
831
normalized = strings.TrimSuffix(normalized, "/")
689
832
690
833
return hashString(normalized)
+2
-2
backend/internal/oauth/client.go
+2
-2
backend/internal/oauth/client.go
···
205
205
"jti": base64.RawURLEncoding.EncodeToString(jti),
206
206
"htm": method,
207
207
"htu": uri,
208
-
"iat": now.Unix(),
208
+
"iat": now.Add(-30 * time.Second).Unix(),
209
209
"exp": now.Add(5 * time.Minute).Unix(),
210
210
}
211
211
if nonce != "" {
···
243
243
Issuer: c.ClientID,
244
244
Subject: c.ClientID,
245
245
Audience: jwt.Audience{issuer},
246
-
IssuedAt: jwt.NewNumericDate(now),
246
+
IssuedAt: jwt.NewNumericDate(now.Add(-30 * time.Second)),
247
247
Expiry: jwt.NewNumericDate(now.Add(5 * time.Minute)),
248
248
ID: base64.RawURLEncoding.EncodeToString(jti),
249
249
}
+1
backend/internal/oauth/handler.go
+1
backend/internal/oauth/handler.go
···
244
244
245
245
parResp, state, dpopNonce, err := client.SendPAR(meta, req.Handle, scope, dpopKey, pkceChallenge)
246
246
if err != nil {
247
+
log.Printf("PAR request failed: %v", err)
247
248
w.Header().Set("Content-Type", "application/json")
248
249
w.WriteHeader(http.StatusInternalServerError)
249
250
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to initiate authentication"})
+2
-1
backend/internal/xrpc/records.go
+2
-1
backend/internal/xrpc/records.go
···
78
78
CreatedAt string `json:"createdAt"`
79
79
}
80
80
81
-
func NewHighlightRecord(url, urlHash string, selector interface{}, color string) *HighlightRecord {
81
+
func NewHighlightRecord(url, urlHash string, selector interface{}, color string, tags []string) *HighlightRecord {
82
82
return &HighlightRecord{
83
83
Type: CollectionHighlight,
84
84
Target: AnnotationTarget{
···
87
87
Selector: selector,
88
88
},
89
89
Color: color,
90
+
Tags: tags,
90
91
CreatedAt: time.Now().UTC().Format(time.RFC3339),
91
92
}
92
93
}
+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 {
191
+
/* ignore */
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();
+952
-241
extension/content/content.js
+952
-241
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
+
7
+
let activeItems = [];
8
+
let currentSelection = null;
8
9
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);
10
+
const OVERLAY_STYLES = `
11
+
:host { all: initial; }
12
+
.margin-overlay {
13
+
position: absolute;
14
+
top: 0;
15
+
left: 0;
16
+
width: 100%;
17
+
height: 100%;
18
+
pointer-events: none;
19
+
}
20
+
21
+
.margin-popover {
22
+
position: absolute;
23
+
width: 320px;
24
+
background: #09090b;
25
+
border: 1px solid #27272a;
26
+
border-radius: 12px;
27
+
padding: 0;
28
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
29
+
display: flex;
30
+
flex-direction: column;
31
+
pointer-events: auto;
32
+
z-index: 2147483647;
33
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
34
+
color: #e4e4e7;
35
+
opacity: 0;
36
+
transform: scale(0.95);
37
+
animation: popover-in 0.15s forwards;
38
+
max-height: 480px;
39
+
overflow: hidden;
40
+
}
41
+
@keyframes popover-in { to { opacity: 1; transform: scale(1); } }
42
+
.popover-header {
43
+
padding: 12px 16px;
44
+
border-bottom: 1px solid #27272a;
45
+
display: flex;
46
+
justify-content: space-between;
47
+
align-items: center;
48
+
background: #0f0f12;
49
+
border-radius: 12px 12px 0 0;
50
+
font-weight: 600;
51
+
font-size: 13px;
52
+
}
53
+
.popover-scroll-area {
54
+
overflow-y: auto;
55
+
max-height: 400px;
56
+
}
57
+
.popover-item-block {
58
+
border-bottom: 1px solid #27272a;
59
+
margin-bottom: 0;
60
+
animation: fade-in 0.2s;
61
+
}
62
+
.popover-item-block:last-child {
63
+
border-bottom: none;
64
+
}
65
+
.popover-item-header {
66
+
padding: 12px 16px 4px;
67
+
display: flex;
68
+
align-items: center;
69
+
gap: 8px;
70
+
}
71
+
.popover-avatar {
72
+
width: 24px; height: 24px; border-radius: 50%; background: #27272a;
73
+
display: flex; align-items: center; justify-content: center;
74
+
font-size: 10px; color: #a1a1aa;
75
+
}
76
+
.popover-handle { font-size: 12px; font-weight: 600; color: #e4e4e7; }
77
+
.popover-close { background: none; border: none; color: #71717a; cursor: pointer; padding: 4px; }
78
+
.popover-close:hover { color: #e4e4e7; }
79
+
.popover-content { padding: 4px 16px 12px; font-size: 13px; line-height: 1.5; color: #e4e4e7; }
80
+
.popover-quote {
81
+
margin-top: 8px; padding: 6px 10px; background: #18181b;
82
+
border-left: 2px solid #6366f1; border-radius: 4px;
83
+
font-size: 11px; color: #a1a1aa; font-style: italic;
84
+
}
85
+
.popover-actions {
86
+
padding: 8px 16px;
87
+
display: flex; justify-content: flex-end; gap: 8px;
88
+
}
89
+
.btn-action {
90
+
background: none; border: 1px solid #27272a; border-radius: 4px;
91
+
padding: 4px 8px; color: #a1a1aa; font-size: 11px; cursor: pointer;
92
+
}
93
+
.btn-action:hover { background: #27272a; color: #e4e4e7; }
94
+
95
+
.margin-selection-popup {
96
+
position: fixed;
97
+
display: flex;
98
+
gap: 4px;
99
+
padding: 6px;
100
+
background: #09090b;
101
+
border: 1px solid #27272a;
102
+
border-radius: 8px;
103
+
box-shadow: 0 8px 16px rgba(0,0,0,0.4);
104
+
z-index: 2147483647;
105
+
pointer-events: auto;
106
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
107
+
animation: popover-in 0.15s forwards;
108
+
}
109
+
.selection-btn {
110
+
display: flex;
111
+
align-items: center;
112
+
gap: 6px;
113
+
padding: 6px 12px;
114
+
background: transparent;
115
+
border: none;
116
+
border-radius: 6px;
117
+
color: #e4e4e7;
118
+
font-size: 12px;
119
+
font-weight: 500;
120
+
cursor: pointer;
121
+
transition: background 0.15s;
122
+
}
123
+
.selection-btn:hover {
124
+
background: #27272a;
125
+
}
126
+
.selection-btn svg {
127
+
width: 14px;
128
+
height: 14px;
129
+
}
130
+
.inline-compose-modal {
131
+
position: fixed;
132
+
width: 340px;
133
+
max-width: calc(100vw - 40px);
134
+
background: #09090b;
135
+
border: 1px solid #27272a;
136
+
border-radius: 12px;
137
+
padding: 16px;
138
+
box-sizing: border-box;
139
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
140
+
z-index: 2147483647;
141
+
pointer-events: auto;
142
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
143
+
color: #e4e4e7;
144
+
animation: popover-in 0.15s forwards;
145
+
overflow: hidden;
146
+
}
147
+
.inline-compose-modal * {
148
+
box-sizing: border-box;
149
+
}
150
+
.inline-compose-quote {
151
+
padding: 8px 12px;
152
+
background: #18181b;
153
+
border-left: 3px solid #6366f1;
154
+
border-radius: 4px;
155
+
font-size: 12px;
156
+
color: #a1a1aa;
157
+
font-style: italic;
158
+
margin-bottom: 12px;
159
+
max-height: 60px;
160
+
overflow: hidden;
161
+
word-break: break-word;
162
+
}
163
+
.inline-compose-textarea {
164
+
width: 100%;
165
+
min-height: 80px;
166
+
padding: 10px 12px;
167
+
background: #18181b;
168
+
border: 1px solid #27272a;
169
+
border-radius: 8px;
170
+
color: #e4e4e7;
171
+
font-family: inherit;
172
+
font-size: 13px;
173
+
resize: vertical;
174
+
margin-bottom: 12px;
175
+
box-sizing: border-box;
176
+
}
177
+
.inline-compose-textarea:focus {
178
+
outline: none;
179
+
border-color: #6366f1;
180
+
}
181
+
.inline-compose-actions {
182
+
display: flex;
183
+
justify-content: flex-end;
184
+
gap: 8px;
185
+
}
186
+
.btn-cancel {
187
+
padding: 8px 16px;
188
+
background: transparent;
189
+
border: 1px solid #27272a;
190
+
border-radius: 6px;
191
+
color: #a1a1aa;
192
+
font-size: 13px;
193
+
cursor: pointer;
194
+
}
195
+
.btn-cancel:hover {
196
+
background: #27272a;
197
+
color: #e4e4e7;
198
+
}
199
+
.btn-submit {
200
+
padding: 8px 16px;
201
+
background: #6366f1;
202
+
border: none;
203
+
border-radius: 6px;
204
+
color: white;
205
+
font-size: 13px;
206
+
font-weight: 500;
207
+
cursor: pointer;
208
+
}
209
+
.btn-submit:hover {
210
+
background: #4f46e5;
211
+
}
212
+
.btn-submit:disabled {
213
+
opacity: 0.5;
214
+
cursor: not-allowed;
215
+
}
216
+
.reply-section {
217
+
border-top: 1px solid #27272a;
218
+
padding: 12px 16px;
219
+
background: #0f0f12;
220
+
border-radius: 0 0 12px 12px;
221
+
}
222
+
.reply-textarea {
223
+
width: 100%;
224
+
min-height: 60px;
225
+
padding: 8px 10px;
226
+
background: #18181b;
227
+
border: 1px solid #27272a;
228
+
border-radius: 6px;
229
+
color: #e4e4e7;
230
+
font-family: inherit;
231
+
font-size: 12px;
232
+
resize: none;
233
+
margin-bottom: 8px;
234
+
}
235
+
.reply-textarea:focus {
236
+
outline: none;
237
+
border-color: #6366f1;
238
+
}
239
+
.reply-submit {
240
+
padding: 6px 12px;
241
+
background: #6366f1;
242
+
border: none;
243
+
border-radius: 4px;
244
+
color: white;
245
+
font-size: 11px;
246
+
font-weight: 500;
247
+
cursor: pointer;
248
+
float: right;
249
+
}
250
+
.reply-submit:disabled {
251
+
opacity: 0.5;
18
252
}
253
+
.reply-item {
254
+
padding: 8px 0;
255
+
border-top: 1px solid #27272a;
256
+
}
257
+
.reply-item:first-child {
258
+
border-top: none;
259
+
}
260
+
.reply-author {
261
+
font-size: 11px;
262
+
font-weight: 600;
263
+
color: #a1a1aa;
264
+
margin-bottom: 4px;
265
+
}
266
+
.reply-text {
267
+
font-size: 12px;
268
+
color: #e4e4e7;
269
+
line-height: 1.4;
270
+
}
271
+
`;
19
272
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);
273
+
class DOMTextMatcher {
274
+
constructor() {
275
+
this.textNodes = [];
276
+
this.corpus = "";
277
+
this.indices = [];
278
+
this.buildMap();
29
279
}
30
280
31
-
return {
32
-
type: "TextQuoteSelector",
33
-
exact: exact,
34
-
prefix: prefix || undefined,
35
-
suffix: suffix || undefined,
36
-
};
37
-
}
281
+
buildMap() {
282
+
const walker = document.createTreeWalker(
283
+
document.body,
284
+
NodeFilter.SHOW_TEXT,
285
+
{
286
+
acceptNode: (node) => {
287
+
if (!node.parentNode) return NodeFilter.FILTER_REJECT;
288
+
const tag = node.parentNode.tagName;
289
+
if (
290
+
["SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA", "INPUT"].includes(tag)
291
+
)
292
+
return NodeFilter.FILTER_REJECT;
293
+
if (node.textContent.trim().length === 0)
294
+
return NodeFilter.FILTER_SKIP;
38
295
39
-
function findAndScrollToText(selector) {
40
-
if (!selector || !selector.exact) return false;
296
+
if (node.parentNode.offsetParent === null)
297
+
return NodeFilter.FILTER_REJECT;
41
298
42
-
const searchText = selector.exact.trim();
43
-
const normalizedSearch = searchText.replace(/\s+/g, " ");
299
+
return NodeFilter.FILTER_ACCEPT;
300
+
},
301
+
},
302
+
);
303
+
304
+
let currentNode;
305
+
let index = 0;
306
+
while ((currentNode = walker.nextNode())) {
307
+
const text = currentNode.textContent;
308
+
this.textNodes.push(currentNode);
309
+
this.corpus += text;
310
+
this.indices.push({
311
+
start: index,
312
+
node: currentNode,
313
+
length: text.length,
314
+
});
315
+
index += text.length;
316
+
}
317
+
}
44
318
45
-
const treeWalker = document.createTreeWalker(
46
-
document.body,
47
-
NodeFilter.SHOW_TEXT,
48
-
null,
49
-
false,
50
-
);
319
+
findRange(searchText) {
320
+
if (!searchText) return null;
51
321
52
-
let currentNode;
53
-
while ((currentNode = treeWalker.nextNode())) {
54
-
const nodeText = currentNode.textContent;
55
-
const normalizedNode = nodeText.replace(/\s+/g, " ");
322
+
let matchIndex = this.corpus.indexOf(searchText);
56
323
57
-
let index = nodeText.indexOf(searchText);
324
+
if (matchIndex === -1) {
325
+
const normalizedSearch = searchText.replace(/\s+/g, " ").trim();
326
+
matchIndex = this.corpus.indexOf(normalizedSearch);
58
327
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;
328
+
if (matchIndex === -1) {
329
+
const fuzzyMatch = this.fuzzyFindInCorpus(searchText);
330
+
if (fuzzyMatch) {
331
+
const start = this.mapIndexToPoint(fuzzyMatch.start);
332
+
const end = this.mapIndexToPoint(fuzzyMatch.end);
333
+
if (start && end) {
334
+
const range = document.createRange();
335
+
range.setStart(start.node, start.offset);
336
+
range.setEnd(end.node, end.offset);
337
+
return range;
338
+
}
339
+
}
340
+
return null;
64
341
}
65
342
}
66
343
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);
344
+
const start = this.mapIndexToPoint(matchIndex);
345
+
const end = this.mapIndexToPoint(matchIndex + searchText.length);
73
346
74
-
if (typeof CSS !== "undefined" && CSS.highlights) {
75
-
const highlight = new Highlight(range);
76
-
CSS.highlights.set("margin-scroll-highlight", highlight);
347
+
if (start && end) {
348
+
const range = document.createRange();
349
+
range.setStart(start.node, start.offset);
350
+
range.setEnd(end.node, end.offset);
351
+
return range;
352
+
}
353
+
return null;
354
+
}
77
355
78
-
setTimeout(() => {
79
-
CSS.highlights.delete("margin-scroll-highlight");
80
-
}, 3000);
81
-
}
356
+
fuzzyFindInCorpus(searchText) {
357
+
const searchWords = searchText
358
+
.trim()
359
+
.split(/\s+/)
360
+
.filter((w) => w.length > 0);
361
+
if (searchWords.length === 0) return null;
82
362
83
-
const rect = range.getBoundingClientRect();
84
-
window.scrollTo({
85
-
top: window.scrollY + rect.top - window.innerHeight / 3,
86
-
behavior: "smooth",
87
-
});
363
+
const corpusLower = this.corpus.toLowerCase();
88
364
89
-
window.scrollTo({
90
-
top: window.scrollY + rect.top - window.innerHeight / 3,
91
-
behavior: "smooth",
92
-
});
365
+
const firstWord = searchWords[0].toLowerCase();
366
+
let searchStart = 0;
367
+
368
+
while (searchStart < corpusLower.length) {
369
+
const wordStart = corpusLower.indexOf(firstWord, searchStart);
370
+
if (wordStart === -1) break;
371
+
372
+
let corpusPos = wordStart;
373
+
let matched = true;
374
+
let lastMatchEnd = wordStart;
375
+
376
+
for (const word of searchWords) {
377
+
const wordLower = word.toLowerCase();
378
+
while (
379
+
corpusPos < corpusLower.length &&
380
+
/\s/.test(this.corpus[corpusPos])
381
+
) {
382
+
corpusPos++;
383
+
}
384
+
const corpusSlice = corpusLower.slice(
385
+
corpusPos,
386
+
corpusPos + wordLower.length,
387
+
);
388
+
if (corpusSlice !== wordLower) {
389
+
matched = false;
390
+
break;
391
+
}
93
392
94
-
return true;
95
-
} catch (e) {
96
-
console.warn("Could not create range:", e);
393
+
corpusPos += wordLower.length;
394
+
lastMatchEnd = corpusPos;
97
395
}
396
+
397
+
if (matched) {
398
+
return { start: wordStart, end: lastMatchEnd };
399
+
}
400
+
401
+
searchStart = wordStart + 1;
98
402
}
403
+
404
+
return null;
99
405
}
100
406
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
-
});
407
+
mapIndexToPoint(corpusIndex) {
408
+
for (const info of this.indices) {
409
+
if (
410
+
corpusIndex >= info.start &&
411
+
corpusIndex < info.start + info.length
412
+
) {
413
+
return { node: info.node, offset: corpusIndex - info.start };
414
+
}
415
+
}
416
+
if (this.indices.length > 0) {
417
+
const last = this.indices[this.indices.length - 1];
418
+
if (corpusIndex === last.start + last.length) {
419
+
return { node: last.node, offset: last.length };
113
420
}
114
-
return true;
115
421
}
422
+
return null;
116
423
}
424
+
}
117
425
118
-
return false;
426
+
function initOverlay() {
427
+
sidebarHost = document.createElement("div");
428
+
sidebarHost.id = "margin-overlay-host";
429
+
const getScrollHeight = () => {
430
+
const bodyH = document.body?.scrollHeight || 0;
431
+
const docH = document.documentElement?.scrollHeight || 0;
432
+
return Math.max(bodyH, docH);
433
+
};
434
+
435
+
sidebarHost.style.cssText = `
436
+
position: absolute; top: 0; left: 0; width: 100%;
437
+
height: ${getScrollHeight()}px;
438
+
pointer-events: none; z-index: 2147483647;
439
+
`;
440
+
document.body?.appendChild(sidebarHost) ||
441
+
document.documentElement.appendChild(sidebarHost);
442
+
443
+
sidebarShadow = sidebarHost.attachShadow({ mode: "open" });
444
+
const styleEl = document.createElement("style");
445
+
styleEl.textContent = OVERLAY_STYLES;
446
+
sidebarShadow.appendChild(styleEl);
447
+
448
+
const container = document.createElement("div");
449
+
container.className = "margin-overlay";
450
+
container.id = "margin-overlay-container";
451
+
sidebarShadow.appendChild(container);
452
+
453
+
const observer = new ResizeObserver(() => {
454
+
sidebarHost.style.height = `${getScrollHeight()}px`;
455
+
});
456
+
if (document.body) observer.observe(document.body);
457
+
if (document.documentElement) observer.observe(document.documentElement);
458
+
459
+
if (typeof chrome !== "undefined" && chrome.storage) {
460
+
chrome.storage.local.get(["showOverlay"], (result) => {
461
+
if (result.showOverlay === false) {
462
+
sidebarHost.style.display = "none";
463
+
} else {
464
+
fetchAnnotations();
465
+
}
466
+
});
467
+
} else {
468
+
fetchAnnotations();
469
+
}
470
+
471
+
document.addEventListener("mousemove", handleMouseMove);
472
+
document.addEventListener("click", handleDocumentClick, true);
119
473
}
120
474
121
-
function renderPageHighlights(highlights) {
122
-
if (!highlights || !Array.isArray(highlights) || !CSS.highlights) return;
475
+
function showInlineComposeModal() {
476
+
if (!sidebarShadow || !currentSelection) return;
477
+
478
+
const container = sidebarShadow.getElementById("margin-overlay-container");
479
+
if (!container) return;
480
+
481
+
const existingModal = container.querySelector(".inline-compose-modal");
482
+
if (existingModal) existingModal.remove();
483
+
484
+
const modal = document.createElement("div");
485
+
modal.className = "inline-compose-modal";
123
486
124
-
const ranges = [];
487
+
modal.style.left = `${Math.max(20, (window.innerWidth - 340) / 2)}px`;
488
+
modal.style.top = `${Math.min(200, window.innerHeight / 4)}px`;
125
489
126
-
highlights.forEach((item) => {
127
-
const selector = item.target?.selector;
128
-
if (!selector?.exact) return;
490
+
const truncatedQuote =
491
+
currentSelection.text.length > 100
492
+
? currentSelection.text.substring(0, 100) + "..."
493
+
: currentSelection.text;
129
494
130
-
const searchText = selector.exact;
131
-
const treeWalker = document.createTreeWalker(
132
-
document.body,
133
-
NodeFilter.SHOW_TEXT,
134
-
null,
135
-
false,
495
+
modal.innerHTML = `
496
+
<div class="inline-compose-quote">"${truncatedQuote}"</div>
497
+
<textarea class="inline-compose-textarea" placeholder="Add your annotation..." autofocus></textarea>
498
+
<div class="inline-compose-actions">
499
+
<button class="btn-cancel">Cancel</button>
500
+
<button class="btn-submit">Post Annotation</button>
501
+
</div>
502
+
`;
503
+
504
+
const textarea = modal.querySelector("textarea");
505
+
const submitBtn = modal.querySelector(".btn-submit");
506
+
const cancelBtn = modal.querySelector(".btn-cancel");
507
+
508
+
cancelBtn.addEventListener("click", () => {
509
+
modal.remove();
510
+
});
511
+
512
+
submitBtn.addEventListener("click", async () => {
513
+
const text = textarea.value.trim();
514
+
if (!text) return;
515
+
516
+
submitBtn.disabled = true;
517
+
submitBtn.textContent = "Posting...";
518
+
519
+
chrome.runtime.sendMessage(
520
+
{
521
+
type: "CREATE_ANNOTATION",
522
+
data: {
523
+
url: currentSelection.url || window.location.href,
524
+
title: currentSelection.title || document.title,
525
+
text: text,
526
+
selector: currentSelection.selector,
527
+
},
528
+
},
529
+
(res) => {
530
+
if (res && res.success) {
531
+
modal.remove();
532
+
fetchAnnotations();
533
+
} else {
534
+
submitBtn.disabled = false;
535
+
submitBtn.textContent = "Post Annotation";
536
+
alert(
537
+
"Failed to create annotation: " + (res?.error || "Unknown error"),
538
+
);
539
+
}
540
+
},
136
541
);
542
+
});
137
543
138
-
let currentNode;
139
-
while ((currentNode = treeWalker.nextNode())) {
140
-
const nodeText = currentNode.textContent;
141
-
const index = nodeText.indexOf(searchText);
544
+
container.appendChild(modal);
545
+
textarea.focus();
142
546
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);
547
+
const handleEscape = (e) => {
548
+
if (e.key === "Escape") {
549
+
modal.remove();
550
+
document.removeEventListener("keydown", handleEscape);
551
+
}
552
+
};
553
+
document.addEventListener("keydown", handleEscape);
554
+
}
555
+
556
+
let hoverIndicator = null;
557
+
558
+
function handleMouseMove(e) {
559
+
const x = e.clientX;
560
+
const y = e.clientY;
561
+
let foundItems = [];
562
+
let firstRange = null;
563
+
for (const { range, item } of activeItems) {
564
+
const rects = range.getClientRects();
565
+
for (const rect of rects) {
566
+
if (
567
+
x >= rect.left &&
568
+
x <= rect.right &&
569
+
y >= rect.top &&
570
+
y <= rect.bottom
571
+
) {
572
+
if (!firstRange) firstRange = range;
573
+
if (!foundItems.some((f) => f.item === item)) {
574
+
foundItems.push({ range, item, rect });
151
575
}
152
576
break;
153
577
}
154
578
}
155
-
});
579
+
}
156
580
157
-
if (ranges.length > 0) {
158
-
const highlight = new Highlight(...ranges);
159
-
CSS.highlights.set("margin-page-highlights", highlight);
160
-
}
161
-
}
581
+
if (foundItems.length > 0) {
582
+
document.body.style.cursor = "pointer";
162
583
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;
584
+
if (!hoverIndicator && sidebarShadow) {
585
+
const container = sidebarShadow.getElementById(
586
+
"margin-overlay-container",
587
+
);
588
+
if (container) {
589
+
hoverIndicator = document.createElement("div");
590
+
hoverIndicator.className = "margin-hover-indicator";
591
+
hoverIndicator.style.cssText = `
592
+
position: fixed;
593
+
display: flex;
594
+
align-items: center;
595
+
pointer-events: none;
596
+
z-index: 2147483647;
597
+
opacity: 0;
598
+
transition: opacity 0.15s, transform 0.15s;
599
+
transform: scale(0.8);
600
+
`;
601
+
container.appendChild(hoverIndicator);
602
+
}
169
603
}
170
604
171
-
const selector = buildTextQuoteSelector(selection);
172
-
sendResponse({ selector: selector });
173
-
return true;
605
+
if (hoverIndicator) {
606
+
const authorsMap = new Map();
607
+
foundItems.forEach(({ item }) => {
608
+
const author = item.author || item.creator || {};
609
+
const id = author.did || author.handle || "unknown";
610
+
if (!authorsMap.has(id)) {
611
+
authorsMap.set(id, author);
612
+
}
613
+
});
614
+
const uniqueAuthors = Array.from(authorsMap.values());
615
+
616
+
const maxShow = 3;
617
+
const displayAuthors = uniqueAuthors.slice(0, maxShow);
618
+
const overflow = uniqueAuthors.length - maxShow;
619
+
620
+
let html = displayAuthors
621
+
.map((author, i) => {
622
+
const avatar = author.avatar;
623
+
const handle = author.handle || "U";
624
+
const marginLeft = i === 0 ? "0" : "-8px";
625
+
626
+
if (avatar) {
627
+
return `<img src="${avatar}" style="width: 24px; height: 24px; border-radius: 50%; object-fit: cover; border: 2px solid #09090b; margin-left: ${marginLeft};">`;
628
+
} else {
629
+
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>`;
630
+
}
631
+
})
632
+
.join("");
633
+
634
+
if (overflow > 0) {
635
+
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>`;
636
+
}
637
+
638
+
hoverIndicator.innerHTML = html;
639
+
640
+
const firstRect = firstRange.getClientRects()[0];
641
+
const totalWidth =
642
+
Math.min(uniqueAuthors.length, maxShow + (overflow > 0 ? 1 : 0)) *
643
+
18 +
644
+
8;
645
+
const leftPos = firstRect.left - totalWidth;
646
+
const topPos = firstRect.top + firstRect.height / 2 - 12;
647
+
648
+
hoverIndicator.style.left = `${leftPos}px`;
649
+
hoverIndicator.style.top = `${topPos}px`;
650
+
hoverIndicator.style.opacity = "1";
651
+
hoverIndicator.style.transform = "scale(1)";
652
+
}
653
+
} else {
654
+
document.body.style.cursor = "";
655
+
if (hoverIndicator) {
656
+
hoverIndicator.style.opacity = "0";
657
+
hoverIndicator.style.transform = "scale(0.8)";
658
+
}
174
659
}
660
+
}
175
661
176
-
if (request.type === "GET_SELECTOR_FOR_ANNOTATE") {
177
-
const selection = window.getSelection();
178
-
if (!selection || selection.toString().trim().length === 0) {
662
+
function handleDocumentClick(e) {
663
+
const x = e.clientX;
664
+
const y = e.clientY;
665
+
if (popoverEl && sidebarShadow) {
666
+
const rect = popoverEl.getBoundingClientRect();
667
+
if (
668
+
x >= rect.left &&
669
+
x <= rect.right &&
670
+
y >= rect.top &&
671
+
y <= rect.bottom
672
+
) {
179
673
return;
180
674
}
675
+
}
181
676
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
-
});
677
+
let clickedItems = [];
678
+
for (const { range, item } of activeItems) {
679
+
const rects = range.getClientRects();
680
+
for (const rect of rects) {
681
+
if (
682
+
x >= rect.left &&
683
+
x <= rect.right &&
684
+
y >= rect.top &&
685
+
y <= rect.bottom
686
+
) {
687
+
if (!clickedItems.includes(item)) {
688
+
clickedItems.push(item);
689
+
}
690
+
break;
691
+
}
191
692
}
192
693
}
193
694
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;
695
+
if (clickedItems.length > 0) {
696
+
e.preventDefault();
697
+
e.stopPropagation();
698
+
699
+
if (popoverEl) {
700
+
const currentIds = popoverEl.dataset.itemIds;
701
+
const newIds = clickedItems
702
+
.map((i) => i.uri || i.id)
703
+
.sort()
704
+
.join(",");
705
+
706
+
if (currentIds === newIds) {
707
+
popoverEl.remove();
708
+
popoverEl = null;
709
+
return;
710
+
}
711
+
}
712
+
713
+
const firstItem = clickedItems[0];
714
+
const match = activeItems.find((x) => x.item === firstItem);
715
+
if (match) {
716
+
const rects = match.range.getClientRects();
717
+
if (rects.length > 0) {
718
+
const rect = rects[0];
719
+
const top = rect.top + window.scrollY;
720
+
const left = rect.left + window.scrollX;
721
+
showPopover(clickedItems, top, left);
722
+
}
199
723
}
724
+
} else {
725
+
if (popoverEl) {
726
+
popoverEl.remove();
727
+
popoverEl = null;
728
+
}
729
+
}
730
+
}
200
731
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");
732
+
function renderBadges(annotations) {
733
+
if (!sidebarShadow) return;
215
734
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
-
}
735
+
const itemsToRender = annotations || [];
736
+
activeItems = [];
737
+
const rangesByColor = {};
225
738
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;
739
+
const matcher = new DOMTextMatcher();
740
+
741
+
itemsToRender.forEach((item) => {
742
+
const selector = item.target?.selector || item.selector;
743
+
if (!selector?.exact) return;
744
+
745
+
const range = matcher.findRange(selector.exact);
746
+
if (range) {
747
+
activeItems.push({ range, item });
748
+
749
+
const color = item.color || "#6366f1";
750
+
if (!rangesByColor[color]) rangesByColor[color] = [];
751
+
rangesByColor[color].push(range);
241
752
}
242
-
sendResponse({ success: false, error: "Could not build selector" });
243
-
return true;
244
-
}
753
+
});
245
754
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");
755
+
if (typeof CSS !== "undefined" && CSS.highlights) {
756
+
CSS.highlights.clear();
757
+
for (const [color, ranges] of Object.entries(rangesByColor)) {
758
+
const highlight = new Highlight(...ranges);
759
+
const safeColor = color.replace(/[^a-zA-Z0-9]/g, "");
760
+
const name = `margin-hl-${safeColor}`;
761
+
CSS.highlights.set(name, highlight);
762
+
injectHighlightStyle(name, color);
250
763
}
251
764
}
765
+
}
252
766
253
-
if (request.type === "RENDER_HIGHLIGHTS") {
254
-
renderPageHighlights(request.highlights);
767
+
const injectedStyles = new Set();
768
+
function injectHighlightStyle(name, color) {
769
+
if (injectedStyles.has(name)) return;
770
+
const style = document.createElement("style");
771
+
style.textContent = `
772
+
::highlight(${name}) {
773
+
text-decoration: underline;
774
+
text-decoration-color: ${color};
775
+
text-decoration-thickness: 2px;
776
+
text-underline-offset: 2px;
777
+
cursor: pointer;
778
+
}
779
+
`;
780
+
document.head.appendChild(style);
781
+
injectedStyles.add(name);
782
+
}
783
+
784
+
function showPopover(items, top, left) {
785
+
if (popoverEl) popoverEl.remove();
786
+
const container = sidebarShadow.getElementById("margin-overlay-container");
787
+
popoverEl = document.createElement("div");
788
+
popoverEl.className = "margin-popover";
789
+
790
+
const ids = items
791
+
.map((i) => i.uri || i.id)
792
+
.sort()
793
+
.join(",");
794
+
popoverEl.dataset.itemIds = ids;
795
+
796
+
const popWidth = 320;
797
+
const screenWidth = window.innerWidth;
798
+
let finalLeft = left;
799
+
if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20;
800
+
801
+
popoverEl.style.top = `${top + 20}px`;
802
+
popoverEl.style.left = `${finalLeft}px`;
803
+
804
+
const hasHighlights = items.some((item) => item.type === "Highlight");
805
+
const hasAnnotations = items.some((item) => item.type !== "Highlight");
806
+
let title;
807
+
if (items.length > 1) {
808
+
if (hasHighlights && hasAnnotations) {
809
+
title = `${items.length} Items`;
810
+
} else if (hasHighlights) {
811
+
title = `${items.length} Highlights`;
812
+
} else {
813
+
title = `${items.length} Annotations`;
814
+
}
815
+
} else {
816
+
title = items[0]?.type === "Highlight" ? "Highlight" : "Annotation";
255
817
}
256
818
257
-
return true;
258
-
});
819
+
let contentHtml = items
820
+
.map((item) => {
821
+
const author = item.author || item.creator || {};
822
+
const handle = author.handle || "User";
823
+
const avatar = author.avatar;
824
+
const text = item.body?.value || item.text || "";
825
+
const quote =
826
+
item.target?.selector?.exact || item.selector?.exact || "";
827
+
const id = item.id || item.uri;
259
828
260
-
function showNotification(message, type = "info") {
261
-
const existing = document.querySelector(".margin-notification");
262
-
if (existing) existing.remove();
829
+
let avatarHtml = `<div class="popover-avatar">${handle[0]?.toUpperCase() || "U"}</div>`;
830
+
if (avatar) {
831
+
avatarHtml = `<img src="${avatar}" class="popover-avatar" style="object-fit: cover;">`;
832
+
}
263
833
264
-
const notification = document.createElement("div");
265
-
notification.className = "margin-notification";
266
-
notification.textContent = message;
834
+
const isHighlight = item.type === "Highlight";
835
+
836
+
let bodyHtml = "";
837
+
if (isHighlight) {
838
+
bodyHtml = `<div class="popover-text" style="font-style: italic; color: #a1a1aa;">"${quote}"</div>`;
839
+
} else {
840
+
bodyHtml = `<div class="popover-text">${text}</div>`;
841
+
if (quote) {
842
+
bodyHtml += `<div class="popover-quote">"${quote}"</div>`;
843
+
}
844
+
}
267
845
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;
846
+
return `
847
+
<div class="popover-item-block">
848
+
<div class="popover-item-header">
849
+
<div class="popover-author">
850
+
${avatarHtml}
851
+
<span class="popover-handle">@${handle}</span>
852
+
</div>
853
+
</div>
854
+
<div class="popover-content">
855
+
${bodyHtml}
856
+
</div>
857
+
<div class="popover-actions">
858
+
${!isHighlight ? `<button class="btn-action btn-reply" data-id="${id}">Reply</button>` : ""}
859
+
<button class="btn-action btn-share" data-id="${id}" data-text="${text}" data-quote="${quote}">Share</button>
860
+
</div>
861
+
</div>
284
862
`;
863
+
})
864
+
.join("");
285
865
286
-
document.body.appendChild(notification);
866
+
popoverEl.innerHTML = `
867
+
<div class="popover-header">
868
+
<span>${title}</span>
869
+
<button class="popover-close">โ</button>
870
+
</div>
871
+
<div class="popover-scroll-area">
872
+
${contentHtml}
873
+
</div>
874
+
`;
287
875
288
-
setTimeout(() => {
289
-
notification.style.animation = "margin-slide-out 0.2s ease forwards";
290
-
setTimeout(() => notification.remove(), 200);
291
-
}, 3000);
292
-
}
876
+
popoverEl.querySelector(".popover-close").addEventListener("click", (e) => {
877
+
e.stopPropagation();
878
+
popoverEl.remove();
879
+
popoverEl = null;
880
+
});
293
881
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); }
882
+
const replyBtns = popoverEl.querySelectorAll(".btn-reply");
883
+
replyBtns.forEach((btn) => {
884
+
btn.addEventListener("click", (e) => {
885
+
e.stopPropagation();
886
+
const id = btn.getAttribute("data-id");
887
+
if (id) {
888
+
chrome.runtime.sendMessage({
889
+
type: "OPEN_APP_URL",
890
+
data: { path: `/annotation/${encodeURIComponent(id)}` },
891
+
});
299
892
}
300
-
@keyframes margin-slide-out {
301
-
from { opacity: 1; transform: translateY(0); }
302
-
to { opacity: 0; transform: translateY(10px); }
893
+
});
894
+
});
895
+
896
+
const shareBtns = popoverEl.querySelectorAll(".btn-share");
897
+
shareBtns.forEach((btn) => {
898
+
btn.addEventListener("click", async () => {
899
+
const id = btn.getAttribute("data-id");
900
+
const text = btn.getAttribute("data-text");
901
+
const quote = btn.getAttribute("data-quote");
902
+
const u = `https://margin.at/annotation/${encodeURIComponent(id)}`;
903
+
const shareText = `${text ? text + "\n" : ""}${quote ? `"${quote}"\n` : ""}${u}`;
904
+
905
+
try {
906
+
await navigator.clipboard.writeText(shareText);
907
+
const originalText = btn.innerText;
908
+
btn.innerText = "Copied!";
909
+
setTimeout(() => (btn.innerText = originalText), 2000);
910
+
} catch (e) {
911
+
console.error("Failed to copy", e);
303
912
}
304
-
::highlight(margin-highlight-preview) {
305
-
background-color: rgba(168, 85, 247, 0.3);
306
-
color: inherit;
913
+
});
914
+
});
915
+
916
+
container.appendChild(popoverEl);
917
+
918
+
setTimeout(() => {
919
+
document.addEventListener("click", closePopoverOutside);
920
+
}, 0);
921
+
}
922
+
923
+
function closePopoverOutside() {
924
+
if (popoverEl) {
925
+
popoverEl.remove();
926
+
popoverEl = null;
927
+
document.removeEventListener("click", closePopoverOutside);
928
+
}
929
+
}
930
+
931
+
function fetchAnnotations(retryCount = 0) {
932
+
if (typeof chrome !== "undefined" && chrome.runtime) {
933
+
chrome.runtime.sendMessage(
934
+
{
935
+
type: "GET_ANNOTATIONS",
936
+
data: { url: window.location.href },
937
+
},
938
+
(res) => {
939
+
if (res && res.success && res.data && res.data.length > 0) {
940
+
renderBadges(res.data);
941
+
} else if (retryCount < 3) {
942
+
setTimeout(
943
+
() => fetchAnnotations(retryCount + 1),
944
+
1000 * (retryCount + 1),
945
+
);
946
+
}
947
+
},
948
+
);
949
+
}
950
+
}
951
+
952
+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
953
+
if (request.type === "GET_SELECTOR_FOR_ANNOTATE_INLINE") {
954
+
const sel = window.getSelection();
955
+
if (!sel || !sel.toString()) {
956
+
sendResponse({ selector: null });
957
+
return true;
958
+
}
959
+
const exact = sel.toString().trim();
960
+
sendResponse({ selector: { type: "TextQuoteSelector", exact } });
961
+
return true;
962
+
}
963
+
964
+
if (request.type === "SHOW_INLINE_ANNOTATE") {
965
+
currentSelection = {
966
+
text: request.data.selector?.exact || "",
967
+
selector: request.data.selector,
968
+
url: request.data.url,
969
+
title: request.data.title,
970
+
};
971
+
showInlineComposeModal();
972
+
sendResponse({ success: true });
973
+
return true;
974
+
}
975
+
976
+
if (request.type === "UPDATE_OVERLAY_VISIBILITY") {
977
+
if (sidebarHost) {
978
+
sidebarHost.style.display = request.show ? "block" : "none";
979
+
}
980
+
if (request.show) {
981
+
fetchAnnotations();
982
+
} else {
983
+
if (typeof CSS !== "undefined" && CSS.highlights) {
984
+
CSS.highlights.clear();
307
985
}
308
-
::highlight(margin-scroll-highlight) {
309
-
background-color: rgba(99, 102, 241, 0.4);
310
-
color: inherit;
986
+
}
987
+
sendResponse({ success: true });
988
+
return true;
989
+
}
990
+
991
+
if (request.type === "SCROLL_TO_TEXT") {
992
+
const selector = request.selector;
993
+
if (selector?.exact) {
994
+
const matcher = new DOMTextMatcher();
995
+
const range = matcher.findRange(selector.exact);
996
+
if (range) {
997
+
const rect = range.getBoundingClientRect();
998
+
window.scrollTo({
999
+
top: window.scrollY + rect.top - window.innerHeight / 3,
1000
+
behavior: "smooth",
1001
+
});
1002
+
const highlight = new Highlight(range);
1003
+
CSS.highlights.set("margin-scroll-flash", highlight);
1004
+
injectHighlightStyle("margin-scroll-flash", "#8b5cf6");
1005
+
setTimeout(() => CSS.highlights.delete("margin-scroll-flash"), 2000);
311
1006
}
312
-
::highlight(margin-page-highlights) {
313
-
background-color: rgba(252, 211, 77, 0.3);
314
-
color: inherit;
1007
+
}
1008
+
}
1009
+
return true;
1010
+
});
1011
+
1012
+
if (document.readyState === "loading") {
1013
+
document.addEventListener("DOMContentLoaded", initOverlay);
1014
+
} else {
1015
+
initOverlay();
1016
+
}
1017
+
1018
+
window.addEventListener("load", () => {
1019
+
if (typeof chrome !== "undefined" && chrome.storage) {
1020
+
chrome.storage.local.get(["showOverlay"], (result) => {
1021
+
if (result.showOverlay !== false) {
1022
+
setTimeout(() => fetchAnnotations(), 500);
315
1023
}
316
-
`;
317
-
document.head.appendChild(style);
1024
+
});
1025
+
} else {
1026
+
setTimeout(() => fetchAnnotations(), 500);
1027
+
}
1028
+
});
318
1029
})();
+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
+
}
+4
-4
extension/manifest.firefox.json
+4
-4
extension/manifest.firefox.json
···
50
50
"browser_specific_settings": {
51
51
"gecko": {
52
52
"id": "hello@margin.at",
53
-
"strict_min_version": "109.0"
54
-
},
55
-
"gecko_android": {
56
-
"strict_min_version": "113.0"
53
+
"strict_min_version": "140.0",
54
+
"data_collection_permissions": {
55
+
"required": ["none"]
56
+
}
57
57
}
58
58
}
59
59
}
+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
}
+49
-23
web/src/App.jsx
+49
-23
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";
···
14
16
import CollectionDetail from "./pages/CollectionDetail";
15
17
import Privacy from "./pages/Privacy";
16
18
19
+
import Terms from "./pages/Terms";
20
+
21
+
import ScrollToTop from "./components/ScrollToTop";
22
+
17
23
function AppContent() {
18
24
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 path="/collection/*" element={<CollectionDetail />} />
38
-
<Route path="/privacy" element={<Privacy />} />
39
-
</Routes>
40
-
</main>
25
+
<div className="layout">
26
+
<ScrollToTop />
27
+
<Sidebar />
28
+
<div className="main-layout">
29
+
<main className="main-content-wrapper">
30
+
<Routes>
31
+
<Route path="/" element={<Feed />} />
32
+
<Route path="/url" element={<Url />} />
33
+
<Route path="/new" element={<New />} />
34
+
<Route path="/bookmarks" element={<Bookmarks />} />
35
+
<Route path="/highlights" element={<Highlights />} />
36
+
<Route path="/notifications" element={<Notifications />} />
37
+
<Route path="/profile/:handle" element={<Profile />} />
38
+
<Route path="/login" element={<Login />} />
39
+
<Route path="/at/:did/:rkey" element={<AnnotationDetail />} />
40
+
<Route path="/annotation/:uri" element={<AnnotationDetail />} />
41
+
<Route path="/collections" element={<Collections />} />
42
+
<Route path="/collections/:rkey" element={<CollectionDetail />} />
43
+
<Route
44
+
path="/:handle/collection/:rkey"
45
+
element={<CollectionDetail />}
46
+
/>
47
+
<Route
48
+
path="/:handle/annotation/:rkey"
49
+
element={<AnnotationDetail />}
50
+
/>
51
+
<Route
52
+
path="/:handle/highlight/:rkey"
53
+
element={<AnnotationDetail />}
54
+
/>
55
+
<Route
56
+
path="/:handle/bookmark/:rkey"
57
+
element={<AnnotationDetail />}
58
+
/>
59
+
<Route path="/collection/*" element={<CollectionDetail />} />
60
+
<Route path="/privacy" element={<Privacy />} />
61
+
<Route path="/terms" element={<Terms />} />
62
+
</Routes>
63
+
</main>
64
+
</div>
65
+
<RightSidebar />
66
+
<MobileNav />
41
67
</div>
42
68
);
43
69
}
+86
-33
web/src/api/client.js
+86
-33
web/src/api/client.js
···
23
23
return request(`${API_BASE}/url-metadata?url=${encodeURIComponent(url)}`);
24
24
}
25
25
26
-
export async function getAnnotationFeed(limit = 50, offset = 0) {
27
-
return request(
28
-
`${API_BASE}/annotations/feed?limit=${limit}&offset=${offset}`,
29
-
);
26
+
export async function getAnnotationFeed(
27
+
limit = 50,
28
+
offset = 0,
29
+
tag = "",
30
+
creator = "",
31
+
) {
32
+
let url = `${API_BASE}/annotations/feed?limit=${limit}&offset=${offset}`;
33
+
if (tag) url += `&tag=${encodeURIComponent(tag)}`;
34
+
if (creator) url += `&creator=${encodeURIComponent(creator)}`;
35
+
return request(url);
30
36
}
31
37
32
38
export async function getAnnotations({
···
210
216
});
211
217
}
212
218
213
-
export async function createAnnotation({ url, text, quote, title, selector }) {
219
+
export async function createHighlight({ url, title, selector, color, tags }) {
220
+
return request(`${API_BASE}/highlights`, {
221
+
method: "POST",
222
+
body: JSON.stringify({ url, title, selector, color, tags }),
223
+
});
224
+
}
225
+
226
+
export async function createAnnotation({
227
+
url,
228
+
text,
229
+
quote,
230
+
title,
231
+
selector,
232
+
tags,
233
+
}) {
214
234
return request(`${API_BASE}/annotations`, {
215
235
method: "POST",
216
-
body: JSON.stringify({ url, text, quote, title, selector }),
236
+
body: JSON.stringify({ url, text, quote, title, selector, tags }),
217
237
});
218
238
}
219
239
···
283
303
284
304
if (item.type === "Annotation") {
285
305
return {
286
-
uri: item.id,
287
-
author: item.creator,
288
-
url: item.target?.source,
289
-
title: item.target?.title,
290
-
text: item.body?.value,
291
-
selector: item.target?.selector,
306
+
type: item.type,
307
+
uri: item.uri || item.id,
308
+
author: item.author || item.creator,
309
+
url: item.url || item.target?.source,
310
+
title: item.title || item.target?.title,
311
+
text: item.text || item.body?.value,
312
+
selector: item.selector || item.target?.selector,
292
313
motivation: item.motivation,
293
314
tags: item.tags || [],
294
-
createdAt: item.created,
315
+
createdAt: item.createdAt || item.created,
295
316
cid: item.cid || item.CID,
317
+
likeCount: item.likeCount || 0,
318
+
replyCount: item.replyCount || 0,
319
+
viewerHasLiked: item.viewerHasLiked || false,
296
320
};
297
321
}
298
322
299
323
if (item.type === "Bookmark") {
300
324
return {
301
-
uri: item.id,
302
-
author: item.creator,
303
-
url: item.source,
325
+
type: item.type,
326
+
uri: item.uri || item.id,
327
+
author: item.author || item.creator,
328
+
url: item.url || item.source,
304
329
title: item.title,
305
330
description: item.description,
306
331
tags: item.tags || [],
307
-
createdAt: item.created,
332
+
createdAt: item.createdAt || item.created,
308
333
cid: item.cid || item.CID,
334
+
likeCount: item.likeCount || 0,
335
+
replyCount: item.replyCount || 0,
336
+
viewerHasLiked: item.viewerHasLiked || false,
309
337
};
310
338
}
311
339
312
340
if (item.type === "Highlight") {
313
341
return {
314
-
uri: item.id,
315
-
author: item.creator,
316
-
url: item.target?.source,
317
-
title: item.target?.title,
318
-
selector: item.target?.selector,
342
+
type: item.type,
343
+
uri: item.uri || item.id,
344
+
author: item.author || item.creator,
345
+
url: item.url || item.target?.source,
346
+
title: item.title || item.target?.title,
347
+
selector: item.selector || item.target?.selector,
319
348
color: item.color,
320
349
tags: item.tags || [],
321
-
createdAt: item.created,
350
+
createdAt: item.createdAt || item.created,
322
351
cid: item.cid || item.CID,
352
+
likeCount: item.likeCount || 0,
353
+
replyCount: item.replyCount || 0,
354
+
viewerHasLiked: item.viewerHasLiked || false,
323
355
};
324
356
}
325
357
···
335
367
tags: item.tags || [],
336
368
createdAt: item.createdAt || item.created,
337
369
cid: item.cid || item.CID,
370
+
likeCount: item.likeCount || 0,
371
+
replyCount: item.replyCount || 0,
372
+
viewerHasLiked: item.viewerHasLiked || false,
338
373
};
339
374
}
340
375
341
376
export function normalizeHighlight(highlight) {
342
377
return {
343
-
uri: highlight.id,
344
-
author: highlight.creator,
345
-
url: highlight.target?.source,
346
-
title: highlight.target?.title,
347
-
selector: highlight.target?.selector,
378
+
uri: highlight.uri || highlight.id,
379
+
author: highlight.author || highlight.creator,
380
+
url: highlight.url || highlight.target?.source,
381
+
title: highlight.title || highlight.target?.title,
382
+
selector: highlight.selector || highlight.target?.selector,
348
383
color: highlight.color,
349
384
tags: highlight.tags || [],
350
-
createdAt: highlight.created,
385
+
createdAt: highlight.createdAt || highlight.created,
386
+
likeCount: highlight.likeCount || 0,
387
+
replyCount: highlight.replyCount || 0,
388
+
viewerHasLiked: highlight.viewerHasLiked || false,
351
389
};
352
390
}
353
391
354
392
export function normalizeBookmark(bookmark) {
355
393
return {
356
-
uri: bookmark.id,
357
-
author: bookmark.creator,
358
-
url: bookmark.source,
394
+
uri: bookmark.uri || bookmark.id,
395
+
author: bookmark.author || bookmark.creator,
396
+
url: bookmark.url || bookmark.source,
359
397
title: bookmark.title,
360
398
description: bookmark.description,
361
399
tags: bookmark.tags || [],
362
-
createdAt: bookmark.created,
400
+
createdAt: bookmark.createdAt || bookmark.created,
401
+
likeCount: bookmark.likeCount || 0,
402
+
replyCount: bookmark.replyCount || 0,
403
+
viewerHasLiked: bookmark.viewerHasLiked || false,
363
404
};
364
405
}
365
406
···
371
412
return res.json();
372
413
}
373
414
415
+
export async function resolveHandle(handle) {
416
+
const res = await fetch(
417
+
`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`,
418
+
);
419
+
if (!res.ok) throw new Error("Failed to resolve handle");
420
+
const data = await res.json();
421
+
return data.did;
422
+
}
423
+
374
424
export async function startLogin(handle, inviteCode) {
375
425
return request(`${AUTH_BASE}/start`, {
376
426
method: "POST",
377
427
body: JSON.stringify({ handle, invite_code: inviteCode }),
378
428
});
379
429
}
430
+
export async function getTrendingTags(limit = 10) {
431
+
return request(`${API_BASE}/tags/trending?limit=${limit}`);
432
+
}
+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>
+9
-5
web/src/components/AddToCollectionModal.jsx
+9
-5
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,
···
23
23
24
24
useEffect(() => {
25
25
if (isOpen && user) {
26
+
if (!annotationUri) {
27
+
setLoading(false);
28
+
return;
29
+
}
26
30
loadCollections();
27
31
setError(null);
28
32
}
29
-
}, [isOpen, user]);
33
+
}, [isOpen, user, annotationUri, loadCollections]);
30
34
31
-
const loadCollections = async () => {
35
+
const loadCollections = useCallback(async () => {
32
36
try {
33
37
setLoading(true);
34
38
const [data, existingURIs] = await Promise.all([
···
45
49
} finally {
46
50
setLoading(false);
47
51
}
48
-
};
52
+
}, [user?.did, annotationUri]);
49
53
50
54
const handleAdd = async (collectionUri) => {
51
55
if (addedTo.has(collectionUri)) return;
···
71
75
className="modal-container"
72
76
style={{
73
77
maxWidth: "380px",
74
-
maxHeight: "80vh",
78
+
maxHeight: "80dvh",
75
79
display: "flex",
76
80
flexDirection: "column",
77
81
}}
+425
-343
web/src/components/AnnotationCard.jsx
+425
-343
web/src/components/AnnotationCard.jsx
···
5
5
import {
6
6
normalizeAnnotation,
7
7
normalizeHighlight,
8
-
deleteAnnotation,
9
8
likeAnnotation,
10
9
unlikeAnnotation,
11
10
getReplies,
12
11
createReply,
13
12
deleteReply,
14
-
getLikeCount,
15
13
updateAnnotation,
16
14
updateHighlight,
17
-
updateBookmark,
18
15
getEditHistory,
16
+
deleteAnnotation,
19
17
} from "../api/client";
20
18
import {
21
-
HeartIcon,
22
-
MessageIcon,
23
-
TrashIcon,
24
-
ExternalLinkIcon,
25
-
HighlightIcon,
26
-
BookmarkIcon,
27
-
} from "./Icons";
28
-
import { Folder, Edit2, Save, X, Clock } from "lucide-react";
29
-
import AddToCollectionModal from "./AddToCollectionModal";
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) {
···
59
58
}
60
59
};
61
60
62
-
export default function AnnotationCard({ annotation, onDelete }) {
61
+
export default function AnnotationCard({
62
+
annotation,
63
+
onDelete,
64
+
onAddToCollection,
65
+
}) {
63
66
const { user, login } = useAuth();
64
67
const data = normalizeAnnotation(annotation);
65
68
66
-
const [likeCount, setLikeCount] = useState(0);
67
-
const [isLiked, setIsLiked] = useState(false);
69
+
const [likeCount, setLikeCount] = useState(data.likeCount || 0);
70
+
const [isLiked, setIsLiked] = useState(data.viewerHasLiked || false);
68
71
const [deleting, setDeleting] = useState(false);
69
-
const [showAddToCollection, setShowAddToCollection] = useState(false);
70
72
const [isEditing, setIsEditing] = useState(false);
71
73
const [editText, setEditText] = useState(data.text || "");
74
+
const [editTags, setEditTags] = useState(data.tags?.join(", ") || "");
72
75
const [saving, setSaving] = useState(false);
73
76
74
77
const [showHistory, setShowHistory] = useState(false);
···
76
79
const [loadingHistory, setLoadingHistory] = useState(false);
77
80
78
81
const [replies, setReplies] = useState([]);
79
-
const [replyCount, setReplyCount] = useState(0);
82
+
const [replyCount, setReplyCount] = useState(data.replyCount || 0);
80
83
const [showReplies, setShowReplies] = useState(false);
81
84
const [replyingTo, setReplyingTo] = useState(null);
82
85
const [replyText, setReplyText] = useState("");
···
87
90
const [hasEditHistory, setHasEditHistory] = useState(false);
88
91
89
92
useEffect(() => {
90
-
let mounted = true;
91
-
async function fetchData() {
92
-
try {
93
-
const repliesRes = await getReplies(data.uri);
94
-
if (mounted && repliesRes.items) {
95
-
setReplies(repliesRes.items);
96
-
setReplyCount(repliesRes.items.length);
97
-
}
98
-
99
-
const likeRes = await getLikeCount(data.uri);
100
-
if (mounted) {
101
-
if (likeRes.count !== undefined) {
102
-
setLikeCount(likeRes.count);
103
-
}
104
-
if (likeRes.liked !== undefined) {
105
-
setIsLiked(likeRes.liked);
93
+
if (data.uri && !data.color && !data.description) {
94
+
getEditHistory(data.uri)
95
+
.then((history) => {
96
+
if (history && history.length > 0) {
97
+
setHasEditHistory(true);
106
98
}
107
-
}
108
-
109
-
if (!data.color && !data.description) {
110
-
try {
111
-
const history = await getEditHistory(data.uri);
112
-
if (mounted && history && history.length > 0) {
113
-
setHasEditHistory(true);
114
-
}
115
-
} catch {}
116
-
}
117
-
} catch (err) {
118
-
console.error("Failed to fetch data:", err);
119
-
}
120
-
}
121
-
if (data.uri) {
122
-
fetchData();
99
+
})
100
+
.catch(() => {});
123
101
}
124
-
return () => {
125
-
mounted = false;
126
-
};
127
-
}, [data.uri]);
102
+
}, [data.uri, data.color, data.description]);
128
103
129
104
const fetchHistory = async () => {
130
105
if (showHistory) {
···
181
156
const handleSaveEdit = async () => {
182
157
try {
183
158
setSaving(true);
184
-
await updateAnnotation(data.uri, editText, data.tags);
159
+
const tagList = editTags
160
+
.split(",")
161
+
.map((t) => t.trim())
162
+
.filter(Boolean);
163
+
await updateAnnotation(data.uri, editText, tagList);
185
164
setIsEditing(false);
186
165
if (annotation.body) annotation.body.value = editText;
187
166
else if (annotation.text) annotation.text = editText;
167
+
if (annotation.tags) annotation.tags = tagList;
168
+
data.tags = tagList;
188
169
} catch (err) {
189
170
alert("Failed to update: " + err.message);
190
171
} finally {
···
244
225
}
245
226
};
246
227
247
-
const handleShare = async () => {
248
-
const uriParts = data.uri.split("/");
249
-
const did = uriParts[2];
250
-
const rkey = uriParts[uriParts.length - 1];
251
-
const shareUrl = `${window.location.origin}/at/${did}/${rkey}`;
252
-
253
-
if (navigator.share) {
254
-
try {
255
-
await navigator.share({
256
-
title: "Margin Annotation",
257
-
text: data.text?.substring(0, 100),
258
-
url: shareUrl,
259
-
});
260
-
} catch (err) {}
261
-
} else {
262
-
try {
263
-
await navigator.clipboard.writeText(shareUrl);
264
-
alert("Link copied!");
265
-
} catch {
266
-
prompt("Copy this link:", shareUrl);
267
-
}
268
-
}
269
-
};
270
-
271
228
const handleDelete = async () => {
272
229
if (!confirm("Delete this annotation? This cannot be undone.")) return;
273
230
try {
···
287
244
return (
288
245
<article className="card annotation-card">
289
246
<header className="annotation-header">
290
-
<Link to={marginProfileUrl || "#"} className="annotation-avatar-link">
291
-
<div className="annotation-avatar">
292
-
{authorAvatar ? (
293
-
<img src={authorAvatar} alt={authorDisplayName} />
294
-
) : (
295
-
<span>
296
-
{(authorDisplayName || authorHandle || "??")
297
-
?.substring(0, 2)
298
-
.toUpperCase()}
299
-
</span>
300
-
)}
301
-
</div>
302
-
</Link>
303
-
<div className="annotation-meta">
304
-
<div className="annotation-author-row">
305
-
<Link
306
-
to={marginProfileUrl || "#"}
307
-
className="annotation-author-link"
308
-
>
309
-
<span className="annotation-author">{authorDisplayName}</span>
310
-
</Link>
311
-
{authorHandle && (
312
-
<a
313
-
href={`https://bsky.app/profile/${authorHandle}`}
314
-
target="_blank"
315
-
rel="noopener noreferrer"
316
-
className="annotation-handle"
247
+
<div className="annotation-header-left">
248
+
<Link to={marginProfileUrl || "#"} className="annotation-avatar-link">
249
+
<div className="annotation-avatar">
250
+
{authorAvatar ? (
251
+
<img src={authorAvatar} alt={authorDisplayName} />
252
+
) : (
253
+
<span>
254
+
{(authorDisplayName || authorHandle || "??")
255
+
?.substring(0, 2)
256
+
.toUpperCase()}
257
+
</span>
258
+
)}
259
+
</div>
260
+
</Link>
261
+
<div className="annotation-meta">
262
+
<div className="annotation-author-row">
263
+
<Link
264
+
to={marginProfileUrl || "#"}
265
+
className="annotation-author-link"
317
266
>
318
-
@{authorHandle} <ExternalLinkIcon size={12} />
319
-
</a>
320
-
)}
321
-
</div>
322
-
<div className="annotation-time">{formatDate(data.createdAt)}</div>
323
-
</div>
324
-
<div className="action-buttons">
325
-
{}
326
-
{hasEditHistory && !data.color && !data.description && (
327
-
<button
328
-
className="annotation-edit-btn"
329
-
onClick={fetchHistory}
330
-
title="View Edit History"
331
-
>
332
-
<Clock size={16} />
333
-
</button>
334
-
)}
335
-
{}
336
-
{isOwner && (
337
-
<>
338
-
{!data.color && !data.description && (
339
-
<button
340
-
className="annotation-edit-btn"
341
-
onClick={() => setIsEditing(!isEditing)}
342
-
title="Edit"
267
+
<span className="annotation-author">{authorDisplayName}</span>
268
+
</Link>
269
+
{authorHandle && (
270
+
<a
271
+
href={`https://bsky.app/profile/${authorHandle}`}
272
+
target="_blank"
273
+
rel="noopener noreferrer"
274
+
className="annotation-handle"
343
275
>
344
-
<Edit2 size={16} />
345
-
</button>
276
+
@{authorHandle}
277
+
</a>
346
278
)}
279
+
</div>
280
+
<div className="annotation-time">{formatDate(data.createdAt)}</div>
281
+
</div>
282
+
</div>
283
+
<div className="annotation-header-right">
284
+
<div style={{ display: "flex", gap: "4px" }}>
285
+
{hasEditHistory && !data.color && !data.description && (
347
286
<button
348
-
className="annotation-delete"
349
-
onClick={handleDelete}
350
-
disabled={deleting}
351
-
title="Delete"
287
+
className="annotation-action action-icon-only"
288
+
onClick={fetchHistory}
289
+
title="View Edit History"
352
290
>
353
-
<TrashIcon size={16} />
291
+
<Clock size={16} />
354
292
</button>
355
-
</>
356
-
)}
293
+
)}
294
+
295
+
{isOwner && (
296
+
<>
297
+
{!data.color && !data.description && (
298
+
<button
299
+
className="annotation-action action-icon-only"
300
+
onClick={() => setIsEditing(!isEditing)}
301
+
title="Edit"
302
+
>
303
+
<Edit2 size={16} />
304
+
</button>
305
+
)}
306
+
<button
307
+
className="annotation-action action-icon-only"
308
+
onClick={handleDelete}
309
+
disabled={deleting}
310
+
title="Delete"
311
+
>
312
+
<Trash2 size={16} />
313
+
</button>
314
+
</>
315
+
)}
316
+
</div>
357
317
</div>
358
318
</header>
359
319
360
-
{}
361
-
{}
362
320
{showHistory && (
363
321
<div className="history-panel">
364
322
<div className="history-header">
···
390
348
</div>
391
349
)}
392
350
393
-
<a
394
-
href={data.url}
395
-
target="_blank"
396
-
rel="noopener noreferrer"
397
-
className="annotation-source"
398
-
>
399
-
{truncateUrl(data.url)}
400
-
{data.title && (
401
-
<span className="annotation-source-title"> โข {data.title}</span>
402
-
)}
403
-
</a>
404
-
405
-
{highlightedText && (
351
+
<div className="annotation-content">
406
352
<a
407
-
href={fragmentUrl}
353
+
href={data.url}
408
354
target="_blank"
409
355
rel="noopener noreferrer"
410
-
className="annotation-highlight"
356
+
className="annotation-source"
411
357
>
412
-
<mark>"{highlightedText}"</mark>
358
+
{truncateUrl(data.url)}
359
+
{data.title && (
360
+
<span className="annotation-source-title"> โข {data.title}</span>
361
+
)}
413
362
</a>
414
-
)}
415
363
416
-
{isEditing ? (
417
-
<div className="mt-3">
418
-
<textarea
419
-
value={editText}
420
-
onChange={(e) => setEditText(e.target.value)}
421
-
className="reply-input"
422
-
rows={3}
423
-
style={{ marginBottom: "8px" }}
424
-
/>
425
-
<div className="action-buttons-end">
426
-
<button
427
-
onClick={() => setIsEditing(false)}
428
-
className="btn btn-ghost"
429
-
>
430
-
Cancel
431
-
</button>
432
-
<button
433
-
onClick={handleSaveEdit}
434
-
disabled={saving}
435
-
className="btn btn-primary btn-sm"
436
-
>
437
-
{saving ? (
438
-
"Saving..."
439
-
) : (
440
-
<>
441
-
<Save size={14} /> Save
442
-
</>
443
-
)}
444
-
</button>
364
+
{highlightedText && (
365
+
<a
366
+
href={fragmentUrl}
367
+
target="_blank"
368
+
rel="noopener noreferrer"
369
+
className="annotation-highlight"
370
+
style={{
371
+
borderLeftColor: data.color || "var(--accent)",
372
+
}}
373
+
>
374
+
<mark>"{highlightedText}"</mark>
375
+
</a>
376
+
)}
377
+
378
+
{isEditing ? (
379
+
<div className="mt-3">
380
+
<textarea
381
+
value={editText}
382
+
onChange={(e) => setEditText(e.target.value)}
383
+
className="reply-input"
384
+
rows={3}
385
+
style={{ marginBottom: "8px" }}
386
+
/>
387
+
<input
388
+
type="text"
389
+
className="reply-input"
390
+
placeholder="Tags (comma separated)..."
391
+
value={editTags}
392
+
onChange={(e) => setEditTags(e.target.value)}
393
+
style={{ marginBottom: "8px" }}
394
+
/>
395
+
<div className="action-buttons-end">
396
+
<button
397
+
onClick={() => setIsEditing(false)}
398
+
className="btn btn-ghost"
399
+
>
400
+
Cancel
401
+
</button>
402
+
<button
403
+
onClick={handleSaveEdit}
404
+
disabled={saving}
405
+
className="btn btn-primary btn-sm"
406
+
>
407
+
{saving ? (
408
+
"Saving..."
409
+
) : (
410
+
<>
411
+
<Save size={14} /> Save
412
+
</>
413
+
)}
414
+
</button>
415
+
</div>
445
416
</div>
446
-
</div>
447
-
) : (
448
-
data.text && <p className="annotation-text">{data.text}</p>
449
-
)}
417
+
) : (
418
+
data.text && <p className="annotation-text">{data.text}</p>
419
+
)}
450
420
451
-
{data.tags?.length > 0 && (
452
-
<div className="annotation-tags">
453
-
{data.tags.map((tag, i) => (
454
-
<span key={i} className="annotation-tag">
455
-
#{tag}
456
-
</span>
457
-
))}
458
-
</div>
459
-
)}
421
+
{data.tags?.length > 0 && (
422
+
<div className="annotation-tags">
423
+
{data.tags.map((tag, i) => (
424
+
<Link
425
+
key={i}
426
+
to={`/?tag=${encodeURIComponent(tag)}`}
427
+
className="annotation-tag"
428
+
>
429
+
#{tag}
430
+
</Link>
431
+
))}
432
+
</div>
433
+
)}
434
+
</div>
460
435
461
436
<footer className="annotation-actions">
462
-
<button
463
-
className={`annotation-action ${isLiked ? "liked" : ""}`}
464
-
onClick={handleLike}
465
-
>
466
-
<HeartIcon filled={isLiked} size={16} />
467
-
{likeCount > 0 && <span>{likeCount}</span>}
468
-
</button>
469
-
<button
470
-
className={`annotation-action ${showReplies ? "active" : ""}`}
471
-
onClick={() => setShowReplies(!showReplies)}
472
-
>
473
-
<MessageIcon size={16} />
474
-
<span>{replyCount > 0 ? `${replyCount}` : "Reply"}</span>
475
-
</button>
476
-
<ShareMenu uri={data.uri} text={data.text} />
477
-
<button
478
-
className="annotation-action"
479
-
onClick={() => {
480
-
if (!user) {
481
-
login();
482
-
return;
483
-
}
484
-
setShowAddToCollection(true);
485
-
}}
486
-
>
487
-
<Folder size={16} />
488
-
<span>Collect</span>
489
-
</button>
437
+
<div className="annotation-actions-left">
438
+
<button
439
+
className={`annotation-action ${isLiked ? "liked" : ""}`}
440
+
onClick={handleLike}
441
+
>
442
+
<Heart filled={isLiked} size={16} />
443
+
{likeCount > 0 && <span>{likeCount}</span>}
444
+
</button>
445
+
<button
446
+
className={`annotation-action ${showReplies ? "active" : ""}`}
447
+
onClick={async () => {
448
+
if (!showReplies && replies.length === 0) {
449
+
try {
450
+
const res = await getReplies(data.uri);
451
+
if (res.items) setReplies(res.items);
452
+
} catch (err) {
453
+
console.error("Failed to load replies:", err);
454
+
}
455
+
}
456
+
setShowReplies(!showReplies);
457
+
}}
458
+
>
459
+
<MessageSquare size={16} />
460
+
<span>{replyCount > 0 ? `${replyCount}` : "Reply"}</span>
461
+
</button>
462
+
<ShareMenu
463
+
uri={data.uri}
464
+
text={data.title || data.url}
465
+
handle={data.author?.handle}
466
+
type="Annotation"
467
+
/>
468
+
<button
469
+
className="annotation-action"
470
+
onClick={() => {
471
+
if (!user) {
472
+
login();
473
+
return;
474
+
}
475
+
if (onAddToCollection) onAddToCollection();
476
+
}}
477
+
>
478
+
<Folder size={16} />
479
+
<span>Collect</span>
480
+
</button>
481
+
</div>
490
482
</footer>
491
483
492
484
{showReplies && (
···
554
546
onChange={(e) => setReplyText(e.target.value)}
555
547
onFocus={(e) => {
556
548
if (!user) {
557
-
e.target.blur();
558
-
login();
549
+
e.preventDefault();
550
+
alert("Please sign in to like annotations");
559
551
}
560
552
}}
561
553
rows={2}
···
578
570
</div>
579
571
</div>
580
572
)}
581
-
582
-
<AddToCollectionModal
583
-
isOpen={showAddToCollection}
584
-
onClose={() => setShowAddToCollection(false)}
585
-
annotationUri={data.uri}
586
-
/>
587
573
</article>
588
574
);
589
575
}
590
576
591
-
export function HighlightCard({ highlight, onDelete }) {
577
+
export function HighlightCard({
578
+
highlight,
579
+
onDelete,
580
+
onAddToCollection,
581
+
onUpdate,
582
+
}) {
592
583
const { user, login } = useAuth();
593
584
const data = normalizeHighlight(highlight);
594
585
const highlightedText =
595
586
data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null;
596
587
const fragmentUrl = buildTextFragmentUrl(data.url, data.selector);
597
588
const isOwner = user?.did && data.author?.did === user.did;
598
-
const [showAddToCollection, setShowAddToCollection] = useState(false);
599
589
const [isEditing, setIsEditing] = useState(false);
600
590
const [editColor, setEditColor] = useState(data.color || "#f59e0b");
591
+
const [editTags, setEditTags] = useState(data.tags?.join(", ") || "");
601
592
602
593
const handleSaveEdit = async () => {
603
594
try {
604
-
await updateHighlight(data.uri, editColor, []);
605
-
setIsEditing(false);
595
+
const tagList = editTags
596
+
.split(",")
597
+
.map((t) => t.trim())
598
+
.filter(Boolean);
606
599
607
-
if (highlight.color) highlight.color = editColor;
600
+
await updateHighlight(data.uri, editColor, tagList);
601
+
setIsEditing(false);
602
+
if (typeof onUpdate === "function")
603
+
onUpdate({ ...highlight, color: editColor, tags: tagList });
608
604
} catch (err) {
609
605
alert("Failed to update: " + err.message);
610
606
}
···
633
629
return (
634
630
<article className="card annotation-card">
635
631
<header className="annotation-header">
636
-
<Link
637
-
to={data.author?.did ? `/profile/${data.author.did}` : "#"}
638
-
className="annotation-avatar-link"
639
-
>
640
-
<div className="annotation-avatar">
641
-
{data.author?.avatar ? (
642
-
<img src={data.author.avatar} alt="avatar" />
643
-
) : (
644
-
<span>??</span>
632
+
<div className="annotation-header-left">
633
+
<Link
634
+
to={data.author?.did ? `/profile/${data.author.did}` : "#"}
635
+
className="annotation-avatar-link"
636
+
>
637
+
<div className="annotation-avatar">
638
+
{data.author?.avatar ? (
639
+
<img src={data.author.avatar} alt="avatar" />
640
+
) : (
641
+
<span>??</span>
642
+
)}
643
+
</div>
644
+
</Link>
645
+
<div className="annotation-meta">
646
+
<Link to="#" className="annotation-author-link">
647
+
<span className="annotation-author">
648
+
{data.author?.displayName || "Unknown"}
649
+
</span>
650
+
</Link>
651
+
<div className="annotation-time">{formatDate(data.createdAt)}</div>
652
+
{data.author?.handle && (
653
+
<a
654
+
href={`https://bsky.app/profile/${data.author.handle}`}
655
+
target="_blank"
656
+
rel="noopener noreferrer"
657
+
className="annotation-handle"
658
+
>
659
+
@{data.author.handle}
660
+
</a>
645
661
)}
646
662
</div>
647
-
</Link>
648
-
<div className="annotation-meta">
649
-
<Link to="#" className="annotation-author-link">
650
-
<span className="annotation-author">
651
-
{data.author?.displayName || "Unknown"}
652
-
</span>
653
-
</Link>
654
-
<div className="annotation-time">{formatDate(data.createdAt)}</div>
655
663
</div>
656
-
<div className="action-buttons">
657
-
{isOwner && (
658
-
<>
659
-
<button
660
-
className="annotation-edit-btn"
661
-
onClick={() => setIsEditing(!isEditing)}
662
-
title="Edit Color"
663
-
>
664
-
<Edit2 size={16} />
665
-
</button>
666
-
<button
667
-
className="annotation-delete"
668
-
onClick={(e) => {
669
-
e.preventDefault();
670
-
onDelete && onDelete(highlight.id || highlight.uri);
671
-
}}
672
-
>
673
-
<TrashIcon size={16} />
674
-
</button>
675
-
</>
676
-
)}
664
+
665
+
<div className="annotation-header-right">
666
+
<div style={{ display: "flex", gap: "4px" }}>
667
+
{isOwner && (
668
+
<>
669
+
<button
670
+
className="annotation-action action-icon-only"
671
+
onClick={() => setIsEditing(!isEditing)}
672
+
title="Edit Color"
673
+
>
674
+
<Edit2 size={16} />
675
+
</button>
676
+
<button
677
+
className="annotation-action action-icon-only"
678
+
onClick={(e) => {
679
+
e.preventDefault();
680
+
onDelete && onDelete(highlight.id || highlight.uri);
681
+
}}
682
+
>
683
+
<TrashIcon size={16} />
684
+
</button>
685
+
</>
686
+
)}
687
+
</div>
677
688
</div>
678
689
</header>
679
690
680
-
<a
681
-
href={data.url}
682
-
target="_blank"
683
-
rel="noopener noreferrer"
684
-
className="annotation-source"
685
-
>
686
-
{truncateUrl(data.url)}
687
-
</a>
688
-
689
-
{highlightedText && (
691
+
<div className="annotation-content">
690
692
<a
691
-
href={fragmentUrl}
693
+
href={data.url}
692
694
target="_blank"
693
695
rel="noopener noreferrer"
694
-
className="annotation-highlight"
695
-
style={{
696
-
borderLeftColor: isEditing ? editColor : data.color || "#f59e0b",
697
-
}}
696
+
className="annotation-source"
698
697
>
699
-
<mark>"{highlightedText}"</mark>
698
+
{truncateUrl(data.url)}
700
699
</a>
701
-
)}
702
700
703
-
{isEditing && (
704
-
<div
705
-
className="mt-3"
706
-
style={{ display: "flex", alignItems: "center", gap: "8px" }}
707
-
>
708
-
<span style={{ fontSize: "0.9rem" }}>Color:</span>
709
-
<input
710
-
type="color"
711
-
value={editColor}
712
-
onChange={(e) => setEditColor(e.target.value)}
701
+
{highlightedText && (
702
+
<a
703
+
href={fragmentUrl}
704
+
target="_blank"
705
+
rel="noopener noreferrer"
706
+
className="annotation-highlight"
713
707
style={{
714
-
height: "32px",
715
-
width: "64px",
716
-
padding: 0,
717
-
border: "none",
718
-
borderRadius: "var(--radius-sm)",
719
-
overflow: "hidden",
708
+
borderLeftColor: isEditing ? editColor : data.color || "#f59e0b",
720
709
}}
710
+
>
711
+
<mark>"{highlightedText}"</mark>
712
+
</a>
713
+
)}
714
+
715
+
{isEditing && (
716
+
<div
717
+
className="mt-3"
718
+
style={{
719
+
display: "flex",
720
+
gap: "8px",
721
+
alignItems: "center",
722
+
padding: "8px",
723
+
background: "var(--bg-secondary)",
724
+
borderRadius: "var(--radius-md)",
725
+
border: "1px solid var(--border)",
726
+
}}
727
+
>
728
+
<div
729
+
className="color-picker-compact"
730
+
style={{
731
+
position: "relative",
732
+
width: "28px",
733
+
height: "28px",
734
+
flexShrink: 0,
735
+
}}
736
+
>
737
+
<div
738
+
style={{
739
+
backgroundColor: editColor,
740
+
width: "100%",
741
+
height: "100%",
742
+
borderRadius: "50%",
743
+
border: "2px solid var(--bg-card)",
744
+
boxShadow: "0 0 0 1px var(--border)",
745
+
}}
746
+
/>
747
+
<input
748
+
type="color"
749
+
value={editColor}
750
+
onChange={(e) => setEditColor(e.target.value)}
751
+
style={{
752
+
position: "absolute",
753
+
top: 0,
754
+
left: 0,
755
+
width: "100%",
756
+
height: "100%",
757
+
opacity: 0,
758
+
cursor: "pointer",
759
+
}}
760
+
title="Change Color"
761
+
/>
762
+
</div>
763
+
764
+
<input
765
+
type="text"
766
+
className="reply-input"
767
+
placeholder="e.g. tag1, tag2"
768
+
value={editTags}
769
+
onChange={(e) => setEditTags(e.target.value)}
770
+
style={{
771
+
margin: 0,
772
+
flex: 1,
773
+
fontSize: "0.9rem",
774
+
padding: "6px 10px",
775
+
height: "32px",
776
+
border: "none",
777
+
background: "transparent",
778
+
}}
779
+
/>
780
+
781
+
<button
782
+
onClick={handleSaveEdit}
783
+
className="btn btn-primary btn-sm"
784
+
style={{ padding: "0 10px", height: "32px", minWidth: "auto" }}
785
+
title="Save"
786
+
>
787
+
<Save size={16} />
788
+
</button>
789
+
</div>
790
+
)}
791
+
792
+
{data.tags?.length > 0 && (
793
+
<div className="annotation-tags">
794
+
{data.tags.map((tag, i) => (
795
+
<Link
796
+
key={i}
797
+
to={`/?tag=${encodeURIComponent(tag)}`}
798
+
className="annotation-tag"
799
+
>
800
+
#{tag}
801
+
</Link>
802
+
))}
803
+
</div>
804
+
)}
805
+
</div>
806
+
807
+
<footer className="annotation-actions">
808
+
<div className="annotation-actions-left">
809
+
<span
810
+
className="annotation-action"
811
+
style={{
812
+
color: data.color || "#f59e0b",
813
+
background: "none",
814
+
paddingLeft: 0,
815
+
}}
816
+
>
817
+
<HighlightIcon size={14} /> Highlight
818
+
</span>
819
+
<ShareMenu
820
+
uri={data.uri}
821
+
text={data.title || data.description}
822
+
handle={data.author?.handle}
823
+
type="Highlight"
721
824
/>
722
825
<button
723
-
onClick={handleSaveEdit}
724
-
className="btn btn-primary btn-sm"
725
-
style={{ marginLeft: "auto" }}
826
+
className="annotation-action"
827
+
onClick={() => {
828
+
if (!user) {
829
+
login();
830
+
return;
831
+
}
832
+
if (onAddToCollection) onAddToCollection();
833
+
}}
726
834
>
727
-
Save
835
+
<Folder size={16} />
836
+
<span>Collect</span>
728
837
</button>
729
838
</div>
730
-
)}
731
-
732
-
<footer className="annotation-actions">
733
-
<span
734
-
className="annotation-action annotation-type-badge"
735
-
style={{ color: data.color || "#f59e0b" }}
736
-
>
737
-
<HighlightIcon size={14} /> Highlight
738
-
</span>
739
-
<button
740
-
className="annotation-action"
741
-
onClick={() => {
742
-
if (!user) {
743
-
login();
744
-
return;
745
-
}
746
-
setShowAddToCollection(true);
747
-
}}
748
-
>
749
-
<Folder size={16} />
750
-
<span>Collect</span>
751
-
</button>
752
839
</footer>
753
-
<AddToCollectionModal
754
-
isOpen={showAddToCollection}
755
-
onClose={() => setShowAddToCollection(false)}
756
-
annotationUri={data.uri}
757
-
/>
758
840
</article>
759
841
);
760
842
}
+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
+
}
+124
-133
web/src/components/BookmarkCard.jsx
+124
-133
web/src/components/BookmarkCard.jsx
···
3
3
import { Link } from "react-router-dom";
4
4
import {
5
5
normalizeAnnotation,
6
+
normalizeBookmark,
6
7
likeAnnotation,
7
8
unlikeAnnotation,
8
9
getLikeCount,
9
10
deleteBookmark,
10
11
} from "../api/client";
11
-
import { HeartIcon, TrashIcon, ExternalLinkIcon, BookmarkIcon } from "./Icons";
12
+
import { HeartIcon, TrashIcon, BookmarkIcon } from "./Icons";
12
13
import { Folder } from "lucide-react";
13
-
import AddToCollectionModal from "./AddToCollectionModal";
14
14
import ShareMenu from "./ShareMenu";
15
15
16
-
export default function BookmarkCard({ bookmark, annotation, onDelete }) {
16
+
export default function BookmarkCard({
17
+
bookmark,
18
+
onAddToCollection,
19
+
onDelete,
20
+
}) {
17
21
const { user, login } = useAuth();
18
-
const data = normalizeAnnotation(bookmark || annotation);
22
+
const raw = bookmark;
23
+
const data =
24
+
raw.type === "Bookmark" ? normalizeBookmark(raw) : normalizeAnnotation(raw);
19
25
20
26
const [likeCount, setLikeCount] = useState(0);
21
27
const [isLiked, setIsLiked] = useState(false);
22
28
const [deleting, setDeleting] = useState(false);
23
-
const [showAddToCollection, setShowAddToCollection] = useState(false);
24
29
25
30
const isOwner = user?.did && data.author?.did === user.did;
26
31
···
33
38
if (likeRes.count !== undefined) setLikeCount(likeRes.count);
34
39
if (likeRes.liked !== undefined) setIsLiked(likeRes.liked);
35
40
}
36
-
} catch (err) {
37
-
console.error("Failed to fetch data:", err);
41
+
} catch {
42
+
/* ignore */
38
43
}
39
44
}
40
45
if (data.uri) fetchData();
···
59
64
const cid = data.cid || "";
60
65
if (data.uri && cid) await likeAnnotation(data.uri, cid);
61
66
}
62
-
} catch (err) {
67
+
} catch {
63
68
setIsLiked(!isLiked);
64
69
setLikeCount((prev) => (isLiked ? prev + 1 : prev - 1));
65
70
}
66
71
};
67
72
68
73
const handleDelete = async () => {
74
+
if (onDelete) {
75
+
onDelete(data.uri);
76
+
return;
77
+
}
78
+
69
79
if (!confirm("Delete this bookmark?")) return;
70
80
try {
71
81
setDeleting(true);
72
82
const parts = data.uri.split("/");
73
83
const rkey = parts[parts.length - 1];
74
84
await deleteBookmark(rkey);
75
-
if (onDelete) onDelete(data.uri);
76
-
else window.location.reload();
85
+
window.location.reload();
77
86
} catch (err) {
78
87
alert("Failed to delete: " + err.message);
79
88
} finally {
···
81
90
}
82
91
};
83
92
84
-
const handleShare = async () => {
85
-
const uriParts = data.uri.split("/");
86
-
const did = uriParts[2];
87
-
const rkey = uriParts[uriParts.length - 1];
88
-
const shareUrl = `${window.location.origin}/at/${did}/${rkey}`;
89
-
if (navigator.share) {
90
-
try {
91
-
await navigator.share({ title: "Bookmark", url: shareUrl });
92
-
} catch {}
93
-
} else {
94
-
try {
95
-
await navigator.clipboard.writeText(shareUrl);
96
-
alert("Link copied!");
97
-
} catch {
98
-
prompt("Copy:", shareUrl);
99
-
}
100
-
}
101
-
};
102
-
103
93
const formatDate = (dateString) => {
104
94
if (!dateString) return "";
105
95
const date = new Date(dateString);
···
118
108
let domain = "";
119
109
try {
120
110
if (data.url) domain = new URL(data.url).hostname.replace("www.", "");
121
-
} catch {}
111
+
} catch {
112
+
/* ignore */
113
+
}
122
114
123
115
const authorDisplayName = data.author?.displayName || data.author?.handle;
124
116
const authorHandle = data.author?.handle;
···
127
119
const marginProfileUrl = authorDid ? `/profile/${authorDid}` : null;
128
120
129
121
return (
130
-
<article className="card bookmark-card">
131
-
{}
122
+
<article className="card annotation-card bookmark-card">
132
123
<header className="annotation-header">
133
-
<Link to={marginProfileUrl || "#"} className="annotation-avatar-link">
134
-
<div className="annotation-avatar">
135
-
{authorAvatar ? (
136
-
<img src={authorAvatar} alt={authorDisplayName} />
137
-
) : (
138
-
<span>
139
-
{(authorDisplayName || authorHandle || "??")
140
-
?.substring(0, 2)
141
-
.toUpperCase()}
142
-
</span>
143
-
)}
124
+
<div className="annotation-header-left">
125
+
<Link to={marginProfileUrl || "#"} className="annotation-avatar-link">
126
+
<div className="annotation-avatar">
127
+
{authorAvatar ? (
128
+
<img src={authorAvatar} alt={authorDisplayName} />
129
+
) : (
130
+
<span>
131
+
{(authorDisplayName || authorHandle || "??")
132
+
?.substring(0, 2)
133
+
.toUpperCase()}
134
+
</span>
135
+
)}
136
+
</div>
137
+
</Link>
138
+
<div className="annotation-meta">
139
+
<div className="annotation-author-row">
140
+
<Link
141
+
to={marginProfileUrl || "#"}
142
+
className="annotation-author-link"
143
+
>
144
+
<span className="annotation-author">{authorDisplayName}</span>
145
+
</Link>
146
+
{authorHandle && (
147
+
<a
148
+
href={`https://bsky.app/profile/${authorHandle}`}
149
+
target="_blank"
150
+
rel="noopener noreferrer"
151
+
className="annotation-handle"
152
+
>
153
+
@{authorHandle}
154
+
</a>
155
+
)}
156
+
</div>
157
+
<div className="annotation-time">{formatDate(data.createdAt)}</div>
144
158
</div>
145
-
</Link>
146
-
<div className="annotation-meta">
147
-
<div className="annotation-author-row">
148
-
<Link
149
-
to={marginProfileUrl || "#"}
150
-
className="annotation-author-link"
151
-
>
152
-
<span className="annotation-author">{authorDisplayName}</span>
153
-
</Link>
154
-
{authorHandle && (
155
-
<a
156
-
href={`https://bsky.app/profile/${authorHandle}`}
157
-
target="_blank"
158
-
rel="noopener noreferrer"
159
-
className="annotation-handle"
159
+
</div>
160
+
161
+
<div className="annotation-header-right">
162
+
<div style={{ display: "flex", gap: "4px" }}>
163
+
{(isOwner || onDelete) && (
164
+
<button
165
+
className="annotation-action action-icon-only"
166
+
onClick={handleDelete}
167
+
disabled={deleting}
168
+
title="Delete"
160
169
>
161
-
@{authorHandle} <ExternalLinkIcon size={12} />
162
-
</a>
170
+
<TrashIcon size={16} />
171
+
</button>
163
172
)}
164
173
</div>
165
-
<div className="annotation-time">{formatDate(data.createdAt)}</div>
166
-
</div>
167
-
<div className="action-buttons">
168
-
{isOwner && (
169
-
<button
170
-
className="annotation-delete"
171
-
onClick={handleDelete}
172
-
disabled={deleting}
173
-
title="Delete"
174
-
>
175
-
<TrashIcon size={16} />
176
-
</button>
177
-
)}
178
174
</div>
179
175
</header>
180
176
181
-
{}
182
-
<a
183
-
href={data.url}
184
-
target="_blank"
185
-
rel="noopener noreferrer"
186
-
className="bookmark-preview"
187
-
>
188
-
<div className="bookmark-preview-content">
189
-
<div className="bookmark-preview-site">
190
-
<BookmarkIcon size={14} />
191
-
<span>{domain}</span>
177
+
<div className="annotation-content">
178
+
<a
179
+
href={data.url}
180
+
target="_blank"
181
+
rel="noopener noreferrer"
182
+
className="bookmark-preview"
183
+
>
184
+
<div className="bookmark-preview-content">
185
+
<div className="bookmark-preview-site">
186
+
<BookmarkIcon size={14} />
187
+
<span>{domain}</span>
188
+
</div>
189
+
<h3 className="bookmark-preview-title">{data.title || data.url}</h3>
190
+
{data.description && (
191
+
<p className="bookmark-preview-desc">{data.description}</p>
192
+
)}
192
193
</div>
193
-
<h3 className="bookmark-preview-title">{data.title || data.url}</h3>
194
-
{data.description && (
195
-
<p className="bookmark-preview-desc">{data.description}</p>
196
-
)}
197
-
</div>
198
-
<div className="bookmark-preview-arrow">
199
-
<ExternalLinkIcon size={18} />
200
-
</div>
201
-
</a>
194
+
</a>
202
195
203
-
{}
204
-
{data.tags?.length > 0 && (
205
-
<div className="annotation-tags">
206
-
{data.tags.map((tag, i) => (
207
-
<span key={i} className="annotation-tag">
208
-
#{tag}
209
-
</span>
210
-
))}
211
-
</div>
212
-
)}
196
+
{data.tags?.length > 0 && (
197
+
<div className="annotation-tags">
198
+
{data.tags.map((tag, i) => (
199
+
<span key={i} className="annotation-tag">
200
+
#{tag}
201
+
</span>
202
+
))}
203
+
</div>
204
+
)}
205
+
</div>
213
206
214
-
{}
215
207
<footer className="annotation-actions">
216
-
<button
217
-
className={`annotation-action ${isLiked ? "liked" : ""}`}
218
-
onClick={handleLike}
219
-
>
220
-
<HeartIcon filled={isLiked} size={16} />
221
-
{likeCount > 0 && <span>{likeCount}</span>}
222
-
</button>
223
-
<ShareMenu uri={data.uri} text={data.title || data.description} />
224
-
<button
225
-
className="annotation-action"
226
-
onClick={() => {
227
-
if (!user) {
228
-
login();
229
-
return;
230
-
}
231
-
setShowAddToCollection(true);
232
-
}}
233
-
>
234
-
<Folder size={16} />
235
-
<span>Collect</span>
236
-
</button>
208
+
<div className="annotation-actions-left">
209
+
<button
210
+
className={`annotation-action ${isLiked ? "liked" : ""}`}
211
+
onClick={handleLike}
212
+
>
213
+
<HeartIcon filled={isLiked} size={16} />
214
+
{likeCount > 0 && <span>{likeCount}</span>}
215
+
</button>
216
+
<ShareMenu
217
+
uri={data.uri}
218
+
text={data.title || data.description}
219
+
handle={data.author?.handle}
220
+
type="Bookmark"
221
+
/>
222
+
<button
223
+
className="annotation-action"
224
+
onClick={() => {
225
+
if (!user) {
226
+
login();
227
+
return;
228
+
}
229
+
if (onAddToCollection) onAddToCollection();
230
+
}}
231
+
>
232
+
<Folder size={16} />
233
+
<span>Collect</span>
234
+
</button>
235
+
</div>
237
236
</footer>
238
-
239
-
{showAddToCollection && (
240
-
<AddToCollectionModal
241
-
isOpen={showAddToCollection}
242
-
annotationUri={data.uri}
243
-
onClose={() => setShowAddToCollection(false)}
244
-
/>
245
-
)}
246
237
</article>
247
238
);
248
239
}
+4
-3
web/src/components/CollectionItemCard.jsx
+4
-3
web/src/components/CollectionItemCard.jsx
···
1
-
import React from "react";
2
1
import { Link } from "react-router-dom";
3
2
import AnnotationCard, { HighlightCard } from "./AnnotationCard";
4
3
import BookmarkCard from "./BookmarkCard";
···
54
53
</span>{" "}
55
54
added to{" "}
56
55
<Link
57
-
to={`/collection/${encodeURIComponent(collection.uri)}?author=${encodeURIComponent(author.did)}`}
56
+
to={`/${author.handle}/collection/${collection.uri.split("/").pop()}`}
58
57
style={{
59
58
display: "inline-flex",
60
59
alignItems: "center",
···
70
69
</span>
71
70
<div style={{ marginLeft: "auto" }}>
72
71
<ShareMenu
73
-
customUrl={`${window.location.origin}/collection/${encodeURIComponent(collection.uri)}?author=${encodeURIComponent(author.did)}`}
72
+
uri={collection.uri}
73
+
handle={author.handle}
74
+
type="Collection"
74
75
text={`Check out this collection by ${author.displayName}: ${collection.name}`}
75
76
/>
76
77
</div>
-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
+5
-3
web/src/components/CollectionRow.jsx
+5
-3
web/src/components/CollectionRow.jsx
···
6
6
return (
7
7
<div className="collection-row">
8
8
<Link
9
-
to={`/collection/${encodeURIComponent(collection.uri)}?author=${encodeURIComponent(
10
-
collection.authorDid || collection.author?.did,
11
-
)}`}
9
+
to={
10
+
collection.creator?.handle
11
+
? `/${collection.creator.handle}/collection/${collection.uri.split("/").pop()}`
12
+
: `/collection/${encodeURIComponent(collection.uri)}`
13
+
}
12
14
className="collection-row-content"
13
15
>
14
16
<div className="collection-row-icon">
+38
-10
web/src/components/Composer.jsx
+38
-10
web/src/components/Composer.jsx
···
1
1
import { useState } from "react";
2
-
import { createAnnotation } from "../api/client";
2
+
import { createAnnotation, createHighlight } from "../api/client";
3
3
4
4
export default function Composer({
5
5
url,
···
9
9
}) {
10
10
const [text, setText] = useState("");
11
11
const [quoteText, setQuoteText] = useState("");
12
+
const [tags, setTags] = useState("");
12
13
const [selector, setSelector] = useState(initialSelector);
13
14
const [loading, setLoading] = useState(false);
14
15
const [error, setError] = useState(null);
···
19
20
20
21
const handleSubmit = async (e) => {
21
22
e.preventDefault();
22
-
if (!text.trim()) return;
23
+
if (!text.trim() && !highlightedText && !quoteText.trim()) return;
23
24
24
25
try {
25
26
setLoading(true);
···
33
34
};
34
35
}
35
36
36
-
await createAnnotation({
37
-
url,
38
-
text,
39
-
selector: finalSelector || undefined,
40
-
});
37
+
const tagList = tags
38
+
.split(",")
39
+
.map((t) => t.trim())
40
+
.filter(Boolean);
41
+
42
+
if (!text.trim()) {
43
+
await createHighlight({
44
+
url,
45
+
selector: finalSelector,
46
+
color: "yellow",
47
+
tags: tagList,
48
+
});
49
+
} else {
50
+
await createAnnotation({
51
+
url,
52
+
text,
53
+
selector: finalSelector || undefined,
54
+
tags: tagList,
55
+
});
56
+
}
41
57
42
58
setText("");
43
59
setQuoteText("");
···
75
91
ร
76
92
</button>
77
93
<blockquote>
78
-
<mark className="quote-exact">"{highlightedText}"</mark>
94
+
<mark className="quote-exact">"{highlightedText}"</mark>
79
95
</blockquote>
80
96
</div>
81
97
)}
···
123
139
className="composer-input"
124
140
rows={4}
125
141
maxLength={3000}
126
-
required
127
142
disabled={loading}
128
143
/>
129
144
145
+
<div className="composer-tags">
146
+
<input
147
+
type="text"
148
+
value={tags}
149
+
onChange={(e) => setTags(e.target.value)}
150
+
placeholder="Add tags (comma separated)..."
151
+
className="composer-tags-input"
152
+
disabled={loading}
153
+
/>
154
+
</div>
155
+
130
156
<div className="composer-footer">
131
157
<span className="composer-count">{text.length}/3000</span>
132
158
<div className="composer-actions">
···
143
169
<button
144
170
type="submit"
145
171
className="btn btn-primary"
146
-
disabled={loading || !text.trim()}
172
+
disabled={
173
+
loading || (!text.trim() && !highlightedText && !quoteText)
174
+
}
147
175
>
148
176
{loading ? "Posting..." : "Post"}
149
177
</button>
-1
web/src/components/ReplyList.jsx
-1
web/src/components/ReplyList.jsx
+156
web/src/components/RightSidebar.jsx
+156
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 { SiFirefox, SiGooglechrome, SiGithub, SiBluesky } from "react-icons/si";
5
+
import { FaEdge } from "react-icons/fa";
6
+
import { useAuth } from "../context/AuthContext";
7
+
import { getTrendingTags } from "../api/client";
8
+
9
+
const isFirefox =
10
+
typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent);
11
+
const isEdge =
12
+
typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent);
13
+
14
+
function getExtensionInfo() {
15
+
if (isFirefox) {
16
+
return {
17
+
url: "https://addons.mozilla.org/en-US/firefox/addon/margin/",
18
+
icon: SiFirefox,
19
+
name: "Firefox",
20
+
};
21
+
}
22
+
if (isEdge) {
23
+
return {
24
+
url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn",
25
+
icon: FaEdge,
26
+
name: "Edge",
27
+
};
28
+
}
29
+
return {
30
+
url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/",
31
+
icon: SiGooglechrome,
32
+
name: "Chrome",
33
+
};
34
+
}
35
+
36
+
export default function RightSidebar() {
37
+
const { isAuthenticated } = useAuth();
38
+
const ext = getExtensionInfo();
39
+
const ExtIcon = ext.icon;
40
+
const [trendingTags, setTrendingTags] = useState([]);
41
+
const [loading, setLoading] = useState(true);
42
+
43
+
useEffect(() => {
44
+
getTrendingTags()
45
+
.then((tags) => setTrendingTags(tags))
46
+
.catch((err) => console.error("Failed to fetch trending tags:", err))
47
+
.finally(() => setLoading(false));
48
+
}, []);
49
+
50
+
return (
51
+
<aside className="right-sidebar">
52
+
<div className="right-section">
53
+
<h3 className="right-section-title">Get the Extension</h3>
54
+
<p className="right-section-desc">
55
+
Annotate, highlight, and bookmark any webpage
56
+
</p>
57
+
<a
58
+
href={ext.url}
59
+
target="_blank"
60
+
rel="noopener noreferrer"
61
+
className="right-extension-btn"
62
+
>
63
+
<ExtIcon size={18} />
64
+
Install for {ext.name}
65
+
<ExternalLink size={14} />
66
+
</a>
67
+
</div>
68
+
69
+
{isAuthenticated ? (
70
+
<div className="right-section">
71
+
<h3 className="right-section-title">Trending Tags</h3>
72
+
<div className="right-links">
73
+
{loading ? (
74
+
<span className="right-section-desc">Loading...</span>
75
+
) : trendingTags.length > 0 ? (
76
+
trendingTags.map(({ tag, count }) => (
77
+
<Link
78
+
key={tag}
79
+
to={`/?tag=${encodeURIComponent(tag)}`}
80
+
className="right-link"
81
+
>
82
+
<span>#{tag}</span>
83
+
<span style={{ fontSize: "0.75rem", opacity: 0.6 }}>
84
+
{count}
85
+
</span>
86
+
</Link>
87
+
))
88
+
) : (
89
+
<span className="right-section-desc">No trending tags yet</span>
90
+
)}
91
+
</div>
92
+
</div>
93
+
) : (
94
+
<div className="right-section">
95
+
<h3 className="right-section-title">Explore</h3>
96
+
<nav className="right-links">
97
+
<Link to="/url" className="right-link">
98
+
Browse by URL
99
+
</Link>
100
+
<Link to="/highlights" className="right-link">
101
+
Public Highlights
102
+
</Link>
103
+
</nav>
104
+
</div>
105
+
)}
106
+
107
+
<div className="right-section">
108
+
<h3 className="right-section-title">Resources</h3>
109
+
<nav className="right-links">
110
+
<a
111
+
href="https://github.com/margin-at/margin"
112
+
target="_blank"
113
+
rel="noopener noreferrer"
114
+
className="right-link"
115
+
>
116
+
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
117
+
<SiGithub size={16} />
118
+
GitHub
119
+
</div>
120
+
<ExternalLink size={12} />
121
+
</a>
122
+
<a
123
+
href="https://tangled.org/margin.at/margin"
124
+
target="_blank"
125
+
rel="noopener noreferrer"
126
+
className="right-link"
127
+
>
128
+
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
129
+
<div className="tangled-icon" />
130
+
Tangled
131
+
</div>
132
+
<ExternalLink size={12} />
133
+
</a>
134
+
<a
135
+
href="https://bsky.app/profile/margin.at"
136
+
target="_blank"
137
+
rel="noopener noreferrer"
138
+
className="right-link"
139
+
>
140
+
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
141
+
<SiBluesky size={16} />
142
+
Bluesky
143
+
</div>
144
+
<ExternalLink size={12} />
145
+
</a>
146
+
</nav>
147
+
</div>
148
+
149
+
<div className="right-footer">
150
+
<Link to="/privacy">Privacy</Link>
151
+
<span>ยท</span>
152
+
<Link to="/terms">Terms</Link>
153
+
</div>
154
+
</aside>
155
+
);
156
+
}
+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) {
+473
web/src/css/annotations.css
+473
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
+
}
186
+
187
+
.annotation-text {
188
+
font-size: 0.95rem;
189
+
line-height: 1.6;
190
+
color: var(--text-primary);
191
+
white-space: pre-wrap;
192
+
}
193
+
194
+
.annotation-tags {
195
+
display: flex;
196
+
flex-wrap: wrap;
197
+
gap: 6px;
198
+
margin-top: 4px;
199
+
}
200
+
201
+
.annotation-tag {
202
+
font-size: 0.8rem;
203
+
color: var(--accent);
204
+
text-decoration: none;
205
+
font-weight: 500;
206
+
opacity: 0.9;
207
+
transition: opacity 0.15s;
208
+
}
209
+
210
+
.annotation-tag:hover {
211
+
opacity: 1;
212
+
text-decoration: underline;
213
+
}
214
+
215
+
.annotation-actions {
216
+
display: flex;
217
+
align-items: center;
218
+
justify-content: space-between;
219
+
margin-top: 4px;
220
+
padding-left: 46px;
221
+
}
222
+
223
+
.annotation-actions-left {
224
+
display: flex;
225
+
align-items: center;
226
+
gap: 16px;
227
+
}
228
+
229
+
.annotation-action {
230
+
display: flex;
231
+
align-items: center;
232
+
gap: 6px;
233
+
color: var(--text-tertiary);
234
+
font-size: 0.8rem;
235
+
font-weight: 500;
236
+
padding: 6px;
237
+
margin-left: -6px;
238
+
border-radius: var(--radius-sm);
239
+
transition: all 0.15s ease;
240
+
background: transparent;
241
+
cursor: pointer;
242
+
border: none;
243
+
}
244
+
245
+
.annotation-action:hover {
246
+
color: var(--text-secondary);
247
+
background: var(--bg-tertiary);
248
+
}
249
+
250
+
.annotation-action.liked {
251
+
color: #ef4444;
252
+
}
253
+
254
+
.annotation-action.liked svg {
255
+
fill: #ef4444;
256
+
}
257
+
258
+
.annotation-action.active {
259
+
color: var(--accent);
260
+
}
261
+
262
+
.action-icon-only {
263
+
padding: 6px;
264
+
}
265
+
266
+
.annotation-header-right {
267
+
opacity: 0;
268
+
transition: opacity 0.15s;
269
+
}
270
+
271
+
.annotation-card:hover .annotation-header-right {
272
+
opacity: 1;
273
+
}
274
+
275
+
.inline-replies {
276
+
margin-top: 12px;
277
+
padding-left: 46px;
278
+
}
279
+
280
+
@media (max-width: 600px) {
281
+
.annotation-content,
282
+
.annotation-actions,
283
+
.inline-replies {
284
+
padding-left: 0;
285
+
}
286
+
287
+
.annotation-header-right {
288
+
opacity: 1;
289
+
}
290
+
}
291
+
292
+
.replies-list-threaded {
293
+
margin-top: 16px;
294
+
display: flex;
295
+
flex-direction: column;
296
+
}
297
+
298
+
.reply-card-threaded {
299
+
position: relative;
300
+
padding-left: 0;
301
+
transition: background 0.15s;
302
+
}
303
+
304
+
.reply-header {
305
+
display: flex;
306
+
align-items: center;
307
+
gap: 10px;
308
+
margin-bottom: 6px;
309
+
}
310
+
311
+
.reply-avatar {
312
+
width: 28px;
313
+
height: 28px;
314
+
border-radius: 50%;
315
+
background: var(--bg-tertiary);
316
+
overflow: hidden;
317
+
flex-shrink: 0;
318
+
display: flex;
319
+
align-items: center;
320
+
justify-content: center;
321
+
}
322
+
323
+
.reply-avatar img {
324
+
width: 100%;
325
+
height: 100%;
326
+
object-fit: cover;
327
+
}
328
+
329
+
.reply-avatar span {
330
+
font-size: 0.7rem;
331
+
font-weight: 600;
332
+
color: var(--text-secondary);
333
+
}
334
+
335
+
.reply-meta {
336
+
display: flex;
337
+
align-items: baseline;
338
+
gap: 6px;
339
+
flex: 1;
340
+
min-width: 0;
341
+
}
342
+
343
+
.reply-author {
344
+
font-weight: 600;
345
+
font-size: 0.85rem;
346
+
color: var(--text-primary);
347
+
white-space: nowrap;
348
+
overflow: hidden;
349
+
text-overflow: ellipsis;
350
+
}
351
+
352
+
.reply-handle {
353
+
font-size: 0.8rem;
354
+
color: var(--text-tertiary);
355
+
text-decoration: none;
356
+
white-space: nowrap;
357
+
overflow: hidden;
358
+
text-overflow: ellipsis;
359
+
}
360
+
361
+
.reply-time {
362
+
font-size: 0.75rem;
363
+
color: var(--text-tertiary);
364
+
white-space: nowrap;
365
+
}
366
+
367
+
.reply-dot {
368
+
color: var(--text-tertiary);
369
+
font-size: 0.7rem;
370
+
}
371
+
372
+
.reply-text {
373
+
font-size: 0.9rem;
374
+
line-height: 1.5;
375
+
color: var(--text-primary);
376
+
margin: 0;
377
+
padding-left: 38px;
378
+
}
379
+
380
+
.reply-actions {
381
+
display: flex;
382
+
align-items: center;
383
+
gap: 4px;
384
+
opacity: 0;
385
+
transition: opacity 0.15s;
386
+
}
387
+
388
+
.reply-card-threaded:hover .reply-actions {
389
+
opacity: 1;
390
+
}
391
+
392
+
.reply-action-btn {
393
+
background: none;
394
+
border: none;
395
+
padding: 4px;
396
+
color: var(--text-tertiary);
397
+
cursor: pointer;
398
+
border-radius: 4px;
399
+
display: flex;
400
+
align-items: center;
401
+
justify-content: center;
402
+
}
403
+
404
+
.reply-action-btn:hover {
405
+
background: var(--bg-tertiary);
406
+
color: var(--text-secondary);
407
+
}
408
+
409
+
.reply-action-delete:hover {
410
+
color: #ef4444;
411
+
background: rgba(239, 68, 68, 0.1);
412
+
}
413
+
414
+
.reply-form {
415
+
border: 1px solid var(--border);
416
+
border-radius: var(--radius-md);
417
+
padding: 16px;
418
+
background: var(--bg-secondary);
419
+
margin-bottom: 24px;
420
+
}
421
+
422
+
.replying-to-banner {
423
+
display: flex;
424
+
justify-content: space-between;
425
+
align-items: center;
426
+
background: var(--bg-tertiary);
427
+
padding: 8px 12px;
428
+
border-radius: var(--radius-sm);
429
+
margin-bottom: 12px;
430
+
font-size: 0.85rem;
431
+
color: var(--text-secondary);
432
+
}
433
+
434
+
.cancel-reply {
435
+
background: none;
436
+
border: none;
437
+
color: var(--text-tertiary);
438
+
cursor: pointer;
439
+
font-size: 1.2rem;
440
+
padding: 0 4px;
441
+
line-height: 1;
442
+
}
443
+
444
+
.cancel-reply:hover {
445
+
color: var(--text-primary);
446
+
}
447
+
448
+
.reply-input {
449
+
width: 100%;
450
+
background: var(--bg-primary);
451
+
border: 1px solid var(--border);
452
+
border-radius: var(--radius-sm);
453
+
padding: 12px;
454
+
color: var(--text-primary);
455
+
font-family: inherit;
456
+
font-size: 0.95rem;
457
+
resize: vertical;
458
+
min-height: 80px;
459
+
transition: border-color 0.15s;
460
+
display: block;
461
+
box-sizing: border-box;
462
+
}
463
+
464
+
.reply-input:focus {
465
+
outline: none;
466
+
border-color: var(--accent);
467
+
}
468
+
469
+
.reply-form-actions {
470
+
display: flex;
471
+
justify-content: flex-end;
472
+
margin-top: 12px;
473
+
}
+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
+
}
+310
web/src/css/collections.css
+310
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
+
.collection-detail-desc {
175
+
color: var(--text-secondary);
176
+
font-size: 1rem;
177
+
line-height: 1.5;
178
+
margin-bottom: 12px;
179
+
max-width: 600px;
180
+
}
181
+
182
+
.collection-detail-stats {
183
+
display: flex;
184
+
align-items: center;
185
+
gap: 8px;
186
+
font-size: 0.85rem;
187
+
color: var(--text-tertiary);
188
+
}
189
+
190
+
.collection-detail-actions {
191
+
position: absolute;
192
+
top: 20px;
193
+
right: 20px;
194
+
display: flex;
195
+
align-items: center;
196
+
gap: 8px;
197
+
}
198
+
199
+
.collection-detail-actions .share-menu-container {
200
+
display: flex;
201
+
align-items: center;
202
+
}
203
+
204
+
.collection-detail-actions .annotation-action {
205
+
padding: 10px;
206
+
color: var(--text-tertiary);
207
+
background: none;
208
+
border: none;
209
+
border-radius: var(--radius-sm);
210
+
cursor: pointer;
211
+
transition: all 0.15s ease;
212
+
}
213
+
214
+
.collection-detail-actions .annotation-action:hover {
215
+
color: var(--accent);
216
+
background: var(--bg-tertiary);
217
+
}
218
+
219
+
.collection-detail-edit,
220
+
.collection-detail-delete {
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-edit:hover {
231
+
color: var(--accent);
232
+
background: var(--bg-tertiary);
233
+
}
234
+
235
+
.collection-detail-delete:hover {
236
+
color: var(--error);
237
+
background: rgba(239, 68, 68, 0.1);
238
+
}
239
+
240
+
.collection-item-wrapper {
241
+
position: relative;
242
+
}
243
+
244
+
.collection-item-remove {
245
+
position: absolute;
246
+
top: 12px;
247
+
left: -40px;
248
+
z-index: 10;
249
+
padding: 8px;
250
+
background: var(--bg-card);
251
+
border: 1px solid var(--border);
252
+
border-radius: var(--radius-sm);
253
+
color: var(--text-tertiary);
254
+
cursor: pointer;
255
+
opacity: 0;
256
+
transition: all 0.15s ease;
257
+
}
258
+
259
+
.collection-item-wrapper:hover .collection-item-remove {
260
+
opacity: 1;
261
+
}
262
+
263
+
.collection-item-remove:hover {
264
+
color: var(--error);
265
+
border-color: var(--error);
266
+
background: rgba(239, 68, 68, 0.05);
267
+
}
268
+
269
+
.collection-list-item {
270
+
width: 100%;
271
+
text-align: left;
272
+
padding: 12px 16px;
273
+
border-radius: var(--radius-md);
274
+
background: var(--bg-primary);
275
+
border: 1px solid transparent;
276
+
color: var(--text-primary);
277
+
transition: all 0.15s ease;
278
+
display: flex;
279
+
align-items: center;
280
+
justify-content: space-between;
281
+
cursor: pointer;
282
+
}
283
+
284
+
.collection-list-item:hover {
285
+
background: var(--bg-hover);
286
+
border-color: var(--border);
287
+
}
288
+
289
+
.collection-list-item:hover .collection-list-item-icon {
290
+
opacity: 1;
291
+
}
292
+
293
+
.collection-list-item:disabled {
294
+
opacity: 0.6;
295
+
cursor: not-allowed;
296
+
}
297
+
298
+
.item-delete-overlay {
299
+
position: absolute;
300
+
top: 16px;
301
+
right: 16px;
302
+
z-index: 10;
303
+
opacity: 0;
304
+
transition: opacity 0.15s ease;
305
+
}
306
+
307
+
.card:hover .item-delete-overlay,
308
+
div:hover > .item-delete-overlay {
309
+
opacity: 1;
310
+
}
+139
web/src/css/feed.css
+139
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
+
}
28
+
29
+
.filter-tab {
30
+
padding: 8px 16px;
31
+
font-size: 0.9rem;
32
+
font-weight: 500;
33
+
color: var(--text-secondary);
34
+
background: transparent;
35
+
border: none;
36
+
border-radius: var(--radius-md);
37
+
cursor: pointer;
38
+
transition: all 0.15s ease;
39
+
}
40
+
41
+
.filter-tab:hover {
42
+
color: var(--text-primary);
43
+
background: var(--bg-hover);
44
+
}
45
+
46
+
.filter-tab.active {
47
+
color: var(--text-primary);
48
+
background: var(--bg-card);
49
+
box-shadow: var(--shadow-sm);
50
+
}
51
+
52
+
.page-header {
53
+
margin-bottom: 32px;
54
+
}
55
+
56
+
.page-title {
57
+
font-size: 2rem;
58
+
font-weight: 700;
59
+
margin-bottom: 8px;
60
+
}
61
+
62
+
.page-description {
63
+
color: var(--text-secondary);
64
+
font-size: 1.1rem;
65
+
}
66
+
67
+
.url-input-wrapper {
68
+
margin-bottom: 24px;
69
+
}
70
+
71
+
.url-input-container {
72
+
display: flex;
73
+
gap: 12px;
74
+
}
75
+
76
+
.url-input {
77
+
width: 100%;
78
+
padding: 16px;
79
+
background: var(--bg-secondary);
80
+
border: 1px solid var(--border);
81
+
border-radius: var(--radius-md);
82
+
color: var(--text-primary);
83
+
font-size: 1.1rem;
84
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
85
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
86
+
}
87
+
88
+
.url-input:focus {
89
+
outline: none;
90
+
border-color: var(--accent);
91
+
box-shadow: 0 0 0 4px var(--accent-subtle);
92
+
background: var(--bg-primary);
93
+
}
94
+
95
+
.url-input::placeholder {
96
+
color: var(--text-tertiary);
97
+
}
98
+
99
+
.url-results-header {
100
+
display: flex;
101
+
align-items: center;
102
+
justify-content: space-between;
103
+
margin-bottom: 16px;
104
+
flex-wrap: wrap;
105
+
gap: 12px;
106
+
}
107
+
108
+
.back-link {
109
+
display: inline-flex;
110
+
align-items: center;
111
+
gap: 8px;
112
+
color: var(--text-secondary);
113
+
font-size: 0.9rem;
114
+
text-decoration: none;
115
+
margin-bottom: 24px;
116
+
transition: color 0.15s;
117
+
}
118
+
119
+
.back-link:hover {
120
+
color: var(--accent);
121
+
}
122
+
123
+
.new-page {
124
+
max-width: 600px;
125
+
margin: 0 auto;
126
+
display: flex;
127
+
flex-direction: column;
128
+
gap: 32px;
129
+
}
130
+
131
+
@media (max-width: 640px) {
132
+
.main-content {
133
+
padding: 16px 12px;
134
+
}
135
+
136
+
.page-title {
137
+
font-size: 1.5rem;
138
+
}
139
+
}
+465
web/src/css/layout.css
+465
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
+
}
455
+
456
+
.main-content-wrapper {
457
+
padding: 20px 16px;
458
+
max-width: 100%;
459
+
overflow-x: hidden;
460
+
}
461
+
462
+
.mobile-nav {
463
+
display: block;
464
+
}
465
+
}
+297
web/src/css/login.css
+297
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
+
.login-at-logo {
14
+
font-size: 5rem;
15
+
font-weight: 800;
16
+
color: var(--accent);
17
+
margin-bottom: 24px;
18
+
line-height: 1;
19
+
}
20
+
21
+
.login-logo-img {
22
+
width: 80px;
23
+
height: 80px;
24
+
margin-bottom: 24px;
25
+
object-fit: contain;
26
+
}
27
+
28
+
.login-heading {
29
+
font-size: 1.5rem;
30
+
font-weight: 600;
31
+
margin-bottom: 32px;
32
+
display: flex;
33
+
align-items: center;
34
+
gap: 10px;
35
+
text-align: center;
36
+
line-height: 1.4;
37
+
}
38
+
39
+
.login-help-btn {
40
+
background: none;
41
+
border: none;
42
+
color: var(--text-tertiary);
43
+
cursor: pointer;
44
+
padding: 4px;
45
+
display: flex;
46
+
align-items: center;
47
+
transition: color 0.15s;
48
+
flex-shrink: 0;
49
+
}
50
+
51
+
.login-help-btn:hover {
52
+
color: var(--accent);
53
+
}
54
+
55
+
.login-help-text {
56
+
background: var(--bg-elevated);
57
+
border: 1px solid var(--border);
58
+
border-radius: var(--radius-md);
59
+
padding: 16px 20px;
60
+
margin-bottom: 24px;
61
+
font-size: 0.95rem;
62
+
color: var(--text-secondary);
63
+
line-height: 1.6;
64
+
text-align: center;
65
+
}
66
+
67
+
.login-help-text code {
68
+
background: var(--bg-tertiary);
69
+
padding: 2px 8px;
70
+
border-radius: var(--radius-sm);
71
+
font-size: 0.9rem;
72
+
}
73
+
74
+
.login-form {
75
+
display: flex;
76
+
flex-direction: column;
77
+
gap: 16px;
78
+
width: 100%;
79
+
}
80
+
81
+
.login-input-wrapper {
82
+
position: relative;
83
+
}
84
+
85
+
.login-input {
86
+
width: 100%;
87
+
padding: 14px 16px;
88
+
background: var(--bg-elevated);
89
+
border: 1px solid var(--border);
90
+
border-radius: var(--radius-md);
91
+
color: var(--text-primary);
92
+
font-size: 1rem;
93
+
transition:
94
+
border-color 0.15s,
95
+
box-shadow 0.15s;
96
+
}
97
+
98
+
.login-input:focus {
99
+
outline: none;
100
+
border-color: var(--accent);
101
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
102
+
}
103
+
104
+
.login-input::placeholder {
105
+
color: var(--text-tertiary);
106
+
}
107
+
108
+
.login-suggestions {
109
+
position: absolute;
110
+
top: calc(100% + 4px);
111
+
left: 0;
112
+
right: 0;
113
+
background: var(--bg-card);
114
+
border: 1px solid var(--border);
115
+
border-radius: var(--radius-md);
116
+
box-shadow: var(--shadow-lg);
117
+
overflow: hidden;
118
+
z-index: 100;
119
+
}
120
+
121
+
.login-suggestion {
122
+
display: flex;
123
+
align-items: center;
124
+
gap: 12px;
125
+
width: 100%;
126
+
padding: 12px 16px;
127
+
background: transparent;
128
+
border: none;
129
+
cursor: pointer;
130
+
text-align: left;
131
+
transition: background 0.1s;
132
+
}
133
+
134
+
.login-suggestion:hover,
135
+
.login-suggestion.selected {
136
+
background: var(--bg-elevated);
137
+
}
138
+
139
+
.login-suggestion-avatar {
140
+
width: 40px;
141
+
height: 40px;
142
+
border-radius: var(--radius-full);
143
+
background: linear-gradient(135deg, var(--accent), #a855f7);
144
+
display: flex;
145
+
align-items: center;
146
+
justify-content: center;
147
+
flex-shrink: 0;
148
+
overflow: hidden;
149
+
font-size: 0.875rem;
150
+
font-weight: 600;
151
+
color: white;
152
+
}
153
+
154
+
.login-suggestion-avatar img {
155
+
width: 100%;
156
+
height: 100%;
157
+
object-fit: cover;
158
+
}
159
+
160
+
.login-suggestion-info {
161
+
display: flex;
162
+
flex-direction: column;
163
+
min-width: 0;
164
+
}
165
+
166
+
.login-suggestion-name {
167
+
font-weight: 600;
168
+
color: var(--text-primary);
169
+
white-space: nowrap;
170
+
overflow: hidden;
171
+
text-overflow: ellipsis;
172
+
}
173
+
174
+
.login-suggestion-handle {
175
+
font-size: 0.875rem;
176
+
color: var(--text-secondary);
177
+
white-space: nowrap;
178
+
overflow: hidden;
179
+
text-overflow: ellipsis;
180
+
}
181
+
182
+
.login-error {
183
+
padding: 12px 16px;
184
+
background: rgba(239, 68, 68, 0.1);
185
+
border: 1px solid rgba(239, 68, 68, 0.3);
186
+
border-radius: var(--radius-md);
187
+
color: #ef4444;
188
+
font-size: 0.875rem;
189
+
}
190
+
191
+
.login-legal {
192
+
font-size: 0.75rem;
193
+
color: var(--text-tertiary);
194
+
line-height: 1.5;
195
+
margin-top: 16px;
196
+
}
197
+
198
+
.login-brand {
199
+
display: flex;
200
+
align-items: center;
201
+
justify-content: center;
202
+
gap: 12px;
203
+
margin-bottom: 24px;
204
+
}
205
+
206
+
.login-brand-icon {
207
+
width: 48px;
208
+
height: 48px;
209
+
background: linear-gradient(135deg, var(--accent), #a855f7);
210
+
border-radius: var(--radius-lg);
211
+
display: flex;
212
+
align-items: center;
213
+
justify-content: center;
214
+
font-size: 1.75rem;
215
+
font-weight: 800;
216
+
color: white;
217
+
}
218
+
219
+
.login-brand-name {
220
+
font-size: 1.75rem;
221
+
font-weight: 700;
222
+
}
223
+
224
+
.login-avatar {
225
+
width: 72px;
226
+
height: 72px;
227
+
border-radius: var(--radius-full);
228
+
background: linear-gradient(135deg, var(--accent), #a855f7);
229
+
display: flex;
230
+
align-items: center;
231
+
justify-content: center;
232
+
margin: 0 auto 16px;
233
+
font-weight: 700;
234
+
font-size: 1.5rem;
235
+
color: white;
236
+
overflow: hidden;
237
+
}
238
+
239
+
.login-avatar img {
240
+
width: 100%;
241
+
height: 100%;
242
+
object-fit: cover;
243
+
}
244
+
245
+
.login-avatar-large {
246
+
width: 100px;
247
+
height: 100px;
248
+
border-radius: var(--radius-full);
249
+
background: linear-gradient(135deg, var(--accent), #a855f7);
250
+
display: flex;
251
+
align-items: center;
252
+
justify-content: center;
253
+
margin-bottom: 20px;
254
+
font-weight: 700;
255
+
font-size: 2rem;
256
+
color: white;
257
+
overflow: hidden;
258
+
}
259
+
260
+
.login-avatar-large img {
261
+
width: 100%;
262
+
height: 100%;
263
+
object-fit: cover;
264
+
}
265
+
266
+
.login-welcome {
267
+
font-size: 1.5rem;
268
+
font-weight: 600;
269
+
margin-bottom: 32px;
270
+
text-align: center;
271
+
}
272
+
273
+
.login-welcome-name {
274
+
font-size: 1.25rem;
275
+
font-weight: 600;
276
+
margin-bottom: 24px;
277
+
}
278
+
279
+
.login-actions {
280
+
display: flex;
281
+
flex-direction: column;
282
+
gap: 12px;
283
+
width: 100%;
284
+
}
285
+
286
+
.login-btn {
287
+
width: 100%;
288
+
padding: 14px 24px;
289
+
font-size: 1rem;
290
+
font-weight: 600;
291
+
}
292
+
293
+
.login-submit {
294
+
padding: 18px 32px;
295
+
font-size: 1.1rem;
296
+
font-weight: 600;
297
+
}
+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
+
}
+65
web/src/css/notifications.css
+65
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
+
}
57
+
58
+
.notification-text strong {
59
+
font-weight: 600;
60
+
}
61
+
62
+
.notification-time {
63
+
font-size: 0.85rem;
64
+
color: var(--text-tertiary);
65
+
}
+250
web/src/css/profile.css
+250
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
+
}
49
+
50
+
.profile-handle-row {
51
+
display: flex;
52
+
align-items: center;
53
+
gap: 12px;
54
+
margin-top: 4px;
55
+
flex-wrap: wrap;
56
+
}
57
+
58
+
.profile-handle-link {
59
+
color: var(--text-tertiary);
60
+
text-decoration: none;
61
+
font-size: 1rem;
62
+
transition: color 0.15s;
63
+
}
64
+
65
+
.profile-handle-link:hover {
66
+
color: var(--text-secondary);
67
+
}
68
+
69
+
.profile-bluesky-link {
70
+
display: inline-flex;
71
+
align-items: center;
72
+
gap: 6px;
73
+
color: #3b82f6;
74
+
text-decoration: none;
75
+
font-size: 0.85rem;
76
+
font-weight: 500;
77
+
padding: 2px 8px;
78
+
border-radius: var(--radius-sm);
79
+
background: rgba(59, 130, 246, 0.1);
80
+
transition: all 0.15s ease;
81
+
}
82
+
83
+
.profile-bluesky-link:hover {
84
+
background: rgba(59, 130, 246, 0.15);
85
+
}
86
+
87
+
.profile-stats {
88
+
display: flex;
89
+
gap: 24px;
90
+
margin-top: 12px;
91
+
}
92
+
93
+
.profile-stat {
94
+
color: var(--text-tertiary);
95
+
font-size: 0.9rem;
96
+
}
97
+
98
+
.profile-stat strong {
99
+
color: var(--text-primary);
100
+
font-weight: 600;
101
+
}
102
+
103
+
.profile-tabs {
104
+
display: flex;
105
+
gap: 24px;
106
+
margin-bottom: 24px;
107
+
border-bottom: 1px solid var(--border);
108
+
}
109
+
110
+
.profile-tab {
111
+
padding: 12px 0;
112
+
font-size: 0.95rem;
113
+
font-weight: 500;
114
+
color: var(--text-tertiary);
115
+
background: transparent;
116
+
border: none;
117
+
cursor: pointer;
118
+
transition: all 0.15s ease;
119
+
position: relative;
120
+
}
121
+
122
+
.profile-tab:hover {
123
+
color: var(--text-primary);
124
+
}
125
+
126
+
.profile-tab.active {
127
+
color: var(--text-primary);
128
+
}
129
+
130
+
.profile-tab.active::after {
131
+
content: "";
132
+
position: absolute;
133
+
bottom: -1px;
134
+
left: 0;
135
+
right: 0;
136
+
height: 2px;
137
+
background: var(--text-primary);
138
+
}
139
+
140
+
.profile-badge-wrapper {
141
+
display: inline-flex;
142
+
align-items: center;
143
+
}
144
+
145
+
.profile-badge-clickable {
146
+
position: relative;
147
+
display: inline-flex;
148
+
align-items: center;
149
+
cursor: pointer;
150
+
margin-left: 8px;
151
+
}
152
+
153
+
.badge-info-popover {
154
+
position: absolute;
155
+
top: calc(100% + 8px);
156
+
left: 50%;
157
+
transform: translateX(-50%);
158
+
padding: 16px;
159
+
background: var(--bg-elevated);
160
+
border: 1px solid var(--border);
161
+
border-radius: var(--radius-md);
162
+
box-shadow: var(--shadow-lg);
163
+
font-size: 0.85rem;
164
+
white-space: nowrap;
165
+
z-index: 100;
166
+
min-width: 200px;
167
+
}
168
+
169
+
.badge-info-title {
170
+
font-weight: 600;
171
+
color: var(--text-primary);
172
+
margin-bottom: 8px;
173
+
}
174
+
175
+
.verifier-link {
176
+
display: flex;
177
+
align-items: center;
178
+
gap: 8px;
179
+
padding: 8px;
180
+
background: var(--bg-tertiary);
181
+
border-radius: var(--radius-sm);
182
+
text-decoration: none;
183
+
transition: background 0.15s ease;
184
+
}
185
+
186
+
.verifier-link:hover {
187
+
background: var(--bg-hover);
188
+
}
189
+
190
+
.verifier-avatar {
191
+
width: 24px;
192
+
height: 24px;
193
+
border-radius: 50%;
194
+
object-fit: cover;
195
+
}
196
+
197
+
.verifier-name {
198
+
color: var(--text-primary);
199
+
font-size: 0.85rem;
200
+
font-weight: 500;
201
+
}
202
+
203
+
.profile-suspended {
204
+
display: flex;
205
+
flex-direction: column;
206
+
align-items: center;
207
+
justify-content: center;
208
+
padding: 60px 20px;
209
+
text-align: center;
210
+
background: var(--bg-secondary);
211
+
border-radius: var(--radius-lg);
212
+
margin-top: 20px;
213
+
border: 1px solid var(--border);
214
+
}
215
+
216
+
.suspended-icon {
217
+
font-size: 40px;
218
+
margin-bottom: 16px;
219
+
color: var(--text-tertiary);
220
+
}
221
+
222
+
.profile-suspended h2 {
223
+
color: var(--text-primary);
224
+
margin-bottom: 8px;
225
+
font-size: 1.25rem;
226
+
}
227
+
228
+
@media (max-width: 640px) {
229
+
.profile-header {
230
+
flex-direction: column;
231
+
text-align: center;
232
+
}
233
+
234
+
.profile-info {
235
+
align-items: center;
236
+
}
237
+
238
+
.profile-handle-row {
239
+
justify-content: center;
240
+
}
241
+
242
+
.profile-stats {
243
+
justify-content: center;
244
+
}
245
+
246
+
.profile-tabs {
247
+
justify-content: center;
248
+
gap: 16px;
249
+
}
250
+
}
+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
+
}
+730
web/src/css/utilities.css
+730
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
+
}
265
+
266
+
.composer-quote-remove {
267
+
position: absolute;
268
+
top: 8px;
269
+
right: 8px;
270
+
width: 24px;
271
+
height: 24px;
272
+
border-radius: var(--radius-full);
273
+
background: var(--bg-tertiary);
274
+
color: var(--text-secondary);
275
+
font-size: 1rem;
276
+
display: flex;
277
+
align-items: center;
278
+
justify-content: center;
279
+
}
280
+
281
+
.composer-quote-remove:hover {
282
+
background: var(--bg-hover);
283
+
color: var(--text-primary);
284
+
}
285
+
286
+
.composer-tags {
287
+
flex: 1;
288
+
}
289
+
290
+
.composer-meta-row {
291
+
display: flex;
292
+
gap: 12px;
293
+
margin-top: 12px;
294
+
align-items: flex-start;
295
+
}
296
+
297
+
.composer-labels-wrapper {
298
+
position: relative;
299
+
}
300
+
301
+
.composer-labels-btn {
302
+
display: flex;
303
+
align-items: center;
304
+
justify-content: center;
305
+
width: 42px;
306
+
height: 42px;
307
+
background: var(--bg-secondary);
308
+
border: 1px solid var(--border);
309
+
border-radius: var(--radius-md);
310
+
cursor: pointer;
311
+
color: var(--text-tertiary);
312
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
313
+
position: relative;
314
+
}
315
+
316
+
.composer-labels-btn:hover {
317
+
color: var(--text-primary);
318
+
background: var(--bg-hover);
319
+
border-color: var(--text-tertiary);
320
+
}
321
+
322
+
.composer-labels-btn.active {
323
+
color: var(--accent);
324
+
background: var(--accent-subtle);
325
+
border-color: var(--accent);
326
+
}
327
+
328
+
.composer-labels-badge {
329
+
position: absolute;
330
+
top: -4px;
331
+
right: -4px;
332
+
background: var(--error);
333
+
color: white;
334
+
font-size: 0.7rem;
335
+
width: 18px;
336
+
height: 18px;
337
+
border-radius: 50%;
338
+
display: flex;
339
+
align-items: center;
340
+
justify-content: center;
341
+
font-weight: bold;
342
+
border: 2px solid var(--bg-primary);
343
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
344
+
}
345
+
346
+
.composer-labels-picker {
347
+
position: absolute;
348
+
bottom: 100%;
349
+
right: 0;
350
+
margin-bottom: 12px;
351
+
background: var(--bg-elevated);
352
+
border: 1px solid var(--border);
353
+
border-radius: var(--radius-md);
354
+
padding: 8px 0;
355
+
min-width: 200px;
356
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
357
+
z-index: 50;
358
+
animation: scaleIn 0.2s ease-out forwards;
359
+
transform-origin: bottom right;
360
+
}
361
+
362
+
@keyframes scaleIn {
363
+
from {
364
+
opacity: 0;
365
+
transform: scale(0.95) translateY(5px);
366
+
}
367
+
368
+
to {
369
+
opacity: 1;
370
+
transform: scale(1) translateY(0);
371
+
}
372
+
}
373
+
374
+
.picker-header {
375
+
font-size: 0.75rem;
376
+
font-weight: 600;
377
+
color: var(--text-tertiary);
378
+
text-transform: uppercase;
379
+
letter-spacing: 0.05em;
380
+
margin-bottom: 4px;
381
+
padding: 4px 12px 8px;
382
+
border-bottom: 1px solid var(--border);
383
+
}
384
+
385
+
.picker-item {
386
+
display: flex;
387
+
align-items: center;
388
+
gap: 10px;
389
+
padding: 10px 14px;
390
+
cursor: pointer;
391
+
color: var(--text-secondary);
392
+
font-size: 0.9rem;
393
+
transition: all 0.15s ease;
394
+
user-select: none;
395
+
}
396
+
397
+
.picker-item:hover {
398
+
background: var(--bg-hover);
399
+
color: var(--text-primary);
400
+
}
401
+
402
+
.picker-checkbox-wrapper {
403
+
position: relative;
404
+
width: 18px;
405
+
height: 18px;
406
+
display: flex;
407
+
align-items: center;
408
+
justify-content: center;
409
+
}
410
+
411
+
.picker-checkbox-wrapper input {
412
+
position: absolute;
413
+
opacity: 0;
414
+
width: 100%;
415
+
height: 100%;
416
+
cursor: pointer;
417
+
z-index: 10;
418
+
}
419
+
420
+
.picker-checkbox-custom {
421
+
width: 18px;
422
+
height: 18px;
423
+
border: 2px solid var(--text-tertiary);
424
+
border-radius: 4px;
425
+
display: flex;
426
+
align-items: center;
427
+
justify-content: center;
428
+
background: transparent;
429
+
transition: all 0.2s ease;
430
+
color: white;
431
+
}
432
+
433
+
.picker-item:hover .picker-checkbox-custom {
434
+
border-color: var(--text-secondary);
435
+
}
436
+
437
+
.picker-checkbox-wrapper input:checked + .picker-checkbox-custom {
438
+
background: var(--accent);
439
+
border-color: var(--accent);
440
+
color: white;
441
+
}
442
+
443
+
.composer-tags-input {
444
+
width: 100%;
445
+
padding: 12px 16px;
446
+
background: var(--bg-secondary);
447
+
border: 1px solid var(--border);
448
+
border-radius: var(--radius-md);
449
+
color: var(--text-primary);
450
+
font-size: 0.95rem;
451
+
transition: all 0.15s ease;
452
+
}
453
+
454
+
.composer-tags-input:focus {
455
+
outline: none;
456
+
border-color: var(--accent);
457
+
box-shadow: 0 0 0 3px var(--accent-subtle);
458
+
}
459
+
460
+
.composer-tags-input::placeholder {
461
+
color: var(--text-tertiary);
462
+
}
463
+
464
+
.history-panel {
465
+
background: var(--bg-tertiary);
466
+
border: 1px solid var(--border);
467
+
border-radius: var(--radius-md);
468
+
padding: 1rem;
469
+
margin-bottom: 1rem;
470
+
font-size: 0.9rem;
471
+
animation: fadeIn 0.2s ease-out;
472
+
}
473
+
474
+
.history-header {
475
+
display: flex;
476
+
justify-content: space-between;
477
+
align-items: center;
478
+
margin-bottom: 1rem;
479
+
padding-bottom: 0.5rem;
480
+
border-bottom: 1px solid var(--border);
481
+
}
482
+
483
+
.history-title {
484
+
font-weight: 600;
485
+
text-transform: uppercase;
486
+
letter-spacing: 0.05em;
487
+
font-size: 0.75rem;
488
+
color: var(--text-secondary);
489
+
}
490
+
491
+
.history-list {
492
+
list-style: none;
493
+
display: flex;
494
+
flex-direction: column;
495
+
gap: 1rem;
496
+
}
497
+
498
+
.history-item {
499
+
position: relative;
500
+
padding-left: 1rem;
501
+
border-left: 2px solid var(--border);
502
+
}
503
+
504
+
.history-date {
505
+
font-size: 0.75rem;
506
+
color: var(--text-tertiary);
507
+
margin-bottom: 0.25rem;
508
+
}
509
+
510
+
.history-content {
511
+
color: var(--text-secondary);
512
+
white-space: pre-wrap;
513
+
}
514
+
515
+
.history-close-btn {
516
+
color: var(--text-tertiary);
517
+
padding: 4px;
518
+
border-radius: var(--radius-sm);
519
+
transition: all 0.2s;
520
+
display: flex;
521
+
align-items: center;
522
+
justify-content: center;
523
+
}
524
+
525
+
.history-close-btn:hover {
526
+
background: var(--bg-hover);
527
+
color: var(--text-primary);
528
+
}
529
+
530
+
.history-status {
531
+
text-align: center;
532
+
color: var(--text-tertiary);
533
+
font-style: italic;
534
+
padding: 1rem;
535
+
}
536
+
537
+
.share-menu-container {
538
+
position: relative;
539
+
}
540
+
541
+
.share-menu {
542
+
position: absolute;
543
+
top: 100%;
544
+
right: 0;
545
+
margin-top: 8px;
546
+
background: var(--bg-primary);
547
+
border: 1px solid var(--border);
548
+
border-radius: var(--radius-lg);
549
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
550
+
min-width: 180px;
551
+
padding: 8px 0;
552
+
z-index: 100;
553
+
animation: fadeInUp 0.15s ease;
554
+
}
555
+
556
+
@keyframes fadeInUp {
557
+
from {
558
+
opacity: 0;
559
+
transform: translateY(-8px);
560
+
}
561
+
562
+
to {
563
+
opacity: 1;
564
+
transform: translateY(0);
565
+
}
566
+
}
567
+
568
+
.share-menu-section {
569
+
display: flex;
570
+
flex-direction: column;
571
+
}
572
+
573
+
.share-menu-label {
574
+
padding: 4px 12px 8px;
575
+
font-size: 0.7rem;
576
+
font-weight: 600;
577
+
text-transform: uppercase;
578
+
letter-spacing: 0.05em;
579
+
color: var(--text-tertiary);
580
+
}
581
+
582
+
.share-menu-item {
583
+
display: flex;
584
+
align-items: center;
585
+
gap: 10px;
586
+
padding: 10px 14px;
587
+
background: none;
588
+
border: none;
589
+
width: 100%;
590
+
text-align: left;
591
+
font-size: 0.9rem;
592
+
color: var(--text-primary);
593
+
cursor: pointer;
594
+
transition: all 0.1s ease;
595
+
}
596
+
597
+
.share-menu-item:hover {
598
+
background: var(--bg-tertiary);
599
+
}
600
+
601
+
.share-menu-icon {
602
+
font-size: 1.1rem;
603
+
width: 24px;
604
+
text-align: center;
605
+
}
606
+
607
+
.share-menu-divider {
608
+
height: 1px;
609
+
background: var(--border);
610
+
margin: 6px 0;
611
+
}
612
+
613
+
.bookmark-card {
614
+
display: flex;
615
+
flex-direction: column;
616
+
gap: 16px;
617
+
}
618
+
619
+
.bookmark-preview {
620
+
display: flex;
621
+
flex-direction: column;
622
+
background: var(--bg-secondary);
623
+
border: 1px solid var(--border);
624
+
border-radius: var(--radius-md);
625
+
overflow: hidden;
626
+
text-decoration: none;
627
+
transition: all 0.2s ease;
628
+
position: relative;
629
+
}
630
+
631
+
.bookmark-preview:hover {
632
+
border-color: var(--accent);
633
+
box-shadow: var(--shadow-sm);
634
+
transform: translateY(-1px);
635
+
}
636
+
637
+
.bookmark-preview::before {
638
+
content: "";
639
+
position: absolute;
640
+
left: 0;
641
+
top: 0;
642
+
bottom: 0;
643
+
width: 4px;
644
+
background: var(--accent);
645
+
opacity: 0.7;
646
+
}
647
+
648
+
.bookmark-preview-content {
649
+
padding: 16px 20px;
650
+
display: flex;
651
+
flex-direction: column;
652
+
gap: 8px;
653
+
}
654
+
655
+
.bookmark-preview-header {
656
+
display: flex;
657
+
align-items: center;
658
+
gap: 8px;
659
+
margin-bottom: 4px;
660
+
}
661
+
662
+
.bookmark-preview-site {
663
+
display: flex;
664
+
align-items: center;
665
+
gap: 6px;
666
+
font-size: 0.75rem;
667
+
font-weight: 600;
668
+
color: var(--accent);
669
+
text-transform: uppercase;
670
+
letter-spacing: 0.03em;
671
+
}
672
+
673
+
.bookmark-preview-title {
674
+
font-size: 1rem;
675
+
font-weight: 600;
676
+
line-height: 1.4;
677
+
color: var(--text-primary);
678
+
margin: 0;
679
+
display: -webkit-box;
680
+
-webkit-line-clamp: 2;
681
+
line-clamp: 2;
682
+
-webkit-box-orient: vertical;
683
+
overflow: hidden;
684
+
}
685
+
686
+
.bookmark-preview-desc {
687
+
font-size: 0.875rem;
688
+
color: var(--text-secondary);
689
+
line-height: 1.5;
690
+
margin: 0;
691
+
display: -webkit-box;
692
+
-webkit-line-clamp: 2;
693
+
line-clamp: 2;
694
+
-webkit-box-orient: vertical;
695
+
overflow: hidden;
696
+
}
697
+
698
+
.bookmark-preview-arrow {
699
+
display: flex;
700
+
align-items: center;
701
+
justify-content: center;
702
+
color: var(--text-tertiary);
703
+
padding: 0 4px;
704
+
transition: all 0.2s ease;
705
+
}
706
+
707
+
.bookmark-preview:hover .bookmark-preview-arrow {
708
+
color: var(--accent);
709
+
transform: translateX(2px);
710
+
}
711
+
712
+
.bookmark-description {
713
+
font-size: 0.9rem;
714
+
color: var(--text-secondary);
715
+
margin: 0;
716
+
line-height: 1.5;
717
+
}
718
+
719
+
.bookmark-meta {
720
+
display: flex;
721
+
align-items: center;
722
+
gap: 12px;
723
+
margin-top: 12px;
724
+
font-size: 0.85rem;
725
+
color: var(--text-tertiary);
726
+
}
727
+
728
+
.bookmark-time {
729
+
color: var(--text-tertiary);
730
+
}
+13
-3190
web/src/index.css
+13
-3190
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: 20px;
144
-
transition: all 0.2s ease;
145
-
}
146
-
147
-
.card:hover {
148
-
border-color: var(--border-hover);
149
-
box-shadow: var(--shadow-sm);
150
-
}
151
-
152
-
.annotation-card {
153
-
display: flex;
154
-
flex-direction: column;
155
-
gap: 12px;
156
-
}
157
-
158
-
.annotation-header {
159
-
display: flex;
160
-
align-items: center;
161
-
gap: 12px;
162
-
}
163
-
164
-
.annotation-avatar {
165
-
width: 42px;
166
-
height: 42px;
167
-
min-width: 42px;
168
-
border-radius: var(--radius-full);
169
-
background: linear-gradient(135deg, var(--accent), #a855f7);
170
-
display: flex;
171
-
align-items: center;
172
-
justify-content: center;
173
-
font-weight: 600;
174
-
font-size: 1rem;
175
-
color: white;
176
-
overflow: hidden;
177
-
}
178
-
179
-
.annotation-avatar img {
180
-
width: 100%;
181
-
height: 100%;
182
-
object-fit: cover;
183
-
}
184
-
185
-
.annotation-meta {
186
-
flex: 1;
187
-
min-width: 0;
188
-
}
189
-
190
-
.annotation-avatar-link {
191
-
text-decoration: none;
192
-
}
193
-
194
-
.annotation-author-row {
195
-
display: flex;
196
-
align-items: center;
197
-
gap: 6px;
198
-
flex-wrap: wrap;
199
-
}
200
-
201
-
.annotation-author {
202
-
font-weight: 600;
203
-
color: var(--text-primary);
204
-
}
205
-
206
-
.annotation-handle {
207
-
font-size: 0.9rem;
208
-
color: var(--text-tertiary);
209
-
text-decoration: none;
210
-
}
211
-
212
-
.annotation-handle:hover {
213
-
color: var(--accent);
214
-
text-decoration: underline;
215
-
}
216
-
217
-
.annotation-time {
218
-
font-size: 0.85rem;
219
-
color: var(--text-tertiary);
220
-
}
221
-
222
-
.annotation-source {
223
-
display: block;
224
-
font-size: 0.85rem;
225
-
color: var(--text-tertiary);
226
-
text-decoration: none;
227
-
margin-bottom: 8px;
228
-
}
229
-
230
-
.annotation-source:hover {
231
-
color: var(--accent);
232
-
}
233
-
234
-
.annotation-source-title {
235
-
color: var(--text-secondary);
236
-
}
237
-
238
-
.annotation-highlight {
239
-
display: block;
240
-
padding: 12px 16px;
241
-
background: linear-gradient(
242
-
135deg,
243
-
rgba(79, 70, 229, 0.05),
244
-
rgba(168, 85, 247, 0.05)
245
-
);
246
-
border-left: 3px solid var(--accent);
247
-
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
248
-
text-decoration: none;
249
-
transition: all 0.15s ease;
250
-
margin-bottom: 12px;
251
-
}
252
-
253
-
.annotation-highlight:hover {
254
-
background: linear-gradient(
255
-
135deg,
256
-
rgba(79, 70, 229, 0.1),
257
-
rgba(168, 85, 247, 0.1)
258
-
);
259
-
}
260
-
261
-
.annotation-highlight mark {
262
-
background: transparent;
263
-
color: var(--text-primary);
264
-
font-style: italic;
265
-
font-size: 0.95rem;
266
-
}
267
-
268
-
.annotation-text {
269
-
font-size: 1rem;
270
-
line-height: 1.65;
271
-
color: var(--text-primary);
272
-
}
273
-
274
-
.annotation-actions {
275
-
display: flex;
276
-
align-items: center;
277
-
gap: 16px;
278
-
padding-top: 8px;
279
-
}
280
-
281
-
.annotation-action {
282
-
display: flex;
283
-
align-items: center;
284
-
gap: 6px;
285
-
color: var(--text-tertiary);
286
-
font-size: 0.85rem;
287
-
padding: 6px 10px;
288
-
border-radius: var(--radius-sm);
289
-
transition: all 0.15s ease;
290
-
}
291
-
292
-
.annotation-action:hover {
293
-
color: var(--text-secondary);
294
-
background: var(--bg-tertiary);
295
-
}
296
-
297
-
.annotation-action.liked {
298
-
color: #ef4444;
299
-
}
300
-
301
-
.annotation-delete {
302
-
background: none;
303
-
border: none;
304
-
cursor: pointer;
305
-
padding: 6px 8px;
306
-
font-size: 1rem;
307
-
color: var(--text-tertiary);
308
-
transition: all 0.15s ease;
309
-
border-radius: var(--radius-sm);
310
-
}
311
-
312
-
.annotation-delete:hover {
313
-
color: var(--error);
314
-
background: rgba(239, 68, 68, 0.1);
315
-
}
316
-
317
-
.annotation-delete:disabled {
318
-
cursor: not-allowed;
319
-
opacity: 0.3;
320
-
}
321
-
322
-
.share-menu-container {
323
-
position: relative;
324
-
}
325
-
326
-
.share-menu {
327
-
position: absolute;
328
-
top: 100%;
329
-
right: 0;
330
-
margin-top: 8px;
331
-
background: var(--bg-primary);
332
-
border: 1px solid var(--border);
333
-
border-radius: var(--radius-lg);
334
-
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
335
-
min-width: 180px;
336
-
padding: 8px 0;
337
-
z-index: 100;
338
-
animation: fadeInUp 0.15s ease;
339
-
}
340
-
341
-
@keyframes fadeInUp {
342
-
from {
343
-
opacity: 0;
344
-
transform: translateY(-8px);
345
-
}
346
-
347
-
to {
348
-
opacity: 1;
349
-
transform: translateY(0);
350
-
}
351
-
}
352
-
353
-
.share-menu-section {
354
-
display: flex;
355
-
flex-direction: column;
356
-
}
357
-
358
-
.share-menu-label {
359
-
padding: 4px 12px 8px;
360
-
font-size: 0.7rem;
361
-
font-weight: 600;
362
-
text-transform: uppercase;
363
-
letter-spacing: 0.05em;
364
-
color: var(--text-tertiary);
365
-
}
366
-
367
-
.share-menu-item {
368
-
display: flex;
369
-
align-items: center;
370
-
gap: 10px;
371
-
padding: 10px 14px;
372
-
background: none;
373
-
border: none;
374
-
width: 100%;
375
-
text-align: left;
376
-
font-size: 0.9rem;
377
-
color: var(--text-primary);
378
-
cursor: pointer;
379
-
transition: all 0.1s ease;
380
-
}
381
-
382
-
.share-menu-item:hover {
383
-
background: var(--bg-tertiary);
384
-
}
385
-
386
-
.share-menu-icon {
387
-
font-size: 1.1rem;
388
-
width: 24px;
389
-
text-align: center;
390
-
}
391
-
392
-
.share-menu-divider {
393
-
height: 1px;
394
-
background: var(--border);
395
-
margin: 6px 0;
396
-
}
397
-
398
-
.feed {
399
-
display: flex;
400
-
flex-direction: column;
401
-
gap: 16px;
402
-
}
403
-
404
-
.feed-header {
405
-
display: flex;
406
-
align-items: center;
407
-
justify-content: space-between;
408
-
margin-bottom: 8px;
409
-
}
410
-
411
-
.feed-title {
412
-
font-size: 1.5rem;
413
-
font-weight: 700;
414
-
}
415
-
416
-
.page-header {
417
-
margin-bottom: 32px;
418
-
}
419
-
420
-
.page-title {
421
-
font-size: 2rem;
422
-
font-weight: 700;
423
-
margin-bottom: 8px;
424
-
}
425
-
426
-
.page-description {
427
-
color: var(--text-secondary);
428
-
font-size: 1.1rem;
429
-
}
430
-
431
-
.url-input-wrapper {
432
-
margin-bottom: 32px;
433
-
}
434
-
435
-
.url-input-container {
436
-
display: flex;
437
-
gap: 12px;
438
-
}
439
-
440
-
.url-input {
441
-
flex: 1;
442
-
padding: 14px 18px;
443
-
background: var(--bg-secondary);
444
-
border: 1px solid var(--border);
445
-
border-radius: var(--radius-md);
446
-
color: var(--text-primary);
447
-
font-size: 1rem;
448
-
transition: all 0.15s ease;
449
-
}
450
-
451
-
.url-input:focus {
452
-
outline: none;
453
-
border-color: var(--accent);
454
-
box-shadow: 0 0 0 3px var(--accent-subtle);
455
-
}
456
-
457
-
.url-input::placeholder {
458
-
color: var(--text-tertiary);
459
-
}
460
-
461
-
.empty-state {
462
-
text-align: center;
463
-
padding: 60px 20px;
464
-
color: var(--text-secondary);
465
-
}
466
-
467
-
.empty-state-icon {
468
-
font-size: 3rem;
469
-
margin-bottom: 16px;
470
-
opacity: 0.5;
471
-
}
472
-
473
-
.empty-state-title {
474
-
font-size: 1.25rem;
475
-
font-weight: 600;
476
-
color: var(--text-primary);
477
-
margin-bottom: 8px;
478
-
}
479
-
480
-
.empty-state-text {
481
-
font-size: 1rem;
482
-
max-width: 400px;
483
-
margin: 0 auto;
484
-
}
485
-
486
-
.feed-filters {
487
-
display: flex;
488
-
gap: 8px;
489
-
margin-bottom: 24px;
490
-
padding: 4px;
491
-
background: var(--bg-tertiary);
492
-
border-radius: var(--radius-lg);
493
-
width: fit-content;
494
-
}
495
-
496
-
.login-page {
497
-
display: flex;
498
-
flex-direction: column;
499
-
align-items: center;
500
-
justify-content: center;
501
-
min-height: 70vh;
502
-
padding: 60px 20px;
503
-
width: 100%;
504
-
max-width: 500px;
505
-
margin: 0 auto;
506
-
}
507
-
508
-
.login-at-logo {
509
-
font-size: 5rem;
510
-
font-weight: 800;
511
-
color: var(--accent);
512
-
margin-bottom: 24px;
513
-
line-height: 1;
514
-
}
515
-
516
-
.login-heading {
517
-
font-size: 1.5rem;
518
-
font-weight: 600;
519
-
margin-bottom: 32px;
520
-
display: flex;
521
-
align-items: center;
522
-
gap: 10px;
523
-
text-align: center;
524
-
line-height: 1.4;
525
-
}
526
-
527
-
.login-help-btn {
528
-
background: none;
529
-
border: none;
530
-
color: var(--text-tertiary);
531
-
cursor: pointer;
532
-
padding: 4px;
533
-
display: flex;
534
-
align-items: center;
535
-
transition: color 0.15s;
536
-
flex-shrink: 0;
537
-
}
538
-
539
-
.login-help-btn:hover {
540
-
color: var(--accent);
541
-
}
542
-
543
-
.login-help-text {
544
-
background: var(--bg-elevated);
545
-
border: 1px solid var(--border);
546
-
border-radius: var(--radius-md);
547
-
padding: 16px 20px;
548
-
margin-bottom: 24px;
549
-
font-size: 0.95rem;
550
-
color: var(--text-secondary);
551
-
line-height: 1.6;
552
-
text-align: center;
553
-
}
554
-
555
-
.login-help-text code {
556
-
background: var(--bg-tertiary);
557
-
padding: 2px 8px;
558
-
border-radius: var(--radius-sm);
559
-
font-size: 0.9rem;
560
-
}
561
-
562
-
.login-form {
563
-
display: flex;
564
-
flex-direction: column;
565
-
gap: 20px;
566
-
width: 100%;
567
-
}
568
-
569
-
.login-input-wrapper {
570
-
position: relative;
571
-
}
572
-
573
-
.login-input {
574
-
width: 100%;
575
-
padding: 18px 20px;
576
-
background: var(--bg-elevated);
577
-
border: 2px solid var(--border);
578
-
border-radius: var(--radius-lg);
579
-
color: var(--text-primary);
580
-
font-size: 1.1rem;
581
-
transition:
582
-
border-color 0.15s,
583
-
box-shadow 0.15s;
584
-
}
585
-
586
-
.login-input:focus {
587
-
outline: none;
588
-
border-color: var(--accent);
589
-
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15);
590
-
}
591
-
592
-
.login-input::placeholder {
593
-
color: var(--text-tertiary);
594
-
}
595
-
596
-
.login-suggestions {
597
-
position: absolute;
598
-
top: calc(100% + 8px);
599
-
left: 0;
600
-
right: 0;
601
-
background: var(--bg-card);
602
-
border: 1px solid var(--border);
603
-
border-radius: var(--radius-lg);
604
-
box-shadow: var(--shadow-lg);
605
-
overflow: hidden;
606
-
z-index: 100;
607
-
}
608
-
609
-
.login-suggestion {
610
-
display: flex;
611
-
align-items: center;
612
-
gap: 14px;
613
-
width: 100%;
614
-
padding: 14px 18px;
615
-
background: transparent;
616
-
border: none;
617
-
cursor: pointer;
618
-
text-align: left;
619
-
color: var(--text-primary);
620
-
transition: background 0.1s;
621
-
}
622
-
623
-
.login-suggestion:hover,
624
-
.login-suggestion.selected {
625
-
background: var(--bg-elevated);
626
-
}
627
-
628
-
.login-suggestion-avatar {
629
-
width: 44px;
630
-
height: 44px;
631
-
border-radius: var(--radius-full);
632
-
background: linear-gradient(135deg, var(--accent), #a855f7);
633
-
display: flex;
634
-
align-items: center;
635
-
justify-content: center;
636
-
flex-shrink: 0;
637
-
overflow: hidden;
638
-
font-size: 0.9rem;
639
-
font-weight: 600;
640
-
color: white;
641
-
}
642
-
643
-
.login-suggestion-avatar img {
644
-
width: 100%;
645
-
height: 100%;
646
-
object-fit: cover;
647
-
}
648
-
649
-
.login-suggestion-info {
650
-
display: flex;
651
-
flex-direction: column;
652
-
gap: 2px;
653
-
min-width: 0;
654
-
}
655
-
656
-
.login-suggestion-name {
657
-
font-weight: 600;
658
-
font-size: 1rem;
659
-
color: var(--text-primary);
660
-
white-space: nowrap;
661
-
overflow: hidden;
662
-
text-overflow: ellipsis;
663
-
}
664
-
665
-
.login-suggestion-handle {
666
-
font-size: 0.9rem;
667
-
color: var(--text-secondary);
668
-
white-space: nowrap;
669
-
overflow: hidden;
670
-
text-overflow: ellipsis;
671
-
}
672
-
673
-
.login-error {
674
-
padding: 12px 16px;
675
-
background: rgba(239, 68, 68, 0.1);
676
-
border: 1px solid rgba(239, 68, 68, 0.3);
677
-
border-radius: var(--radius-md);
678
-
color: #ef4444;
679
-
font-size: 0.9rem;
680
-
text-align: center;
681
-
}
682
-
683
-
.login-submit {
684
-
padding: 18px 32px;
685
-
font-size: 1.1rem;
686
-
font-weight: 600;
687
-
}
688
-
689
-
.login-avatar-large {
690
-
width: 100px;
691
-
height: 100px;
692
-
border-radius: var(--radius-full);
693
-
background: linear-gradient(135deg, var(--accent), #a855f7);
694
-
display: flex;
695
-
align-items: center;
696
-
justify-content: center;
697
-
margin-bottom: 20px;
698
-
font-weight: 700;
699
-
font-size: 2rem;
700
-
color: white;
701
-
overflow: hidden;
702
-
}
703
-
704
-
.login-avatar-large img {
705
-
width: 100%;
706
-
height: 100%;
707
-
object-fit: cover;
708
-
}
709
-
710
-
.login-welcome {
711
-
font-size: 1.5rem;
712
-
font-weight: 600;
713
-
margin-bottom: 32px;
714
-
text-align: center;
715
-
}
716
-
717
-
.login-actions {
718
-
display: flex;
719
-
flex-direction: column;
720
-
gap: 12px;
721
-
width: 100%;
722
-
}
723
-
724
-
.login-avatar {
725
-
width: 72px;
726
-
height: 72px;
727
-
border-radius: var(--radius-full);
728
-
background: linear-gradient(135deg, var(--accent), #a855f7);
729
-
display: flex;
730
-
align-items: center;
731
-
justify-content: center;
732
-
margin: 0 auto 16px;
733
-
font-weight: 700;
734
-
font-size: 1.5rem;
735
-
color: white;
736
-
overflow: hidden;
737
-
}
738
-
739
-
.login-avatar img {
740
-
width: 100%;
741
-
height: 100%;
742
-
object-fit: cover;
743
-
}
744
-
745
-
.login-welcome-name {
746
-
font-size: 1.25rem;
747
-
font-weight: 600;
748
-
margin-bottom: 24px;
749
-
}
750
-
751
-
.login-actions {
752
-
display: flex;
753
-
flex-direction: column;
754
-
gap: 12px;
755
-
}
756
-
757
-
.btn-bluesky {
758
-
background: #0085ff;
759
-
color: white;
760
-
display: flex;
761
-
align-items: center;
762
-
justify-content: center;
763
-
gap: 10px;
764
-
transition:
765
-
background 0.2s,
766
-
transform 0.2s;
767
-
}
768
-
769
-
.btn-bluesky:hover {
770
-
background: #0070dd;
771
-
transform: translateY(-1px);
772
-
}
773
-
774
-
.login-btn {
775
-
width: 100%;
776
-
padding: 14px 24px;
777
-
font-size: 1rem;
778
-
font-weight: 600;
779
-
}
780
-
781
-
.login-brand {
782
-
display: flex;
783
-
align-items: center;
784
-
justify-content: center;
785
-
gap: 12px;
786
-
margin-bottom: 24px;
787
-
}
788
-
789
-
.login-brand-icon {
790
-
width: 48px;
791
-
height: 48px;
792
-
background: linear-gradient(135deg, var(--accent), #a855f7);
793
-
border-radius: var(--radius-lg);
794
-
display: flex;
795
-
align-items: center;
796
-
justify-content: center;
797
-
font-size: 1.75rem;
798
-
font-weight: 800;
799
-
color: white;
800
-
}
801
-
802
-
.login-brand-name {
803
-
font-size: 1.75rem;
804
-
font-weight: 700;
805
-
}
806
-
807
-
.login-form {
808
-
display: flex;
809
-
flex-direction: column;
810
-
gap: 16px;
811
-
}
812
-
813
-
.login-input-wrapper {
814
-
position: relative;
815
-
}
816
-
817
-
.login-input {
818
-
width: 100%;
819
-
padding: 14px 16px;
820
-
background: var(--bg-elevated);
821
-
border: 1px solid var(--border);
822
-
border-radius: var(--radius-md);
823
-
color: var(--text-primary);
824
-
font-size: 1rem;
825
-
transition:
826
-
border-color 0.15s,
827
-
box-shadow 0.15s;
828
-
}
829
-
830
-
.login-input:focus {
831
-
outline: none;
832
-
border-color: var(--accent);
833
-
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
834
-
}
835
-
836
-
.login-input::placeholder {
837
-
color: var(--text-tertiary);
838
-
}
839
-
840
-
.login-suggestions {
841
-
position: absolute;
842
-
top: calc(100% + 4px);
843
-
left: 0;
844
-
right: 0;
845
-
background: var(--bg-card);
846
-
border: 1px solid var(--border);
847
-
border-radius: var(--radius-md);
848
-
box-shadow: var(--shadow-lg);
849
-
overflow: hidden;
850
-
z-index: 100;
851
-
}
852
-
853
-
.login-suggestion {
854
-
display: flex;
855
-
align-items: center;
856
-
gap: 12px;
857
-
width: 100%;
858
-
padding: 12px 16px;
859
-
background: transparent;
860
-
border: none;
861
-
cursor: pointer;
862
-
text-align: left;
863
-
transition: background 0.1s;
864
-
}
865
-
866
-
.login-suggestion:hover,
867
-
.login-suggestion.selected {
868
-
background: var(--bg-elevated);
869
-
}
870
-
871
-
.login-suggestion-avatar {
872
-
width: 40px;
873
-
height: 40px;
874
-
border-radius: var(--radius-full);
875
-
background: linear-gradient(135deg, var(--accent), #a855f7);
876
-
display: flex;
877
-
align-items: center;
878
-
justify-content: center;
879
-
flex-shrink: 0;
880
-
overflow: hidden;
881
-
font-size: 0.875rem;
882
-
font-weight: 600;
883
-
color: white;
884
-
}
885
-
886
-
.login-suggestion-avatar img {
887
-
width: 100%;
888
-
height: 100%;
889
-
object-fit: cover;
890
-
}
891
-
892
-
.login-suggestion-info {
893
-
display: flex;
894
-
flex-direction: column;
895
-
min-width: 0;
896
-
}
897
-
898
-
.login-suggestion-name {
899
-
font-weight: 600;
900
-
color: var(--text-primary);
901
-
white-space: nowrap;
902
-
overflow: hidden;
903
-
text-overflow: ellipsis;
904
-
}
905
-
906
-
.login-suggestion-handle {
907
-
font-size: 0.875rem;
908
-
color: var(--text-secondary);
909
-
white-space: nowrap;
910
-
overflow: hidden;
911
-
text-overflow: ellipsis;
912
-
}
913
-
914
-
.login-error {
915
-
padding: 12px 16px;
916
-
background: rgba(239, 68, 68, 0.1);
917
-
border: 1px solid rgba(239, 68, 68, 0.3);
918
-
border-radius: var(--radius-md);
919
-
color: #ef4444;
920
-
font-size: 0.875rem;
921
-
}
922
-
923
-
.login-legal {
924
-
font-size: 0.75rem;
925
-
color: var(--text-tertiary);
926
-
line-height: 1.5;
927
-
margin-top: 16px;
928
-
}
929
-
930
-
.profile-header {
931
-
display: flex;
932
-
align-items: center;
933
-
gap: 20px;
934
-
margin-bottom: 32px;
935
-
padding-bottom: 24px;
936
-
border-bottom: 1px solid var(--border);
937
-
}
938
-
939
-
.profile-avatar {
940
-
width: 80px;
941
-
height: 80px;
942
-
min-width: 80px;
943
-
border-radius: var(--radius-full);
944
-
background: linear-gradient(135deg, var(--accent), #a855f7);
945
-
display: flex;
946
-
align-items: center;
947
-
justify-content: center;
948
-
font-weight: 700;
949
-
font-size: 2rem;
950
-
color: white;
951
-
overflow: hidden;
952
-
}
953
-
954
-
.profile-avatar img {
955
-
width: 100%;
956
-
height: 100%;
957
-
object-fit: cover;
958
-
}
959
-
960
-
.profile-avatar-link {
961
-
text-decoration: none;
962
-
}
963
-
964
-
.profile-info {
965
-
flex: 1;
966
-
}
967
-
968
-
.profile-name {
969
-
font-size: 1.5rem;
970
-
font-weight: 700;
971
-
}
972
-
973
-
.profile-handle-link {
974
-
color: var(--text-secondary);
975
-
text-decoration: none;
976
-
}
977
-
978
-
.profile-handle-link:hover {
979
-
color: var(--accent);
980
-
text-decoration: underline;
981
-
}
982
-
983
-
.profile-bluesky-link {
984
-
display: inline-flex;
985
-
align-items: center;
986
-
gap: 6px;
987
-
color: #0085ff;
988
-
text-decoration: none;
989
-
font-size: 0.95rem;
990
-
padding: 4px 10px;
991
-
border-radius: var(--radius-md);
992
-
background: rgba(0, 133, 255, 0.1);
993
-
transition: all 0.15s ease;
994
-
}
995
-
996
-
.profile-bluesky-link:hover {
997
-
background: rgba(0, 133, 255, 0.2);
998
-
color: #0070dd;
999
-
}
1000
-
1001
-
.profile-stats {
1002
-
display: flex;
1003
-
gap: 24px;
1004
-
margin-top: 8px;
1005
-
}
1006
-
1007
-
.profile-stat {
1008
-
color: var(--text-secondary);
1009
-
font-size: 0.9rem;
1010
-
}
1011
-
1012
-
.profile-stat strong {
1013
-
color: var(--text-primary);
1014
-
}
1015
-
1016
-
.profile-tabs {
1017
-
display: flex;
1018
-
gap: 0;
1019
-
margin-bottom: 24px;
1020
-
border-bottom: 1px solid var(--border);
1021
-
}
1022
-
1023
-
.profile-tab {
1024
-
padding: 12px 20px;
1025
-
font-size: 0.9rem;
1026
-
font-weight: 500;
1027
-
color: var(--text-secondary);
1028
-
background: transparent;
1029
-
border: none;
1030
-
border-bottom: 2px solid transparent;
1031
-
cursor: pointer;
1032
-
transition: all 0.15s ease;
1033
-
margin-bottom: -1px;
1034
-
}
1035
-
1036
-
.profile-tab:hover {
1037
-
color: var(--text-primary);
1038
-
background: var(--bg-tertiary);
1039
-
}
1040
-
1041
-
.profile-tab.active {
1042
-
color: var(--accent);
1043
-
border-bottom-color: var(--accent);
1044
-
}
1045
-
1046
-
.bookmark-card {
1047
-
padding: 16px 20px;
1048
-
}
1049
-
1050
-
.bookmark-header {
1051
-
display: flex;
1052
-
align-items: flex-start;
1053
-
justify-content: space-between;
1054
-
gap: 12px;
1055
-
}
1056
-
1057
-
.bookmark-link {
1058
-
text-decoration: none;
1059
-
flex: 1;
1060
-
}
1061
-
1062
-
.bookmark-title {
1063
-
font-size: 1rem;
1064
-
font-weight: 600;
1065
-
color: var(--text-primary);
1066
-
margin: 0 0 4px 0;
1067
-
line-height: 1.4;
1068
-
}
1069
-
1070
-
.bookmark-title:hover {
1071
-
color: var(--accent);
1072
-
}
1073
-
1074
-
.bookmark-description {
1075
-
font-size: 0.9rem;
1076
-
color: var(--text-secondary);
1077
-
margin: 0;
1078
-
line-height: 1.5;
1079
-
}
1080
-
1081
-
.bookmark-meta {
1082
-
display: flex;
1083
-
align-items: center;
1084
-
gap: 12px;
1085
-
margin-top: 12px;
1086
-
font-size: 0.85rem;
1087
-
color: var(--text-tertiary);
1088
-
}
1089
-
1090
-
.bookmark-time {
1091
-
color: var(--text-tertiary);
1092
-
}
1093
-
1094
-
.composer {
1095
-
margin-bottom: 24px;
1096
-
}
1097
-
1098
-
.composer-textarea {
1099
-
width: 100%;
1100
-
min-height: 120px;
1101
-
padding: 16px;
1102
-
background: var(--bg-secondary);
1103
-
border: 1px solid var(--border);
1104
-
border-radius: var(--radius-md);
1105
-
color: var(--text-primary);
1106
-
font-size: 1rem;
1107
-
resize: vertical;
1108
-
transition: all 0.15s ease;
1109
-
}
1110
-
1111
-
.composer-textarea:focus {
1112
-
outline: none;
1113
-
border-color: var(--accent);
1114
-
box-shadow: 0 0 0 3px var(--accent-subtle);
1115
-
}
1116
-
1117
-
.composer-footer {
1118
-
display: flex;
1119
-
justify-content: space-between;
1120
-
align-items: center;
1121
-
margin-top: 12px;
1122
-
}
1123
-
1124
-
.composer-char-count {
1125
-
font-size: 0.85rem;
1126
-
color: var(--text-tertiary);
1127
-
}
1128
-
1129
-
.composer-char-count.warning {
1130
-
color: var(--warning);
1131
-
}
1132
-
1133
-
.composer-char-count.error {
1134
-
color: var(--error);
1135
-
}
1136
-
1137
-
.composer-add-quote {
1138
-
width: 100%;
1139
-
padding: 12px 16px;
1140
-
margin-bottom: 12px;
1141
-
background: var(--bg-tertiary);
1142
-
border: 1px dashed var(--border);
1143
-
border-radius: var(--radius-md);
1144
-
color: var(--text-secondary);
1145
-
font-size: 0.9rem;
1146
-
cursor: pointer;
1147
-
transition: all 0.15s ease;
1148
-
}
1149
-
1150
-
.composer-add-quote:hover {
1151
-
border-color: var(--accent);
1152
-
color: var(--accent);
1153
-
background: var(--accent-subtle);
1154
-
}
1155
-
1156
-
.composer-quote-input-wrapper {
1157
-
margin-bottom: 12px;
1158
-
}
1159
-
1160
-
.composer-quote-input {
1161
-
width: 100%;
1162
-
padding: 12px 16px;
1163
-
background: linear-gradient(
1164
-
135deg,
1165
-
rgba(79, 70, 229, 0.05),
1166
-
rgba(168, 85, 247, 0.05)
1167
-
);
1168
-
border: 1px solid var(--border);
1169
-
border-left: 3px solid var(--accent);
1170
-
border-radius: 0 var(--radius-md) var(--radius-md) 0;
1171
-
color: var(--text-primary);
1172
-
font-size: 0.95rem;
1173
-
font-style: italic;
1174
-
resize: vertical;
1175
-
font-family: inherit;
1176
-
transition: all 0.15s ease;
1177
-
}
1178
-
1179
-
.composer-quote-input:focus {
1180
-
outline: none;
1181
-
border-color: var(--accent);
1182
-
}
1183
-
1184
-
.composer-quote-input::placeholder {
1185
-
color: var(--text-tertiary);
1186
-
font-style: italic;
1187
-
}
1188
-
1189
-
.composer-quote-remove-btn {
1190
-
margin-top: 8px;
1191
-
padding: 6px 12px;
1192
-
background: none;
1193
-
border: none;
1194
-
color: var(--text-tertiary);
1195
-
font-size: 0.85rem;
1196
-
cursor: pointer;
1197
-
}
1198
-
1199
-
.composer-quote-remove-btn:hover {
1200
-
color: var(--error);
1201
-
}
1202
-
1203
-
@keyframes shimmer {
1204
-
0% {
1205
-
background-position: -200% 0;
1206
-
}
1207
-
1208
-
100% {
1209
-
background-position: 200% 0;
1210
-
}
1211
-
}
1212
-
1213
-
.skeleton {
1214
-
background: linear-gradient(
1215
-
90deg,
1216
-
var(--bg-tertiary) 25%,
1217
-
var(--bg-hover) 50%,
1218
-
var(--bg-tertiary) 75%
1219
-
);
1220
-
background-size: 200% 100%;
1221
-
animation: shimmer 1.5s infinite;
1222
-
border-radius: var(--radius-sm);
1223
-
}
1224
-
1225
-
.skeleton-text {
1226
-
height: 1em;
1227
-
margin-bottom: 8px;
1228
-
}
1229
-
1230
-
.skeleton-text:last-child {
1231
-
width: 60%;
1232
-
}
1233
-
1234
-
@media (max-width: 640px) {
1235
-
.main-content {
1236
-
padding: 16px 12px;
1237
-
}
1238
-
1239
-
.navbar-inner {
1240
-
padding: 0 16px;
1241
-
}
1242
-
1243
-
.page-title {
1244
-
font-size: 1.5rem;
1245
-
}
1246
-
1247
-
.url-input-container {
1248
-
flex-direction: column;
1249
-
}
1250
-
1251
-
.profile-header {
1252
-
flex-direction: column;
1253
-
text-align: center;
1254
-
}
1255
-
1256
-
.profile-stats {
1257
-
justify-content: center;
1258
-
}
1259
-
}
1260
-
1261
-
.main {
1262
-
flex: 1;
1263
-
width: 100%;
1264
-
}
1265
-
1266
-
.page-container {
1267
-
max-width: 680px;
1268
-
margin: 0 auto;
1269
-
padding: 24px 16px;
1270
-
}
1271
-
1272
-
.navbar-logo {
1273
-
width: 32px;
1274
-
height: 32px;
1275
-
background: linear-gradient(135deg, var(--accent), #8b5cf6);
1276
-
border-radius: var(--radius-sm);
1277
-
display: flex;
1278
-
align-items: center;
1279
-
justify-content: center;
1280
-
font-weight: 700;
1281
-
font-size: 1rem;
1282
-
color: white;
1283
-
}
1284
-
1285
-
.navbar-user {
1286
-
display: flex;
1287
-
align-items: center;
1288
-
gap: 8px;
1289
-
}
1290
-
1291
-
.navbar-avatar {
1292
-
width: 36px;
1293
-
height: 36px;
1294
-
border-radius: var(--radius-full);
1295
-
background: linear-gradient(135deg, var(--accent), #a855f7);
1296
-
display: flex;
1297
-
align-items: center;
1298
-
justify-content: center;
1299
-
font-weight: 600;
1300
-
font-size: 0.85rem;
1301
-
color: white;
1302
-
text-decoration: none;
1303
-
}
1304
-
1305
-
.btn-sm {
1306
-
padding: 6px 12px;
1307
-
font-size: 0.85rem;
1308
-
}
1309
-
1310
-
.composer-url {
1311
-
font-size: 0.85rem;
1312
-
color: var(--text-secondary);
1313
-
word-break: break-all;
1314
-
}
1315
-
1316
-
.composer-quote {
1317
-
position: relative;
1318
-
padding: 12px 16px;
1319
-
padding-right: 36px;
1320
-
background: var(--bg-secondary);
1321
-
border-left: 3px solid var(--accent);
1322
-
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
1323
-
margin-bottom: 16px;
1324
-
font-style: italic;
1325
-
color: var(--text-secondary);
1326
-
}
1327
-
1328
-
.composer-quote-remove {
1329
-
position: absolute;
1330
-
top: 8px;
1331
-
right: 8px;
1332
-
width: 24px;
1333
-
height: 24px;
1334
-
border-radius: var(--radius-full);
1335
-
background: var(--bg-tertiary);
1336
-
color: var(--text-secondary);
1337
-
font-size: 1rem;
1338
-
display: flex;
1339
-
align-items: center;
1340
-
justify-content: center;
1341
-
}
1342
-
1343
-
.composer-quote-remove:hover {
1344
-
background: var(--bg-hover);
1345
-
color: var(--text-primary);
1346
-
}
1347
-
1348
-
.composer-input {
1349
-
width: 100%;
1350
-
min-height: 120px;
1351
-
padding: 16px;
1352
-
background: var(--bg-secondary);
1353
-
border: 1px solid var(--border);
1354
-
border-radius: var(--radius-md);
1355
-
color: var(--text-primary);
1356
-
font-size: 1rem;
1357
-
resize: vertical;
1358
-
transition: all 0.15s ease;
1359
-
}
1360
-
1361
-
.composer-input:focus {
1362
-
outline: none;
1363
-
border-color: var(--accent);
1364
-
box-shadow: 0 0 0 3px var(--accent-subtle);
1365
-
}
1366
-
1367
-
.composer-input::placeholder {
1368
-
color: var(--text-tertiary);
1369
-
}
1370
-
1371
-
.composer-footer {
1372
-
display: flex;
1373
-
justify-content: space-between;
1374
-
align-items: center;
1375
-
margin-top: 12px;
1376
-
}
1377
-
1378
-
.composer-count {
1379
-
font-size: 0.85rem;
1380
-
color: var(--text-tertiary);
1381
-
}
1382
-
1383
-
.composer-actions {
1384
-
display: flex;
1385
-
gap: 8px;
1386
-
}
1387
-
1388
-
.composer-error {
1389
-
margin-top: 12px;
1390
-
padding: 12px;
1391
-
background: rgba(239, 68, 68, 0.1);
1392
-
border: 1px solid rgba(239, 68, 68, 0.3);
1393
-
border-radius: var(--radius-md);
1394
-
color: var(--error);
1395
-
font-size: 0.9rem;
1396
-
}
1397
-
1398
-
.annotation-detail-page {
1399
-
max-width: 680px;
1400
-
margin: 0 auto;
1401
-
padding: 24px 16px;
1402
-
}
1403
-
1404
-
.annotation-detail-header {
1405
-
margin-bottom: 24px;
1406
-
}
1407
-
1408
-
.back-link {
1409
-
color: var(--text-secondary);
1410
-
text-decoration: none;
1411
-
font-size: 0.9rem;
1412
-
}
1413
-
1414
-
.back-link:hover {
1415
-
color: var(--accent);
1416
-
}
1417
-
1418
-
.replies-section {
1419
-
margin-top: 32px;
1420
-
}
1421
-
1422
-
.replies-title {
1423
-
font-size: 1.1rem;
1424
-
font-weight: 600;
1425
-
margin-bottom: 16px;
1426
-
color: var(--text-primary);
1427
-
}
1428
-
1429
-
.reply-form {
1430
-
margin-bottom: 24px;
1431
-
}
1432
-
1433
-
.reply-input {
1434
-
width: 100%;
1435
-
padding: 12px;
1436
-
border: 1px solid var(--border);
1437
-
border-radius: var(--radius-md);
1438
-
font-size: 0.95rem;
1439
-
resize: vertical;
1440
-
margin-bottom: 12px;
1441
-
font-family: inherit;
1442
-
}
1443
-
1444
-
.reply-input:focus {
1445
-
outline: none;
1446
-
border-color: var(--accent);
1447
-
box-shadow: 0 0 0 3px var(--accent-subtle);
1448
-
}
1449
-
1450
-
.replies-list {
1451
-
display: flex;
1452
-
flex-direction: column;
1453
-
gap: 12px;
1454
-
}
1455
-
1456
-
.reply-card {
1457
-
padding: 16px;
1458
-
background: var(--bg-secondary);
1459
-
border-radius: var(--radius-md);
1460
-
border: 1px solid var(--border);
1461
-
}
1462
-
1463
-
.reply-header {
1464
-
display: flex;
1465
-
align-items: center;
1466
-
gap: 12px;
1467
-
margin-bottom: 12px;
1468
-
}
1469
-
1470
-
.reply-avatar-link {
1471
-
text-decoration: none;
1472
-
}
1473
-
1474
-
.reply-avatar {
1475
-
width: 36px;
1476
-
height: 36px;
1477
-
min-width: 36px;
1478
-
border-radius: var(--radius-full);
1479
-
background: linear-gradient(135deg, var(--accent), #a855f7);
1480
-
display: flex;
1481
-
align-items: center;
1482
-
justify-content: center;
1483
-
font-weight: 600;
1484
-
font-size: 0.85rem;
1485
-
color: white;
1486
-
overflow: hidden;
1487
-
}
1488
-
1489
-
.reply-avatar img {
1490
-
width: 100%;
1491
-
height: 100%;
1492
-
object-fit: cover;
1493
-
}
1494
-
1495
-
.reply-meta {
1496
-
flex: 1;
1497
-
min-width: 0;
1498
-
}
1499
-
1500
-
.reply-author {
1501
-
font-weight: 600;
1502
-
color: var(--text-primary);
1503
-
}
1504
-
1505
-
.reply-handle {
1506
-
font-size: 0.85rem;
1507
-
color: var(--text-tertiary);
1508
-
text-decoration: none;
1509
-
margin-left: 6px;
1510
-
}
1511
-
1512
-
.reply-handle:hover {
1513
-
color: var(--accent);
1514
-
text-decoration: underline;
1515
-
}
1516
-
1517
-
.reply-time {
1518
-
font-size: 0.85rem;
1519
-
color: var(--text-tertiary);
1520
-
white-space: nowrap;
1521
-
}
1522
-
1523
-
.reply-text {
1524
-
color: var(--text-primary);
1525
-
line-height: 1.5;
1526
-
margin: 0;
1527
-
}
1528
-
1529
-
.replies-title {
1530
-
display: flex;
1531
-
align-items: center;
1532
-
gap: 8px;
1533
-
}
1534
-
1535
-
.replies-title svg {
1536
-
color: var(--accent);
1537
-
}
1538
-
1539
-
.replies-list-threaded {
1540
-
display: flex;
1541
-
flex-direction: column;
1542
-
gap: 8px;
1543
-
}
1544
-
1545
-
.reply-card-threaded {
1546
-
padding: 16px;
1547
-
transition: background 0.15s ease;
1548
-
}
1549
-
1550
-
.reply-card-threaded .reply-header {
1551
-
margin-bottom: 8px;
1552
-
}
1553
-
1554
-
.reply-card-threaded .reply-meta {
1555
-
display: flex;
1556
-
align-items: center;
1557
-
gap: 6px;
1558
-
flex-wrap: wrap;
1559
-
}
1560
-
1561
-
.reply-dot {
1562
-
color: var(--text-tertiary);
1563
-
font-size: 0.75rem;
1564
-
}
1565
-
1566
-
.reply-actions {
1567
-
display: flex;
1568
-
gap: 4px;
1569
-
margin-left: auto;
1570
-
}
1571
-
1572
-
.reply-action-btn {
1573
-
background: none;
1574
-
border: none;
1575
-
padding: 4px 8px;
1576
-
color: var(--text-tertiary);
1577
-
cursor: pointer;
1578
-
border-radius: var(--radius-sm);
1579
-
transition: all 0.15s ease;
1580
-
display: flex;
1581
-
align-items: center;
1582
-
justify-content: center;
1583
-
}
1584
-
1585
-
.reply-action-btn:hover {
1586
-
color: var(--accent);
1587
-
background: var(--accent-subtle);
1588
-
}
1589
-
1590
-
.reply-action-delete:hover {
1591
-
color: var(--error);
1592
-
background: rgba(239, 68, 68, 0.1);
1593
-
}
1594
-
1595
-
.replying-to-banner {
1596
-
display: flex;
1597
-
align-items: center;
1598
-
justify-content: space-between;
1599
-
padding: 8px 12px;
1600
-
margin-bottom: 12px;
1601
-
background: var(--accent-subtle);
1602
-
border-radius: var(--radius-sm);
1603
-
font-size: 0.85rem;
1604
-
color: var(--text-secondary);
1605
-
}
1606
-
1607
-
.cancel-reply {
1608
-
background: none;
1609
-
border: none;
1610
-
font-size: 1.2rem;
1611
-
color: var(--text-tertiary);
1612
-
cursor: pointer;
1613
-
padding: 0 4px;
1614
-
line-height: 1;
1615
-
}
1616
-
1617
-
.cancel-reply:hover {
1618
-
color: var(--text-primary);
1619
-
}
1620
-
1621
-
.reply-form.card {
1622
-
padding: 16px;
1623
-
margin-bottom: 16px;
1624
-
}
1625
-
1626
-
.reply-form-actions {
1627
-
display: flex;
1628
-
justify-content: flex-end;
1629
-
}
1630
-
1631
-
.inline-replies {
1632
-
margin-top: 16px;
1633
-
padding-top: 16px;
1634
-
border-top: 1px solid var(--border);
1635
-
display: flex;
1636
-
flex-direction: column;
1637
-
gap: 16px;
1638
-
}
1639
-
1640
-
.main-reply-composer {
1641
-
margin-top: 16px;
1642
-
background: var(--bg-secondary);
1643
-
padding: 12px;
1644
-
border-radius: var(--radius-md);
1645
-
}
1646
-
1647
-
.reply-input {
1648
-
width: 100%;
1649
-
min-height: 80px;
1650
-
padding: 12px;
1651
-
border: 1px solid var(--border);
1652
-
border-radius: var(--radius-md);
1653
-
background: var(--bg-card);
1654
-
color: var(--text-primary);
1655
-
font-family: inherit;
1656
-
font-size: 0.95rem;
1657
-
resize: vertical;
1658
-
display: block;
1659
-
}
1660
-
1661
-
.reply-input:focus {
1662
-
border-color: var(--accent);
1663
-
outline: none;
1664
-
}
1665
-
1666
-
.reply-input.small {
1667
-
min-height: 60px;
1668
-
font-size: 0.9rem;
1669
-
margin-bottom: 8px;
1670
-
}
1671
-
1672
-
.composer-actions {
1673
-
display: flex;
1674
-
justify-content: flex-end;
1675
-
}
1676
-
1677
-
.btn-block {
1678
-
width: 100%;
1679
-
text-align: left;
1680
-
padding: 8px 12px;
1681
-
color: var(--text-secondary);
1682
-
background: var(--bg-tertiary);
1683
-
border-radius: var(--radius-md);
1684
-
margin-top: 8px;
1685
-
font-size: 0.9rem;
1686
-
cursor: pointer;
1687
-
transition: all 0.2s;
1688
-
}
1689
-
1690
-
.btn-block:hover {
1691
-
background: var(--border);
1692
-
color: var(--text-primary);
1693
-
}
1694
-
1695
-
.annotation-action.active {
1696
-
color: var(--accent);
1697
-
}
1698
-
1699
-
.new-page {
1700
-
max-width: 600px;
1701
-
margin: 0 auto;
1702
-
display: flex;
1703
-
flex-direction: column;
1704
-
gap: 32px;
1705
-
}
1706
-
1707
-
.loading-spinner {
1708
-
width: 32px;
1709
-
height: 32px;
1710
-
border: 3px solid var(--border);
1711
-
border-top-color: var(--accent);
1712
-
border-radius: 50%;
1713
-
animation: spin 0.8s linear infinite;
1714
-
margin: 60px auto;
1715
-
}
1716
-
1717
-
@keyframes spin {
1718
-
to {
1719
-
transform: rotate(360deg);
1720
-
}
1721
-
}
1722
-
1723
-
.navbar {
1724
-
position: sticky;
1725
-
top: 0;
1726
-
z-index: 1000;
1727
-
background: rgba(12, 10, 20, 0.95);
1728
-
backdrop-filter: blur(12px);
1729
-
-webkit-backdrop-filter: blur(12px);
1730
-
border-bottom: 1px solid var(--border);
1731
-
}
1732
-
1733
-
.navbar-inner {
1734
-
max-width: 1200px;
1735
-
margin: 0 auto;
1736
-
padding: 12px 24px;
1737
-
display: flex;
1738
-
align-items: center;
1739
-
justify-content: space-between;
1740
-
gap: 24px;
1741
-
}
1742
-
1743
-
.navbar-brand {
1744
-
display: flex;
1745
-
align-items: center;
1746
-
gap: 10px;
1747
-
text-decoration: none;
1748
-
flex-shrink: 0;
1749
-
}
1750
-
1751
-
.navbar-logo {
1752
-
width: 32px;
1753
-
height: 32px;
1754
-
background: linear-gradient(135deg, var(--accent), #8b5cf6);
1755
-
border-radius: 8px;
1756
-
display: flex;
1757
-
align-items: center;
1758
-
justify-content: center;
1759
-
font-weight: 700;
1760
-
font-size: 1rem;
1761
-
color: white;
1762
-
}
1763
-
1764
-
.navbar-title {
1765
-
font-weight: 700;
1766
-
font-size: 1.25rem;
1767
-
color: var(--text-primary);
1768
-
}
1769
-
1770
-
.navbar-center {
1771
-
display: flex;
1772
-
align-items: center;
1773
-
gap: 8px;
1774
-
background: var(--bg-tertiary);
1775
-
padding: 4px;
1776
-
border-radius: var(--radius-lg);
1777
-
}
1778
-
1779
-
.navbar-link {
1780
-
display: flex;
1781
-
align-items: center;
1782
-
gap: 6px;
1783
-
padding: 8px 16px;
1784
-
font-size: 0.9rem;
1785
-
font-weight: 500;
1786
-
color: var(--text-secondary);
1787
-
text-decoration: none;
1788
-
border-radius: var(--radius-md);
1789
-
transition: all 0.15s ease;
1790
-
}
1791
-
1792
-
.navbar-link:hover {
1793
-
color: var(--text-primary);
1794
-
background: var(--bg-hover);
1795
-
}
1796
-
1797
-
.navbar-link.active {
1798
-
color: var(--text-primary);
1799
-
background: var(--bg-card);
1800
-
box-shadow: var(--shadow-sm);
1801
-
}
1802
-
1803
-
.navbar-right {
1804
-
display: flex;
1805
-
align-items: center;
1806
-
gap: 12px;
1807
-
flex-shrink: 0;
1808
-
}
1809
-
1810
-
.navbar-icon-link {
1811
-
display: flex;
1812
-
align-items: center;
1813
-
justify-content: center;
1814
-
width: 36px;
1815
-
height: 36px;
1816
-
color: var(--text-tertiary);
1817
-
border-radius: var(--radius-md);
1818
-
transition: all 0.15s ease;
1819
-
}
1820
-
1821
-
.navbar-icon-link:hover {
1822
-
color: var(--text-primary);
1823
-
background: var(--bg-tertiary);
1824
-
}
1825
-
1826
-
.navbar-icon-link.active {
1827
-
color: var(--accent);
1828
-
background: var(--accent-subtle);
1829
-
}
1830
-
1831
-
.navbar-new-btn {
1832
-
display: flex;
1833
-
align-items: center;
1834
-
gap: 6px;
1835
-
padding: 8px 14px;
1836
-
background: linear-gradient(135deg, var(--accent), #8b5cf6);
1837
-
color: white;
1838
-
font-size: 0.85rem;
1839
-
font-weight: 600;
1840
-
text-decoration: none;
1841
-
border-radius: var(--radius-full);
1842
-
transition: all 0.2s ease;
1843
-
}
1844
-
1845
-
.navbar-new-btn:hover {
1846
-
transform: translateY(-1px);
1847
-
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
1848
-
color: white;
1849
-
}
1850
-
1851
-
.navbar-user-section {
1852
-
display: flex;
1853
-
align-items: center;
1854
-
gap: 4px;
1855
-
}
1856
-
1857
-
.navbar-avatar {
1858
-
width: 32px;
1859
-
height: 32px;
1860
-
border-radius: var(--radius-full);
1861
-
background: linear-gradient(135deg, var(--accent), #a855f7);
1862
-
display: flex;
1863
-
align-items: center;
1864
-
justify-content: center;
1865
-
font-weight: 600;
1866
-
font-size: 0.75rem;
1867
-
color: white;
1868
-
text-decoration: none;
1869
-
transition: transform 0.15s ease;
1870
-
}
1871
-
1872
-
.navbar-avatar:hover {
1873
-
transform: scale(1.05);
1874
-
}
1875
-
1876
-
.navbar-logout {
1877
-
width: 24px;
1878
-
height: 24px;
1879
-
border: none;
1880
-
background: transparent;
1881
-
color: var(--text-tertiary);
1882
-
font-size: 1.25rem;
1883
-
cursor: pointer;
1884
-
border-radius: var(--radius-sm);
1885
-
transition: all 0.15s ease;
1886
-
display: flex;
1887
-
align-items: center;
1888
-
justify-content: center;
1889
-
}
1890
-
1891
-
.navbar-logout:hover {
1892
-
color: var(--error);
1893
-
background: rgba(239, 68, 68, 0.1);
1894
-
}
1895
-
1896
-
.navbar-signin {
1897
-
padding: 8px 16px;
1898
-
background: var(--accent);
1899
-
color: white;
1900
-
font-size: 0.9rem;
1901
-
font-weight: 500;
1902
-
text-decoration: none;
1903
-
border-radius: var(--radius-full);
1904
-
transition: all 0.15s ease;
1905
-
}
1906
-
1907
-
.navbar-signin:hover {
1908
-
background: var(--accent-hover);
1909
-
color: white;
1910
-
}
1911
-
1912
-
.navbar-user-menu {
1913
-
position: relative;
1914
-
}
1915
-
1916
-
.navbar-avatar-btn {
1917
-
width: 36px;
1918
-
height: 36px;
1919
-
border-radius: var(--radius-full);
1920
-
background: linear-gradient(135deg, var(--accent), #a855f7);
1921
-
border: none;
1922
-
cursor: pointer;
1923
-
overflow: hidden;
1924
-
display: flex;
1925
-
align-items: center;
1926
-
justify-content: center;
1927
-
transition:
1928
-
transform 0.15s ease,
1929
-
box-shadow 0.15s ease;
1930
-
}
1931
-
1932
-
.navbar-avatar-btn:hover {
1933
-
transform: scale(1.05);
1934
-
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
1935
-
}
1936
-
1937
-
.navbar-avatar-img {
1938
-
width: 100%;
1939
-
height: 100%;
1940
-
object-fit: cover;
1941
-
}
1942
-
1943
-
.navbar-avatar-text {
1944
-
font-weight: 600;
1945
-
font-size: 0.75rem;
1946
-
color: white;
1947
-
}
1948
-
1949
-
.navbar-dropdown {
1950
-
position: absolute;
1951
-
top: calc(100% + 8px);
1952
-
right: 0;
1953
-
min-width: 200px;
1954
-
background: var(--bg-card);
1955
-
border: 1px solid var(--border);
1956
-
border-radius: var(--radius-lg);
1957
-
box-shadow: var(--shadow-lg);
1958
-
overflow: hidden;
1959
-
z-index: 1001;
1960
-
animation: dropdownFade 0.15s ease;
1961
-
}
1962
-
1963
-
@keyframes dropdownFade {
1964
-
from {
1965
-
opacity: 0;
1966
-
transform: translateY(-8px);
1967
-
}
1968
-
1969
-
to {
1970
-
opacity: 1;
1971
-
transform: translateY(0);
1972
-
}
1973
-
}
1974
-
1975
-
.navbar-dropdown-header {
1976
-
padding: 12px 16px;
1977
-
background: var(--bg-secondary);
1978
-
}
1979
-
1980
-
.navbar-dropdown-name {
1981
-
display: block;
1982
-
font-weight: 600;
1983
-
color: var(--text-primary);
1984
-
font-size: 0.9rem;
1985
-
}
1986
-
1987
-
.navbar-dropdown-handle {
1988
-
display: block;
1989
-
color: var(--text-tertiary);
1990
-
font-size: 0.8rem;
1991
-
margin-top: 2px;
1992
-
}
1993
-
1994
-
.navbar-dropdown-divider {
1995
-
height: 1px;
1996
-
background: var(--border);
1997
-
}
1998
-
1999
-
.navbar-dropdown-item {
2000
-
display: flex;
2001
-
align-items: center;
2002
-
gap: 10px;
2003
-
width: 100%;
2004
-
padding: 12px 16px;
2005
-
font-size: 0.9rem;
2006
-
color: var(--text-primary);
2007
-
text-decoration: none;
2008
-
background: none;
2009
-
border: none;
2010
-
cursor: pointer;
2011
-
transition: background 0.15s ease;
2012
-
text-align: left;
2013
-
}
2014
-
2015
-
.navbar-dropdown-item:hover {
2016
-
background: var(--bg-tertiary);
2017
-
}
2018
-
2019
-
.navbar-dropdown-logout {
2020
-
color: var(--error);
2021
-
border-top: 1px solid var(--border);
2022
-
}
2023
-
2024
-
.navbar-dropdown-logout:hover {
2025
-
background: rgba(239, 68, 68, 0.1);
2026
-
}
2027
-
2028
-
@media (max-width: 768px) {
2029
-
.navbar-inner {
2030
-
padding: 10px 16px;
2031
-
}
2032
-
2033
-
.navbar-title {
2034
-
display: none;
2035
-
}
2036
-
2037
-
.navbar-center {
2038
-
display: none;
2039
-
}
2040
-
2041
-
.navbar-new-btn span {
2042
-
display: none;
2043
-
}
2044
-
2045
-
.navbar-new-btn {
2046
-
width: 36px;
2047
-
height: 36px;
2048
-
padding: 0;
2049
-
justify-content: center;
2050
-
}
2051
-
}
2052
-
2053
-
.collections-list {
2054
-
display: flex;
2055
-
flex-direction: column;
2056
-
gap: 2px;
2057
-
background: var(--bg-card);
2058
-
border: 1px solid var(--border);
2059
-
border-radius: var(--radius-lg);
2060
-
overflow: hidden;
2061
-
}
2062
-
2063
-
.collection-row {
2064
-
display: flex;
2065
-
align-items: center;
2066
-
background: var(--bg-card);
2067
-
transition: background 0.15s ease;
2068
-
}
2069
-
2070
-
.collection-row:not(:last-child) {
2071
-
border-bottom: 1px solid var(--border);
2072
-
}
2073
-
2074
-
.collection-row:hover {
2075
-
background: var(--bg-secondary);
2076
-
}
2077
-
2078
-
.collection-row-content {
2079
-
flex: 1;
2080
-
display: flex;
2081
-
align-items: center;
2082
-
gap: 16px;
2083
-
padding: 16px 20px;
2084
-
text-decoration: none;
2085
-
min-width: 0;
2086
-
}
2087
-
2088
-
.collection-row-icon {
2089
-
width: 44px;
2090
-
height: 44px;
2091
-
min-width: 44px;
2092
-
display: flex;
2093
-
align-items: center;
2094
-
justify-content: center;
2095
-
background: linear-gradient(
2096
-
135deg,
2097
-
rgba(79, 70, 229, 0.1),
2098
-
rgba(168, 85, 247, 0.15)
2099
-
);
2100
-
color: var(--accent);
2101
-
border-radius: var(--radius-md);
2102
-
transition: all 0.2s ease;
2103
-
}
2104
-
2105
-
.collection-row:hover .collection-row-icon {
2106
-
background: linear-gradient(
2107
-
135deg,
2108
-
rgba(79, 70, 229, 0.15),
2109
-
rgba(168, 85, 247, 0.2)
2110
-
);
2111
-
transform: scale(1.05);
2112
-
}
2113
-
2114
-
.collection-row-info {
2115
-
flex: 1;
2116
-
min-width: 0;
2117
-
}
2118
-
2119
-
.collection-row-name {
2120
-
font-size: 1rem;
2121
-
font-weight: 600;
2122
-
color: var(--text-primary);
2123
-
margin: 0 0 2px 0;
2124
-
white-space: nowrap;
2125
-
overflow: hidden;
2126
-
text-overflow: ellipsis;
2127
-
}
2128
-
2129
-
.collection-row:hover .collection-row-name {
2130
-
color: var(--accent);
2131
-
}
2132
-
2133
-
.collection-row-desc {
2134
-
font-size: 0.85rem;
2135
-
color: var(--text-secondary);
2136
-
margin: 0;
2137
-
white-space: nowrap;
2138
-
overflow: hidden;
2139
-
text-overflow: ellipsis;
2140
-
}
2141
-
2142
-
.collection-row-arrow {
2143
-
color: var(--text-tertiary);
2144
-
opacity: 0;
2145
-
transition: all 0.2s ease;
2146
-
}
2147
-
2148
-
.collection-row:hover .collection-row-arrow {
2149
-
opacity: 1;
2150
-
color: var(--accent);
2151
-
transform: translateX(2px);
2152
-
}
2153
-
2154
-
.collection-row-edit {
2155
-
padding: 10px;
2156
-
margin-right: 12px;
2157
-
color: var(--text-tertiary);
2158
-
background: none;
2159
-
border: none;
2160
-
border-radius: var(--radius-sm);
2161
-
cursor: pointer;
2162
-
opacity: 0;
2163
-
transition: all 0.15s ease;
2164
-
}
2165
-
2166
-
.collection-row:hover .collection-row-edit {
2167
-
opacity: 1;
2168
-
}
2169
-
2170
-
.collection-row-edit:hover {
2171
-
color: var(--text-primary);
2172
-
background: var(--bg-tertiary);
2173
-
}
2174
-
2175
-
.back-link {
2176
-
display: inline-flex;
2177
-
align-items: center;
2178
-
gap: 6px;
2179
-
color: var(--text-tertiary);
2180
-
font-size: 0.9rem;
2181
-
font-weight: 500;
2182
-
text-decoration: none;
2183
-
margin-bottom: 24px;
2184
-
transition: color 0.15s ease;
2185
-
}
2186
-
2187
-
.back-link:hover {
2188
-
color: var(--accent);
2189
-
}
2190
-
2191
-
.collection-detail-header {
2192
-
display: flex;
2193
-
gap: 20px;
2194
-
padding: 24px;
2195
-
background: var(--bg-card);
2196
-
border: 1px solid var(--border);
2197
-
border-radius: var(--radius-lg);
2198
-
margin-bottom: 32px;
2199
-
position: relative;
2200
-
}
2201
-
2202
-
.collection-detail-icon {
2203
-
width: 56px;
2204
-
height: 56px;
2205
-
min-width: 56px;
2206
-
display: flex;
2207
-
align-items: center;
2208
-
justify-content: center;
2209
-
background: linear-gradient(
2210
-
135deg,
2211
-
rgba(79, 70, 229, 0.1),
2212
-
rgba(168, 85, 247, 0.1)
2213
-
);
2214
-
color: var(--accent);
2215
-
border-radius: var(--radius-md);
2216
-
}
2217
-
2218
-
.collection-detail-info {
2219
-
flex: 1;
2220
-
min-width: 0;
2221
-
}
2222
-
2223
-
.collection-detail-visibility {
2224
-
display: flex;
2225
-
align-items: center;
2226
-
gap: 6px;
2227
-
font-size: 0.8rem;
2228
-
font-weight: 600;
2229
-
color: var(--accent);
2230
-
text-transform: capitalize;
2231
-
margin-bottom: 8px;
2232
-
}
2233
-
2234
-
.collection-detail-title {
2235
-
font-size: 1.5rem;
2236
-
font-weight: 700;
2237
-
color: var(--text-primary);
2238
-
margin-bottom: 8px;
2239
-
line-height: 1.3;
2240
-
}
2241
-
2242
-
.collection-detail-desc {
2243
-
color: var(--text-secondary);
2244
-
font-size: 1rem;
2245
-
line-height: 1.5;
2246
-
margin-bottom: 12px;
2247
-
max-width: 600px;
2248
-
}
2249
-
2250
-
.collection-detail-stats {
2251
-
display: flex;
2252
-
align-items: center;
2253
-
gap: 8px;
2254
-
font-size: 0.85rem;
2255
-
color: var(--text-tertiary);
2256
-
}
2257
-
2258
-
.collection-detail-actions {
2259
-
position: absolute;
2260
-
top: 20px;
2261
-
right: 20px;
2262
-
display: flex;
2263
-
align-items: center;
2264
-
gap: 8px;
2265
-
}
2266
-
2267
-
.collection-detail-actions .share-menu-container {
2268
-
display: flex;
2269
-
align-items: center;
2270
-
}
2271
-
2272
-
.collection-detail-actions .annotation-action {
2273
-
padding: 10px;
2274
-
color: var(--text-tertiary);
2275
-
background: none;
2276
-
border: none;
2277
-
border-radius: var(--radius-sm);
2278
-
cursor: pointer;
2279
-
transition: all 0.15s ease;
2280
-
}
2281
-
2282
-
.collection-detail-actions .annotation-action:hover {
2283
-
color: var(--accent);
2284
-
background: var(--bg-tertiary);
2285
-
}
2286
-
2287
-
.collection-detail-edit,
2288
-
.collection-detail-delete {
2289
-
padding: 10px;
2290
-
color: var(--text-tertiary);
2291
-
background: none;
2292
-
border: none;
2293
-
border-radius: var(--radius-sm);
2294
-
cursor: pointer;
2295
-
transition: all 0.15s ease;
2296
-
}
2297
-
2298
-
.collection-detail-edit:hover {
2299
-
color: var(--accent);
2300
-
background: var(--bg-tertiary);
2301
-
}
2302
-
2303
-
.collection-detail-delete:hover {
2304
-
color: var(--error);
2305
-
background: rgba(239, 68, 68, 0.1);
2306
-
}
2307
-
2308
-
.collection-item-wrapper {
2309
-
position: relative;
2310
-
}
2311
-
2312
-
.collection-item-remove {
2313
-
position: absolute;
2314
-
top: 12px;
2315
-
left: -40px;
2316
-
z-index: 10;
2317
-
padding: 8px;
2318
-
background: var(--bg-card);
2319
-
border: 1px solid var(--border);
2320
-
border-radius: var(--radius-sm);
2321
-
color: var(--text-tertiary);
2322
-
cursor: pointer;
2323
-
opacity: 0;
2324
-
transition: all 0.15s ease;
2325
-
}
2326
-
2327
-
.collection-item-wrapper:hover .collection-item-remove {
2328
-
opacity: 1;
2329
-
}
2330
-
2331
-
.collection-item-remove:hover {
2332
-
color: var(--error);
2333
-
border-color: var(--error);
2334
-
background: rgba(239, 68, 68, 0.05);
2335
-
}
2336
-
2337
-
.modal-overlay {
2338
-
position: fixed;
2339
-
inset: 0;
2340
-
background: rgba(0, 0, 0, 0.5);
2341
-
display: flex;
2342
-
align-items: center;
2343
-
justify-content: center;
2344
-
padding: 16px;
2345
-
z-index: 50;
2346
-
animation: fadeIn 0.2s ease-out;
2347
-
}
2348
-
2349
-
.modal-container {
2350
-
background: var(--bg-secondary);
2351
-
border-radius: var(--radius-lg);
2352
-
width: 100%;
2353
-
max-width: 28rem;
2354
-
border: 1px solid var(--border);
2355
-
box-shadow: var(--shadow-lg);
2356
-
animation: zoomIn 0.2s ease-out;
2357
-
}
2358
-
2359
-
.modal-header {
2360
-
display: flex;
2361
-
align-items: center;
2362
-
justify-content: space-between;
2363
-
padding: 16px;
2364
-
border-bottom: 1px solid var(--border);
2365
-
}
2366
-
2367
-
.modal-title {
2368
-
font-size: 1.25rem;
2369
-
font-weight: 700;
2370
-
color: var(--text-primary);
2371
-
}
2372
-
2373
-
.modal-close-btn {
2374
-
padding: 8px;
2375
-
color: var(--text-tertiary);
2376
-
border-radius: var(--radius-md);
2377
-
transition: color 0.15s;
2378
-
}
2379
-
2380
-
.modal-close-btn:hover {
2381
-
color: var(--text-primary);
2382
-
background: var(--bg-hover);
2383
-
}
2384
-
2385
-
.modal-form {
2386
-
padding: 16px;
2387
-
display: flex;
2388
-
flex-direction: column;
2389
-
gap: 16px;
2390
-
}
2391
-
2392
-
.icon-picker-tabs {
2393
-
display: flex;
2394
-
gap: 4px;
2395
-
margin-bottom: 12px;
2396
-
}
2397
-
2398
-
.icon-picker-tab {
2399
-
flex: 1;
2400
-
padding: 8px 12px;
2401
-
background: var(--bg-primary);
2402
-
border: 1px solid var(--border);
2403
-
border-radius: var(--radius-md);
2404
-
color: var(--text-secondary);
2405
-
font-size: 0.85rem;
2406
-
font-weight: 500;
2407
-
cursor: pointer;
2408
-
transition: all 0.15s ease;
2409
-
}
2410
-
2411
-
.icon-picker-tab:hover {
2412
-
background: var(--bg-tertiary);
2413
-
}
2414
-
2415
-
.icon-picker-tab.active {
2416
-
background: var(--accent);
2417
-
border-color: var(--accent);
2418
-
color: white;
2419
-
}
2420
-
2421
-
.emoji-picker-wrapper {
2422
-
display: flex;
2423
-
flex-direction: column;
2424
-
gap: 10px;
2425
-
}
2426
-
2427
-
.emoji-custom-input input {
2428
-
width: 100%;
2429
-
}
2430
-
2431
-
.emoji-picker,
2432
-
.icon-picker {
2433
-
display: flex;
2434
-
flex-wrap: wrap;
2435
-
gap: 4px;
2436
-
max-height: 120px;
2437
-
overflow-y: auto;
2438
-
padding: 8px;
2439
-
background: var(--bg-primary);
2440
-
border: 1px solid var(--border);
2441
-
border-radius: var(--radius-md);
2442
-
}
2443
-
2444
-
.emoji-option,
2445
-
.icon-option {
2446
-
width: 36px;
2447
-
height: 36px;
2448
-
display: flex;
2449
-
align-items: center;
2450
-
justify-content: center;
2451
-
font-size: 1.2rem;
2452
-
background: transparent;
2453
-
border: 2px solid transparent;
2454
-
border-radius: var(--radius-sm);
2455
-
cursor: pointer;
2456
-
transition: all 0.15s ease;
2457
-
color: var(--text-secondary);
2458
-
}
2459
-
2460
-
.emoji-option:hover,
2461
-
.icon-option:hover {
2462
-
background: var(--bg-tertiary);
2463
-
transform: scale(1.1);
2464
-
color: var(--text-primary);
2465
-
}
2466
-
2467
-
.emoji-option.selected,
2468
-
.icon-option.selected {
2469
-
border-color: var(--accent);
2470
-
background: var(--accent-subtle);
2471
-
color: var(--accent);
2472
-
}
2473
-
2474
-
.form-group {
2475
-
margin-bottom: 0;
2476
-
}
2477
-
2478
-
.form-label {
2479
-
display: block;
2480
-
font-size: 0.875rem;
2481
-
font-weight: 500;
2482
-
color: var(--text-secondary);
2483
-
margin-bottom: 4px;
2484
-
}
2485
-
2486
-
.form-input,
2487
-
.form-textarea,
2488
-
.form-select {
2489
-
width: 100%;
2490
-
padding: 8px 12px;
2491
-
background: var(--bg-primary);
2492
-
border: 1px solid var(--border);
2493
-
border-radius: var(--radius-md);
2494
-
color: var(--text-primary);
2495
-
transition: all 0.15s;
2496
-
}
2497
-
2498
-
.form-input:focus,
2499
-
.form-textarea:focus,
2500
-
.form-select:focus {
2501
-
outline: none;
2502
-
border-color: var(--accent);
2503
-
box-shadow: 0 0 0 2px var(--accent-subtle);
2504
-
}
2505
-
2506
-
.form-textarea {
2507
-
resize: none;
2508
-
}
2509
-
2510
-
.modal-actions {
2511
-
display: flex;
2512
-
justify-content: flex-end;
2513
-
gap: 12px;
2514
-
padding-top: 8px;
2515
-
}
2516
-
2517
-
@keyframes fadeIn {
2518
-
from {
2519
-
opacity: 0;
2520
-
}
2521
-
2522
-
to {
2523
-
opacity: 1;
2524
-
}
2525
-
}
2526
-
2527
-
@keyframes zoomIn {
2528
-
from {
2529
-
opacity: 0;
2530
-
transform: scale(0.95);
2531
-
}
2532
-
2533
-
to {
2534
-
opacity: 1;
2535
-
transform: scale(1);
2536
-
}
2537
-
}
2538
-
2539
-
.annotation-detail-page {
2540
-
max-width: 680px;
2541
-
margin: 0 auto;
2542
-
padding: 24px 16px;
2543
-
}
2544
-
2545
-
.annotation-detail-header {
2546
-
margin-bottom: 24px;
2547
-
}
2548
-
2549
-
.back-link {
2550
-
display: inline-flex;
2551
-
align-items: center;
2552
-
gap: 8px;
2553
-
color: var(--text-secondary);
2554
-
font-size: 0.9rem;
2555
-
transition: color 0.15s;
2556
-
}
2557
-
2558
-
.back-link:hover {
2559
-
color: var(--text-primary);
2560
-
}
2561
-
2562
-
.text-secondary {
2563
-
color: var(--text-secondary);
2564
-
}
2565
-
2566
-
.text-error {
2567
-
color: var(--error);
2568
-
}
2569
-
2570
-
.text-center {
2571
-
text-align: center;
2572
-
}
2573
-
2574
-
.flex {
2575
-
display: flex;
2576
-
}
2577
-
2578
-
.items-center {
2579
-
align-items: center;
2580
-
}
2581
-
2582
-
.justify-center {
2583
-
justify-content: center;
2584
-
}
2585
-
2586
-
.justify-end {
2587
-
justify-content: flex-end;
2588
-
}
2589
-
2590
-
.gap-2 {
2591
-
gap: 8px;
2592
-
}
2593
-
2594
-
.gap-3 {
2595
-
gap: 12px;
2596
-
}
2597
-
2598
-
.mt-3 {
2599
-
margin-top: 12px;
2600
-
}
2601
-
2602
-
.mb-6 {
2603
-
margin-bottom: 24px;
2604
-
}
2605
-
2606
-
.btn-text {
2607
-
background: none;
2608
-
border: none;
2609
-
color: var(--text-secondary);
2610
-
font-size: 0.9rem;
2611
-
padding: 8px 12px;
2612
-
cursor: pointer;
2613
-
transition: color 0.15s;
2614
-
}
2615
-
2616
-
.btn-text:hover {
2617
-
color: var(--text-primary);
2618
-
}
2619
-
2620
-
.btn-sm {
2621
-
padding: 6px 12px;
2622
-
font-size: 0.85rem;
2623
-
}
2624
-
2625
-
.annotation-edit-btn {
2626
-
background: none;
2627
-
border: none;
2628
-
cursor: pointer;
2629
-
padding: 6px 8px;
2630
-
color: var(--text-tertiary);
2631
-
border-radius: var(--radius-sm);
2632
-
transition: all 0.15s ease;
2633
-
}
2634
-
2635
-
.annotation-edit-btn:hover {
2636
-
color: var(--accent);
2637
-
background: var(--accent-subtle);
2638
-
}
2639
-
2640
-
.spinner {
2641
-
width: 32px;
2642
-
height: 32px;
2643
-
border: 3px solid var(--border);
2644
-
border-top-color: var(--accent);
2645
-
border-radius: 50%;
2646
-
animation: spin 0.8s linear infinite;
2647
-
}
2648
-
2649
-
.spinner-sm {
2650
-
width: 16px;
2651
-
height: 16px;
2652
-
border-width: 2px;
2653
-
}
2654
-
2655
-
@keyframes spin {
2656
-
to {
2657
-
transform: rotate(360deg);
2658
-
}
2659
-
}
2660
-
2661
-
.collection-list-item {
2662
-
width: 100%;
2663
-
text-align: left;
2664
-
padding: 12px 16px;
2665
-
border-radius: var(--radius-md);
2666
-
background: var(--bg-primary);
2667
-
border: 1px solid transparent;
2668
-
color: var(--text-primary);
2669
-
transition: all 0.15s ease;
2670
-
display: flex;
2671
-
align-items: center;
2672
-
justify-content: space-between;
2673
-
cursor: pointer;
2674
-
}
2675
-
2676
-
.collection-list-item:hover {
2677
-
background: var(--bg-hover);
2678
-
border-color: var(--border);
2679
-
}
2680
-
2681
-
.collection-list-item:hover .collection-list-item-icon {
2682
-
opacity: 1;
2683
-
}
2684
-
2685
-
.collection-list-item:disabled {
2686
-
opacity: 0.6;
2687
-
cursor: not-allowed;
2688
-
}
2689
-
2690
-
.item-delete-overlay {
2691
-
position: absolute;
2692
-
top: 16px;
2693
-
right: 16px;
2694
-
z-index: 10;
2695
-
opacity: 0;
2696
-
transition: opacity 0.15s ease;
2697
-
}
2698
-
2699
-
.card:hover .item-delete-overlay,
2700
-
div:hover > .item-delete-overlay {
2701
-
opacity: 1;
2702
-
}
2703
-
2704
-
.btn-icon-danger {
2705
-
padding: 8px;
2706
-
background: var(--error);
2707
-
color: white;
2708
-
border: none;
2709
-
border-radius: var(--radius-md);
2710
-
cursor: pointer;
2711
-
box-shadow: var(--shadow-md);
2712
-
transition: all 0.15s ease;
2713
-
display: flex;
2714
-
align-items: center;
2715
-
justify-content: center;
2716
-
}
2717
-
2718
-
.btn-icon-danger:hover {
2719
-
background: #dc2626;
2720
-
transform: scale(1.05);
2721
-
}
2722
-
2723
-
.action-buttons {
2724
-
display: flex;
2725
-
gap: 8px;
2726
-
}
2727
-
2728
-
.action-buttons-end {
2729
-
display: flex;
2730
-
justify-content: flex-end;
2731
-
gap: 8px;
2732
-
}
2733
-
2734
-
.filter-tab {
2735
-
padding: 8px 16px;
2736
-
font-size: 0.9rem;
2737
-
font-weight: 500;
2738
-
color: var(--text-secondary);
2739
-
background: transparent;
2740
-
border: none;
2741
-
border-radius: var(--radius-md);
2742
-
cursor: pointer;
2743
-
transition: all 0.15s ease;
2744
-
}
2745
-
2746
-
.filter-tab:hover {
2747
-
color: var(--text-primary);
2748
-
background: var(--bg-hover);
2749
-
}
2750
-
2751
-
.filter-tab.active {
2752
-
color: var(--text-primary);
2753
-
background: var(--bg-card);
2754
-
box-shadow: var(--shadow-sm);
2755
-
}
2756
-
2757
-
.inline-reply {
2758
-
padding: 12px 16px;
2759
-
border-bottom: 1px solid var(--border);
2760
-
}
2761
-
2762
-
.inline-reply:last-child {
2763
-
border-bottom: none;
2764
-
}
2765
-
2766
-
.inline-reply-avatar {
2767
-
width: 28px;
2768
-
height: 28px;
2769
-
min-width: 28px;
2770
-
border-radius: var(--radius-full);
2771
-
background: linear-gradient(135deg, var(--accent), #a855f7);
2772
-
display: flex;
2773
-
align-items: center;
2774
-
justify-content: center;
2775
-
font-weight: 600;
2776
-
font-size: 0.7rem;
2777
-
color: white;
2778
-
overflow: hidden;
2779
-
}
2780
-
2781
-
.inline-reply-avatar img,
2782
-
.inline-reply-avatar-placeholder {
2783
-
width: 100%;
2784
-
height: 100%;
2785
-
object-fit: cover;
2786
-
}
2787
-
2788
-
.inline-reply-avatar-placeholder {
2789
-
display: flex;
2790
-
align-items: center;
2791
-
justify-content: center;
2792
-
font-weight: 600;
2793
-
font-size: 0.7rem;
2794
-
color: white;
2795
-
}
2796
-
2797
-
.inline-reply-content {
2798
-
flex: 1;
2799
-
min-width: 0;
2800
-
}
2801
-
2802
-
.inline-reply-header {
2803
-
display: flex;
2804
-
align-items: center;
2805
-
gap: 8px;
2806
-
margin-bottom: 4px;
2807
-
}
2808
-
2809
-
.inline-reply-author {
2810
-
font-weight: 600;
2811
-
font-size: 0.85rem;
2812
-
color: var(--text-primary);
2813
-
}
2814
-
2815
-
.inline-reply-handle {
2816
-
color: var(--text-tertiary);
2817
-
font-size: 0.8rem;
2818
-
text-decoration: none;
2819
-
}
2820
-
2821
-
.inline-reply-time {
2822
-
color: var(--text-tertiary);
2823
-
font-size: 0.75rem;
2824
-
margin-left: auto;
2825
-
}
2826
-
2827
-
.inline-reply-text {
2828
-
font-size: 0.9rem;
2829
-
color: var(--text-primary);
2830
-
line-height: 1.5;
2831
-
}
2832
-
2833
-
.inline-reply-action {
2834
-
display: flex;
2835
-
align-items: center;
2836
-
gap: 4px;
2837
-
padding: 4px 8px;
2838
-
font-size: 0.8rem;
2839
-
color: var(--text-tertiary);
2840
-
background: none;
2841
-
border: none;
2842
-
border-radius: var(--radius-sm);
2843
-
cursor: pointer;
2844
-
transition: all 0.15s ease;
2845
-
}
2846
-
2847
-
.inline-reply-action:hover {
2848
-
color: var(--text-secondary);
2849
-
background: var(--bg-hover);
2850
-
}
2851
-
2852
-
.inline-reply-composer {
2853
-
display: flex;
2854
-
align-items: flex-start;
2855
-
gap: 12px;
2856
-
padding: 12px 16px;
2857
-
}
2858
-
2859
-
.history-panel {
2860
-
background: var(--bg-tertiary);
2861
-
border: 1px solid var(--border);
2862
-
border-radius: var(--radius-md);
2863
-
padding: 1rem;
2864
-
margin-bottom: 1rem;
2865
-
font-size: 0.9rem;
2866
-
animation: fadeIn 0.2s ease-out;
2867
-
}
2868
-
2869
-
.history-header {
2870
-
display: flex;
2871
-
justify-content: space-between;
2872
-
align-items: center;
2873
-
margin-bottom: 1rem;
2874
-
padding-bottom: 0.5rem;
2875
-
border-bottom: 1px solid var(--border);
2876
-
}
2877
-
2878
-
.history-title {
2879
-
font-weight: 600;
2880
-
text-transform: uppercase;
2881
-
letter-spacing: 0.05em;
2882
-
font-size: 0.75rem;
2883
-
color: var(--text-secondary);
2884
-
}
2885
-
2886
-
.history-list {
2887
-
list-style: none;
2888
-
display: flex;
2889
-
flex-direction: column;
2890
-
gap: 1rem;
2891
-
}
2892
-
2893
-
.history-item {
2894
-
position: relative;
2895
-
padding-left: 1rem;
2896
-
border-left: 2px solid var(--border);
2897
-
}
2898
-
2899
-
.history-date {
2900
-
font-size: 0.75rem;
2901
-
color: var(--text-tertiary);
2902
-
margin-bottom: 0.25rem;
2903
-
}
2904
-
2905
-
.history-content {
2906
-
color: var(--text-secondary);
2907
-
white-space: pre-wrap;
2908
-
}
2909
-
2910
-
.history-close-btn {
2911
-
color: var(--text-tertiary);
2912
-
padding: 4px;
2913
-
border-radius: var(--radius-sm);
2914
-
transition: all 0.2s;
2915
-
display: flex;
2916
-
align-items: center;
2917
-
justify-content: center;
2918
-
}
2919
-
2920
-
.history-close-btn:hover {
2921
-
background: var(--bg-hover);
2922
-
color: var(--text-primary);
2923
-
}
2924
-
2925
-
.history-status {
2926
-
text-align: center;
2927
-
color: var(--text-tertiary);
2928
-
font-style: italic;
2929
-
padding: 1rem;
2930
-
}
2931
-
2932
-
.bookmark-card {
2933
-
display: flex;
2934
-
flex-direction: column;
2935
-
gap: 12px;
2936
-
}
2937
-
2938
-
.bookmark-preview {
2939
-
display: flex;
2940
-
align-items: stretch;
2941
-
gap: 16px;
2942
-
padding: 14px 16px;
2943
-
background: var(--bg-secondary);
2944
-
border: 1px solid var(--border);
2945
-
border-radius: var(--radius-md);
2946
-
text-decoration: none;
2947
-
transition: all 0.2s ease;
2948
-
}
2949
-
2950
-
.bookmark-preview:hover {
2951
-
background: var(--bg-tertiary);
2952
-
border-color: var(--accent-subtle);
2953
-
transform: translateY(-1px);
2954
-
}
2955
-
2956
-
.bookmark-preview-content {
2957
-
flex: 1;
2958
-
min-width: 0;
2959
-
display: flex;
2960
-
flex-direction: column;
2961
-
gap: 6px;
2962
-
}
2963
-
2964
-
.bookmark-preview-site {
2965
-
display: flex;
2966
-
align-items: center;
2967
-
gap: 6px;
2968
-
font-size: 0.75rem;
2969
-
font-weight: 600;
2970
-
color: var(--accent);
2971
-
text-transform: uppercase;
2972
-
letter-spacing: 0.03em;
2973
-
}
2974
-
2975
-
.bookmark-preview-title {
2976
-
font-size: 1rem;
2977
-
font-weight: 600;
2978
-
line-height: 1.4;
2979
-
color: var(--text-primary);
2980
-
margin: 0;
2981
-
display: -webkit-box;
2982
-
-webkit-line-clamp: 2;
2983
-
line-clamp: 2;
2984
-
-webkit-box-orient: vertical;
2985
-
overflow: hidden;
2986
-
}
2987
-
2988
-
.bookmark-preview-desc {
2989
-
font-size: 0.875rem;
2990
-
color: var(--text-secondary);
2991
-
line-height: 1.5;
2992
-
margin: 0;
2993
-
display: -webkit-box;
2994
-
-webkit-line-clamp: 2;
2995
-
line-clamp: 2;
2996
-
-webkit-box-orient: vertical;
2997
-
overflow: hidden;
2998
-
}
2999
-
3000
-
.bookmark-preview-arrow {
3001
-
display: flex;
3002
-
align-items: center;
3003
-
justify-content: center;
3004
-
color: var(--text-tertiary);
3005
-
padding: 0 4px;
3006
-
transition: all 0.2s ease;
3007
-
}
3008
-
3009
-
.bookmark-preview:hover .bookmark-preview-arrow {
3010
-
color: var(--accent);
3011
-
transform: translateX(2px);
3012
-
}
3013
-
3014
-
.navbar-logo-img {
3015
-
width: 24px;
3016
-
height: 24px;
3017
-
object-fit: contain;
3018
-
}
3019
-
3020
-
.login-logo-img {
3021
-
width: 80px;
3022
-
height: 80px;
3023
-
margin-bottom: 24px;
3024
-
object-fit: contain;
3025
-
}
3026
-
3027
-
.legal-content {
3028
-
max-width: 800px;
3029
-
margin: 0 auto;
3030
-
padding: 20px;
3031
-
}
3032
-
3033
-
.legal-content h1 {
3034
-
font-size: 2rem;
3035
-
margin-bottom: 8px;
3036
-
color: var(--text-primary);
3037
-
}
3038
-
3039
-
.legal-content h2 {
3040
-
font-size: 1.4rem;
3041
-
margin-top: 32px;
3042
-
margin-bottom: 12px;
3043
-
color: var(--text-primary);
3044
-
}
3045
-
3046
-
.legal-content h3 {
3047
-
font-size: 1.1rem;
3048
-
margin-top: 20px;
3049
-
margin-bottom: 8px;
3050
-
color: var(--text-primary);
3051
-
}
3052
-
3053
-
.legal-content p {
3054
-
color: var(--text-secondary);
3055
-
line-height: 1.7;
3056
-
margin-bottom: 12px;
3057
-
}
3058
-
3059
-
.legal-content ul {
3060
-
color: var(--text-secondary);
3061
-
line-height: 1.7;
3062
-
margin-left: 24px;
3063
-
margin-bottom: 12px;
3064
-
}
3065
-
3066
-
.legal-content li {
3067
-
margin-bottom: 6px;
3068
-
}
3069
-
3070
-
.legal-content a {
3071
-
color: var(--accent);
3072
-
text-decoration: none;
3073
-
}
3074
-
3075
-
.legal-content a:hover {
3076
-
text-decoration: underline;
3077
-
}
3078
-
3079
-
.legal-content section {
3080
-
margin-bottom: 24px;
3081
-
}
3082
-
3083
-
.input {
3084
-
width: 100%;
3085
-
padding: 12px 14px;
3086
-
font-size: 0.95rem;
3087
-
color: var(--text-primary);
3088
-
background: var(--bg-secondary);
3089
-
border: 1px solid var(--border);
3090
-
border-radius: var(--radius-md);
3091
-
outline: none;
3092
-
transition: all 0.15s ease;
3093
-
}
3094
-
3095
-
.input:focus {
3096
-
border-color: var(--accent);
3097
-
box-shadow: 0 0 0 3px var(--accent-subtle);
3098
-
}
3099
-
3100
-
.input::placeholder {
3101
-
color: var(--text-tertiary);
3102
-
}
3103
-
3104
-
.notifications-page {
3105
-
max-width: 680px;
3106
-
margin: 0 auto;
3107
-
}
3108
-
3109
-
.notifications-list {
3110
-
display: flex;
3111
-
flex-direction: column;
3112
-
gap: 12px;
3113
-
}
3114
-
3115
-
.notification-item {
3116
-
display: flex;
3117
-
gap: 16px;
3118
-
align-items: flex-start;
3119
-
text-decoration: none;
3120
-
color: inherit;
3121
-
}
3122
-
3123
-
.notification-item:hover {
3124
-
background: var(--bg-hover);
3125
-
}
3126
-
3127
-
.notification-icon {
3128
-
width: 36px;
3129
-
height: 36px;
3130
-
border-radius: var(--radius-full);
3131
-
display: flex;
3132
-
align-items: center;
3133
-
justify-content: center;
3134
-
background: var(--bg-tertiary);
3135
-
color: var(--text-secondary);
3136
-
flex-shrink: 0;
3137
-
}
3138
-
3139
-
.notification-icon[data-type="like"] {
3140
-
color: #ef4444;
3141
-
background: rgba(239, 68, 68, 0.1);
3142
-
}
3143
-
3144
-
.notification-icon[data-type="reply"] {
3145
-
color: #3b82f6;
3146
-
background: rgba(59, 130, 246, 0.1);
3147
-
}
3148
-
3149
-
.notification-content {
3150
-
flex: 1;
3151
-
min-width: 0;
3152
-
}
3153
-
3154
-
.notification-text {
3155
-
font-size: 0.95rem;
3156
-
margin-bottom: 4px;
3157
-
line-height: 1.4;
3158
-
color: var(--text-primary);
3159
-
}
3160
-
3161
-
.notification-text strong {
3162
-
font-weight: 600;
3163
-
}
3164
-
3165
-
.notification-time {
3166
-
font-size: 0.85rem;
3167
-
color: var(--text-tertiary);
3168
-
}
3169
-
3170
-
.notification-link {
3171
-
position: relative;
3172
-
}
3173
-
3174
-
.notification-badge {
3175
-
position: absolute;
3176
-
top: -2px;
3177
-
right: -2px;
3178
-
background: var(--error);
3179
-
color: white;
3180
-
font-size: 0.7rem;
3181
-
font-weight: 700;
3182
-
min-width: 16px;
3183
-
height: 16px;
3184
-
border-radius: var(--radius-full);
3185
-
display: flex;
3186
-
align-items: center;
3187
-
justify-content: center;
3188
-
padding: 0 4px;
3189
-
border: 2px solid var(--bg-primary);
3190
-
}
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";
+121
-62
web/src/pages/AnnotationDetail.jsx
+121
-62
web/src/pages/AnnotationDetail.jsx
···
1
1
import { useState, useEffect } from "react";
2
-
import { useParams, Link } from "react-router-dom";
3
-
import AnnotationCard from "../components/AnnotationCard";
2
+
import { useParams, Link, useLocation } from "react-router-dom";
3
+
import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
4
+
import BookmarkCard from "../components/BookmarkCard";
4
5
import ReplyList from "../components/ReplyList";
5
6
import {
6
7
getAnnotation,
7
8
getReplies,
8
9
createReply,
9
10
deleteReply,
11
+
resolveHandle,
12
+
normalizeAnnotation,
10
13
} from "../api/client";
11
14
import { useAuth } from "../context/AuthContext";
12
15
import { MessageSquare } from "lucide-react";
13
16
14
17
export default function AnnotationDetail() {
15
-
const { uri, did, rkey } = useParams();
18
+
const { uri, did, rkey, handle, type } = useParams();
19
+
const location = useLocation();
16
20
const { isAuthenticated, user } = useAuth();
17
21
const [annotation, setAnnotation] = useState(null);
18
22
const [replies, setReplies] = useState([]);
···
23
27
const [posting, setPosting] = useState(false);
24
28
const [replyingTo, setReplyingTo] = useState(null);
25
29
26
-
const annotationUri = uri || `at://${did}/at.margin.annotation/${rkey}`;
30
+
const [targetUri, setTargetUri] = useState(uri);
31
+
32
+
useEffect(() => {
33
+
async function resolve() {
34
+
if (uri) {
35
+
setTargetUri(uri);
36
+
return;
37
+
}
38
+
39
+
if (handle && rkey) {
40
+
let collection = "at.margin.annotation";
41
+
if (type === "highlight") collection = "at.margin.highlight";
42
+
if (type === "bookmark") collection = "at.margin.bookmark";
43
+
44
+
try {
45
+
const resolvedDid = await resolveHandle(handle);
46
+
if (resolvedDid) {
47
+
setTargetUri(`at://${resolvedDid}/${collection}/${rkey}`);
48
+
}
49
+
} catch (e) {
50
+
console.error("Failed to resolve handle:", e);
51
+
}
52
+
} else if (did && rkey) {
53
+
setTargetUri(`at://${did}/at.margin.annotation/${rkey}`);
54
+
} else {
55
+
const pathParts = location.pathname.split("/");
56
+
const atIndex = pathParts.indexOf("at");
57
+
if (
58
+
atIndex !== -1 &&
59
+
pathParts[atIndex + 1] &&
60
+
pathParts[atIndex + 2]
61
+
) {
62
+
setTargetUri(
63
+
`at://${pathParts[atIndex + 1]}/at.margin.annotation/${pathParts[atIndex + 2]}`,
64
+
);
65
+
}
66
+
}
67
+
}
68
+
resolve();
69
+
}, [uri, did, rkey, handle, type, location.pathname]);
27
70
28
71
const refreshReplies = async () => {
29
-
const repliesData = await getReplies(annotationUri);
72
+
if (!targetUri) return;
73
+
const repliesData = await getReplies(targetUri);
30
74
setReplies(repliesData.items || []);
31
75
};
32
76
33
77
useEffect(() => {
34
78
async function fetchData() {
79
+
if (!targetUri) return;
80
+
35
81
try {
36
82
setLoading(true);
37
83
const [annData, repliesData] = await Promise.all([
38
-
getAnnotation(annotationUri),
39
-
getReplies(annotationUri).catch(() => ({ items: [] })),
84
+
getAnnotation(targetUri),
85
+
getReplies(targetUri).catch(() => ({ items: [] })),
40
86
]);
41
-
setAnnotation(annData);
87
+
setAnnotation(normalizeAnnotation(annData));
42
88
setReplies(repliesData.items || []);
43
89
} catch (err) {
44
90
setError(err.message);
···
47
93
}
48
94
}
49
95
fetchData();
50
-
}, [annotationUri]);
96
+
}, [targetUri]);
51
97
52
98
const handleReply = async (e) => {
53
99
if (e) e.preventDefault();
···
57
103
setPosting(true);
58
104
const parentUri = replyingTo
59
105
? replyingTo.id || replyingTo.uri
60
-
: annotationUri;
106
+
: targetUri;
61
107
const parentCid = replyingTo
62
108
? replyingTo.cid || ""
63
109
: annotation?.cid || "";
···
65
111
await createReply({
66
112
parentUri,
67
113
parentCid,
68
-
rootUri: annotationUri,
114
+
rootUri: targetUri,
69
115
rootCid: annotation?.cid || "",
70
116
text: replyText,
71
117
});
···
130
176
</Link>
131
177
</div>
132
178
133
-
<AnnotationCard annotation={annotation} />
179
+
{annotation.type === "Highlight" ? (
180
+
<HighlightCard
181
+
highlight={annotation}
182
+
onDelete={() => (window.location.href = "/")}
183
+
/>
184
+
) : annotation.type === "Bookmark" ? (
185
+
<BookmarkCard
186
+
bookmark={annotation}
187
+
onDelete={() => (window.location.href = "/")}
188
+
/>
189
+
) : (
190
+
<AnnotationCard annotation={annotation} />
191
+
)}
134
192
135
-
{}
136
-
<div className="replies-section">
137
-
<h3 className="replies-title">
138
-
<MessageSquare size={18} />
139
-
Replies ({replies.length})
140
-
</h3>
193
+
{annotation.type !== "Bookmark" && annotation.type !== "Highlight" && (
194
+
<div className="replies-section">
195
+
<h3 className="replies-title">
196
+
<MessageSquare size={18} />
197
+
Replies ({replies.length})
198
+
</h3>
141
199
142
-
{isAuthenticated && (
143
-
<div className="reply-form card">
144
-
{replyingTo && (
145
-
<div className="replying-to-banner">
146
-
<span>
147
-
Replying to @
148
-
{(replyingTo.creator || replyingTo.author)?.handle ||
149
-
"unknown"}
150
-
</span>
200
+
{isAuthenticated && (
201
+
<div className="reply-form card">
202
+
{replyingTo && (
203
+
<div className="replying-to-banner">
204
+
<span>
205
+
Replying to @
206
+
{(replyingTo.creator || replyingTo.author)?.handle ||
207
+
"unknown"}
208
+
</span>
209
+
<button
210
+
onClick={() => setReplyingTo(null)}
211
+
className="cancel-reply"
212
+
>
213
+
ร
214
+
</button>
215
+
</div>
216
+
)}
217
+
<textarea
218
+
value={replyText}
219
+
onChange={(e) => setReplyText(e.target.value)}
220
+
placeholder={
221
+
replyingTo
222
+
? `Reply to @${(replyingTo.creator || replyingTo.author)?.handle}...`
223
+
: "Write a reply..."
224
+
}
225
+
className="reply-input"
226
+
rows={3}
227
+
disabled={posting}
228
+
/>
229
+
<div className="reply-form-actions">
151
230
<button
152
-
onClick={() => setReplyingTo(null)}
153
-
className="cancel-reply"
231
+
className="btn btn-primary"
232
+
disabled={posting || !replyText.trim()}
233
+
onClick={() => handleReply()}
154
234
>
155
-
ร
235
+
{posting ? "Posting..." : "Reply"}
156
236
</button>
157
237
</div>
158
-
)}
159
-
<textarea
160
-
value={replyText}
161
-
onChange={(e) => setReplyText(e.target.value)}
162
-
placeholder={
163
-
replyingTo
164
-
? `Reply to @${(replyingTo.creator || replyingTo.author)?.handle}...`
165
-
: "Write a reply..."
166
-
}
167
-
className="reply-input"
168
-
rows={3}
169
-
disabled={posting}
170
-
/>
171
-
<div className="reply-form-actions">
172
-
<button
173
-
className="btn btn-primary"
174
-
disabled={posting || !replyText.trim()}
175
-
onClick={() => handleReply()}
176
-
>
177
-
{posting ? "Posting..." : "Reply"}
178
-
</button>
179
238
</div>
180
-
</div>
181
-
)}
239
+
)}
182
240
183
-
<ReplyList
184
-
replies={replies}
185
-
rootUri={annotationUri}
186
-
user={user}
187
-
onReply={(reply) => setReplyingTo(reply)}
188
-
onDelete={handleDeleteReply}
189
-
isInline={false}
190
-
/>
191
-
</div>
241
+
<ReplyList
242
+
replies={replies}
243
+
rootUri={targetUri}
244
+
user={user}
245
+
onReply={(reply) => setReplyingTo(reply)}
246
+
onDelete={handleDeleteReply}
247
+
isInline={false}
248
+
/>
249
+
</div>
250
+
)}
192
251
</div>
193
252
);
194
253
}
+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
) : (
+55
-42
web/src/pages/CollectionDetail.jsx
+55
-42
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 {
···
6
6
getCollectionItems,
7
7
removeItemFromCollection,
8
8
deleteCollection,
9
+
resolveHandle,
9
10
} from "../api/client";
10
11
import { useAuth } from "../context/AuthContext";
11
12
import CollectionModal from "../components/CollectionModal";
···
15
16
import ShareMenu from "../components/ShareMenu";
16
17
17
18
export default function CollectionDetail() {
18
-
const { rkey, "*": wildcardPath } = useParams();
19
+
const { rkey, handle, "*": wildcardPath } = useParams();
19
20
const location = useLocation();
20
21
const navigate = useNavigate();
21
22
const { user } = useAuth();
···
27
28
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
28
29
29
30
const searchParams = new URLSearchParams(location.search);
30
-
const authorDid = searchParams.get("author") || user?.did;
31
+
const paramAuthorDid = searchParams.get("author");
32
+
33
+
const isOwner =
34
+
user?.did &&
35
+
(collection?.creator?.did === user.did || paramAuthorDid === user.did);
36
+
37
+
const fetchContext = useCallback(async () => {
38
+
try {
39
+
setLoading(true);
40
+
41
+
let targetUri = null;
42
+
let targetDid = paramAuthorDid || user?.did;
43
+
44
+
if (handle && rkey) {
45
+
try {
46
+
targetDid = await resolveHandle(handle);
47
+
targetUri = `at://${targetDid}/at.margin.collection/${rkey}`;
48
+
} catch (e) {
49
+
console.error("Failed to resolve handle", e);
50
+
}
51
+
} else if (wildcardPath) {
52
+
targetUri = decodeURIComponent(wildcardPath);
53
+
} else if (rkey && targetDid) {
54
+
targetUri = `at://${targetDid}/at.margin.collection/${rkey}`;
55
+
}
31
56
32
-
const getCollectionUri = () => {
33
-
if (wildcardPath) {
34
-
return decodeURIComponent(wildcardPath);
35
-
}
36
-
if (rkey && authorDid) {
37
-
return `at://${authorDid}/at.margin.collection/${rkey}`;
38
-
}
39
-
return null;
40
-
};
57
+
if (!targetUri) {
58
+
if (!user && !handle && !paramAuthorDid) {
59
+
setError("Please log in to view your collections");
60
+
return;
61
+
}
62
+
setError("Invalid collection URL");
63
+
return;
64
+
}
41
65
42
-
const collectionUri = getCollectionUri();
43
-
const isOwner = user?.did && authorDid === user.did;
66
+
if (!targetDid && targetUri.startsWith("at://")) {
67
+
const parts = targetUri.split("/");
68
+
if (parts.length > 2) targetDid = parts[2];
69
+
}
44
70
45
-
const fetchContext = async () => {
46
-
if (!collectionUri || !authorDid) {
47
-
setError("Invalid collection URL");
48
-
setLoading(false);
49
-
return;
50
-
}
71
+
if (!targetDid) {
72
+
setError("Could not determine collection owner");
73
+
return;
74
+
}
51
75
52
-
try {
53
-
setLoading(true);
54
76
const [cols, itemsData] = await Promise.all([
55
-
getCollections(authorDid),
56
-
getCollectionItems(collectionUri),
77
+
getCollections(targetDid),
78
+
getCollectionItems(targetUri),
57
79
]);
58
80
59
81
const found =
60
-
cols.items?.find((c) => c.uri === collectionUri) ||
82
+
cols.items?.find((c) => c.uri === targetUri) ||
61
83
cols.items?.find(
62
-
(c) =>
63
-
collectionUri && c.uri.endsWith(collectionUri.split("/").pop()),
84
+
(c) => targetUri && c.uri.endsWith(targetUri.split("/").pop()),
64
85
);
86
+
65
87
if (!found) {
66
-
console.error(
67
-
"Collection not found. Looking for:",
68
-
collectionUri,
69
-
"Available:",
70
-
cols.items?.map((c) => c.uri),
71
-
);
72
88
setError("Collection not found");
73
89
return;
74
90
}
···
80
96
} finally {
81
97
setLoading(false);
82
98
}
83
-
};
99
+
}, [paramAuthorDid, user, handle, rkey, wildcardPath]);
84
100
85
101
useEffect(() => {
86
-
if (collectionUri && authorDid) {
87
-
fetchContext();
88
-
} else if (!user && !searchParams.get("author")) {
89
-
setLoading(false);
90
-
setError("Please log in to view your collections");
91
-
}
92
-
}, [rkey, wildcardPath, authorDid, user]);
102
+
fetchContext();
103
+
}, [fetchContext]);
93
104
94
105
const handleEditSuccess = () => {
95
106
fetchContext();
···
171
182
</div>
172
183
<div className="collection-detail-actions">
173
184
<ShareMenu
174
-
customUrl={`${window.location.origin}/collection/${encodeURIComponent(collection.uri)}?author=${encodeURIComponent(authorDid)}`}
185
+
uri={collection.uri}
186
+
handle={collection.creator?.handle}
187
+
type="Collection"
175
188
text={`Check out this collection: ${collection.name}`}
176
189
/>
177
190
{isOwner && (
+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();
+177
-59
web/src/pages/Feed.jsx
+177
-59
web/src/pages/Feed.jsx
···
1
1
import { useState, useEffect } from "react";
2
+
import { useSearchParams } from "react-router-dom";
2
3
import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
3
4
import BookmarkCard from "../components/BookmarkCard";
4
5
import CollectionItemCard from "../components/CollectionItemCard";
5
-
import { getAnnotationFeed } from "../api/client";
6
+
import AnnotationSkeleton from "../components/AnnotationSkeleton";
7
+
import { getAnnotationFeed, deleteHighlight } from "../api/client";
6
8
import { AlertIcon, InboxIcon } from "../components/Icons";
9
+
import { useAuth } from "../context/AuthContext";
10
+
11
+
import AddToCollectionModal from "../components/AddToCollectionModal";
7
12
8
13
export default function Feed() {
14
+
const [searchParams, setSearchParams] = useSearchParams();
15
+
const tagFilter = searchParams.get("tag");
16
+
17
+
const [filter, setFilter] = useState(() => {
18
+
return localStorage.getItem("feedFilter") || "all";
19
+
});
20
+
9
21
const [annotations, setAnnotations] = useState([]);
10
22
const [loading, setLoading] = useState(true);
11
23
const [error, setError] = useState(null);
12
-
const [filter, setFilter] = useState("all");
24
+
25
+
useEffect(() => {
26
+
localStorage.setItem("feedFilter", filter);
27
+
}, [filter]);
28
+
29
+
const [collectionModalState, setCollectionModalState] = useState({
30
+
isOpen: false,
31
+
uri: null,
32
+
});
33
+
34
+
const { user } = useAuth();
13
35
14
36
useEffect(() => {
15
37
async function fetchFeed() {
16
38
try {
17
39
setLoading(true);
18
-
const data = await getAnnotationFeed();
40
+
let creatorDid = "";
41
+
42
+
if (filter === "my-tags") {
43
+
if (user?.did) {
44
+
creatorDid = user.did;
45
+
} else {
46
+
setAnnotations([]);
47
+
setLoading(false);
48
+
return;
49
+
}
50
+
}
51
+
52
+
const data = await getAnnotationFeed(
53
+
50,
54
+
0,
55
+
tagFilter || "",
56
+
creatorDid,
57
+
);
19
58
setAnnotations(data.items || []);
20
59
} catch (err) {
21
60
setError(err.message);
···
24
63
}
25
64
}
26
65
fetchFeed();
27
-
}, []);
66
+
}, [tagFilter, filter, user]);
28
67
29
68
const filteredAnnotations =
30
-
filter === "all"
69
+
filter === "all" || filter === "my-tags"
31
70
? annotations
32
71
: annotations.filter((a) => {
33
72
if (filter === "commenting")
···
46
85
<p className="page-description">
47
86
See what people are annotating, highlighting, and bookmarking
48
87
</p>
88
+
{tagFilter && (
89
+
<div
90
+
style={{
91
+
marginTop: "16px",
92
+
display: "flex",
93
+
alignItems: "center",
94
+
gap: "8px",
95
+
}}
96
+
>
97
+
<span
98
+
style={{ fontSize: "0.9rem", color: "var(--text-secondary)" }}
99
+
>
100
+
Filtering by tag: <strong>#{tagFilter}</strong>
101
+
</span>
102
+
<button
103
+
onClick={() =>
104
+
setSearchParams((prev) => {
105
+
const next = new URLSearchParams(prev);
106
+
next.delete("tag");
107
+
return next;
108
+
})
109
+
}
110
+
className="btn btn-sm"
111
+
style={{ padding: "2px 8px", fontSize: "0.8rem" }}
112
+
>
113
+
Clear
114
+
</button>
115
+
</div>
116
+
)}
49
117
</div>
50
118
51
119
{}
···
56
124
>
57
125
All
58
126
</button>
127
+
{user && (
128
+
<button
129
+
className={`filter-tab ${filter === "my-tags" ? "active" : ""}`}
130
+
onClick={() => setFilter("my-tags")}
131
+
>
132
+
My Feed
133
+
</button>
134
+
)}
59
135
<button
60
136
className={`filter-tab ${filter === "commenting" ? "active" : ""}`}
61
137
onClick={() => setFilter("commenting")}
···
76
152
</button>
77
153
</div>
78
154
79
-
{loading && (
155
+
{loading ? (
80
156
<div className="feed">
81
-
{[1, 2, 3].map((i) => (
82
-
<div key={i} className="card">
83
-
<div
84
-
className="skeleton skeleton-text"
85
-
style={{ width: "40%" }}
86
-
/>
87
-
<div className="skeleton skeleton-text" />
88
-
<div className="skeleton skeleton-text" />
89
-
<div
90
-
className="skeleton skeleton-text"
91
-
style={{ width: "60%" }}
92
-
/>
93
-
</div>
157
+
{[1, 2, 3, 4, 5].map((i) => (
158
+
<AnnotationSkeleton key={i} />
94
159
))}
95
160
</div>
96
-
)}
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
+
)}
97
172
98
-
{error && (
99
-
<div className="empty-state">
100
-
<div className="empty-state-icon">
101
-
<AlertIcon size={32} />
102
-
</div>
103
-
<h3 className="empty-state-title">Something went wrong</h3>
104
-
<p className="empty-state-text">{error}</p>
105
-
</div>
106
-
)}
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
+
)}
107
186
108
-
{!loading && !error && filteredAnnotations.length === 0 && (
109
-
<div className="empty-state">
110
-
<div className="empty-state-icon">
111
-
<InboxIcon size={32} />
112
-
</div>
113
-
<h3 className="empty-state-title">No items yet</h3>
114
-
<p className="empty-state-text">
115
-
{filter === "all"
116
-
? "Be the first to annotate something!"
117
-
: `No ${filter} items found.`}
118
-
</p>
119
-
</div>
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} />;
192
+
}
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
+
</>
120
250
)}
121
251
122
-
{!loading && !error && filteredAnnotations.length > 0 && (
123
-
<div className="feed">
124
-
{filteredAnnotations.map((item) => {
125
-
if (item.type === "CollectionItem") {
126
-
return <CollectionItemCard key={item.id} item={item} />;
127
-
}
128
-
if (
129
-
item.type === "Highlight" ||
130
-
item.motivation === "highlighting"
131
-
) {
132
-
return <HighlightCard key={item.id} highlight={item} />;
133
-
}
134
-
if (item.type === "Bookmark" || item.motivation === "bookmarking") {
135
-
return <BookmarkCard key={item.id} bookmark={item} />;
136
-
}
137
-
return <AnnotationCard key={item.id} annotation={item} />;
138
-
})}
139
-
</div>
252
+
{collectionModalState.isOpen && (
253
+
<AddToCollectionModal
254
+
isOpen={collectionModalState.isOpen}
255
+
onClose={() => setCollectionModalState({ isOpen: false, uri: null })}
256
+
annotationUri={collectionModalState.uri}
257
+
/>
140
258
)}
141
259
</div>
142
260
);
+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 &&
+5
-1
web/src/pages/New.jsx
+5
-1
web/src/pages/New.jsx
···
84
84
85
85
<div className="card">
86
86
<Composer
87
-
url={url || initialUrl}
87
+
url={
88
+
(url || initialUrl) && !/^(?:f|ht)tps?:\/\//.test(url || initialUrl)
89
+
? `https://${url || initialUrl}`
90
+
: url || initialUrl
91
+
}
88
92
selector={initialSelector}
89
93
onSuccess={handleSuccess}
90
94
onCancel={() => navigate(-1)}
+12
-8
web/src/pages/Notifications.jsx
+12
-8
web/src/pages/Notifications.jsx
···
4
4
import { getNotifications, markNotificationsRead } from "../api/client";
5
5
import { BellIcon, HeartIcon, ReplyIcon } from "../components/Icons";
6
6
7
-
function getContentRoute(subjectUri) {
8
-
if (!subjectUri) return "/";
9
-
if (subjectUri.includes("at.margin.bookmark")) {
7
+
function getNotificationRoute(n) {
8
+
if (n.type === "reply" && n.subject?.inReplyTo) {
9
+
return `/annotation/${encodeURIComponent(n.subject.inReplyTo)}`;
10
+
}
11
+
if (!n.subjectUri) return "/";
12
+
if (n.subjectUri.includes("at.margin.bookmark")) {
10
13
return `/bookmarks`;
11
14
}
12
-
if (subjectUri.includes("at.margin.highlight")) {
15
+
if (n.subjectUri.includes("at.margin.highlight")) {
13
16
return `/highlights`;
14
17
}
15
-
return `/annotation/${encodeURIComponent(subjectUri)}`;
18
+
return `/annotation/${encodeURIComponent(n.subjectUri)}`;
16
19
}
17
20
18
21
export default function Notifications() {
···
153
156
<BellIcon size={48} />
154
157
<h3>No notifications yet</h3>
155
158
<p>
156
-
When someone likes or replies to your content, you'll see it here
159
+
When someone likes or replies to your content, you'll see it
160
+
here
157
161
</p>
158
162
</div>
159
163
)}
···
163
167
{notifications.map((n, i) => (
164
168
<Link
165
169
key={n.id || i}
166
-
to={getContentRoute(n.subjectUri)}
170
+
to={getNotificationRoute(n)}
167
171
className="notification-item card"
168
172
style={{ alignItems: "center" }}
169
173
>
170
174
<div
171
175
className="notification-avatar-container"
172
-
style={{ marginRight: 12 }}
176
+
style={{ marginRight: 12, position: "relative" }}
173
177
>
174
178
{n.actor?.avatar ? (
175
179
<img
+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
+5
-21
web/src/pages/Profile.jsx
+5
-21
web/src/pages/Profile.jsx
···
89
89
</div>
90
90
<h3 className="empty-state-title">No annotations</h3>
91
91
<p className="empty-state-text">
92
-
This user hasn't posted any annotations.
92
+
This user hasn't posted any annotations.
93
93
</p>
94
94
</div>
95
95
);
···
108
108
</div>
109
109
<h3 className="empty-state-title">No highlights</h3>
110
110
<p className="empty-state-text">
111
-
This user hasn't saved any highlights.
111
+
This user hasn't saved any highlights.
112
112
</p>
113
113
</div>
114
114
);
···
125
125
</div>
126
126
<h3 className="empty-state-title">No bookmarks</h3>
127
127
<p className="empty-state-text">
128
-
This user hasn't bookmarked any pages.
129
-
</p>
130
-
</div>
131
-
);
132
-
}
133
-
return bookmarks.map((b) => <BookmarkCard key={b.id} annotation={b} />);
134
-
}
135
-
if (activeTab === "bookmarks") {
136
-
if (bookmarks.length === 0) {
137
-
return (
138
-
<div className="empty-state">
139
-
<div className="empty-state-icon">
140
-
<BookmarkIcon size={32} />
141
-
</div>
142
-
<h3 className="empty-state-title">No bookmarks</h3>
143
-
<p className="empty-state-text">
144
-
This user hasn't bookmarked any pages.
128
+
This user hasn't bookmarked any pages.
145
129
</p>
146
130
</div>
147
131
);
148
132
}
149
-
return bookmarks.map((b) => <BookmarkCard key={b.id} annotation={b} />);
133
+
return bookmarks.map((b) => <BookmarkCard key={b.uri} bookmark={b} />);
150
134
}
151
135
152
136
if (activeTab === "collections") {
···
158
142
</div>
159
143
<h3 className="empty-state-title">No collections</h3>
160
144
<p className="empty-state-text">
161
-
This user hasn't created any collections.
145
+
This user hasn't created any collections.
162
146
</p>
163
147
</div>
164
148
);
+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
+
}