A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.

Compare changes

Choose any two refs to compare.

+22
.npmignore
···
+
# Demo and development files
+
demo/
+
src/
+
index.html
+
+
# Build configuration
+
vite.config.ts
+
vite.config.d.ts
+
tsconfig.app.json
+
tsconfig.node.json
+
eslint.config.js
+
tsconfig.lib.tsbuildinfo
+
+
# Dependencies
+
node_modules/
+
package-lock.json
+
bun.lock
+
+
CLAUDE.md
+
+
# Output directory
+
lib/
+42
.tangled/workflows/upload-demo-to-wisp.yml
···
+
when:
+
- event: ['push']
+
branch: ['main']
+
- event: ['manual']
+
engine: 'nixery'
+
clone:
+
skip: false
+
depth: 1
+
submodules: false
+
dependencies:
+
nixpkgs:
+
- nodejs
+
- coreutils
+
- curl
+
github:NixOS/nixpkgs/nixpkgs-unstable:
+
- bun
+
+
environment:
+
SITE_PATH: 'demo'
+
SITE_NAME: 'atproto-ui'
+
WISP_HANDLE: 'ana.pds.nkp.pet'
+
+
steps:
+
- name: build demo
+
command: |
+
export PATH="$HOME/.nix-profile/bin:$PATH"
+
+
# regenerate lockfile, https://github.com/npm/cli/pull/8184 makes rolldown not install
+
rm package-lock.json bun.lock
+
bun install
+
+
# run directly with bun because of shebang issues in nix
+
BUILD_TARGET=demo bun node_modules/.bin/vite build
+
- name: upload to wisp
+
command: |
+
curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli
+
chmod +x wisp-cli
+
./wisp-cli \
+
"$WISP_HANDLE" \
+
--path "$SITE_PATH" \
+
--site "$SITE_NAME" \
+
--password "$WISP_APP_PASSWORD"
+701
CLAUDE.md
···
+
# AtReact Hooks Deep Dive
+
+
## Overview
+
The AtReact hooks system provides a robust, cache-optimized layer for fetching AT Protocol data. All hooks follow React best practices with proper cleanup, cancellation, and stable references.
+
+
---
+
+
## Core Architecture Principles
+
+
### 1. **Three-Tier Caching Strategy**
+
All data flows through three cache layers:
+
- **DidCache** - DID documents, handle mappings, PDS endpoints
+
- **BlobCache** - Media/image blobs with reference counting
+
- **RecordCache** - AT Protocol records with deduplication
+
+
### 2. **Concurrent Request Deduplication**
+
When multiple components request the same data, only one network request is made. Uses reference counting to manage in-flight requests.
+
+
### 3. **Stable Reference Pattern**
+
Caches use memoized snapshots to prevent unnecessary re-renders:
+
```typescript
+
// Only creates new snapshot if data actually changed
+
if (existing && existing.did === did && existing.handle === handle) {
+
return toSnapshot(existing); // Reuse existing
+
}
+
```
+
+
### 4. **Three-Tier Fallback for Bluesky**
+
For `app.bsky.*` collections:
+
1. Try Bluesky appview API (fastest, public)
+
2. Fall back to Slingshot (microcosm service)
+
3. Finally query PDS directly
+
+
---
+
+
## Hook Catalog
+
+
## 1. `useDidResolution`
+
**Purpose:** Resolves handles to DIDs or fetches DID documents
+
+
### Key Features:
+
- **Bidirectional:** Works with handles OR DIDs
+
- **Smart Caching:** Only fetches if not in cache
+
- **Dual Resolution Paths:**
+
- Handle โ†’ DID: Uses Slingshot first, then appview
+
- DID โ†’ Document: Fetches full DID document for handle extraction
+
+
### State Flow:
+
```typescript
+
Input: "alice.bsky.social" or "did:plc:xxx"
+
โ†“
+
Check didCache
+
โ†“
+
If handle: ensureHandle(resolver, handle) โ†’ DID
+
If DID: ensureDidDoc(resolver, did) โ†’ DID doc + handle from alsoKnownAs
+
โ†“
+
Return: { did, handle, loading, error }
+
```
+
+
### Critical Implementation Details:
+
- **Normalizes input** to lowercase for handles
+
- **Memoizes input** to prevent effect re-runs
+
- **Stabilizes error references** - only updates if message changes
+
- **Cleanup:** Cancellation token prevents stale updates
+
+
---
+
+
## 2. `usePdsEndpoint`
+
**Purpose:** Discovers the PDS endpoint for a DID
+
+
### Key Features:
+
- **Depends on DID resolution** (implicit dependency)
+
- **Extracts from DID document** if already cached
+
- **Lazy fetching** - only when endpoint not in cache
+
+
### State Flow:
+
```typescript
+
Input: DID
+
โ†“
+
Check didCache.getByDid(did).pdsEndpoint
+
โ†“
+
If missing: ensurePdsEndpoint(resolver, did)
+
โ”œโ”€ Tries to get from existing DID doc
+
โ””โ”€ Falls back to resolver.pdsEndpointForDid()
+
โ†“
+
Return: { endpoint, loading, error }
+
```
+
+
### Service Discovery:
+
Looks for `AtprotoPersonalDataServer` service in DID document:
+
```json
+
{
+
"service": [{
+
"type": "AtprotoPersonalDataServer",
+
"serviceEndpoint": "https://pds.example.com"
+
}]
+
}
+
```
+
+
---
+
+
## 3. `useAtProtoRecord`
+
**Purpose:** Fetches a single AT Protocol record with smart routing
+
+
### Key Features:
+
- **Collection-aware routing:** Bluesky vs other protocols
+
- **RecordCache deduplication:** Multiple components = one fetch
+
- **Cleanup with reference counting**
+
+
### State Flow:
+
```typescript
+
Input: { did, collection, rkey }
+
โ†“
+
If collection.startsWith("app.bsky."):
+
โ””โ”€ useBlueskyAppview() โ†’ Three-tier fallback
+
Else:
+
โ”œโ”€ useDidResolution(did)
+
โ”œโ”€ usePdsEndpoint(resolved.did)
+
โ””โ”€ recordCache.ensure() โ†’ Fetch from PDS
+
โ†“
+
Return: { record, loading, error }
+
```
+
+
### RecordCache Deduplication:
+
```typescript
+
// First component calling this
+
const { promise, release } = recordCache.ensure(did, collection, rkey, loader)
+
// refCount = 1
+
+
// Second component calling same record
+
const { promise, release } = recordCache.ensure(...) // Same promise!
+
// refCount = 2
+
+
// On cleanup, both call release()
+
// Only aborts when refCount reaches 0
+
```
+
+
---
+
+
## 4. `useBlueskyAppview`
+
**Purpose:** Fetches Bluesky records with appview optimization
+
+
### Key Features:
+
- **Collection-aware endpoints:**
+
- `app.bsky.actor.profile` โ†’ `app.bsky.actor.getProfile`
+
- `app.bsky.feed.post` โ†’ `app.bsky.feed.getPostThread`
+
- **CDN URL extraction:** Parses CDN URLs to extract CIDs
+
- **Atomic state updates:** Uses reducer for complex state
+
+
### Three-Tier Fallback with Source Tracking:
+
```typescript
+
async function fetchWithFallback() {
+
// Tier 1: Appview (if endpoint mapped)
+
try {
+
const result = await fetchFromAppview(did, collection, rkey);
+
return { record: result, source: "appview" };
+
} catch {}
+
+
// Tier 2: Slingshot
+
try {
+
const result = await fetchFromSlingshot(did, collection, rkey);
+
return { record: result, source: "slingshot" };
+
} catch {}
+
+
// Tier 3: PDS
+
try {
+
const result = await fetchFromPds(did, collection, rkey);
+
return { record: result, source: "pds" };
+
} catch {}
+
+
// All tiers failed - provide helpful error for banned Bluesky accounts
+
if (pdsEndpoint.includes('.bsky.network')) {
+
throw new Error('Record unavailable. The Bluesky PDS may be unreachable or the account may be banned.');
+
}
+
+
throw new Error('Failed to fetch record from all sources');
+
}
+
```
+
+
The `source` field in the result accurately indicates which tier successfully fetched the data, enabling debugging and analytics.
+
+
### CDN URL Handling:
+
Appview returns CDN URLs like:
+
```
+
https://cdn.bsky.app/img/avatar/plain/did:plc:xxx/bafkreixxx@jpeg
+
```
+
+
Hook extracts CID (`bafkreixxx`) and creates standard Blob object:
+
```typescript
+
{
+
$type: "blob",
+
ref: { $link: "bafkreixxx" },
+
mimeType: "image/jpeg",
+
size: 0,
+
cdnUrl: "https://cdn.bsky.app/..." // Preserved for fast rendering
+
}
+
```
+
+
### Reducer Pattern:
+
```typescript
+
type Action =
+
| { type: "SET_LOADING"; loading: boolean }
+
| { type: "SET_SUCCESS"; record: T; source: "appview" | "slingshot" | "pds" }
+
| { type: "SET_ERROR"; error: Error }
+
| { type: "RESET" };
+
+
// Atomic state updates, no race conditions
+
dispatch({ type: "SET_SUCCESS", record, source });
+
```
+
+
---
+
+
## 5. `useLatestRecord`
+
**Purpose:** Fetches the most recent record from a collection
+
+
### Key Features:
+
- **Timestamp validation:** Skips records before 2023 (pre-ATProto)
+
- **PDS-only:** Slingshot doesn't support `listRecords`
+
- **Smart fetching:** Gets 3 records to handle invalid timestamps
+
+
### State Flow:
+
```typescript
+
Input: { did, collection }
+
โ†“
+
useDidResolution(did)
+
usePdsEndpoint(did)
+
โ†“
+
callListRecords(endpoint, did, collection, limit: 3)
+
โ†“
+
Filter: isValidTimestamp(record) โ†’ year >= 2023
+
โ†“
+
Return first valid record: { record, rkey, loading, error, empty }
+
```
+
+
### Timestamp Validation:
+
```typescript
+
function isValidTimestamp(record: unknown): boolean {
+
const timestamp = record.createdAt || record.indexedAt;
+
if (!timestamp) return true; // No timestamp, assume valid
+
+
const date = new Date(timestamp);
+
return date.getFullYear() >= 2023; // ATProto created in 2023
+
}
+
```
+
+
---
+
+
## 6. `usePaginatedRecords`
+
**Purpose:** Cursor-based pagination with prefetching
+
+
### Key Features:
+
- **Dual fetching modes:**
+
- Author feed (appview) - for Bluesky posts with filters
+
- Direct PDS - for all other collections
+
- **Smart prefetching:** Loads next page in background
+
- **Invalid timestamp filtering:** Same as `useLatestRecord`
+
- **Request sequencing:** Prevents race conditions with `requestSeq`
+
+
### State Management:
+
```typescript
+
// Pages stored as array
+
pages: [
+
{ records: [...], cursor: "abc" }, // page 0
+
{ records: [...], cursor: "def" }, // page 1
+
{ records: [...], cursor: undefined } // page 2 (last)
+
]
+
pageIndex: 1 // Currently viewing page 1
+
```
+
+
### Prefetch Logic:
+
```typescript
+
useEffect(() => {
+
const cursor = pages[pageIndex]?.cursor;
+
if (!cursor || pages[pageIndex + 1]) return; // No cursor or already loaded
+
+
// Prefetch next page in background
+
fetchPage(identity, cursor, pageIndex + 1, "prefetch");
+
}, [pageIndex, pages]);
+
```
+
+
### Author Feed vs PDS:
+
```typescript
+
if (preferAuthorFeed && collection === "app.bsky.feed.post") {
+
// Use app.bsky.feed.getAuthorFeed
+
const res = await callAppviewRpc("app.bsky.feed.getAuthorFeed", {
+
actor: handle || did,
+
filter: "posts_with_media", // Optional filter
+
includePins: true
+
});
+
} else {
+
// Use com.atproto.repo.listRecords
+
const res = await callListRecords(pdsEndpoint, did, collection, limit);
+
}
+
```
+
+
### Race Condition Prevention:
+
```typescript
+
const requestSeq = useRef(0);
+
+
// On identity change
+
resetState();
+
requestSeq.current += 1; // Invalidate in-flight requests
+
+
// In fetch callback
+
const token = requestSeq.current;
+
// ... do async work ...
+
if (token !== requestSeq.current) return; // Stale request, abort
+
```
+
+
---
+
+
## 7. `useBlob`
+
**Purpose:** Fetches and caches media blobs with object URL management
+
+
### Key Features:
+
- **Automatic cleanup:** Revokes object URLs on unmount
+
- **BlobCache deduplication:** Same blob = one fetch
+
- **Reference counting:** Safe concurrent access
+
+
### State Flow:
+
```typescript
+
Input: { did, cid }
+
โ†“
+
useDidResolution(did)
+
usePdsEndpoint(did)
+
โ†“
+
Check blobCache.get(did, cid)
+
โ†“
+
If missing: blobCache.ensure() โ†’ Fetch from PDS
+
โ”œโ”€ GET /xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}
+
โ””โ”€ Store in cache
+
โ†“
+
Create object URL: URL.createObjectURL(blob)
+
โ†“
+
Return: { url, loading, error }
+
โ†“
+
Cleanup: URL.revokeObjectURL(url)
+
```
+
+
### Object URL Management:
+
```typescript
+
const objectUrlRef = useRef<string>();
+
+
// On successful fetch
+
const nextUrl = URL.createObjectURL(blob);
+
const prevUrl = objectUrlRef.current;
+
objectUrlRef.current = nextUrl;
+
if (prevUrl) URL.revokeObjectURL(prevUrl); // Clean up old URL
+
+
// On unmount
+
useEffect(() => () => {
+
if (objectUrlRef.current) {
+
URL.revokeObjectURL(objectUrlRef.current);
+
}
+
}, []);
+
```
+
+
---
+
+
## 8. `useBlueskyProfile`
+
**Purpose:** Wrapper around `useBlueskyAppview` for profile records
+
+
### Key Features:
+
- **Simplified interface:** Just pass DID
+
- **Type conversion:** Converts ProfileRecord to BlueskyProfileData
+
- **CID extraction:** Extracts avatar/banner CIDs from blobs
+
+
### Implementation:
+
```typescript
+
export function useBlueskyProfile(did: string | undefined) {
+
const { record, loading, error } = useBlueskyAppview<ProfileRecord>({
+
did,
+
collection: "app.bsky.actor.profile",
+
rkey: "self",
+
});
+
+
const data = record ? {
+
did: did || "",
+
handle: "", // Populated by caller
+
displayName: record.displayName,
+
description: record.description,
+
avatar: extractCidFromBlob(record.avatar),
+
banner: extractCidFromBlob(record.banner),
+
createdAt: record.createdAt,
+
} : undefined;
+
+
return { data, loading, error };
+
}
+
```
+
+
---
+
+
## 9. `useBacklinks`
+
**Purpose:** Fetches backlinks from Microcosm Constellation API
+
+
### Key Features:
+
- **Specialized use case:** Tangled stars, etc.
+
- **Abort controller:** Cancels in-flight requests
+
- **Refetch support:** Manual refresh capability
+
+
### State Flow:
+
```typescript
+
Input: { subject: "at://did:plc:xxx/sh.tangled.repo/yyy", source: "sh.tangled.feed.star:subject" }
+
โ†“
+
GET https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinks
+
?subject={subject}&source={source}&limit={limit}
+
โ†“
+
Return: { backlinks: [...], total, loading, error, refetch }
+
```
+
+
---
+
+
## 10. `useRepoLanguages`
+
**Purpose:** Fetches language statistics from Tangled knot server
+
+
### Key Features:
+
- **Branch fallback:** Tries "main", then "master"
+
- **Knot server query:** For repository analysis
+
+
### State Flow:
+
```typescript
+
Input: { knot: "knot.gaze.systems", did, repoName, branch }
+
โ†“
+
GET https://{knot}/xrpc/sh.tangled.repo.languages
+
?repo={did}/{repoName}&ref={branch}
+
โ†“
+
If 404: Try fallback branch
+
โ†“
+
Return: { data: { languages: {...} }, loading, error }
+
```
+
+
---
+
+
## Cache Implementation Deep Dive
+
+
### DidCache
+
**Purpose:** Cache DID documents, handle mappings, PDS endpoints
+
+
```typescript
+
class DidCache {
+
private byHandle = new Map<string, DidCacheEntry>();
+
private byDid = new Map<string, DidCacheEntry>();
+
private handlePromises = new Map<string, Promise<...>>();
+
private docPromises = new Map<string, Promise<...>>();
+
private pdsPromises = new Map<string, Promise<...>>();
+
+
// Memoized snapshots prevent re-renders
+
private toSnapshot(entry): DidCacheSnapshot {
+
if (entry.snapshot) return entry.snapshot; // Reuse
+
entry.snapshot = { did, handle, doc, pdsEndpoint };
+
return entry.snapshot;
+
}
+
}
+
```
+
+
**Key methods:**
+
- `getByHandle(handle)` - Instant cache lookup
+
- `getByDid(did)` - Instant cache lookup
+
- `ensureHandle(resolver, handle)` - Deduplicated resolution
+
- `ensureDidDoc(resolver, did)` - Deduplicated doc fetch
+
- `ensurePdsEndpoint(resolver, did)` - Deduplicated PDS discovery
+
+
**Snapshot stability:**
+
```typescript
+
memoize(entry) {
+
const existing = this.byDid.get(did);
+
+
// Data unchanged? Reuse snapshot (same reference)
+
if (existing && existing.did === did &&
+
existing.handle === handle && ...) {
+
return toSnapshot(existing); // Prevents re-render!
+
}
+
+
// Data changed, create new entry
+
const merged = { did, handle, doc, pdsEndpoint, snapshot: undefined };
+
this.byDid.set(did, merged);
+
return toSnapshot(merged);
+
}
+
```
+
+
### BlobCache
+
**Purpose:** Cache media blobs with reference counting
+
+
```typescript
+
class BlobCache {
+
private store = new Map<string, BlobCacheEntry>();
+
private inFlight = new Map<string, InFlightBlobEntry>();
+
+
ensure(did, cid, loader) {
+
// Already cached?
+
const cached = this.get(did, cid);
+
if (cached) return { promise: Promise.resolve(cached), release: noop };
+
+
// In-flight request?
+
const existing = this.inFlight.get(key);
+
if (existing) {
+
existing.refCount++; // Multiple consumers
+
return { promise: existing.promise, release: () => this.release(key) };
+
}
+
+
// New request
+
const { promise, abort } = loader();
+
this.inFlight.set(key, { promise, abort, refCount: 1 });
+
return { promise, release: () => this.release(key) };
+
}
+
+
private release(key) {
+
const entry = this.inFlight.get(key);
+
entry.refCount--;
+
if (entry.refCount <= 0) {
+
this.inFlight.delete(key);
+
entry.abort(); // Cancel fetch
+
}
+
}
+
}
+
```
+
+
### RecordCache
+
**Purpose:** Cache AT Protocol records with deduplication
+
+
Identical structure to BlobCache but for record data.
+
+
---
+
+
## Common Patterns
+
+
### 1. Cancellation Pattern
+
```typescript
+
useEffect(() => {
+
let cancelled = false;
+
+
const assignState = (next) => {
+
if (cancelled) return; // Don't update unmounted component
+
setState(prev => ({ ...prev, ...next }));
+
};
+
+
// ... async work ...
+
+
return () => {
+
cancelled = true; // Mark as cancelled
+
release?.(); // Decrement refCount
+
};
+
}, [deps]);
+
```
+
+
### 2. Error Stabilization Pattern
+
```typescript
+
setError(prevError =>
+
prevError?.message === newError.message
+
? prevError // Reuse same reference
+
: newError // New error
+
);
+
```
+
+
### 3. Identity Tracking Pattern
+
```typescript
+
const identityRef = useRef<string>();
+
const identity = did && endpoint ? `${did}::${endpoint}` : undefined;
+
+
useEffect(() => {
+
if (identityRef.current !== identity) {
+
identityRef.current = identity;
+
resetState(); // Clear stale data
+
}
+
// ...
+
}, [identity]);
+
```
+
+
### 4. Dual-Mode Resolution
+
```typescript
+
const isDid = input.startsWith("did:");
+
const normalizedHandle = !isDid ? input.toLowerCase() : undefined;
+
+
// Different code paths
+
if (isDid) {
+
snapshot = await didCache.ensureDidDoc(resolver, input);
+
} else {
+
snapshot = await didCache.ensureHandle(resolver, normalizedHandle);
+
}
+
```
+
+
---
+
+
## Performance Optimizations
+
+
### 1. **Memoized Snapshots**
+
Caches return stable references when data unchanged โ†’ prevents re-renders
+
+
### 2. **Reference Counting**
+
Multiple components requesting same data share one fetch
+
+
### 3. **Prefetching**
+
`usePaginatedRecords` loads next page in background
+
+
### 4. **CDN URLs**
+
Bluesky appview returns CDN URLs โ†’ skip blob fetching for images
+
+
### 5. **Smart Routing**
+
Bluesky collections use fast appview โ†’ non-Bluesky goes direct to PDS
+
+
### 6. **Request Deduplication**
+
In-flight request maps prevent duplicate fetches
+
+
### 7. **Timestamp Validation**
+
Skip invalid records early (before 2023) โ†’ fewer wasted cycles
+
+
---
+
+
## Error Handling Strategy
+
+
### 1. **Fallback Chains**
+
Never fail on first attempt โ†’ try multiple sources
+
+
### 2. **Graceful Degradation**
+
```typescript
+
// Slingshot failed? Try appview
+
try {
+
return await fetchFromSlingshot();
+
} catch (slingshotError) {
+
try {
+
return await fetchFromAppview();
+
} catch (appviewError) {
+
// Combine errors for better debugging
+
throw new Error(`${appviewError.message}; Slingshot: ${slingshotError.message}`);
+
}
+
}
+
```
+
+
### 3. **Component Isolation**
+
Errors in one component don't crash others (via error boundaries recommended)
+
+
### 4. **Abort Handling**
+
```typescript
+
try {
+
await fetch(url, { signal });
+
} catch (err) {
+
if (err.name === "AbortError") return; // Expected, ignore
+
throw err;
+
}
+
```
+
+
### 5. **Banned Bluesky Account Detection**
+
When all three tiers fail and the PDS is a `.bsky.network` endpoint, provide a helpful error:
+
```typescript
+
// All tiers failed - check if it's a banned Bluesky account
+
if (pdsEndpoint.includes('.bsky.network')) {
+
throw new Error(
+
'Record unavailable. The Bluesky PDS may be unreachable or the account may be banned.'
+
);
+
}
+
```
+
+
This helps users understand why data is unavailable instead of showing generic fetch errors. Applies to both `useBlueskyAppview` and `useAtProtoRecord` hooks.
+
+
---
+
+
## Testing Considerations
+
+
### Key scenarios to test:
+
1. **Concurrent requests:** Multiple components requesting same data
+
2. **Race conditions:** Component unmounting mid-fetch
+
3. **Cache invalidation:** Identity changes during fetch
+
4. **Error fallbacks:** Slingshot down โ†’ appview works
+
5. **Timestamp filtering:** Records before 2023 skipped
+
6. **Reference counting:** Proper cleanup on unmount
+
7. **Prefetching:** Background loads don't interfere with active loads
+
+
---
+
+
## Common Gotchas
+
+
### 1. **React Rules of Hooks**
+
All hooks called unconditionally, even if results not used:
+
```typescript
+
// Always call, conditionally use results
+
const blueskyResult = useBlueskyAppview({
+
did: isBlueskyCollection ? handleOrDid : undefined, // Pass undefined to skip
+
collection: isBlueskyCollection ? collection : undefined,
+
rkey: isBlueskyCollection ? rkey : undefined,
+
});
+
```
+
+
### 2. **Cleanup Order Matters**
+
```typescript
+
return () => {
+
cancelled = true; // 1. Prevent state updates
+
release?.(); // 2. Decrement refCount
+
revokeObjectURL(...); // 3. Free resources
+
};
+
```
+
+
### 3. **Snapshot Reuse**
+
Don't modify cached snapshots! They're shared across components.
+
+
### 4. **CDN URL Extraction**
+
Bluesky CDN URLs must be parsed carefully:
+
```
+
https://cdn.bsky.app/img/avatar/plain/did:plc:xxx/bafkreixxx@jpeg
+
^^^^^^^^^^^^ ^^^^^^
+
DID CID
+
```
+41 -3
README.md
···
# atproto-ui
-
A React component library for rendering AT Protocol records (Bluesky, Leaflet, Tangled, and more). Handles DID resolution, PDS discovery, and record fetching automatically. [Live demo](https://atproto-ui.wisp.place).
+
A React component library for rendering AT Protocol records (Bluesky, Leaflet, Tangled, and more). Handles DID resolution, PDS discovery, and record fetching automatically as well as caching these so multiple components can render quickly. [Live demo](https://atproto-ui.wisp.place).
+
+
This project is mostly a wrapper on the extremely amazing work [Mary](https://mary.my.id/) has done with [atcute](https://tangled.org/@mary.my.id/atcute), please support it. I have to give thanks to [phil](https://bsky.app/profile/bad-example.com) for microcosm and slingshot. Incredible services being given for free that is responsible for why the components fetch data so quickly.
## Screenshots
···
## Features
-
- **Drop-in components** for common record types (`BlueskyPost`, `BlueskyProfile`, `TangledString`, `LeafletDocument`)
+
- **Drop-in components** for common record types (`BlueskyPost`, `BlueskyProfile`, `TangledRepo`, `LeafletDocument`)
- **Prefetch support** - Pass data directly to skip API calls (perfect for SSR/caching)
+
- **Caching** - Blobs, DIDs, and records are cached so components which use the same ones can render even quicker
- **Customizable theming** - Override CSS variables to match your app's design
- **Composable hooks** - Build custom renderers with protocol primitives
- Built on lightweight [`@atcute/*`](https://tangled.org/@mary.my.id/atcute) clients
···
<LeafletDocument did={did} rkey={rkey} record={documentRecord} />
```
+
### Using atcute Directly
+
+
Use atcute directly to construct records and pass them to componentsโ€”fully compatible!
+
+
```tsx
+
import { Client, simpleFetchHandler, ok } from '@atcute/client';
+
import type { AppBskyFeedPost } from '@atcute/bluesky';
+
import { BlueskyPost } from 'atproto-ui';
+
+
// Create atcute client
+
const client = new Client({
+
handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' })
+
});
+
+
// Fetch a record
+
const data = await ok(
+
client.get('com.atproto.repo.getRecord', {
+
params: {
+
repo: 'did:plc:ttdrpj45ibqunmfhdsb4zdwq',
+
collection: 'app.bsky.feed.post',
+
rkey: '3m45rq4sjes2h'
+
}
+
})
+
);
+
+
const record = data.value as AppBskyFeedPost.Main;
+
+
// Pass atcute record directly to component!
+
<BlueskyPost
+
did="did:plc:ttdrpj45ibqunmfhdsb4zdwq"
+
rkey="3m45rq4sjes2h"
+
record={record}
+
/>
+
```
+
## API Reference
### Components
···
## Demo
-
Check out the [live demo](https://atproto-ui.netlify.app/) to see all components in action.
+
Check out the [live demo](https://atproto-ui.wisp.place/) to see all components in action.
### Running Locally
+697
bun.lock
···
+
{
+
"lockfileVersion": 1,
+
"configVersion": 1,
+
"workspaces": {
+
"": {
+
"name": "atproto-ui",
+
"dependencies": {
+
"@atcute/atproto": "^3.1.7",
+
"@atcute/bluesky": "^3.2.3",
+
"@atcute/client": "^4.0.3",
+
"@atcute/identity-resolver": "^1.1.3",
+
"@atcute/tangled": "^1.0.10",
+
},
+
"devDependencies": {
+
"@eslint/js": "^9.36.0",
+
"@microsoft/api-extractor": "^7.53.1",
+
"@types/node": "^24.6.0",
+
"@types/react": "^19.1.16",
+
"@types/react-dom": "^19.1.9",
+
"@vitejs/plugin-react": "^5.0.4",
+
"eslint": "^9.36.0",
+
"eslint-plugin-react-hooks": "^5.2.0",
+
"eslint-plugin-react-refresh": "^0.4.22",
+
"globals": "^16.4.0",
+
"react": "^19.1.1",
+
"react-dom": "^19.1.1",
+
"rollup-plugin-typescript2": "^0.36.0",
+
"typescript": "~5.9.3",
+
"typescript-eslint": "^8.45.0",
+
"unplugin-dts": "^1.0.0-beta.6",
+
"vite": "npm:rolldown-vite@7.1.14",
+
},
+
"peerDependencies": {
+
"react": "^18.2.0 || ^19.0.0",
+
"react-dom": "^18.2.0 || ^19.0.0",
+
},
+
"optionalPeers": [
+
"react-dom",
+
],
+
},
+
},
+
"packages": {
+
"@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="],
+
+
"@atcute/bluesky": ["@atcute/bluesky@3.2.11", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.5" } }, "sha512-AboS6y4t+zaxIq7E4noue10csSpIuk/Uwo30/l6GgGBDPXrd7STw8Yb5nGZQP+TdG/uC8/c2mm7UnY65SDOh6A=="],
+
+
"@atcute/client": ["@atcute/client@4.1.0", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.5" } }, "sha512-AYhSu3RSDA2VDkVGOmad320NRbUUUf5pCFWJcOzlk25YC/4kyzmMFfpzhf1jjjEcY+anNBXGGhav/kKB1evggQ=="],
+
+
"@atcute/identity": ["@atcute/identity@1.1.3", "", { "dependencies": { "@atcute/lexicons": "^1.2.4", "@badrap/valita": "^0.4.6" } }, "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng=="],
+
+
"@atcute/identity-resolver": ["@atcute/identity-resolver@1.1.4", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@atcute/util-fetch": "^1.0.3", "@badrap/valita": "^0.4.6" }, "peerDependencies": { "@atcute/identity": "^1.0.0" } }, "sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA=="],
+
+
"@atcute/lexicons": ["@atcute/lexicons@1.2.5", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q=="],
+
+
"@atcute/tangled": ["@atcute/tangled@1.0.12", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.3" } }, "sha512-JKA5sOhd8SLhDFhY+PKHqLLytQBBKSiwcaEzfYUJBeyfvqXFPNNAwvRbe3VST4IQ3izoOu3O0R9/b1mjL45UzA=="],
+
+
"@atcute/util-fetch": ["@atcute/util-fetch@1.0.4", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg=="],
+
+
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
+
+
"@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
+
+
"@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
+
+
"@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
+
+
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
+
+
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
+
+
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
+
+
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
+
+
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
+
+
"@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
+
+
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
+
+
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
+
+
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
+
+
"@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
+
+
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
+
+
"@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="],
+
+
"@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
+
+
"@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
+
+
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
+
+
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
+
+
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
+
+
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
+
+
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
+
+
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
+
+
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="],
+
+
"@eslint/js": ["@eslint/js@9.39.1", "", {}, "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw=="],
+
+
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
+
+
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
+
+
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
+
+
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
+
+
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
+
+
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+
+
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
+
+
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
+
+
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+
"@microsoft/api-extractor": ["@microsoft/api-extractor@7.55.1", "", { "dependencies": { "@microsoft/api-extractor-model": "7.32.1", "@microsoft/tsdoc": "~0.16.0", "@microsoft/tsdoc-config": "~0.18.0", "@rushstack/node-core-library": "5.19.0", "@rushstack/rig-package": "0.6.0", "@rushstack/terminal": "0.19.4", "@rushstack/ts-command-line": "5.1.4", "diff": "~8.0.2", "lodash": "~4.17.15", "minimatch": "10.0.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", "typescript": "5.8.2" }, "bin": { "api-extractor": "bin/api-extractor" } }, "sha512-l8Z+8qrLkZFM3HM95Dbpqs6G39fpCa7O5p8A7AkA6hSevxkgwsOlLrEuPv0ADOyj5dI1Af5WVDiwpKG/ya5G3w=="],
+
+
"@microsoft/api-extractor-model": ["@microsoft/api-extractor-model@7.32.1", "", { "dependencies": { "@microsoft/tsdoc": "~0.16.0", "@microsoft/tsdoc-config": "~0.18.0", "@rushstack/node-core-library": "5.19.0" } }, "sha512-u4yJytMYiUAnhcNQcZDTh/tVtlrzKlyKrQnLOV+4Qr/5gV+cpufWzCYAB1Q23URFqD6z2RoL2UYncM9xJVGNKA=="],
+
+
"@microsoft/tsdoc": ["@microsoft/tsdoc@0.16.0", "", {}, "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA=="],
+
+
"@microsoft/tsdoc-config": ["@microsoft/tsdoc-config@0.18.0", "", { "dependencies": { "@microsoft/tsdoc": "0.16.0", "ajv": "~8.12.0", "jju": "~1.4.0", "resolve": "~1.22.2" } }, "sha512-8N/vClYyfOH+l4fLkkr9+myAoR6M7akc8ntBJ4DJdWH2b09uVfr71+LTMpNyG19fNqWDg8KEDZhx5wxuqHyGjw=="],
+
+
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="],
+
+
"@oxc-project/runtime": ["@oxc-project/runtime@0.92.0", "", {}, "sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw=="],
+
+
"@oxc-project/types": ["@oxc-project/types@0.93.0", "", {}, "sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg=="],
+
+
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.41", "", { "os": "android", "cpu": "arm64" }, "sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ=="],
+
+
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.41", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw=="],
+
+
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.41", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ho6lIwGJed98zub7n0xcRKuEtnZgbxevAmO4x3zn3C3N4GVXZD5xvCvTVxSMoeBJwTcIYzkVDRTIhylQNsTgLQ=="],
+
+
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.41", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ijAZETywvL+gACjbT4zBnCp5ez1JhTRs6OxRN4J+D6AzDRbU2zb01Esl51RP5/8ZOlvB37xxsRQ3X4YRVyYb3g=="],
+
+
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.41", "", { "os": "linux", "cpu": "arm" }, "sha512-EgIOZt7UildXKFEFvaiLNBXm+4ggQyGe3E5Z1QP9uRcJJs9omihOnm897FwOBQdCuMvI49iBgjFrkhH+wMJ2MA=="],
+
+
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.41", "", { "os": "linux", "cpu": "arm64" }, "sha512-F8bUwJq8v/JAU8HSwgF4dztoqJ+FjdyjuvX4//3+Fbe2we9UktFeZ27U4lRMXF1vxWtdV4ey6oCSqI7yUrSEeg=="],
+
+
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.41", "", { "os": "linux", "cpu": "arm64" }, "sha512-MioXcCIX/wB1pBnBoJx8q4OGucUAfC1+/X1ilKFsjDK05VwbLZGRgOVD5OJJpUQPK86DhQciNBrfOKDiatxNmg=="],
+
+
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.41", "", { "os": "linux", "cpu": "x64" }, "sha512-m66M61fizvRCwt5pOEiZQMiwBL9/y0bwU/+Kc4Ce/Pef6YfoEkR28y+DzN9rMdjo8Z28NXjsDPq9nH4mXnAP0g=="],
+
+
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.41", "", { "os": "linux", "cpu": "x64" }, "sha512-yRxlSfBvWnnfrdtJfvi9lg8xfG5mPuyoSHm0X01oiE8ArmLRvoJGHUTJydCYz+wbK2esbq5J4B4Tq9WAsOlP1Q=="],
+
+
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.41", "", { "os": "none", "cpu": "arm64" }, "sha512-PHVxYhBpi8UViS3/hcvQQb9RFqCtvFmFU1PvUoTRiUdBtgHA6fONNHU4x796lgzNlVSD3DO/MZNk1s5/ozSMQg=="],
+
+
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.41", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.5" }, "cpu": "none" }, "sha512-OAfcO37ME6GGWmj9qTaDT7jY4rM0T2z0/8ujdQIJQ2x2nl+ztO32EIwURfmXOK0U1tzkyuaKYvE34Pug/ucXlQ=="],
+
+
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.41", "", { "os": "win32", "cpu": "arm64" }, "sha512-NIYGuCcuXaq5BC4Q3upbiMBvmZsTsEPG9k/8QKQdmrch+ocSy5Jv9tdpdmXJyighKqm182nh/zBt+tSJkYoNlg=="],
+
+
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.41", "", { "os": "win32", "cpu": "ia32" }, "sha512-kANdsDbE5FkEOb5NrCGBJBCaZ2Sabp3D7d4PRqMYJqyLljwh9mDyYyYSv5+QNvdAmifj+f3lviNEUUuUZPEFPw=="],
+
+
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.41", "", { "os": "win32", "cpu": "x64" }, "sha512-UlpxKmFdik0Y2VjZrgUCgoYArZJiZllXgIipdBRV1hw6uK45UbQabSTW6Kp6enuOu7vouYWftwhuxfpE8J2JAg=="],
+
+
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="],
+
+
"@rollup/pluginutils": ["@rollup/pluginutils@4.2.1", "", { "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" } }, "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ=="],
+
+
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="],
+
+
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.3", "", { "os": "android", "cpu": "arm64" }, "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w=="],
+
+
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA=="],
+
+
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ=="],
+
+
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w=="],
+
+
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q=="],
+
+
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw=="],
+
+
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg=="],
+
+
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w=="],
+
+
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A=="],
+
+
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g=="],
+
+
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw=="],
+
+
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g=="],
+
+
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A=="],
+
+
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg=="],
+
+
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w=="],
+
+
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q=="],
+
+
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw=="],
+
+
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw=="],
+
+
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA=="],
+
+
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg=="],
+
+
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="],
+
+
"@rushstack/node-core-library": ["@rushstack/node-core-library@5.19.0", "", { "dependencies": { "ajv": "~8.13.0", "ajv-draft-04": "~1.0.0", "ajv-formats": "~3.0.1", "fs-extra": "~11.3.0", "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", "semver": "~7.5.4" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-BxAopbeWBvNJ6VGiUL+5lbJXywTdsnMeOS8j57Cn/xY10r6sV/gbsTlfYKjzVCUBZATX2eRzJHSMCchsMTGN6A=="],
+
+
"@rushstack/problem-matcher": ["@rushstack/problem-matcher@0.1.1", "", { "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-Fm5XtS7+G8HLcJHCWpES5VmeMyjAKaWeyZU5qPzZC+22mPlJzAsOxymHiWIfuirtPckX3aptWws+K2d0BzniJA=="],
+
+
"@rushstack/rig-package": ["@rushstack/rig-package@0.6.0", "", { "dependencies": { "resolve": "~1.22.1", "strip-json-comments": "~3.1.1" } }, "sha512-ZQmfzsLE2+Y91GF15c65L/slMRVhF6Hycq04D4TwtdGaUAbIXXg9c5pKA5KFU7M4QMaihoobp9JJYpYcaY3zOw=="],
+
+
"@rushstack/terminal": ["@rushstack/terminal@0.19.4", "", { "dependencies": { "@rushstack/node-core-library": "5.19.0", "@rushstack/problem-matcher": "0.1.1", "supports-color": "~8.1.1" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-f4XQk02CrKfrMgyOfhYd3qWI944dLC21S4I/LUhrlAP23GTMDNG6EK5effQtFkISwUKCgD9vMBrJZaPSUquxWQ=="],
+
+
"@rushstack/ts-command-line": ["@rushstack/ts-command-line@5.1.4", "", { "dependencies": { "@rushstack/terminal": "0.19.4", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, "sha512-H0I6VdJ6sOUbktDFpP2VW5N29w8v4hRoNZOQz02vtEi6ZTYL1Ju8u+TcFiFawUDrUsx/5MQTUhd79uwZZVwVlA=="],
+
+
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
+
+
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+
"@types/argparse": ["@types/argparse@1.0.38", "", {}, "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="],
+
+
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
+
+
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
+
+
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
+
+
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
+
+
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
+
+
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
+
+
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
+
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.48.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/type-utils": "8.48.1", "@typescript-eslint/utils": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.48.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA=="],
+
+
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.48.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA=="],
+
+
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.48.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.48.1", "@typescript-eslint/types": "^8.48.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w=="],
+
+
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1" } }, "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w=="],
+
+
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.48.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw=="],
+
+
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/utils": "8.48.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg=="],
+
+
"@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="],
+
+
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.48.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.48.1", "@typescript-eslint/tsconfig-utils": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg=="],
+
+
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.48.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA=="],
+
+
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q=="],
+
+
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA=="],
+
+
"@volar/language-core": ["@volar/language-core@2.4.26", "", { "dependencies": { "@volar/source-map": "2.4.26" } }, "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A=="],
+
+
"@volar/source-map": ["@volar/source-map@2.4.26", "", {}, "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw=="],
+
+
"@volar/typescript": ["@volar/typescript@2.4.26", "", { "dependencies": { "@volar/language-core": "2.4.26", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-N87ecLD48Sp6zV9zID/5yuS1+5foj0DfuYGdQ6KHj/IbKvyKv1zNX6VCmnKYwtmHadEO6mFc2EKISiu3RDPAvA=="],
+
+
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+
+
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
+
+
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
+
+
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
+
+
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+
"ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
+
+
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
+
+
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw=="],
+
+
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
+
+
"browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="],
+
+
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+
+
"caniuse-lite": ["caniuse-lite@1.0.30001759", "", {}, "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw=="],
+
+
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
+
+
"compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="],
+
+
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
+
+
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
+
+
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+
+
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
+
+
"electron-to-chromium": ["electron-to-chromium@1.5.263", "", {}, "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg=="],
+
+
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+
"eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="],
+
+
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
+
+
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.24", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w=="],
+
+
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
+
+
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
+
+
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
+
+
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
+
+
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
+
+
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+
+
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
+
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+
+
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
+
+
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
+
+
"find-cache-dir": ["find-cache-dir@3.3.2", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" } }, "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig=="],
+
+
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+
+
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
+
+
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
+
+
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
+
+
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+
"globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
+
+
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
+
+
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
+
+
"import-lazy": ["import-lazy@4.0.0", "", {}, "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw=="],
+
+
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+
+
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
+
+
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+
"jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="],
+
+
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
+
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
+
+
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+
+
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
+
+
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+
+
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
+
+
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+
+
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
+
+
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
+
+
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
+
+
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
+
+
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
+
+
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
+
+
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
+
+
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
+
+
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
+
+
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
+
+
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
+
+
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
+
+
"local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="],
+
+
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
+
+
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
+
+
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
+
+
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
+
+
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+
"make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="],
+
+
"minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
+
+
"mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
+
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+
+
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
+
+
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+
+
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
+
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+
+
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
+
+
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+
+
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
+
+
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
+
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
+
+
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+
"pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="],
+
+
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
+
+
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+
+
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+
+
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
+
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
+
+
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
+
+
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
+
+
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
+
+
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
+
+
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+
+
"rolldown": ["rolldown@1.0.0-beta.41", "", { "dependencies": { "@oxc-project/types": "=0.93.0", "@rolldown/pluginutils": "1.0.0-beta.41", "ansis": "=4.2.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.41", "@rolldown/binding-darwin-arm64": "1.0.0-beta.41", "@rolldown/binding-darwin-x64": "1.0.0-beta.41", "@rolldown/binding-freebsd-x64": "1.0.0-beta.41", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.41", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.41", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.41", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.41", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.41", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.41", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.41", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.41", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.41", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.41" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg=="],
+
+
"rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="],
+
+
"rollup-plugin-typescript2": ["rollup-plugin-typescript2@0.36.0", "", { "dependencies": { "@rollup/pluginutils": "^4.1.2", "find-cache-dir": "^3.3.2", "fs-extra": "^10.0.0", "semver": "^7.5.4", "tslib": "^2.6.2" }, "peerDependencies": { "rollup": ">=1.26.3", "typescript": ">=2.4.0" } }, "sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw=="],
+
+
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+
"semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="],
+
+
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+
+
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
+
+
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
+
+
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
+
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
+
+
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+
+
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
+
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+
"typescript-eslint": ["typescript-eslint@8.48.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.48.1", "@typescript-eslint/parser": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/utils": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A=="],
+
+
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
+
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+
+
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
+
+
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
+
+
"unplugin-dts": ["unplugin-dts@1.0.0-beta.6", "", { "dependencies": { "@rollup/pluginutils": "^5.1.4", "@volar/typescript": "^2.4.17", "compare-versions": "^6.1.1", "debug": "^4.4.0", "kolorist": "^1.8.0", "local-pkg": "^1.1.1", "magic-string": "^0.30.17", "unplugin": "^2.3.2" }, "peerDependencies": { "@microsoft/api-extractor": ">=7", "@rspack/core": "^1", "@vue/language-core": "~3.0.1", "esbuild": "*", "rolldown": "*", "rollup": ">=3", "typescript": ">=4", "vite": ">=3", "webpack": "^4 || ^5" }, "optionalPeers": ["@microsoft/api-extractor", "@rspack/core", "@vue/language-core", "esbuild", "rolldown", "rollup", "vite", "webpack"] }, "sha512-+xbFv5aVFtLZFNBAKI4+kXmd2h+T42/AaP8Bsp0YP/je/uOTN94Ame2Xt3e9isZS+Z7/hrLCLbsVJh+saqFMfQ=="],
+
+
"update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
+
+
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+
+
"vite": ["rolldown-vite@7.1.14", "", { "dependencies": { "@oxc-project/runtime": "0.92.0", "fdir": "^6.5.0", "lightningcss": "^1.30.1", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.41", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw=="],
+
+
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
+
+
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
+
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+
+
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
+
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
+
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
+
+
"@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
+
+
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
+
+
"@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
+
+
"@microsoft/api-extractor/typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
+
+
"@microsoft/tsdoc-config/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="],
+
+
"@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+
"@rushstack/node-core-library/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA=="],
+
+
"@rushstack/node-core-library/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="],
+
+
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+
+
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
+
+
"ajv-formats/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA=="],
+
+
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+
"eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
+
+
"js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+
"make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
+
+
"pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
+
+
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.41", "", {}, "sha512-ycMEPrS3StOIeb87BT3/+bu+blEtyvwQ4zmo2IcJQy0Rd1DAAhKksA0iUZ3MYSpJtjlPhg0Eo6mvVS6ggPhRbw=="],
+
+
"rollup-plugin-typescript2/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
+
+
"unplugin-dts/@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
+
+
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
+
"@microsoft/tsdoc-config/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+
"@rushstack/node-core-library/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+
+
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
+
+
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
+
+
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
+
+
"pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
+
}
+
}
+85 -6
lib/components/BlueskyPost.tsx
···
*/
authorHandle: string;
/**
+
* The author's display name from their profile.
+
*/
+
authorDisplayName?: string;
+
/**
* The DID that owns the post record.
*/
authorDid: string;
···
* Depth of this post in a thread (0 = root, 1 = first reply, etc.).
*/
threadDepth?: number;
+
/**
+
* Whether to show border even when in thread context.
+
*/
+
showThreadBorder?: boolean;
};
export const BLUESKY_POST_COLLECTION = "app.bsky.feed.post";
···
width: "100%",
background: "var(--atproto-color-bg)",
position: "relative",
+
borderRadius: "12px",
+
overflow: "hidden"
};
const parentPostStyle: React.CSSProperties = {
···
const avatar = profile?.avatar;
const avatarCdnUrl = isBlobWithCdn(avatar) ? avatar.cdnUrl : undefined;
const avatarCid = avatarCdnUrl ? undefined : getAvatarCid(profile);
+
const authorDisplayName = profile?.displayName;
const {
record: fetchedRecord,
···
<Comp
{...props}
authorHandle={authorHandle}
+
authorDisplayName={authorDisplayName}
authorDid={repoIdentifier}
avatarUrl={avatarUrl}
iconPlacement={iconPlacement}
showIcon={showIcon}
atUri={atUri}
-
isInThread={true} // Always true for posts rendered in this component
+
isInThread
threadDepth={showParent ? 1 : 0}
+
showThreadBorder={!showParent && !!props.record?.reply?.parent}
/>
);
};
···
avatarCid,
avatarCdnUrl,
authorHandle,
+
authorDisplayName,
iconPlacement,
showIcon,
atUri,
showParent,
]);
+
const WrappedWithoutIcon = useMemo(() => {
+
const WrappedComponent: React.FC<{
+
record: FeedPostRecord;
+
loading: boolean;
+
error?: Error;
+
}> = (props) => {
+
const { url: avatarUrlFromBlob } = useBlob(
+
repoIdentifier,
+
avatarCid,
+
);
+
const avatarUrl = avatarCdnUrl || avatarUrlFromBlob;
+
return (
+
<Comp
+
{...props}
+
authorHandle={authorHandle}
+
authorDisplayName={authorDisplayName}
+
authorDid={repoIdentifier}
+
avatarUrl={avatarUrl}
+
iconPlacement={iconPlacement}
+
showIcon={false}
+
atUri={atUri}
+
isInThread
+
threadDepth={showParent ? 1 : 0}
+
showThreadBorder={!showParent && !!props.record?.reply?.parent}
+
/>
+
);
+
};
+
WrappedComponent.displayName = "BlueskyPostWrappedRendererWithoutIcon";
+
return WrappedComponent;
+
}, [
+
Comp,
+
repoIdentifier,
+
avatarCid,
+
avatarCdnUrl,
+
authorHandle,
+
authorDisplayName,
+
iconPlacement,
+
atUri,
+
showParent,
+
]);
+
if (!displayHandle && resolvingIdentity) {
return <div style={{ padding: 8 }}>Resolving handleโ€ฆ</div>;
}
···
);
};
+
const renderMainPostWithoutIcon = (mainRecord?: FeedPostRecord) => {
+
if (mainRecord !== undefined) {
+
return (
+
<AtProtoRecord<FeedPostRecord>
+
record={mainRecord}
+
renderer={WrappedWithoutIcon}
+
fallback={fallback}
+
loadingIndicator={loadingIndicator}
+
/>
+
);
+
}
+
+
return (
+
<AtProtoRecord<FeedPostRecord>
+
did={repoIdentifier}
+
collection={BLUESKY_POST_COLLECTION}
+
rkey={rkey}
+
renderer={WrappedWithoutIcon}
+
fallback={fallback}
+
loadingIndicator={loadingIndicator}
+
/>
+
);
+
};
+
if (showParent) {
if (currentLoading || (parentLoading && !parentRecord)) {
return (
···
record={parentRecord}
showParent={true}
recursiveParent={true}
-
showIcon={false}
-
iconPlacement="cardBottomRight"
+
showIcon={showIcon}
+
iconPlacement={iconPlacement}
/>
) : (
<BlueskyPost
did={parentDid}
rkey={parentRkey}
record={parentRecord}
-
showIcon={false}
-
iconPlacement="cardBottomRight"
+
showIcon={showIcon}
+
iconPlacement={iconPlacement}
/>
)}
</div>
<div style={replyPostStyle}>
-
{renderMainPost(record || currentRecord)}
+
{renderMainPostWithoutIcon(record || currentRecord)}
</div>
</div>
);
+308 -111
lib/components/BlueskyPostList.tsx
···
type AuthorFeedReason,
type ReplyParentInfo,
} from "../hooks/usePaginatedRecords";
-
import type { FeedPostRecord } from "../types/bluesky";
+
import type { FeedPostRecord, ProfileRecord } from "../types/bluesky";
import { useDidResolution } from "../hooks/useDidResolution";
import { BlueskyIcon } from "./BlueskyIcon";
import { parseAtUri } from "../utils/at-uri";
+
import { useAtProto } from "../providers/AtProtoProvider";
+
import { useAtProtoRecord } from "../hooks/useAtProtoRecord";
+
import { useBlob } from "../hooks/useBlob";
+
import { getAvatarCid } from "../utils/profile";
+
import { isBlobWithCdn } from "../utils/blob";
+
import { BLUESKY_PROFILE_COLLECTION } from "./BlueskyProfile";
+
import { RichText as BlueskyRichText } from "./RichText";
/**
* Options for rendering a paginated list of Bluesky posts.
···
if (error)
return (
-
<div style={{ padding: 8, color: "crimson" }}>
+
<div role="alert" style={{ padding: 8, color: "crimson" }}>
Failed to load posts.
</div>
);
···
record={record.value}
rkey={record.rkey}
did={actorPath}
+
uri={record.uri}
reason={record.reason}
replyParent={record.replyParent}
hasDivider={idx < records.length - 1}
···
record: FeedPostRecord;
rkey: string;
did: string;
+
uri?: string;
reason?: AuthorFeedReason;
replyParent?: ReplyParentInfo;
hasDivider: boolean;
···
record,
rkey,
did,
+
uri,
reason,
replyParent,
hasDivider,
}) => {
+
const { blueskyAppBaseUrl } = useAtProto();
const text = record.text?.trim() ?? "";
const relative = record.createdAt
? formatRelativeTime(record.createdAt)
···
const absolute = record.createdAt
? new Date(record.createdAt).toLocaleString()
: undefined;
-
const href = `https://bsky.app/profile/${did}/post/${rkey}`;
-
const repostLabel =
-
reason?.$type === "app.bsky.feed.defs#reasonRepost"
-
? `${formatActor(reason.by) ?? "Someone"} reposted`
-
: undefined;
+
+
// Parse the URI to get the actual post's DID and rkey
+
const parsedUri = uri ? parseAtUri(uri) : undefined;
+
const postDid = parsedUri?.did ?? did;
+
const postRkey = parsedUri?.rkey ?? rkey;
+
const href = `${blueskyAppBaseUrl}/profile/${postDid}/post/${postRkey}`;
+
+
// Author profile and avatar
+
const { handle: authorHandle } = useDidResolution(postDid);
+
const { record: authorProfile } = useAtProtoRecord<ProfileRecord>({
+
did: postDid,
+
collection: BLUESKY_PROFILE_COLLECTION,
+
rkey: "self",
+
});
+
const authorDisplayName = authorProfile?.displayName;
+
const authorAvatar = authorProfile?.avatar;
+
const authorAvatarCdnUrl = isBlobWithCdn(authorAvatar) ? authorAvatar.cdnUrl : undefined;
+
const authorAvatarCid = authorAvatarCdnUrl ? undefined : getAvatarCid(authorProfile);
+
const { url: authorAvatarUrl } = useBlob(
+
postDid,
+
authorAvatarCid,
+
);
+
const finalAuthorAvatarUrl = authorAvatarCdnUrl ?? authorAvatarUrl;
+
+
// Repost metadata
+
const isRepost = reason?.$type === "app.bsky.feed.defs#reasonRepost";
+
const reposterDid = reason?.by?.did;
+
const { handle: reposterHandle } = useDidResolution(reposterDid);
+
const { record: reposterProfile } = useAtProtoRecord<ProfileRecord>({
+
did: reposterDid,
+
collection: BLUESKY_PROFILE_COLLECTION,
+
rkey: "self",
+
});
+
const reposterDisplayName = reposterProfile?.displayName;
+
const reposterAvatar = reposterProfile?.avatar;
+
const reposterAvatarCdnUrl = isBlobWithCdn(reposterAvatar) ? reposterAvatar.cdnUrl : undefined;
+
const reposterAvatarCid = reposterAvatarCdnUrl ? undefined : getAvatarCid(reposterProfile);
+
const { url: reposterAvatarUrl } = useBlob(
+
reposterDid,
+
reposterAvatarCid,
+
);
+
const finalReposterAvatarUrl = reposterAvatarCdnUrl ?? reposterAvatarUrl;
+
+
// Reply metadata
const parentUri = replyParent?.uri ?? record.reply?.parent?.uri;
-
const parentDid =
-
replyParent?.author?.did ??
-
(parentUri ? parseAtUri(parentUri)?.did : undefined);
-
const { handle: resolvedReplyHandle } = useDidResolution(
+
const parentDid = replyParent?.author?.did ?? (parentUri ? parseAtUri(parentUri)?.did : undefined);
+
const { handle: parentHandle } = useDidResolution(
replyParent?.author?.handle ? undefined : parentDid,
);
-
const replyLabel = formatReplyTarget(
-
parentUri,
-
replyParent,
-
resolvedReplyHandle,
+
const { record: parentProfile } = useAtProtoRecord<ProfileRecord>({
+
did: parentDid,
+
collection: BLUESKY_PROFILE_COLLECTION,
+
rkey: "self",
+
});
+
const parentAvatar = parentProfile?.avatar;
+
const parentAvatarCdnUrl = isBlobWithCdn(parentAvatar) ? parentAvatar.cdnUrl : undefined;
+
const parentAvatarCid = parentAvatarCdnUrl ? undefined : getAvatarCid(parentProfile);
+
const { url: parentAvatarUrl } = useBlob(
+
parentDid,
+
parentAvatarCid,
);
+
const finalParentAvatarUrl = parentAvatarCdnUrl ?? parentAvatarUrl;
+
+
const isReply = !!parentUri;
+
const replyTargetHandle = replyParent?.author?.handle ?? parentHandle;
+
+
const postPreview = text.slice(0, 100);
+
const ariaLabel = text
+
? `Post by ${authorDisplayName ?? authorHandle ?? did}: ${postPreview}${text.length > 100 ? "..." : ""}`
+
: `Post by ${authorDisplayName ?? authorHandle ?? did}`;
return (
-
<a
-
href={href}
-
target="_blank"
-
rel="noopener noreferrer"
+
<div
style={{
-
...listStyles.row,
-
color: `var(--atproto-color-text)`,
-
borderBottom: hasDivider
-
? `1px solid var(--atproto-color-border)`
-
: "none",
+
...listStyles.rowContainer,
+
borderBottom: hasDivider ? `1px solid var(--atproto-color-border)` : "none",
}}
>
-
{repostLabel && (
-
<span style={{ ...listStyles.rowMeta, color: `var(--atproto-color-text-secondary)` }}>
-
{repostLabel}
-
</span>
+
{isRepost && (
+
<div style={listStyles.repostIndicator}>
+
{finalReposterAvatarUrl && (
+
<img
+
src={finalReposterAvatarUrl}
+
alt=""
+
style={listStyles.repostAvatar}
+
/>
+
)}
+
<svg
+
width="16"
+
height="16"
+
viewBox="0 0 16 16"
+
fill="none"
+
style={{ flexShrink: 0 }}
+
>
+
<path
+
d="M5.5 3.5L3 6L5.5 8.5M3 6H10C11.1046 6 12 6.89543 12 8V8.5M10.5 12.5L13 10L10.5 7.5M13 10H6C4.89543 10 4 9.10457 4 8V7.5"
+
stroke="var(--atproto-color-text-secondary)"
+
strokeWidth="1.5"
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
/>
+
</svg>
+
<span style={{ ...listStyles.repostText, color: "var(--atproto-color-text-secondary)" }}>
+
{reposterDisplayName ?? reposterHandle ?? "Someone"} reposted
+
</span>
+
</div>
)}
-
{replyLabel && (
-
<span style={{ ...listStyles.rowMeta, color: `var(--atproto-color-text-secondary)` }}>
-
{replyLabel}
-
</span>
+
+
{isReply && (
+
<div style={listStyles.replyIndicator}>
+
<svg
+
width="14"
+
height="14"
+
viewBox="0 0 14 14"
+
fill="none"
+
style={{ flexShrink: 0 }}
+
>
+
<path
+
d="M11 7H3M3 7L7 3M3 7L7 11"
+
stroke="#1185FE"
+
strokeWidth="1.5"
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
/>
+
</svg>
+
<span style={{ ...listStyles.replyText, color: "var(--atproto-color-text-secondary)" }}>
+
Replying to
+
</span>
+
{finalParentAvatarUrl && (
+
<img
+
src={finalParentAvatarUrl}
+
alt=""
+
style={listStyles.replyAvatar}
+
/>
+
)}
+
<span style={{ color: "#1185FE", fontWeight: 600 }}>
+
@{replyTargetHandle ?? formatDid(parentDid ?? "")}
+
</span>
+
</div>
)}
-
{relative && (
-
<span
-
style={{ ...listStyles.rowTime, color: `var(--atproto-color-text-secondary)` }}
-
title={absolute}
-
>
-
{relative}
-
</span>
-
)}
-
{text && (
-
<p style={{ ...listStyles.rowBody, color: `var(--atproto-color-text)` }}>
-
{text}
-
</p>
-
)}
-
{!text && (
-
<p
-
style={{
-
...listStyles.rowBody,
-
color: `var(--atproto-color-text)`,
-
fontStyle: "italic",
-
}}
-
>
-
No text content.
-
</p>
-
)}
-
</a>
+
+
<div style={listStyles.postContent}>
+
<div style={listStyles.avatarContainer}>
+
{finalAuthorAvatarUrl ? (
+
<img
+
src={finalAuthorAvatarUrl}
+
alt={authorDisplayName ?? authorHandle ?? "User avatar"}
+
style={listStyles.avatar}
+
/>
+
) : (
+
<div style={listStyles.avatarPlaceholder}>
+
{(authorDisplayName ?? authorHandle ?? "?")[0].toUpperCase()}
+
</div>
+
)}
+
</div>
+
+
<div style={listStyles.postMain}>
+
<div style={listStyles.postHeader}>
+
<a
+
href={`${blueskyAppBaseUrl}/profile/${postDid}`}
+
target="_blank"
+
rel="noopener noreferrer"
+
style={{ ...listStyles.authorName, color: "var(--atproto-color-text)" }}
+
onClick={(e) => e.stopPropagation()}
+
>
+
{authorDisplayName ?? authorHandle ?? formatDid(postDid)}
+
</a>
+
<span style={{ ...listStyles.authorHandle, color: "var(--atproto-color-text-secondary)" }}>
+
@{authorHandle ?? formatDid(postDid)}
+
</span>
+
<span style={{ ...listStyles.separator, color: "var(--atproto-color-text-secondary)" }}>ยท</span>
+
<span
+
style={{ ...listStyles.timestamp, color: "var(--atproto-color-text-secondary)" }}
+
title={absolute}
+
>
+
{relative}
+
</span>
+
</div>
+
+
<a
+
href={href}
+
target="_blank"
+
rel="noopener noreferrer"
+
aria-label={ariaLabel}
+
style={{ ...listStyles.postLink, color: "var(--atproto-color-text)" }}
+
>
+
{text && (
+
<p style={listStyles.postText}>
+
<BlueskyRichText text={text} facets={record.facets} />
+
</p>
+
)}
+
{!text && (
+
<p style={{ ...listStyles.postText, fontStyle: "italic", color: "var(--atproto-color-text-secondary)" }}>
+
No text content
+
</p>
+
)}
+
</a>
+
</div>
+
</div>
+
</div>
);
};
···
display: "flex",
alignItems: "center",
justifyContent: "center",
-
//background: 'rgba(17, 133, 254, 0.14)',
borderRadius: "50%",
} satisfies React.CSSProperties,
headerText: {
···
fontSize: 13,
textAlign: "center",
} satisfies React.CSSProperties,
-
row: {
-
padding: "18px",
-
textDecoration: "none",
+
rowContainer: {
+
padding: "16px",
display: "flex",
flexDirection: "column",
-
gap: 6,
+
gap: 8,
transition: "background-color 120ms ease",
+
position: "relative",
} satisfies React.CSSProperties,
-
rowHeader: {
+
repostIndicator: {
display: "flex",
-
gap: 6,
-
alignItems: "baseline",
+
alignItems: "center",
+
gap: 8,
fontSize: 13,
+
fontWeight: 500,
+
paddingLeft: 8,
+
marginBottom: 4,
+
} satisfies React.CSSProperties,
+
repostAvatar: {
+
width: 16,
+
height: 16,
+
borderRadius: "50%",
+
objectFit: "cover",
} satisfies React.CSSProperties,
-
rowTime: {
-
fontSize: 12,
+
repostText: {
+
fontSize: 13,
fontWeight: 500,
} satisfies React.CSSProperties,
-
rowMeta: {
-
fontSize: 12,
+
replyIndicator: {
+
display: "flex",
+
alignItems: "center",
+
gap: 8,
+
fontSize: 13,
fontWeight: 500,
-
letterSpacing: "0.6px",
+
paddingLeft: 8,
+
marginBottom: 4,
+
} satisfies React.CSSProperties,
+
replyAvatar: {
+
width: 16,
+
height: 16,
+
borderRadius: "50%",
+
objectFit: "cover",
+
} satisfies React.CSSProperties,
+
replyText: {
+
fontSize: 13,
+
fontWeight: 500,
+
} satisfies React.CSSProperties,
+
postContent: {
+
display: "flex",
+
gap: 12,
+
} satisfies React.CSSProperties,
+
avatarContainer: {
+
flexShrink: 0,
+
} satisfies React.CSSProperties,
+
avatar: {
+
width: 48,
+
height: 48,
+
borderRadius: "50%",
+
objectFit: "cover",
} satisfies React.CSSProperties,
-
rowBody: {
+
avatarPlaceholder: {
+
width: 48,
+
height: 48,
+
borderRadius: "50%",
+
background: "var(--atproto-color-bg-elevated)",
+
color: "var(--atproto-color-text-secondary)",
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
fontSize: 18,
+
fontWeight: 600,
+
} satisfies React.CSSProperties,
+
postMain: {
+
flex: 1,
+
minWidth: 0,
+
display: "flex",
+
flexDirection: "column",
+
gap: 6,
+
} satisfies React.CSSProperties,
+
postHeader: {
+
display: "flex",
+
alignItems: "baseline",
+
gap: 6,
+
flexWrap: "wrap",
+
} satisfies React.CSSProperties,
+
authorName: {
+
fontWeight: 700,
+
fontSize: 15,
+
textDecoration: "none",
+
maxWidth: "200px",
+
overflow: "hidden",
+
textOverflow: "ellipsis",
+
whiteSpace: "nowrap",
+
} satisfies React.CSSProperties,
+
authorHandle: {
+
fontSize: 15,
+
fontWeight: 400,
+
maxWidth: "150px",
+
overflow: "hidden",
+
textOverflow: "ellipsis",
+
whiteSpace: "nowrap",
+
} satisfies React.CSSProperties,
+
separator: {
+
fontSize: 15,
+
fontWeight: 400,
+
} satisfies React.CSSProperties,
+
timestamp: {
+
fontSize: 15,
+
fontWeight: 400,
+
} satisfies React.CSSProperties,
+
postLink: {
+
textDecoration: "none",
+
display: "block",
+
} satisfies React.CSSProperties,
+
postText: {
margin: 0,
whiteSpace: "pre-wrap",
-
fontSize: 14,
-
lineHeight: 1.45,
+
fontSize: 15,
+
lineHeight: 1.5,
+
wordBreak: "break-word",
} satisfies React.CSSProperties,
footer: {
display: "flex",
···
padding: "12px 18px",
borderTop: "1px solid transparent",
fontSize: 13,
-
} satisfies React.CSSProperties,
-
navButton: {
-
border: "none",
-
borderRadius: 999,
-
padding: "6px 12px",
-
fontSize: 13,
-
fontWeight: 500,
-
background: "transparent",
-
display: "flex",
-
alignItems: "center",
-
gap: 4,
-
transition: "background-color 120ms ease",
} satisfies React.CSSProperties,
pageChips: {
display: "flex",
···
};
export default BlueskyPostList;
-
-
function formatActor(actor?: { handle?: string; did?: string }) {
-
if (!actor) return undefined;
-
if (actor.handle) return `@${actor.handle}`;
-
if (actor.did) return `@${formatDid(actor.did)}`;
-
return undefined;
-
}
-
-
function formatReplyTarget(
-
parentUri?: string,
-
feedParent?: ReplyParentInfo,
-
resolvedHandle?: string,
-
) {
-
const directHandle = feedParent?.author?.handle;
-
const handle = directHandle ?? resolvedHandle;
-
if (handle) {
-
return `Replying to @${handle}`;
-
}
-
const parentDid = feedParent?.author?.did;
-
const targetUri = feedParent?.uri ?? parentUri;
-
if (!targetUri) return undefined;
-
const parsed = parseAtUri(targetUri);
-
const did = parentDid ?? parsed?.did;
-
if (!did) return undefined;
-
return `Replying to @${formatDid(did)}`;
-
}
+143
lib/components/CurrentlyPlaying.tsx
···
+
import React from "react";
+
import { AtProtoRecord } from "../core/AtProtoRecord";
+
import { CurrentlyPlayingRenderer } from "../renderers/CurrentlyPlayingRenderer";
+
import { useDidResolution } from "../hooks/useDidResolution";
+
import type { TealActorStatusRecord } from "../types/teal";
+
+
/**
+
* Props for rendering teal.fm currently playing status.
+
*/
+
export interface CurrentlyPlayingProps {
+
/** DID of the user whose currently playing status to display. */
+
did: string;
+
/** Record key within the `fm.teal.alpha.actor.status` collection (usually "self"). */
+
rkey?: string;
+
/** Prefetched teal.fm status record. When provided, skips fetching from the network. */
+
record?: TealActorStatusRecord;
+
/** Optional renderer override for custom presentation. */
+
renderer?: React.ComponentType<CurrentlyPlayingRendererInjectedProps>;
+
/** Fallback node displayed before loading begins. */
+
fallback?: React.ReactNode;
+
/** Indicator node shown while data is loading. */
+
loadingIndicator?: React.ReactNode;
+
/** Preferred color scheme for theming. */
+
colorScheme?: "light" | "dark" | "system";
+
/** Auto-refresh music data and album art. When true, refreshes every 15 seconds. Defaults to true. */
+
autoRefresh?: boolean;
+
/** Refresh interval in milliseconds. Defaults to 15000 (15 seconds). Only used when autoRefresh is true. */
+
refreshInterval?: number;
+
}
+
+
/**
+
* Values injected into custom currently playing renderer implementations.
+
*/
+
export type CurrentlyPlayingRendererInjectedProps = {
+
/** Loaded teal.fm status record value. */
+
record: TealActorStatusRecord;
+
/** Indicates whether the record is currently loading. */
+
loading: boolean;
+
/** Fetch error, if any. */
+
error?: Error;
+
/** Preferred color scheme for downstream components. */
+
colorScheme?: "light" | "dark" | "system";
+
/** DID associated with the record. */
+
did: string;
+
/** Record key for the status. */
+
rkey: string;
+
/** Label to display. */
+
label?: string;
+
/** Handle to display in not listening state */
+
handle?: string;
+
};
+
+
/** NSID for teal.fm actor status records. */
+
export const CURRENTLY_PLAYING_COLLECTION = "fm.teal.alpha.actor.status";
+
+
/**
+
* Compares two teal.fm status records to determine if the track has changed.
+
* Used to prevent unnecessary re-renders during auto-refresh when the same track is still playing.
+
*/
+
const compareTealRecords = (
+
prev: TealActorStatusRecord | undefined,
+
next: TealActorStatusRecord | undefined
+
): boolean => {
+
if (!prev || !next) return prev === next;
+
+
const prevTrack = prev.item.trackName;
+
const nextTrack = next.item.trackName;
+
const prevArtist = prev.item.artists[0]?.artistName;
+
const nextArtist = next.item.artists[0]?.artistName;
+
+
return prevTrack === nextTrack && prevArtist === nextArtist;
+
};
+
+
/**
+
* Displays the currently playing track from teal.fm with auto-refresh.
+
*
+
* @param did - DID whose currently playing status should be fetched.
+
* @param rkey - Record key within the teal.fm status collection (defaults to "self").
+
* @param renderer - Optional component override that will receive injected props.
+
* @param fallback - Node rendered before the first load begins.
+
* @param loadingIndicator - Node rendered while the status is loading.
+
* @param colorScheme - Preferred color scheme for theming the renderer.
+
* @param autoRefresh - When true (default), refreshes the record every 15 seconds (or custom interval).
+
* @param refreshInterval - Custom refresh interval in milliseconds. Defaults to 15000 (15 seconds).
+
* @returns A JSX subtree representing the currently playing track with loading states handled.
+
*/
+
export const CurrentlyPlaying: React.FC<CurrentlyPlayingProps> = React.memo(({
+
did,
+
rkey = "self",
+
record,
+
renderer,
+
fallback,
+
loadingIndicator,
+
colorScheme,
+
autoRefresh = true,
+
refreshInterval = 15000,
+
}) => {
+
// Resolve handle from DID
+
const { handle } = useDidResolution(did);
+
+
const Comp: React.ComponentType<CurrentlyPlayingRendererInjectedProps> =
+
renderer ?? ((props) => <CurrentlyPlayingRenderer {...props} />);
+
const Wrapped: React.FC<{
+
record: TealActorStatusRecord;
+
loading: boolean;
+
error?: Error;
+
}> = (props) => (
+
<Comp
+
{...props}
+
colorScheme={colorScheme}
+
did={did}
+
rkey={rkey}
+
label="CURRENTLY PLAYING"
+
handle={handle}
+
/>
+
);
+
+
if (record !== undefined) {
+
return (
+
<AtProtoRecord<TealActorStatusRecord>
+
record={record}
+
renderer={Wrapped}
+
fallback={fallback}
+
loadingIndicator={loadingIndicator}
+
/>
+
);
+
}
+
+
return (
+
<AtProtoRecord<TealActorStatusRecord>
+
did={did}
+
collection={CURRENTLY_PLAYING_COLLECTION}
+
rkey={rkey}
+
renderer={Wrapped}
+
fallback={fallback}
+
loadingIndicator={loadingIndicator}
+
refreshInterval={autoRefresh ? refreshInterval : undefined}
+
compareRecords={compareTealRecords}
+
/>
+
);
+
});
+
+
export default CurrentlyPlaying;
+327
lib/components/GrainGallery.tsx
···
+
import React, { useMemo, useEffect, useState } from "react";
+
import { GrainGalleryRenderer, type GrainGalleryPhoto } from "../renderers/GrainGalleryRenderer";
+
import type { GrainGalleryRecord, GrainGalleryItemRecord, GrainPhotoRecord } from "../types/grain";
+
import type { ProfileRecord } from "../types/bluesky";
+
import { useDidResolution } from "../hooks/useDidResolution";
+
import { useAtProtoRecord } from "../hooks/useAtProtoRecord";
+
import { useBacklinks } from "../hooks/useBacklinks";
+
import { useBlob } from "../hooks/useBlob";
+
import { BLUESKY_PROFILE_COLLECTION } from "./BlueskyProfile";
+
import { getAvatarCid } from "../utils/profile";
+
import { formatDidForLabel, parseAtUri } from "../utils/at-uri";
+
import { isBlobWithCdn } from "../utils/blob";
+
import { createAtprotoClient } from "../utils/atproto-client";
+
+
/**
+
* Props for rendering a grain.social gallery.
+
*/
+
export interface GrainGalleryProps {
+
/**
+
* Decentralized identifier for the repository that owns the gallery.
+
*/
+
did: string;
+
/**
+
* Record key identifying the specific gallery within the collection.
+
*/
+
rkey: string;
+
/**
+
* Prefetched gallery record. When provided, skips fetching the gallery from the network.
+
*/
+
record?: GrainGalleryRecord;
+
/**
+
* Custom renderer component that receives resolved gallery data and status flags.
+
*/
+
renderer?: React.ComponentType<GrainGalleryRendererInjectedProps>;
+
/**
+
* React node shown while the gallery query has not yet produced data or an error.
+
*/
+
fallback?: React.ReactNode;
+
/**
+
* React node displayed while the gallery fetch is actively loading.
+
*/
+
loadingIndicator?: React.ReactNode;
+
/**
+
* Constellation API base URL for fetching backlinks.
+
*/
+
constellationBaseUrl?: string;
+
}
+
+
/**
+
* Values injected by `GrainGallery` into a downstream renderer component.
+
*/
+
export type GrainGalleryRendererInjectedProps = {
+
/**
+
* Resolved gallery record
+
*/
+
gallery: GrainGalleryRecord;
+
/**
+
* Array of photos in the gallery with their records and metadata
+
*/
+
photos: GrainGalleryPhoto[];
+
/**
+
* `true` while network operations are in-flight.
+
*/
+
loading: boolean;
+
/**
+
* Error encountered during loading, if any.
+
*/
+
error?: Error;
+
/**
+
* The author's public handle derived from the DID.
+
*/
+
authorHandle?: string;
+
/**
+
* The author's display name from their profile.
+
*/
+
authorDisplayName?: string;
+
/**
+
* Resolved URL for the author's avatar blob, if available.
+
*/
+
avatarUrl?: string;
+
};
+
+
export const GRAIN_GALLERY_COLLECTION = "social.grain.gallery";
+
export const GRAIN_GALLERY_ITEM_COLLECTION = "social.grain.gallery.item";
+
export const GRAIN_PHOTO_COLLECTION = "social.grain.photo";
+
+
/**
+
* Fetches a grain.social gallery, resolves all photos via constellation backlinks,
+
* and renders them in a grid layout.
+
*
+
* @param did - DID of the repository that stores the gallery.
+
* @param rkey - Record key for the gallery.
+
* @param record - Prefetched gallery record.
+
* @param renderer - Optional renderer component to override the default.
+
* @param fallback - Node rendered before the first fetch attempt resolves.
+
* @param loadingIndicator - Node rendered while the gallery is loading.
+
* @param constellationBaseUrl - Constellation API base URL.
+
* @returns A component that renders loading/fallback states and the resolved gallery.
+
*/
+
export const GrainGallery: React.FC<GrainGalleryProps> = React.memo(
+
({
+
did: handleOrDid,
+
rkey,
+
record,
+
renderer,
+
fallback,
+
loadingIndicator,
+
constellationBaseUrl,
+
}) => {
+
const {
+
did: resolvedDid,
+
handle,
+
loading: resolvingIdentity,
+
error: resolutionError,
+
} = useDidResolution(handleOrDid);
+
+
const repoIdentifier = resolvedDid ?? handleOrDid;
+
+
// Fetch author profile
+
const { record: profile } = useAtProtoRecord<ProfileRecord>({
+
did: repoIdentifier,
+
collection: BLUESKY_PROFILE_COLLECTION,
+
rkey: "self",
+
});
+
const avatar = profile?.avatar;
+
const avatarCdnUrl = isBlobWithCdn(avatar) ? avatar.cdnUrl : undefined;
+
const avatarCid = avatarCdnUrl ? undefined : getAvatarCid(profile);
+
const authorDisplayName = profile?.displayName;
+
const { url: avatarUrlFromBlob } = useBlob(repoIdentifier, avatarCid);
+
const avatarUrl = avatarCdnUrl || avatarUrlFromBlob;
+
+
// Fetch gallery record
+
const {
+
record: fetchedGallery,
+
loading: galleryLoading,
+
error: galleryError,
+
} = useAtProtoRecord<GrainGalleryRecord>({
+
did: record ? "" : repoIdentifier,
+
collection: record ? "" : GRAIN_GALLERY_COLLECTION,
+
rkey: record ? "" : rkey,
+
});
+
+
const galleryRecord = record ?? fetchedGallery;
+
const galleryUri = resolvedDid
+
? `at://${resolvedDid}/${GRAIN_GALLERY_COLLECTION}/${rkey}`
+
: undefined;
+
+
// Fetch backlinks to get gallery items
+
const {
+
backlinks,
+
loading: backlinksLoading,
+
error: backlinksError,
+
} = useBacklinks({
+
subject: galleryUri || "",
+
source: `${GRAIN_GALLERY_ITEM_COLLECTION}:gallery`,
+
enabled: !!galleryUri && !!galleryRecord,
+
constellationBaseUrl,
+
});
+
+
// Fetch all gallery item records and photo records
+
const [photos, setPhotos] = useState<GrainGalleryPhoto[]>([]);
+
const [photosLoading, setPhotosLoading] = useState(false);
+
const [photosError, setPhotosError] = useState<Error | undefined>(undefined);
+
+
useEffect(() => {
+
if (!backlinks || backlinks.length === 0) {
+
setPhotos([]);
+
return;
+
}
+
+
let cancelled = false;
+
setPhotosLoading(true);
+
setPhotosError(undefined);
+
+
(async () => {
+
try {
+
const photoPromises = backlinks.map(async (backlink) => {
+
// Create client for gallery item DID (uses slingshot + PDS fallback)
+
const { rpc: galleryItemClient } = await createAtprotoClient({
+
did: backlink.did,
+
});
+
+
// Fetch gallery item record
+
const galleryItemRes = await (
+
galleryItemClient as unknown as {
+
get: (
+
nsid: string,
+
opts: {
+
params: {
+
repo: string;
+
collection: string;
+
rkey: string;
+
};
+
},
+
) => Promise<{ ok: boolean; data: { value: GrainGalleryItemRecord } }>;
+
}
+
).get("com.atproto.repo.getRecord", {
+
params: {
+
repo: backlink.did,
+
collection: GRAIN_GALLERY_ITEM_COLLECTION,
+
rkey: backlink.rkey,
+
},
+
});
+
+
if (!galleryItemRes.ok) return null;
+
+
const galleryItem = galleryItemRes.data.value;
+
+
// Parse photo URI
+
const photoUri = parseAtUri(galleryItem.item);
+
if (!photoUri) return null;
+
+
// Create client for photo DID (uses slingshot + PDS fallback)
+
const { rpc: photoClient } = await createAtprotoClient({
+
did: photoUri.did,
+
});
+
+
// Fetch photo record
+
const photoRes = await (
+
photoClient as unknown as {
+
get: (
+
nsid: string,
+
opts: {
+
params: {
+
repo: string;
+
collection: string;
+
rkey: string;
+
};
+
},
+
) => Promise<{ ok: boolean; data: { value: GrainPhotoRecord } }>;
+
}
+
).get("com.atproto.repo.getRecord", {
+
params: {
+
repo: photoUri.did,
+
collection: photoUri.collection,
+
rkey: photoUri.rkey,
+
},
+
});
+
+
if (!photoRes.ok) return null;
+
+
const photoRecord = photoRes.data.value;
+
+
return {
+
record: photoRecord,
+
did: photoUri.did,
+
rkey: photoUri.rkey,
+
position: galleryItem.position,
+
} as GrainGalleryPhoto;
+
});
+
+
const resolvedPhotos = await Promise.all(photoPromises);
+
const validPhotos = resolvedPhotos.filter((p): p is NonNullable<typeof p> => p !== null) as GrainGalleryPhoto[];
+
+
if (!cancelled) {
+
setPhotos(validPhotos);
+
setPhotosLoading(false);
+
}
+
} catch (err) {
+
if (!cancelled) {
+
setPhotosError(err instanceof Error ? err : new Error("Failed to fetch photos"));
+
setPhotosLoading(false);
+
}
+
}
+
})();
+
+
return () => {
+
cancelled = true;
+
};
+
}, [backlinks]);
+
+
const Comp: React.ComponentType<GrainGalleryRendererInjectedProps> =
+
useMemo(
+
() =>
+
renderer ?? ((props) => <GrainGalleryRenderer {...props} />),
+
[renderer],
+
);
+
+
const displayHandle =
+
handle ??
+
(handleOrDid.startsWith("did:") ? undefined : handleOrDid);
+
const authorHandle =
+
displayHandle ?? formatDidForLabel(resolvedDid ?? handleOrDid);
+
+
if (!displayHandle && resolvingIdentity) {
+
return loadingIndicator || <div role="status" aria-live="polite" style={{ padding: 8 }}>Resolving handleโ€ฆ</div>;
+
}
+
if (!displayHandle && resolutionError) {
+
return (
+
<div style={{ padding: 8, color: "crimson" }}>
+
Could not resolve handle.
+
</div>
+
);
+
}
+
+
if (galleryError || backlinksError || photosError) {
+
return (
+
<div style={{ padding: 8, color: "crimson" }}>
+
Failed to load gallery.
+
</div>
+
);
+
}
+
+
if (!galleryRecord && galleryLoading) {
+
return loadingIndicator || <div style={{ padding: 8 }}>Loading galleryโ€ฆ</div>;
+
}
+
+
if (!galleryRecord) {
+
return fallback || <div style={{ padding: 8 }}>Gallery not found.</div>;
+
}
+
+
const loading = galleryLoading || backlinksLoading || photosLoading;
+
+
return (
+
<Comp
+
gallery={galleryRecord}
+
photos={photos}
+
loading={loading}
+
authorHandle={authorHandle}
+
authorDisplayName={authorDisplayName}
+
avatarUrl={avatarUrl}
+
/>
+
);
+
},
+
);
+
+
export default GrainGallery;
+165
lib/components/LastPlayed.tsx
···
+
import React, { useMemo } from "react";
+
import { useLatestRecord } from "../hooks/useLatestRecord";
+
import { useDidResolution } from "../hooks/useDidResolution";
+
import { CurrentlyPlayingRenderer } from "../renderers/CurrentlyPlayingRenderer";
+
import type { TealFeedPlayRecord, TealActorStatusRecord } from "../types/teal";
+
+
/**
+
* Props for rendering the last played track from teal.fm feed.
+
*/
+
export interface LastPlayedProps {
+
/** DID of the user whose last played track to display. */
+
did: string;
+
/** Optional renderer override for custom presentation. */
+
renderer?: React.ComponentType<LastPlayedRendererInjectedProps>;
+
/** Fallback node displayed before loading begins. */
+
fallback?: React.ReactNode;
+
/** Indicator node shown while data is loading. */
+
loadingIndicator?: React.ReactNode;
+
/** Preferred color scheme for theming. */
+
colorScheme?: "light" | "dark" | "system";
+
/** Auto-refresh music data and album art. Defaults to false for last played. */
+
autoRefresh?: boolean;
+
/** Refresh interval in milliseconds. Defaults to 60000 (60 seconds). */
+
refreshInterval?: number;
+
}
+
+
/**
+
* Values injected into custom last played renderer implementations.
+
*/
+
export type LastPlayedRendererInjectedProps = {
+
/** Loaded teal.fm feed play record value. */
+
record: TealActorStatusRecord;
+
/** Indicates whether the record is currently loading. */
+
loading: boolean;
+
/** Fetch error, if any. */
+
error?: Error;
+
/** Preferred color scheme for downstream components. */
+
colorScheme?: "light" | "dark" | "system";
+
/** DID associated with the record. */
+
did: string;
+
/** Record key for the play record. */
+
rkey: string;
+
/** Handle to display in not listening state */
+
handle?: string;
+
};
+
+
/** NSID for teal.fm feed play records. */
+
export const LAST_PLAYED_COLLECTION = "fm.teal.alpha.feed.play";
+
+
/**
+
* Displays the last played track from teal.fm feed.
+
*
+
* @param did - DID whose last played track should be fetched.
+
* @param renderer - Optional component override that will receive injected props.
+
* @param fallback - Node rendered before the first load begins.
+
* @param loadingIndicator - Node rendered while the data is loading.
+
* @param colorScheme - Preferred color scheme for theming the renderer.
+
* @param autoRefresh - When true, refreshes album art and streaming platform links at the specified interval. Defaults to false.
+
* @param refreshInterval - Refresh interval in milliseconds. Defaults to 60000 (60 seconds).
+
* @returns A JSX subtree representing the last played track with loading states handled.
+
*/
+
export const LastPlayed: React.FC<LastPlayedProps> = React.memo(({
+
did,
+
renderer,
+
fallback,
+
loadingIndicator,
+
colorScheme,
+
autoRefresh = false,
+
refreshInterval = 60000,
+
}) => {
+
// Resolve handle from DID
+
const { handle } = useDidResolution(did);
+
+
// Auto-refresh key for refetching teal.fm record
+
const [refreshKey, setRefreshKey] = React.useState(0);
+
+
// Auto-refresh interval
+
React.useEffect(() => {
+
if (!autoRefresh) return;
+
+
const interval = setInterval(() => {
+
setRefreshKey((prev) => prev + 1);
+
}, refreshInterval);
+
+
return () => clearInterval(interval);
+
}, [autoRefresh, refreshInterval]);
+
+
const { record, rkey, loading, error, empty } = useLatestRecord<TealFeedPlayRecord>(
+
did,
+
LAST_PLAYED_COLLECTION,
+
refreshKey,
+
);
+
+
// Normalize TealFeedPlayRecord to match TealActorStatusRecord structure
+
// Use useMemo to prevent creating new object on every render
+
// MUST be called before any conditional returns (Rules of Hooks)
+
const normalizedRecord = useMemo(() => {
+
if (!record) return null;
+
+
return {
+
$type: "fm.teal.alpha.actor.status" as const,
+
item: {
+
artists: record.artists,
+
originUrl: record.originUrl,
+
trackName: record.trackName,
+
playedTime: record.playedTime,
+
releaseName: record.releaseName,
+
recordingMbId: record.recordingMbId,
+
releaseMbId: record.releaseMbId,
+
submissionClientAgent: record.submissionClientAgent,
+
musicServiceBaseDomain: record.musicServiceBaseDomain,
+
isrc: record.isrc,
+
duration: record.duration,
+
},
+
time: new Date(record.playedTime).getTime().toString(),
+
expiry: undefined,
+
};
+
}, [record]);
+
+
const Comp = renderer ?? CurrentlyPlayingRenderer;
+
+
// Now handle conditional returns after all hooks
+
if (error) {
+
return (
+
<div style={{ padding: 8, color: "var(--atproto-color-error)" }}>
+
Failed to load last played track.
+
</div>
+
);
+
}
+
+
if (loading && !record) {
+
return loadingIndicator ? (
+
<>{loadingIndicator}</>
+
) : (
+
<div style={{ padding: 8, color: "var(--atproto-color-text-secondary)" }}>
+
Loadingโ€ฆ
+
</div>
+
);
+
}
+
+
if (empty || !record || !normalizedRecord) {
+
return fallback ? (
+
<>{fallback}</>
+
) : (
+
<div style={{ padding: 8, color: "var(--atproto-color-text-secondary)" }}>
+
No plays found.
+
</div>
+
);
+
}
+
+
return (
+
<Comp
+
record={normalizedRecord}
+
loading={loading}
+
error={error}
+
colorScheme={colorScheme}
+
did={did}
+
rkey={rkey || "unknown"}
+
label="LAST PLAYED"
+
handle={handle}
+
/>
+
);
+
});
+
+
export default LastPlayed;
+125
lib/components/RichText.tsx
···
+
import React from "react";
+
import type { AppBskyRichtextFacet } from "@atcute/bluesky";
+
import { createTextSegments, type TextSegment } from "../utils/richtext";
+
import { useAtProto } from "../providers/AtProtoProvider";
+
+
export interface RichTextProps {
+
text: string;
+
facets?: AppBskyRichtextFacet.Main[];
+
style?: React.CSSProperties;
+
}
+
+
/**
+
* RichText component that renders text with facets (mentions, links, hashtags).
+
* Properly handles byte offsets and multi-byte characters.
+
*/
+
export const RichText: React.FC<RichTextProps> = ({ text, facets, style }) => {
+
const { blueskyAppBaseUrl } = useAtProto();
+
const segments = createTextSegments(text, facets);
+
+
return (
+
<span style={style}>
+
{segments.map((segment, idx) => (
+
<RichTextSegment key={idx} segment={segment} blueskyAppBaseUrl={blueskyAppBaseUrl} />
+
))}
+
</span>
+
);
+
};
+
+
interface RichTextSegmentProps {
+
segment: TextSegment;
+
blueskyAppBaseUrl: string;
+
}
+
+
const RichTextSegment: React.FC<RichTextSegmentProps> = ({ segment, blueskyAppBaseUrl }) => {
+
if (!segment.facet) {
+
return <>{segment.text}</>;
+
}
+
+
// Find the first feature in the facet
+
const feature = segment.facet.features?.[0];
+
if (!feature) {
+
return <>{segment.text}</>;
+
}
+
+
const featureType = (feature as { $type?: string }).$type;
+
+
// Render based on feature type
+
switch (featureType) {
+
case "app.bsky.richtext.facet#link": {
+
const linkFeature = feature as AppBskyRichtextFacet.Link;
+
return (
+
<a
+
href={linkFeature.uri}
+
target="_blank"
+
rel="noopener noreferrer"
+
style={{
+
color: "var(--atproto-color-link)",
+
textDecoration: "none",
+
}}
+
onMouseEnter={(e) => {
+
e.currentTarget.style.textDecoration = "underline";
+
}}
+
onMouseLeave={(e) => {
+
e.currentTarget.style.textDecoration = "none";
+
}}
+
>
+
{segment.text}
+
</a>
+
);
+
}
+
+
case "app.bsky.richtext.facet#mention": {
+
const mentionFeature = feature as AppBskyRichtextFacet.Mention;
+
const profileUrl = `${blueskyAppBaseUrl}/profile/${mentionFeature.did}`;
+
return (
+
<a
+
href={profileUrl}
+
target="_blank"
+
rel="noopener noreferrer"
+
style={{
+
color: "var(--atproto-color-link)",
+
textDecoration: "none",
+
}}
+
onMouseEnter={(e) => {
+
e.currentTarget.style.textDecoration = "underline";
+
}}
+
onMouseLeave={(e) => {
+
e.currentTarget.style.textDecoration = "none";
+
}}
+
>
+
{segment.text}
+
</a>
+
);
+
}
+
+
case "app.bsky.richtext.facet#tag": {
+
const tagFeature = feature as AppBskyRichtextFacet.Tag;
+
const tagUrl = `${blueskyAppBaseUrl}/hashtag/${encodeURIComponent(tagFeature.tag)}`;
+
return (
+
<a
+
href={tagUrl}
+
target="_blank"
+
rel="noopener noreferrer"
+
style={{
+
color: "var(--atproto-color-link)",
+
textDecoration: "none",
+
}}
+
onMouseEnter={(e) => {
+
e.currentTarget.style.textDecoration = "underline";
+
}}
+
onMouseLeave={(e) => {
+
e.currentTarget.style.textDecoration = "none";
+
}}
+
>
+
{segment.text}
+
</a>
+
);
+
}
+
+
default:
+
return <>{segment.text}</>;
+
}
+
};
+
+
export default RichText;
+648
lib/components/SongHistoryList.tsx
···
+
import React, { useState, useEffect, useMemo } from "react";
+
import { usePaginatedRecords } from "../hooks/usePaginatedRecords";
+
import { useDidResolution } from "../hooks/useDidResolution";
+
import type { TealFeedPlayRecord } from "../types/teal";
+
+
/**
+
* Options for rendering a paginated list of song history from teal.fm.
+
*/
+
export interface SongHistoryListProps {
+
/**
+
* DID whose song history should be fetched.
+
*/
+
did: string;
+
/**
+
* Maximum number of records to list per page. Defaults to `6`.
+
*/
+
limit?: number;
+
/**
+
* Enables pagination controls when `true`. Defaults to `true`.
+
*/
+
enablePagination?: boolean;
+
}
+
+
interface SonglinkResponse {
+
linksByPlatform: {
+
[platform: string]: {
+
url: string;
+
entityUniqueId: string;
+
};
+
};
+
entitiesByUniqueId: {
+
[id: string]: {
+
thumbnailUrl?: string;
+
title?: string;
+
artistName?: string;
+
};
+
};
+
entityUniqueId?: string;
+
}
+
+
/**
+
* Fetches a user's song history from teal.fm and renders them with album art focus.
+
*
+
* @param did - DID whose song history should be displayed.
+
* @param limit - Maximum number of songs per page. Default `6`.
+
* @param enablePagination - Whether pagination controls should render. Default `true`.
+
* @returns A card-like list element with loading, empty, and error handling.
+
*/
+
export const SongHistoryList: React.FC<SongHistoryListProps> = React.memo(({
+
did,
+
limit = 6,
+
enablePagination = true,
+
}) => {
+
const { handle: resolvedHandle } = useDidResolution(did);
+
const actorLabel = resolvedHandle ?? formatDid(did);
+
+
const {
+
records,
+
loading,
+
error,
+
hasNext,
+
hasPrev,
+
loadNext,
+
loadPrev,
+
pageIndex,
+
pagesCount,
+
} = usePaginatedRecords<TealFeedPlayRecord>({
+
did,
+
collection: "fm.teal.alpha.feed.play",
+
limit,
+
});
+
+
const pageLabel = useMemo(() => {
+
const knownTotal = Math.max(pageIndex + 1, pagesCount);
+
if (!enablePagination) return undefined;
+
if (hasNext && knownTotal === pageIndex + 1)
+
return `${pageIndex + 1}/โ€ฆ`;
+
return `${pageIndex + 1}/${knownTotal}`;
+
}, [enablePagination, hasNext, pageIndex, pagesCount]);
+
+
if (error)
+
return (
+
<div role="alert" style={{ padding: 8, color: "crimson" }}>
+
Failed to load song history.
+
</div>
+
);
+
+
return (
+
<div style={{ ...listStyles.card, background: `var(--atproto-color-bg)`, borderWidth: "1px", borderStyle: "solid", borderColor: `var(--atproto-color-border)` }}>
+
<div style={{ ...listStyles.header, background: `var(--atproto-color-bg-elevated)`, color: `var(--atproto-color-text)` }}>
+
<div style={listStyles.headerInfo}>
+
<div style={listStyles.headerIcon}>
+
<svg
+
width="24"
+
height="24"
+
viewBox="0 0 24 24"
+
fill="none"
+
stroke="currentColor"
+
strokeWidth="2"
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
>
+
<path d="M9 18V5l12-2v13" />
+
<circle cx="6" cy="18" r="3" />
+
<circle cx="18" cy="16" r="3" />
+
</svg>
+
</div>
+
<div style={listStyles.headerText}>
+
<span style={listStyles.title}>Listening History</span>
+
<span
+
style={{
+
...listStyles.subtitle,
+
color: `var(--atproto-color-text-secondary)`,
+
}}
+
>
+
@{actorLabel}
+
</span>
+
</div>
+
</div>
+
{pageLabel && (
+
<span
+
style={{ ...listStyles.pageMeta, color: `var(--atproto-color-text-secondary)` }}
+
>
+
{pageLabel}
+
</span>
+
)}
+
</div>
+
<div style={listStyles.items}>
+
{loading && records.length === 0 && (
+
<div style={{ ...listStyles.empty, color: `var(--atproto-color-text-secondary)` }}>
+
Loading songsโ€ฆ
+
</div>
+
)}
+
{records.map((record, idx) => (
+
<SongRow
+
key={`${record.rkey}-${record.value.playedTime}`}
+
record={record.value}
+
hasDivider={idx < records.length - 1}
+
/>
+
))}
+
{!loading && records.length === 0 && (
+
<div style={{ ...listStyles.empty, color: `var(--atproto-color-text-secondary)` }}>
+
No songs found.
+
</div>
+
)}
+
</div>
+
{enablePagination && (
+
<div style={{ ...listStyles.footer, borderTopColor: `var(--atproto-color-border)`, color: `var(--atproto-color-text)` }}>
+
<button
+
type="button"
+
style={{
+
...listStyles.pageButton,
+
background: `var(--atproto-color-button-bg)`,
+
color: `var(--atproto-color-button-text)`,
+
cursor: hasPrev ? "pointer" : "not-allowed",
+
opacity: hasPrev ? 1 : 0.5,
+
}}
+
onClick={loadPrev}
+
disabled={!hasPrev}
+
>
+
โ€น Prev
+
</button>
+
<div style={listStyles.pageChips}>
+
<span
+
style={{
+
...listStyles.pageChipActive,
+
color: `var(--atproto-color-button-text)`,
+
background: `var(--atproto-color-button-bg)`,
+
borderWidth: "1px",
+
borderStyle: "solid",
+
borderColor: `var(--atproto-color-button-bg)`,
+
}}
+
>
+
{pageIndex + 1}
+
</span>
+
{(hasNext || pagesCount > pageIndex + 1) && (
+
<span
+
style={{
+
...listStyles.pageChip,
+
color: `var(--atproto-color-text-secondary)`,
+
borderWidth: "1px",
+
borderStyle: "solid",
+
borderColor: `var(--atproto-color-border)`,
+
background: `var(--atproto-color-bg)`,
+
}}
+
>
+
{pageIndex + 2}
+
</span>
+
)}
+
</div>
+
<button
+
type="button"
+
style={{
+
...listStyles.pageButton,
+
background: `var(--atproto-color-button-bg)`,
+
color: `var(--atproto-color-button-text)`,
+
cursor: hasNext ? "pointer" : "not-allowed",
+
opacity: hasNext ? 1 : 0.5,
+
}}
+
onClick={loadNext}
+
disabled={!hasNext}
+
>
+
Next โ€บ
+
</button>
+
</div>
+
)}
+
{loading && records.length > 0 && (
+
<div
+
style={{ ...listStyles.loadingBar, background: `var(--atproto-color-bg-elevated)`, color: `var(--atproto-color-text-secondary)` }}
+
>
+
Updatingโ€ฆ
+
</div>
+
)}
+
</div>
+
);
+
});
+
+
interface SongRowProps {
+
record: TealFeedPlayRecord;
+
hasDivider: boolean;
+
}
+
+
const SongRow: React.FC<SongRowProps> = ({ record, hasDivider }) => {
+
const [albumArt, setAlbumArt] = useState<string | undefined>(undefined);
+
const [artLoading, setArtLoading] = useState(true);
+
+
const artistNames = record.artists.map((a) => a.artistName).join(", ");
+
const relative = record.playedTime
+
? formatRelativeTime(record.playedTime)
+
: undefined;
+
const absolute = record.playedTime
+
? new Date(record.playedTime).toLocaleString()
+
: undefined;
+
+
useEffect(() => {
+
let cancelled = false;
+
setArtLoading(true);
+
setAlbumArt(undefined);
+
+
const fetchAlbumArt = async () => {
+
try {
+
// Try ISRC first
+
if (record.isrc) {
+
const response = await fetch(
+
`https://api.song.link/v1-alpha.1/links?platform=isrc&type=song&id=${encodeURIComponent(record.isrc)}&songIfSingle=true`
+
);
+
if (cancelled) return;
+
if (response.ok) {
+
const data: SonglinkResponse = await response.json();
+
const entityId = data.entityUniqueId;
+
const entity = entityId ? data.entitiesByUniqueId?.[entityId] : undefined;
+
if (entity?.thumbnailUrl) {
+
setAlbumArt(entity.thumbnailUrl);
+
setArtLoading(false);
+
return;
+
}
+
}
+
}
+
+
// Fallback to iTunes search
+
const iTunesSearchUrl = `https://itunes.apple.com/search?term=${encodeURIComponent(
+
`${record.trackName} ${artistNames}`
+
)}&media=music&entity=song&limit=1`;
+
+
const iTunesResponse = await fetch(iTunesSearchUrl);
+
if (cancelled) return;
+
+
if (iTunesResponse.ok) {
+
const iTunesData = await iTunesResponse.json();
+
if (iTunesData.results && iTunesData.results.length > 0) {
+
const match = iTunesData.results[0];
+
const artworkUrl = match.artworkUrl100?.replace('100x100', '600x600') || match.artworkUrl100;
+
if (artworkUrl) {
+
setAlbumArt(artworkUrl);
+
}
+
}
+
}
+
setArtLoading(false);
+
} catch (err) {
+
console.error(`Failed to fetch album art for "${record.trackName}":`, err);
+
setArtLoading(false);
+
}
+
};
+
+
fetchAlbumArt();
+
+
return () => {
+
cancelled = true;
+
};
+
}, [record.trackName, artistNames, record.isrc]);
+
+
return (
+
<div
+
style={{
+
...listStyles.row,
+
color: `var(--atproto-color-text)`,
+
borderBottom: hasDivider
+
? `1px solid var(--atproto-color-border)`
+
: "none",
+
}}
+
>
+
{/* Album Art - Large and prominent */}
+
<div style={listStyles.albumArtContainer}>
+
{artLoading ? (
+
<div style={listStyles.albumArtPlaceholder}>
+
<div style={listStyles.loadingSpinner} />
+
</div>
+
) : albumArt ? (
+
<img
+
src={albumArt}
+
alt={`${record.releaseName || "Album"} cover`}
+
style={listStyles.albumArt}
+
onError={(e) => {
+
e.currentTarget.style.display = "none";
+
const parent = e.currentTarget.parentElement;
+
if (parent) {
+
const placeholder = document.createElement("div");
+
Object.assign(placeholder.style, listStyles.albumArtPlaceholder);
+
placeholder.innerHTML = `
+
<svg
+
width="48"
+
height="48"
+
viewBox="0 0 24 24"
+
fill="none"
+
stroke="currentColor"
+
stroke-width="1.5"
+
>
+
<circle cx="12" cy="12" r="10" />
+
<circle cx="12" cy="12" r="3" />
+
<path d="M12 2v3M12 19v3M2 12h3M19 12h3" />
+
</svg>
+
`;
+
parent.appendChild(placeholder);
+
}
+
}}
+
/>
+
) : (
+
<div style={listStyles.albumArtPlaceholder}>
+
<svg
+
width="48"
+
height="48"
+
viewBox="0 0 24 24"
+
fill="none"
+
stroke="currentColor"
+
strokeWidth="1.5"
+
>
+
<circle cx="12" cy="12" r="10" />
+
<circle cx="12" cy="12" r="3" />
+
<path d="M12 2v3M12 19v3M2 12h3M19 12h3" />
+
</svg>
+
</div>
+
)}
+
</div>
+
+
{/* Song Info */}
+
<div style={listStyles.songInfo}>
+
<div style={listStyles.trackName}>{record.trackName}</div>
+
<div style={{ ...listStyles.artistName, color: `var(--atproto-color-text-secondary)` }}>
+
{artistNames}
+
</div>
+
{record.releaseName && (
+
<div style={{ ...listStyles.releaseName, color: `var(--atproto-color-text-secondary)` }}>
+
{record.releaseName}
+
</div>
+
)}
+
{relative && (
+
<div
+
style={{ ...listStyles.playedTime, color: `var(--atproto-color-text-secondary)` }}
+
title={absolute}
+
>
+
{relative}
+
</div>
+
)}
+
</div>
+
+
{/* External Link */}
+
{record.originUrl && (
+
<a
+
href={record.originUrl}
+
target="_blank"
+
rel="noopener noreferrer"
+
style={listStyles.externalLink}
+
title="Listen on streaming service"
+
aria-label={`Listen to ${record.trackName} by ${artistNames}`}
+
>
+
<svg
+
width="20"
+
height="20"
+
viewBox="0 0 24 24"
+
fill="none"
+
stroke="currentColor"
+
strokeWidth="2"
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
>
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
+
<polyline points="15 3 21 3 21 9" />
+
<line x1="10" y1="14" x2="21" y2="3" />
+
</svg>
+
</a>
+
)}
+
</div>
+
);
+
};
+
+
function formatDid(did: string) {
+
return did.replace(/^did:(plc:)?/, "");
+
}
+
+
function formatRelativeTime(iso: string): string {
+
const date = new Date(iso);
+
const diffSeconds = (date.getTime() - Date.now()) / 1000;
+
const absSeconds = Math.abs(diffSeconds);
+
const thresholds: Array<{
+
limit: number;
+
unit: Intl.RelativeTimeFormatUnit;
+
divisor: number;
+
}> = [
+
{ limit: 60, unit: "second", divisor: 1 },
+
{ limit: 3600, unit: "minute", divisor: 60 },
+
{ limit: 86400, unit: "hour", divisor: 3600 },
+
{ limit: 604800, unit: "day", divisor: 86400 },
+
{ limit: 2629800, unit: "week", divisor: 604800 },
+
{ limit: 31557600, unit: "month", divisor: 2629800 },
+
{ limit: Infinity, unit: "year", divisor: 31557600 },
+
];
+
const threshold =
+
thresholds.find((t) => absSeconds < t.limit) ??
+
thresholds[thresholds.length - 1];
+
const value = diffSeconds / threshold.divisor;
+
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
+
return rtf.format(Math.round(value), threshold.unit);
+
}
+
+
const listStyles = {
+
card: {
+
borderRadius: 16,
+
borderWidth: "1px",
+
borderStyle: "solid",
+
borderColor: "transparent",
+
boxShadow: "0 8px 18px -12px rgba(15, 23, 42, 0.25)",
+
overflow: "hidden",
+
display: "flex",
+
flexDirection: "column",
+
} satisfies React.CSSProperties,
+
header: {
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "space-between",
+
padding: "14px 18px",
+
fontSize: 14,
+
fontWeight: 500,
+
borderBottom: "1px solid var(--atproto-color-border)",
+
} satisfies React.CSSProperties,
+
headerInfo: {
+
display: "flex",
+
alignItems: "center",
+
gap: 12,
+
} satisfies React.CSSProperties,
+
headerIcon: {
+
width: 28,
+
height: 28,
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
borderRadius: "50%",
+
color: "var(--atproto-color-text)",
+
} satisfies React.CSSProperties,
+
headerText: {
+
display: "flex",
+
flexDirection: "column",
+
gap: 2,
+
} satisfies React.CSSProperties,
+
title: {
+
fontSize: 15,
+
fontWeight: 600,
+
} satisfies React.CSSProperties,
+
subtitle: {
+
fontSize: 12,
+
fontWeight: 500,
+
} satisfies React.CSSProperties,
+
pageMeta: {
+
fontSize: 12,
+
} satisfies React.CSSProperties,
+
items: {
+
display: "flex",
+
flexDirection: "column",
+
} satisfies React.CSSProperties,
+
empty: {
+
padding: "24px 18px",
+
fontSize: 13,
+
textAlign: "center",
+
} satisfies React.CSSProperties,
+
row: {
+
padding: "18px",
+
display: "flex",
+
gap: 16,
+
alignItems: "center",
+
transition: "background-color 120ms ease",
+
position: "relative",
+
} satisfies React.CSSProperties,
+
albumArtContainer: {
+
width: 96,
+
height: 96,
+
flexShrink: 0,
+
borderRadius: 8,
+
overflow: "hidden",
+
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
+
} satisfies React.CSSProperties,
+
albumArt: {
+
width: "100%",
+
height: "100%",
+
objectFit: "cover",
+
display: "block",
+
} satisfies React.CSSProperties,
+
albumArtPlaceholder: {
+
width: "100%",
+
height: "100%",
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
+
color: "rgba(255, 255, 255, 0.6)",
+
} satisfies React.CSSProperties,
+
loadingSpinner: {
+
width: 28,
+
height: 28,
+
border: "3px solid rgba(255, 255, 255, 0.3)",
+
borderTop: "3px solid rgba(255, 255, 255, 0.9)",
+
borderRadius: "50%",
+
animation: "spin 1s linear infinite",
+
} satisfies React.CSSProperties,
+
songInfo: {
+
flex: 1,
+
display: "flex",
+
flexDirection: "column",
+
gap: 4,
+
minWidth: 0,
+
} satisfies React.CSSProperties,
+
trackName: {
+
fontSize: 16,
+
fontWeight: 600,
+
lineHeight: 1.3,
+
color: "var(--atproto-color-text)",
+
overflow: "hidden",
+
textOverflow: "ellipsis",
+
whiteSpace: "nowrap",
+
} satisfies React.CSSProperties,
+
artistName: {
+
fontSize: 14,
+
fontWeight: 500,
+
overflow: "hidden",
+
textOverflow: "ellipsis",
+
whiteSpace: "nowrap",
+
} satisfies React.CSSProperties,
+
releaseName: {
+
fontSize: 13,
+
overflow: "hidden",
+
textOverflow: "ellipsis",
+
whiteSpace: "nowrap",
+
} satisfies React.CSSProperties,
+
playedTime: {
+
fontSize: 12,
+
fontWeight: 500,
+
marginTop: 2,
+
} satisfies React.CSSProperties,
+
externalLink: {
+
flexShrink: 0,
+
width: 36,
+
height: 36,
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
borderRadius: "50%",
+
background: "var(--atproto-color-bg-elevated)",
+
border: "1px solid var(--atproto-color-border)",
+
color: "var(--atproto-color-text-secondary)",
+
cursor: "pointer",
+
transition: "all 0.2s ease",
+
textDecoration: "none",
+
} satisfies React.CSSProperties,
+
footer: {
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "space-between",
+
padding: "12px 18px",
+
borderTop: "1px solid transparent",
+
fontSize: 13,
+
} satisfies React.CSSProperties,
+
pageChips: {
+
display: "flex",
+
gap: 6,
+
alignItems: "center",
+
} satisfies React.CSSProperties,
+
pageChip: {
+
padding: "4px 10px",
+
borderRadius: 999,
+
fontSize: 13,
+
borderWidth: "1px",
+
borderStyle: "solid",
+
borderColor: "transparent",
+
} satisfies React.CSSProperties,
+
pageChipActive: {
+
padding: "4px 10px",
+
borderRadius: 999,
+
fontSize: 13,
+
fontWeight: 600,
+
borderWidth: "1px",
+
borderStyle: "solid",
+
borderColor: "transparent",
+
} satisfies React.CSSProperties,
+
pageButton: {
+
border: "none",
+
borderRadius: 999,
+
padding: "6px 12px",
+
fontSize: 13,
+
fontWeight: 500,
+
background: "transparent",
+
display: "flex",
+
alignItems: "center",
+
gap: 4,
+
transition: "background-color 120ms ease",
+
} satisfies React.CSSProperties,
+
loadingBar: {
+
padding: "4px 18px 14px",
+
fontSize: 12,
+
textAlign: "right",
+
color: "#64748b",
+
} satisfies React.CSSProperties,
+
};
+
+
// Add keyframes and hover styles
+
if (typeof document !== "undefined") {
+
const styleId = "song-history-styles";
+
if (!document.getElementById(styleId)) {
+
const styleElement = document.createElement("style");
+
styleElement.id = styleId;
+
styleElement.textContent = `
+
@keyframes spin {
+
0% { transform: rotate(0deg); }
+
100% { transform: rotate(360deg); }
+
}
+
`;
+
document.head.appendChild(styleElement);
+
}
+
}
+
+
export default SongHistoryList;
+131
lib/components/TangledRepo.tsx
···
+
import React from "react";
+
import { AtProtoRecord } from "../core/AtProtoRecord";
+
import { TangledRepoRenderer } from "../renderers/TangledRepoRenderer";
+
import type { TangledRepoRecord } from "../types/tangled";
+
import { useAtProto } from "../providers/AtProtoProvider";
+
+
/**
+
* Props for rendering Tangled Repo records.
+
*/
+
export interface TangledRepoProps {
+
/** DID of the repository that stores the repo record. */
+
did: string;
+
/** Record key within the `sh.tangled.repo` collection. */
+
rkey: string;
+
/** Prefetched Tangled Repo record. When provided, skips fetching from the network. */
+
record?: TangledRepoRecord;
+
/** Optional renderer override for custom presentation. */
+
renderer?: React.ComponentType<TangledRepoRendererInjectedProps>;
+
/** Fallback node displayed before loading begins. */
+
fallback?: React.ReactNode;
+
/** Indicator node shown while data is loading. */
+
loadingIndicator?: React.ReactNode;
+
/** Preferred color scheme for theming. */
+
colorScheme?: "light" | "dark" | "system";
+
/** Whether to show star count from backlinks. Defaults to true. */
+
showStarCount?: boolean;
+
/** Branch to query for language information. Defaults to trying "main", then "master". */
+
branch?: string;
+
/** Prefetched language names (e.g., ['TypeScript', 'React']). When provided, skips fetching languages from the knot server. */
+
languages?: string[];
+
}
+
+
/**
+
* Values injected into custom Tangled Repo renderer implementations.
+
*/
+
export type TangledRepoRendererInjectedProps = {
+
/** Loaded Tangled Repo record value. */
+
record: TangledRepoRecord;
+
/** Indicates whether the record is currently loading. */
+
loading: boolean;
+
/** Fetch error, if any. */
+
error?: Error;
+
/** Preferred color scheme for downstream components. */
+
colorScheme?: "light" | "dark" | "system";
+
/** DID associated with the record. */
+
did: string;
+
/** Record key for the repo. */
+
rkey: string;
+
/** Canonical external URL for linking to the repo. */
+
canonicalUrl: string;
+
/** Whether to show star count from backlinks. */
+
showStarCount?: boolean;
+
/** Branch to query for language information. */
+
branch?: string;
+
/** Prefetched language names. */
+
languages?: string[];
+
};
+
+
/** NSID for Tangled Repo records. */
+
export const TANGLED_REPO_COLLECTION = "sh.tangled.repo";
+
+
/**
+
* Resolves a Tangled Repo record and renders it with optional overrides while computing a canonical link.
+
*
+
* @param did - DID whose Tangled Repo should be fetched.
+
* @param rkey - Record key within the Tangled Repo collection.
+
* @param renderer - Optional component override that will receive injected props.
+
* @param fallback - Node rendered before the first load begins.
+
* @param loadingIndicator - Node rendered while the Tangled Repo is loading.
+
* @param colorScheme - Preferred color scheme for theming the renderer.
+
* @param showStarCount - Whether to show star count from backlinks. Defaults to true.
+
* @param branch - Branch to query for language information. Defaults to trying "main", then "master".
+
* @param languages - Prefetched language names (e.g., ['TypeScript', 'React']). When provided, skips fetching languages from the knot server.
+
* @returns A JSX subtree representing the Tangled Repo record with loading states handled.
+
*/
+
export const TangledRepo: React.FC<TangledRepoProps> = React.memo(({
+
did,
+
rkey,
+
record,
+
renderer,
+
fallback,
+
loadingIndicator,
+
colorScheme,
+
showStarCount = true,
+
branch,
+
languages,
+
}) => {
+
const { tangledBaseUrl } = useAtProto();
+
const Comp: React.ComponentType<TangledRepoRendererInjectedProps> =
+
renderer ?? ((props) => <TangledRepoRenderer {...props} />);
+
const Wrapped: React.FC<{
+
record: TangledRepoRecord;
+
loading: boolean;
+
error?: Error;
+
}> = (props) => (
+
<Comp
+
{...props}
+
colorScheme={colorScheme}
+
did={did}
+
rkey={rkey}
+
canonicalUrl={`${tangledBaseUrl}/${did}/${encodeURIComponent(props.record.name)}`}
+
showStarCount={showStarCount}
+
branch={branch}
+
languages={languages}
+
/>
+
);
+
+
if (record !== undefined) {
+
return (
+
<AtProtoRecord<TangledRepoRecord>
+
record={record}
+
renderer={Wrapped}
+
fallback={fallback}
+
loadingIndicator={loadingIndicator}
+
/>
+
);
+
}
+
+
return (
+
<AtProtoRecord<TangledRepoRecord>
+
did={did}
+
collection={TANGLED_REPO_COLLECTION}
+
rkey={rkey}
+
renderer={Wrapped}
+
fallback={fallback}
+
loadingIndicator={loadingIndicator}
+
/>
+
);
+
});
+
+
export default TangledRepo;
+4 -2
lib/components/TangledString.tsx
···
import React from "react";
import { AtProtoRecord } from "../core/AtProtoRecord";
import { TangledStringRenderer } from "../renderers/TangledStringRenderer";
-
import type { TangledStringRecord } from "../renderers/TangledStringRenderer";
+
import type { TangledStringRecord } from "../types/tangled";
+
import { useAtProto } from "../providers/AtProtoProvider";
/**
* Props for rendering Tangled String records.
···
loadingIndicator,
colorScheme,
}) => {
+
const { tangledBaseUrl } = useAtProto();
const Comp: React.ComponentType<TangledStringRendererInjectedProps> =
renderer ?? ((props) => <TangledStringRenderer {...props} />);
const Wrapped: React.FC<{
···
colorScheme={colorScheme}
did={did}
rkey={rkey}
-
canonicalUrl={`https://tangled.org/strings/${did}/${encodeURIComponent(rkey)}`}
+
canonicalUrl={`${tangledBaseUrl}/strings/${did}/${encodeURIComponent(rkey)}`}
/>
);
+68 -6
lib/core/AtProtoRecord.tsx
···
-
import React from "react";
+
import React, { useState, useEffect, useRef } from "react";
import { useAtProtoRecord } from "../hooks/useAtProtoRecord";
/**
···
fallback?: React.ReactNode;
/** React node shown while the record is being fetched. */
loadingIndicator?: React.ReactNode;
+
/** Auto-refresh interval in milliseconds. When set, the record will be refetched at this interval. */
+
refreshInterval?: number;
+
/** Comparison function to determine if a record has changed. Used to prevent unnecessary re-renders during auto-refresh. */
+
compareRecords?: (prev: T | undefined, next: T | undefined) => boolean;
}
/**
···
*
* When no custom renderer is provided, displays the record as formatted JSON.
*
+
* **Auto-refresh**: Set `refreshInterval` to automatically refetch the record at the specified interval.
+
* The component intelligently avoids re-rendering if the record hasn't changed (using `compareRecords`).
+
*
* @example
* ```tsx
* // Fetch mode - retrieves record from network
···
* />
* ```
*
+
* @example
+
* ```tsx
+
* // Auto-refresh mode - refetches every 15 seconds
+
* <AtProtoRecord
+
* did="did:plc:example"
+
* collection="fm.teal.alpha.actor.status"
+
* rkey="self"
+
* refreshInterval={15000}
+
* compareRecords={(prev, next) => JSON.stringify(prev) === JSON.stringify(next)}
+
* renderer={MyCustomRenderer}
+
* />
+
* ```
+
*
* @param props - Either fetch props (did/collection/rkey) or prefetch props (record).
* @returns A rendered AT Protocol record with loading/error states handled.
*/
···
renderer: Renderer,
fallback = null,
loadingIndicator = "Loadingโ€ฆ",
+
refreshInterval,
+
compareRecords,
} = props;
const hasProvidedRecord = "record" in props;
const providedRecord = hasProvidedRecord ? props.record : undefined;
+
// Extract fetch props for logging
+
const fetchDid = hasProvidedRecord ? undefined : (props as any).did;
+
const fetchCollection = hasProvidedRecord ? undefined : (props as any).collection;
+
const fetchRkey = hasProvidedRecord ? undefined : (props as any).rkey;
+
+
// State for managing auto-refresh
+
const [refreshKey, setRefreshKey] = useState(0);
+
const [stableRecord, setStableRecord] = useState<T | undefined>(providedRecord);
+
const previousRecordRef = useRef<T | undefined>(providedRecord);
+
+
// Auto-refresh interval
+
useEffect(() => {
+
if (!refreshInterval || hasProvidedRecord) return;
+
+
const interval = setInterval(() => {
+
setRefreshKey((prev) => prev + 1);
+
}, refreshInterval);
+
+
return () => clearInterval(interval);
+
}, [refreshInterval, hasProvidedRecord, fetchCollection, fetchDid]);
+
const {
record: fetchedRecord,
error,
loading,
} = useAtProtoRecord<T>({
-
did: hasProvidedRecord ? undefined : props.did,
-
collection: hasProvidedRecord ? undefined : props.collection,
-
rkey: hasProvidedRecord ? undefined : props.rkey,
+
did: fetchDid,
+
collection: fetchCollection,
+
rkey: fetchRkey,
+
bypassCache: !!refreshInterval && refreshKey > 0, // Bypass cache on auto-refresh (but not initial load)
+
_refreshKey: refreshKey, // Force hook to re-run
});
-
const record = providedRecord ?? fetchedRecord;
-
const isLoading = loading && !providedRecord;
+
// Determine which record to use
+
const currentRecord = providedRecord ?? fetchedRecord;
+
+
// Handle record changes with optional comparison
+
useEffect(() => {
+
if (!currentRecord) return;
+
+
const hasChanged = compareRecords
+
? !compareRecords(previousRecordRef.current, currentRecord)
+
: previousRecordRef.current !== currentRecord;
+
+
if (hasChanged) {
+
setStableRecord(currentRecord);
+
previousRecordRef.current = currentRecord;
+
}
+
}, [currentRecord, compareRecords]);
+
+
const record = stableRecord;
+
const isLoading = loading && !providedRecord && !stableRecord;
if (error && !record) return <>{fallback}</>;
if (!record) return <>{isLoading ? loadingIndicator : fallback}</>;
+158 -29
lib/hooks/useAtProtoRecord.ts
···
-
import { useEffect, useState } from "react";
+
import { useEffect, useState, useRef } from "react";
import { useDidResolution } from "./useDidResolution";
import { usePdsEndpoint } from "./usePdsEndpoint";
import { createAtprotoClient } from "../utils/atproto-client";
import { useBlueskyAppview } from "./useBlueskyAppview";
+
import { useAtProto } from "../providers/AtProtoProvider";
/**
* Identifier trio required to address an AT Protocol record.
···
collection?: string;
/** Record key string uniquely identifying the record within the collection. */
rkey?: string;
+
/** Force bypass cache and refetch from network. Useful for auto-refresh scenarios. */
+
bypassCache?: boolean;
+
/** Internal refresh trigger - changes to this value force a refetch. */
+
_refreshKey?: number;
}
/**
···
* @param did - DID (or handle before resolution) that owns the record.
* @param collection - NSID collection from which to fetch the record.
* @param rkey - Record key identifying the record within the collection.
+
* @param bypassCache - Force bypass cache and refetch from network. Useful for auto-refresh scenarios.
+
* @param _refreshKey - Internal parameter used to trigger refetches.
* @returns {AtProtoRecordState<T>} Object containing the resolved record, any error, and a loading flag.
*/
export function useAtProtoRecord<T = unknown>({
did: handleOrDid,
collection,
rkey,
+
bypassCache = false,
+
_refreshKey = 0,
}: AtProtoRecordKey): AtProtoRecordState<T> {
+
const { recordCache } = useAtProto();
const isBlueskyCollection = collection?.startsWith("app.bsky.");
-
+
// Always call all hooks (React rules) - conditionally use results
const blueskyResult = useBlueskyAppview<T>({
did: isBlueskyCollection ? handleOrDid : undefined,
collection: isBlueskyCollection ? collection : undefined,
rkey: isBlueskyCollection ? rkey : undefined,
});
-
+
const {
did,
error: didError,
···
const [state, setState] = useState<AtProtoRecordState<T>>({
loading: !!(handleOrDid && collection && rkey),
});
+
+
const releaseRef = useRef<(() => void) | undefined>(undefined);
useEffect(() => {
let cancelled = false;
···
});
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
···
assignState({ loading: false, error: didError });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
···
assignState({ loading: false, error: endpointError });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
···
assignState({ loading: true, error: undefined });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
assignState({ loading: true, error: undefined, record: undefined });
-
(async () => {
-
try {
-
const { rpc } = await createAtprotoClient({
-
service: endpoint,
+
// Bypass cache if requested (for auto-refresh scenarios)
+
if (bypassCache) {
+
assignState({ loading: true, error: undefined });
+
+
// Skip cache and fetch directly
+
const controller = new AbortController();
+
+
const fetchPromise = (async () => {
+
try {
+
const { rpc } = await createAtprotoClient({
+
service: endpoint,
+
});
+
const res = await (
+
rpc as unknown as {
+
get: (
+
nsid: string,
+
opts: {
+
params: {
+
repo: string;
+
collection: string;
+
rkey: string;
+
};
+
},
+
) => Promise<{ ok: boolean; data: { value: T } }>;
+
}
+
).get("com.atproto.repo.getRecord", {
+
params: { repo: did, collection, rkey },
+
});
+
if (!res.ok) throw new Error("Failed to load record");
+
return (res.data as { value: T }).value;
+
} catch (err) {
+
// Provide helpful error for banned/unreachable Bluesky PDSes
+
if (endpoint.includes('.bsky.network')) {
+
throw new Error(
+
`Record unavailable. The Bluesky PDS (${endpoint}) may be unreachable or the account may be banned.`
+
);
+
}
+
throw err;
+
}
+
})();
+
+
fetchPromise
+
.then((record) => {
+
if (!cancelled) {
+
assignState({ record, loading: false });
+
}
+
})
+
.catch((e) => {
+
if (!cancelled) {
+
const err = e instanceof Error ? e : new Error(String(e));
+
assignState({ error: err, loading: false });
+
}
});
-
const res = await (
-
rpc as unknown as {
-
get: (
-
nsid: string,
-
opts: {
-
params: {
-
repo: string;
-
collection: string;
-
rkey: string;
-
};
-
},
-
) => Promise<{ ok: boolean; data: { value: T } }>;
+
+
return () => {
+
cancelled = true;
+
controller.abort();
+
};
+
}
+
+
// Use recordCache.ensure for deduplication and caching
+
const { promise, release } = recordCache.ensure<T>(
+
did,
+
collection,
+
rkey,
+
() => {
+
const controller = new AbortController();
+
+
const fetchPromise = (async () => {
+
try {
+
const { rpc } = await createAtprotoClient({
+
service: endpoint,
+
});
+
const res = await (
+
rpc as unknown as {
+
get: (
+
nsid: string,
+
opts: {
+
params: {
+
repo: string;
+
collection: string;
+
rkey: string;
+
};
+
},
+
) => Promise<{ ok: boolean; data: { value: T } }>;
+
}
+
).get("com.atproto.repo.getRecord", {
+
params: { repo: did, collection, rkey },
+
});
+
if (!res.ok) throw new Error("Failed to load record");
+
return (res.data as { value: T }).value;
+
} catch (err) {
+
// Provide helpful error for banned/unreachable Bluesky PDSes
+
if (endpoint.includes('.bsky.network')) {
+
throw new Error(
+
`Record unavailable. The Bluesky PDS (${endpoint}) may be unreachable or the account may be banned.`
+
);
+
}
+
throw err;
}
-
).get("com.atproto.repo.getRecord", {
-
params: { repo: did, collection, rkey },
-
});
-
if (!res.ok) throw new Error("Failed to load record");
-
const record = (res.data as { value: T }).value;
-
assignState({ record, loading: false });
-
} catch (e) {
-
const err = e instanceof Error ? e : new Error(String(e));
-
assignState({ error: err, loading: false });
+
})();
+
+
return {
+
promise: fetchPromise,
+
abort: () => controller.abort(),
+
};
}
-
})();
+
);
+
+
releaseRef.current = release;
+
+
promise
+
.then((record) => {
+
if (!cancelled) {
+
assignState({ record, loading: false });
+
}
+
})
+
.catch((e) => {
+
if (!cancelled) {
+
const err = e instanceof Error ? e : new Error(String(e));
+
assignState({ error: err, loading: false });
+
}
+
});
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}, [
handleOrDid,
···
resolvingEndpoint,
didError,
endpointError,
+
recordCache,
+
bypassCache,
+
_refreshKey,
]);
// Return Bluesky result for app.bsky.* collections
+163
lib/hooks/useBacklinks.ts
···
+
import { useEffect, useState, useCallback, useRef } from "react";
+
+
/**
+
* Individual backlink record returned by Microcosm Constellation.
+
*/
+
export interface BacklinkRecord {
+
/** DID of the author who created the backlink. */
+
did: string;
+
/** Collection type of the backlink record (e.g., "sh.tangled.feed.star"). */
+
collection: string;
+
/** Record key of the backlink. */
+
rkey: string;
+
}
+
+
/**
+
* Response from Microcosm Constellation API.
+
*/
+
export interface BacklinksResponse {
+
/** Total count of backlinks. */
+
total: number;
+
/** Array of backlink records. */
+
records: BacklinkRecord[];
+
/** Cursor for pagination (optional). */
+
cursor?: string;
+
}
+
+
/**
+
* Parameters for fetching backlinks.
+
*/
+
export interface UseBacklinksParams {
+
/** The AT-URI subject to get backlinks for (e.g., "at://did:plc:xxx/sh.tangled.repo/yyy"). */
+
subject: string;
+
/** The source collection and path (e.g., "sh.tangled.feed.star:subject"). */
+
source: string;
+
/** Maximum number of results to fetch (default: 16, max: 100). */
+
limit?: number;
+
/** Base URL for the Microcosm Constellation API. */
+
constellationBaseUrl?: string;
+
/** Whether to automatically fetch backlinks on mount. */
+
enabled?: boolean;
+
}
+
+
const DEFAULT_CONSTELLATION = "https://constellation.microcosm.blue";
+
+
/**
+
* Hook to fetch backlinks from Microcosm Constellation API.
+
*
+
* Backlinks are records that reference another record. For example,
+
* `sh.tangled.feed.star` records are backlinks to `sh.tangled.repo` records,
+
* representing users who have starred a repository.
+
*
+
* @param params - Configuration for fetching backlinks
+
* @returns Object containing backlinks data, loading state, error, and refetch function
+
*
+
* @example
+
* ```tsx
+
* const { backlinks, loading, error, count } = useBacklinks({
+
* subject: "at://did:plc:example/sh.tangled.repo/3k2aexample",
+
* source: "sh.tangled.feed.star:subject",
+
* });
+
* ```
+
*/
+
export function useBacklinks({
+
subject,
+
source,
+
limit = 16,
+
constellationBaseUrl = DEFAULT_CONSTELLATION,
+
enabled = true,
+
}: UseBacklinksParams) {
+
const [backlinks, setBacklinks] = useState<BacklinkRecord[]>([]);
+
const [total, setTotal] = useState(0);
+
const [loading, setLoading] = useState(false);
+
const [error, setError] = useState<Error | undefined>(undefined);
+
const [cursor, setCursor] = useState<string | undefined>(undefined);
+
const abortControllerRef = useRef<AbortController | null>(null);
+
+
const fetchBacklinks = useCallback(
+
async (signal?: AbortSignal) => {
+
if (!subject || !source || !enabled) return;
+
+
try {
+
setLoading(true);
+
setError(undefined);
+
+
const baseUrl = constellationBaseUrl.endsWith("/")
+
? constellationBaseUrl.slice(0, -1)
+
: constellationBaseUrl;
+
+
const params = new URLSearchParams({
+
subject: subject,
+
source: source,
+
limit: limit.toString(),
+
});
+
+
const url = `${baseUrl}/xrpc/blue.microcosm.links.getBacklinks?${params}`;
+
+
const response = await fetch(url, { signal });
+
+
if (!response.ok) {
+
throw new Error(
+
`Failed to fetch backlinks: ${response.status} ${response.statusText}`,
+
);
+
}
+
+
const data: BacklinksResponse = await response.json();
+
setBacklinks(data.records || []);
+
setTotal(data.total || 0);
+
setCursor(data.cursor);
+
} catch (err) {
+
if (err instanceof Error && err.name === "AbortError") {
+
// Ignore abort errors
+
return;
+
}
+
setError(
+
err instanceof Error ? err : new Error("Unknown error fetching backlinks"),
+
);
+
} finally {
+
setLoading(false);
+
}
+
},
+
[subject, source, limit, constellationBaseUrl, enabled],
+
);
+
+
const refetch = useCallback(() => {
+
// Abort any in-flight request
+
if (abortControllerRef.current) {
+
abortControllerRef.current.abort();
+
}
+
+
const controller = new AbortController();
+
abortControllerRef.current = controller;
+
fetchBacklinks(controller.signal);
+
}, [fetchBacklinks]);
+
+
useEffect(() => {
+
if (!enabled) return;
+
+
const controller = new AbortController();
+
abortControllerRef.current = controller;
+
fetchBacklinks(controller.signal);
+
+
return () => {
+
controller.abort();
+
};
+
}, [fetchBacklinks, enabled]);
+
+
return {
+
/** Array of backlink records. */
+
backlinks,
+
/** Whether backlinks are currently being fetched. */
+
loading,
+
/** Error if fetch failed. */
+
error,
+
/** Pagination cursor (not yet implemented for pagination). */
+
cursor,
+
/** Total count of backlinks from the API. */
+
total,
+
/** Total count of backlinks (alias for total). */
+
count: total,
+
/** Function to manually refetch backlinks. */
+
refetch,
+
};
+
}
+151 -74
lib/hooks/useBlueskyAppview.ts
···
-
import { useEffect, useReducer } from "react";
+
import { useEffect, useReducer, useRef } from "react";
import { useDidResolution } from "./useDidResolution";
import { usePdsEndpoint } from "./usePdsEndpoint";
-
import { createAtprotoClient, SLINGSHOT_BASE_URL } from "../utils/atproto-client";
+
import { createAtprotoClient } from "../utils/atproto-client";
+
import { useAtProto } from "../providers/AtProtoProvider";
/**
* Extended blob reference that includes CDN URL from appview responses.
···
avatar?: string;
banner?: string;
createdAt?: string;
+
pronouns?: string;
+
website?: string;
[key: string]: unknown;
}
···
/** Source from which the record was successfully fetched. */
source?: "appview" | "slingshot" | "pds";
}
-
-
export const DEFAULT_APPVIEW_SERVICE = "https://public.api.bsky.app";
/**
* Maps Bluesky collection NSIDs to their corresponding appview API endpoints.
···
appviewService,
skipAppview = false,
}: UseBlueskyAppviewOptions): UseBlueskyAppviewResult<T> {
+
const { recordCache, blueskyAppviewService, resolver } = useAtProto();
+
const effectiveAppviewService = appviewService ?? blueskyAppviewService;
+
+
// Only use this hook for Bluesky collections (app.bsky.*)
+
const isBlueskyCollection = collection?.startsWith("app.bsky.");
+
const {
did,
error: didError,
···
source: undefined,
});
+
const releaseRef = useRef<(() => void) | undefined>(undefined);
+
useEffect(() => {
let cancelled = false;
···
if (!cancelled) dispatch({ type: "RESET" });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
+
};
+
}
+
+
// Return early if not a Bluesky collection - this hook should not be used for other lexicons
+
if (!isBlueskyCollection) {
+
if (!cancelled) dispatch({ type: "RESET" });
+
return () => {
+
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
···
if (!cancelled) dispatch({ type: "SET_ERROR", error: didError });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
···
if (!cancelled) dispatch({ type: "SET_ERROR", error: endpointError });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
···
if (!cancelled) dispatch({ type: "SET_LOADING", loading: true });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
// Start fetching
dispatch({ type: "SET_LOADING", loading: true });
-
(async () => {
-
let lastError: Error | undefined;
+
// Use recordCache.ensure for deduplication and caching
+
const { promise, release } = recordCache.ensure<{ record: T; source: "appview" | "slingshot" | "pds" }>(
+
did,
+
collection,
+
rkey,
+
() => {
+
const controller = new AbortController();
+
+
const fetchPromise = (async (): Promise<{ record: T; source: "appview" | "slingshot" | "pds" }> => {
+
let lastError: Error | undefined;
+
+
// Tier 1: Try Bluesky appview API
+
if (!skipAppview && BLUESKY_COLLECTION_TO_ENDPOINT[collection]) {
+
try {
+
const result = await fetchFromAppview<T>(
+
did,
+
collection,
+
rkey,
+
effectiveAppviewService,
+
);
+
if (result) {
+
return { record: result, source: "appview" };
+
}
+
} catch (err) {
+
lastError = err as Error;
+
// Continue to next tier
+
}
+
}
-
// Tier 1: Try Bluesky appview API
-
if (!skipAppview && BLUESKY_COLLECTION_TO_ENDPOINT[collection]) {
-
try {
-
const result = await fetchFromAppview<T>(
-
did,
-
collection,
-
rkey,
-
appviewService ?? DEFAULT_APPVIEW_SERVICE,
-
);
-
if (!cancelled && result) {
-
dispatch({
-
type: "SET_SUCCESS",
-
record: result,
-
source: "appview",
-
});
-
return;
+
// Tier 2: Try Slingshot getRecord
+
try {
+
const slingshotUrl = resolver.getSlingshotUrl();
+
const result = await fetchFromSlingshot<T>(did, collection, rkey, slingshotUrl);
+
if (result) {
+
return { record: result, source: "slingshot" };
+
}
+
} catch (err) {
+
lastError = err as Error;
+
// Continue to next tier
+
}
+
+
// Tier 3: Try PDS directly
+
try {
+
const result = await fetchFromPds<T>(
+
did,
+
collection,
+
rkey,
+
pdsEndpoint,
+
);
+
if (result) {
+
return { record: result, source: "pds" };
+
}
+
} catch (err) {
+
lastError = err as Error;
+
}
+
+
// All tiers failed - provide helpful error for banned/unreachable Bluesky PDSes
+
if (pdsEndpoint.includes('.bsky.network')) {
+
throw new Error(
+
`Record unavailable. The Bluesky PDS (${pdsEndpoint}) may be unreachable or the account may be banned.`
+
);
}
-
} catch (err) {
-
lastError = err as Error;
-
// Continue to next tier
-
}
+
+
throw lastError ?? new Error("Failed to fetch record from all sources");
+
})();
+
+
return {
+
promise: fetchPromise,
+
abort: () => controller.abort(),
+
};
}
+
);
-
// Tier 2: Try Slingshot getRecord
-
try {
-
const result = await fetchFromSlingshot<T>(did, collection, rkey);
-
if (!cancelled && result) {
+
releaseRef.current = release;
+
+
promise
+
.then(({ record, source }) => {
+
if (!cancelled) {
dispatch({
type: "SET_SUCCESS",
-
record: result,
-
source: "slingshot",
+
record,
+
source,
});
-
return;
}
-
} catch (err) {
-
lastError = err as Error;
-
// Continue to next tier
-
}
-
-
// Tier 3: Try PDS directly
-
try {
-
const result = await fetchFromPds<T>(
-
did,
-
collection,
-
rkey,
-
pdsEndpoint,
-
);
-
if (!cancelled && result) {
+
})
+
.catch((err) => {
+
if (!cancelled) {
dispatch({
-
type: "SET_SUCCESS",
-
record: result,
-
source: "pds",
+
type: "SET_ERROR",
+
error: err instanceof Error ? err : new Error(String(err)),
});
-
return;
}
-
} catch (err) {
-
lastError = err as Error;
-
}
-
-
// All tiers failed
-
if (!cancelled) {
-
dispatch({
-
type: "SET_ERROR",
-
error:
-
lastError ??
-
new Error("Failed to fetch record from all sources"),
-
});
-
}
-
})();
+
});
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}, [
handleOrDid,
···
collection,
rkey,
pdsEndpoint,
-
appviewService,
+
effectiveAppviewService,
skipAppview,
resolvingDid,
resolvingEndpoint,
didError,
endpointError,
+
recordCache,
+
resolver,
]);
return state;
···
createdAt: profile.createdAt,
};
+
// Add pronouns and website if they exist
+
if (profile.pronouns) {
+
record.pronouns = profile.pronouns;
+
}
+
+
if (profile.website) {
+
record.website = profile.website;
+
}
+
if (profile.avatar && avatarCid) {
const avatarBlob: BlobWithCdn = {
$type: "blob",
···
did: string,
collection: string,
rkey: string,
+
slingshotBaseUrl: string,
): Promise<T | undefined> {
-
const res = await callGetRecord<T>(SLINGSHOT_BASE_URL, did, collection, rkey);
+
const res = await callGetRecord<T>(slingshotBaseUrl, did, collection, rkey);
if (!res.ok) throw new Error(`Slingshot getRecord failed for ${did}/${collection}/${rkey}`);
return res.data.value;
}
···
};
}> {
const { rpc } = await createAtprotoClient({ service });
+
+
const params: Record<string, unknown> = {
+
repo: did,
+
collection,
+
limit,
+
cursor,
+
reverse: false,
+
};
+
return await (rpc as unknown as {
get: (
nsid: string,
···
};
}>;
}).get("com.atproto.repo.listRecords", {
-
params: {
-
repo: did,
-
collection,
-
limit,
-
cursor,
-
reverse: false,
-
},
+
params,
});
}
+5 -2
lib/hooks/useLatestRecord.ts
···
/**
* Fetches the most recent record from a collection using `listRecords(limit=3)`.
-
*
+
*
* Note: Slingshot does not support listRecords, so this always queries the actor's PDS directly.
-
*
+
*
* Records with invalid timestamps (before 2023, when ATProto was created) are automatically
* skipped, and additional records are fetched to find a valid one.
*
* @param handleOrDid - Handle or DID that owns the collection.
* @param collection - NSID of the collection to query.
+
* @param refreshKey - Optional key that when changed, triggers a refetch. Use for auto-refresh scenarios.
* @returns {LatestRecordState<T>} Object reporting the latest record value, derived rkey, loading status, emptiness, and any error.
*/
export function useLatestRecord<T = unknown>(
handleOrDid: string | undefined,
collection: string,
+
refreshKey?: number,
): LatestRecordState<T> {
const {
did,
···
resolvingEndpoint,
didError,
endpointError,
+
refreshKey,
]);
return state;
+4 -6
lib/hooks/usePaginatedRecords.ts
···
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useDidResolution } from "./useDidResolution";
import { usePdsEndpoint } from "./usePdsEndpoint";
-
import {
-
DEFAULT_APPVIEW_SERVICE,
-
callAppviewRpc,
-
callListRecords
-
} from "./useBlueskyAppview";
+
import { callAppviewRpc, callListRecords } from "./useBlueskyAppview";
+
import { useAtProto } from "../providers/AtProtoProvider";
/**
* Record envelope returned by paginated AT Protocol queries.
···
authorFeedService,
authorFeedActor,
}: UsePaginatedRecordsOptions): UsePaginatedRecordsResult<T> {
+
const { blueskyAppviewService } = useAtProto();
const {
did,
handle,
···
}
const res = await callAppviewRpc<AuthorFeedResponse>(
-
authorFeedService ?? DEFAULT_APPVIEW_SERVICE,
+
authorFeedService ?? blueskyAppviewService,
"app.bsky.feed.getAuthorFeed",
{
actor: actorIdentifier,
+104
lib/hooks/useRepoLanguages.ts
···
+
import { useState, useEffect } from "react";
+
import type { RepoLanguagesResponse } from "../types/tangled";
+
+
export interface UseRepoLanguagesOptions {
+
/** The knot server URL (e.g., "knot.gaze.systems") */
+
knot?: string;
+
/** DID of the repository owner */
+
did?: string;
+
/** Repository name */
+
repoName?: string;
+
/** Branch to query (defaults to trying "main", then "master") */
+
branch?: string;
+
/** Whether to enable the query */
+
enabled?: boolean;
+
}
+
+
export interface UseRepoLanguagesResult {
+
/** Language data from the knot server */
+
data?: RepoLanguagesResponse;
+
/** Loading state */
+
loading: boolean;
+
/** Error state */
+
error?: Error;
+
}
+
+
/**
+
* Hook to fetch repository language information from a Tangled knot server.
+
* If no branch supplied, tries "main" first, then falls back to "master".
+
*/
+
export function useRepoLanguages({
+
knot,
+
did,
+
repoName,
+
branch,
+
enabled = true,
+
}: UseRepoLanguagesOptions): UseRepoLanguagesResult {
+
const [data, setData] = useState<RepoLanguagesResponse | undefined>();
+
const [loading, setLoading] = useState(false);
+
const [error, setError] = useState<Error | undefined>();
+
+
useEffect(() => {
+
if (!enabled || !knot || !did || !repoName) {
+
return;
+
}
+
+
let cancelled = false;
+
+
const fetchLanguages = async (ref: string): Promise<boolean> => {
+
try {
+
const url = `https://${knot}/xrpc/sh.tangled.repo.languages?repo=${encodeURIComponent(`${did}/${repoName}`)}&ref=${encodeURIComponent(ref)}`;
+
const response = await fetch(url);
+
+
if (!response.ok) {
+
return false;
+
}
+
+
const result = await response.json();
+
if (!cancelled) {
+
setData(result);
+
setError(undefined);
+
}
+
return true;
+
} catch (err) {
+
return false;
+
}
+
};
+
+
const fetchWithFallback = async () => {
+
setLoading(true);
+
setError(undefined);
+
+
if (branch) {
+
const success = await fetchLanguages(branch);
+
if (!cancelled) {
+
if (!success) {
+
setError(new Error(`Failed to fetch languages for branch: ${branch}`));
+
}
+
setLoading(false);
+
}
+
} else {
+
// Try "main" first, then "master"
+
let success = await fetchLanguages("main");
+
if (!success && !cancelled) {
+
success = await fetchLanguages("master");
+
}
+
+
if (!cancelled) {
+
if (!success) {
+
setError(new Error("Failed to fetch languages for main or master branch"));
+
}
+
setLoading(false);
+
}
+
}
+
};
+
+
fetchWithFallback();
+
+
return () => {
+
cancelled = true;
+
};
+
}, [knot, did, repoName, branch, enabled]);
+
+
return { data, loading, error };
+
}
+13
lib/index.ts
···
export * from "./components/BlueskyPostList";
export * from "./components/BlueskyProfile";
export * from "./components/BlueskyQuotePost";
+
export * from "./components/GrainGallery";
export * from "./components/LeafletDocument";
+
export * from "./components/TangledRepo";
export * from "./components/TangledString";
+
export * from "./components/CurrentlyPlaying";
+
export * from "./components/LastPlayed";
+
export * from "./components/SongHistoryList";
// Hooks
export * from "./hooks/useAtProtoRecord";
+
export * from "./hooks/useBacklinks";
export * from "./hooks/useBlob";
export * from "./hooks/useBlueskyAppview";
export * from "./hooks/useBlueskyProfile";
···
export * from "./hooks/useLatestRecord";
export * from "./hooks/usePaginatedRecords";
export * from "./hooks/usePdsEndpoint";
+
export * from "./hooks/useRepoLanguages";
// Renderers
export * from "./renderers/BlueskyPostRenderer";
export * from "./renderers/BlueskyProfileRenderer";
+
export * from "./renderers/GrainGalleryRenderer";
export * from "./renderers/LeafletDocumentRenderer";
+
export * from "./renderers/TangledRepoRenderer";
export * from "./renderers/TangledStringRenderer";
+
export * from "./renderers/CurrentlyPlayingRenderer";
// Types
export * from "./types/bluesky";
+
export * from "./types/grain";
export * from "./types/leaflet";
+
export * from "./types/tangled";
+
export * from "./types/teal";
export * from "./types/theme";
// Utilities
+102 -9
lib/providers/AtProtoProvider.tsx
···
useMemo,
useRef,
} from "react";
-
import { ServiceResolver, normalizeBaseUrl } from "../utils/atproto-client";
-
import { BlobCache, DidCache } from "../utils/cache";
+
import { ServiceResolver, normalizeBaseUrl, DEFAULT_CONFIG } from "../utils/atproto-client";
+
import { BlobCache, DidCache, RecordCache } from "../utils/cache";
/**
* Props for the AT Protocol context provider.
···
children: React.ReactNode;
/** Optional custom PLC directory URL. Defaults to https://plc.directory */
plcDirectory?: string;
+
/** Optional custom identity service URL. Defaults to https://public.api.bsky.app */
+
identityService?: string;
+
/** Optional custom Slingshot service URL. Defaults to https://slingshot.microcosm.blue */
+
slingshotBaseUrl?: string;
+
/** Optional custom Bluesky appview service URL. Defaults to https://public.api.bsky.app */
+
blueskyAppviewService?: string;
+
/** Optional custom Bluesky app base URL for links. Defaults to https://bsky.app */
+
blueskyAppBaseUrl?: string;
+
/** Optional custom Tangled base URL for links. Defaults to https://tangled.org */
+
tangledBaseUrl?: string;
+
/** Optional custom Constellation API URL for backlinks. Defaults to https://constellation.microcosm.blue */
+
constellationBaseUrl?: string;
}
/**
···
resolver: ServiceResolver;
/** Normalized PLC directory base URL. */
plcDirectory: string;
+
/** Normalized Bluesky appview service URL. */
+
blueskyAppviewService: string;
+
/** Normalized Bluesky app base URL for links. */
+
blueskyAppBaseUrl: string;
+
/** Normalized Tangled base URL for links. */
+
tangledBaseUrl: string;
+
/** Normalized Constellation API base URL for backlinks. */
+
constellationBaseUrl: string;
/** Cache for DID documents and handle mappings. */
didCache: DidCache;
/** Cache for fetched blob data. */
blobCache: BlobCache;
+
/** Cache for fetched AT Protocol records. */
+
recordCache: RecordCache;
}
const AtProtoContext = createContext<AtProtoContextValue | undefined>(
···
export function AtProtoProvider({
children,
plcDirectory,
+
identityService,
+
slingshotBaseUrl,
+
blueskyAppviewService,
+
blueskyAppBaseUrl,
+
tangledBaseUrl,
+
constellationBaseUrl,
}: AtProtoProviderProps) {
const normalizedPlc = useMemo(
() =>
normalizeBaseUrl(
plcDirectory && plcDirectory.trim()
? plcDirectory
-
: "https://plc.directory",
+
: DEFAULT_CONFIG.plcDirectory,
),
[plcDirectory],
);
+
const normalizedIdentity = useMemo(
+
() =>
+
normalizeBaseUrl(
+
identityService && identityService.trim()
+
? identityService
+
: DEFAULT_CONFIG.identityService,
+
),
+
[identityService],
+
);
+
const normalizedSlingshot = useMemo(
+
() =>
+
normalizeBaseUrl(
+
slingshotBaseUrl && slingshotBaseUrl.trim()
+
? slingshotBaseUrl
+
: DEFAULT_CONFIG.slingshotBaseUrl,
+
),
+
[slingshotBaseUrl],
+
);
+
const normalizedAppview = useMemo(
+
() =>
+
normalizeBaseUrl(
+
blueskyAppviewService && blueskyAppviewService.trim()
+
? blueskyAppviewService
+
: DEFAULT_CONFIG.blueskyAppviewService,
+
),
+
[blueskyAppviewService],
+
);
+
const normalizedBlueskyApp = useMemo(
+
() =>
+
normalizeBaseUrl(
+
blueskyAppBaseUrl && blueskyAppBaseUrl.trim()
+
? blueskyAppBaseUrl
+
: DEFAULT_CONFIG.blueskyAppBaseUrl,
+
),
+
[blueskyAppBaseUrl],
+
);
+
const normalizedTangled = useMemo(
+
() =>
+
normalizeBaseUrl(
+
tangledBaseUrl && tangledBaseUrl.trim()
+
? tangledBaseUrl
+
: DEFAULT_CONFIG.tangledBaseUrl,
+
),
+
[tangledBaseUrl],
+
);
+
const normalizedConstellation = useMemo(
+
() =>
+
normalizeBaseUrl(
+
constellationBaseUrl && constellationBaseUrl.trim()
+
? constellationBaseUrl
+
: DEFAULT_CONFIG.constellationBaseUrl,
+
),
+
[constellationBaseUrl],
+
);
const resolver = useMemo(
-
() => new ServiceResolver({ plcDirectory: normalizedPlc }),
-
[normalizedPlc],
+
() => new ServiceResolver({
+
plcDirectory: normalizedPlc,
+
identityService: normalizedIdentity,
+
slingshotBaseUrl: normalizedSlingshot,
+
}),
+
[normalizedPlc, normalizedIdentity, normalizedSlingshot],
);
const cachesRef = useRef<{
didCache: DidCache;
blobCache: BlobCache;
+
recordCache: RecordCache;
} | null>(null);
if (!cachesRef.current) {
cachesRef.current = {
didCache: new DidCache(),
blobCache: new BlobCache(),
+
recordCache: new RecordCache(),
};
}
···
() => ({
resolver,
plcDirectory: normalizedPlc,
+
blueskyAppviewService: normalizedAppview,
+
blueskyAppBaseUrl: normalizedBlueskyApp,
+
tangledBaseUrl: normalizedTangled,
+
constellationBaseUrl: normalizedConstellation,
didCache: cachesRef.current!.didCache,
blobCache: cachesRef.current!.blobCache,
+
recordCache: cachesRef.current!.recordCache,
}),
-
[resolver, normalizedPlc],
+
[resolver, normalizedPlc, normalizedAppview, normalizedBlueskyApp, normalizedTangled, normalizedConstellation],
);
return (
···
/**
* Hook that accesses the AT Protocol context provided by `AtProtoProvider`.
*
-
* This hook exposes the service resolver, DID cache, and blob cache for building
-
* custom AT Protocol functionality.
+
* This hook exposes the service resolver, DID cache, blob cache, and record cache
+
* for building custom AT Protocol functionality.
*
* @throws {Error} When called outside of an `AtProtoProvider`.
* @returns {AtProtoContextValue} Object containing resolver, caches, and PLC directory URL.
···
* import { useAtProto } from 'atproto-ui';
*
* function MyCustomComponent() {
-
* const { resolver, didCache, blobCache } = useAtProto();
+
* const { resolver, didCache, blobCache, recordCache } = useAtProto();
* // Use the resolver and caches for custom AT Protocol operations
* }
* ```
+56 -42
lib/renderers/BlueskyPostRenderer.tsx
···
import { useBlob } from "../hooks/useBlob";
import { BlueskyIcon } from "../components/BlueskyIcon";
import { isBlobWithCdn, extractCidFromBlob } from "../utils/blob";
+
import { RichText } from "../components/RichText";
export interface BlueskyPostRendererProps {
record: FeedPostRecord;
···
isInThread?: boolean;
threadDepth?: number;
isQuotePost?: boolean;
+
showThreadBorder?: boolean;
}
export const BlueskyPostRenderer: React.FC<BlueskyPostRendererProps> = ({
···
atUri,
isInThread = false,
threadDepth = 0,
-
isQuotePost = false
+
isQuotePost = false,
+
showThreadBorder = false
}) => {
void threadDepth;
···
if (error) {
return (
-
<div style={{ padding: 8, color: "crimson" }}>
+
<div role="alert" style={{ padding: 8, color: "crimson" }}>
Failed to load post.
</div>
);
}
-
if (loading && !record) return <div style={{ padding: 8 }}>Loadingโ€ฆ</div>;
+
if (loading && !record) return <div role="status" aria-live="polite" style={{ padding: 8 }}>Loadingโ€ฆ</div>;
const text = record.text;
const createdDate = new Date(record.createdAt);
···
const cardStyle: React.CSSProperties = {
...baseStyles.card,
-
border: (isInThread && !isQuotePost) ? "none" : `1px solid var(--atproto-color-border)`,
+
border: (isInThread && !isQuotePost && !showThreadBorder) ? "none" : `1px solid var(--atproto-color-border)`,
background: `var(--atproto-color-bg)`,
color: `var(--atproto-color-text)`,
-
borderRadius: (isInThread && !isQuotePost) ? "0" : "12px",
+
borderRadius: (isInThread && !isQuotePost && !showThreadBorder) ? "0" : "12px",
...(iconPlacement === "cardBottomRight" && showIcon && !isInThread
? { paddingBottom: cardPadding + 16 }
: {}),
···
gap: inline ? 8 : 0,
}}
>
-
<strong style={{ fontSize: 14 }}>{primaryName}</strong>
-
{authorDisplayName && authorHandle && (
+
<strong style={{ fontSize: 14 }}>{authorDisplayName || primaryName}</strong>
+
{authorHandle && (
<span
style={{
...baseStyles.handle,
···
</div>
);
-
const Avatar: React.FC<{ avatarUrl?: string }> = ({ avatarUrl }) =>
+
const Avatar: React.FC<{ avatarUrl?: string; name?: string }> = ({ avatarUrl, name }) =>
avatarUrl ? (
-
<img src={avatarUrl} alt="avatar" style={baseStyles.avatarImg} />
+
<img src={avatarUrl} alt={`${name || 'User'}'s profile picture`} style={baseStyles.avatarImg} />
) : (
-
<div style={baseStyles.avatarPlaceholder} aria-hidden />
+
<div style={baseStyles.avatarPlaceholder} aria-hidden="true" />
);
const ReplyInfo: React.FC<{
···
}) => (
<div style={baseStyles.body}>
<p style={{ ...baseStyles.text, color: `var(--atproto-color-text)` }}>
-
{text}
+
<RichText text={text} facets={record.facets} />
</p>
-
{record.facets && record.facets.length > 0 && (
-
<div style={baseStyles.facets}>
-
{record.facets.map((_, idx) => (
-
<span
-
key={idx}
-
style={{
-
...baseStyles.facetTag,
-
background: `var(--atproto-color-bg-secondary)`,
-
color: `var(--atproto-color-text-secondary)`,
-
}}
-
>
-
facet
-
</span>
-
))}
-
</div>
+
{resolvedEmbed && (
+
<div style={baseStyles.embedContainer}>{resolvedEmbed}</div>
)}
<div style={baseStyles.timestampRow}>
<time
···
</span>
)}
</div>
-
{resolvedEmbed && (
-
<div style={baseStyles.embedContainer}>{resolvedEmbed}</div>
-
)}
</div>
);
const ThreadLayout: React.FC<LayoutProps> = (props) => (
<div style={{ display: "flex", gap: 8, alignItems: "flex-start" }}>
-
<Avatar avatarUrl={props.avatarUrl} />
+
<Avatar avatarUrl={props.avatarUrl} name={props.authorDisplayName || props.authorHandle} />
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
···
const DefaultLayout: React.FC<LayoutProps> = (props) => (
<>
<header style={baseStyles.header}>
-
<Avatar avatarUrl={props.avatarUrl} />
+
<Avatar avatarUrl={props.avatarUrl} name={props.authorDisplayName || props.authorHandle} />
<AuthorInfo
primaryName={props.primaryName}
authorDisplayName={props.authorDisplayName}
···
whiteSpace: "pre-wrap",
overflowWrap: "anywhere",
},
-
facets: {
-
marginTop: 8,
-
display: "flex",
-
gap: 4,
-
},
embedContainer: {
marginTop: 12,
padding: 8,
borderRadius: 12,
+
border: `1px solid var(--atproto-color-border)`,
+
background: `var(--atproto-color-bg-elevated)`,
display: "flex",
flexDirection: "column",
gap: 8,
···
inlineIcon: {
display: "inline-flex",
alignItems: "center",
-
},
-
facetTag: {
-
padding: "2px 6px",
-
borderRadius: 4,
-
fontSize: 11,
},
replyLine: {
fontSize: 12,
···
}
const PostImage: React.FC<PostImageProps> = ({ image, did }) => {
+
const [showAltText, setShowAltText] = React.useState(false);
const imageBlob = image.image;
const cdnUrl = isBlobWithCdn(imageBlob) ? imageBlob.cdnUrl : undefined;
const cid = cdnUrl ? undefined : extractCidFromBlob(imageBlob);
const { url: urlFromBlob, loading, error } = useBlob(did, cid);
const url = cdnUrl || urlFromBlob;
const alt = image.alt?.trim() || "Bluesky attachment";
+
const hasAlt = image.alt && image.alt.trim().length > 0;
const aspect =
image.aspectRatio && image.aspectRatio.height > 0
···
<img src={url} alt={alt} style={imagesBase.img} />
) : (
<div
+
role={error ? "alert" : "status"}
style={{
...imagesBase.placeholder,
color: `var(--atproto-color-text-muted)`,
···
: "Image unavailable"}
</div>
)}
+
{hasAlt && (
+
<button
+
onClick={() => setShowAltText(!showAltText)}
+
style={{
+
...imagesBase.altBadge,
+
background: showAltText
+
? `var(--atproto-color-text)`
+
: `var(--atproto-color-bg-secondary)`,
+
color: showAltText
+
? `var(--atproto-color-bg)`
+
: `var(--atproto-color-text)`,
+
}}
+
title="Toggle alt text"
+
aria-label="Toggle alt text"
+
>
+
ALT
+
</button>
+
)}
</div>
-
{image.alt && image.alt.trim().length > 0 && (
+
{hasAlt && showAltText && (
<figcaption
style={{
...imagesBase.caption,
···
caption: {
fontSize: 12,
lineHeight: 1.3,
+
} satisfies React.CSSProperties,
+
altBadge: {
+
position: "absolute",
+
bottom: 8,
+
right: 8,
+
padding: "4px 8px",
+
fontSize: 10,
+
fontWeight: 600,
+
letterSpacing: "0.5px",
+
border: "none",
+
borderRadius: 4,
+
cursor: "pointer",
+
transition: "background 150ms ease, color 150ms ease",
+
fontFamily: "system-ui, sans-serif",
} satisfies React.CSSProperties,
};
+47 -36
lib/renderers/BlueskyProfileRenderer.tsx
···
import React from "react";
import type { ProfileRecord } from "../types/bluesky";
import { BlueskyIcon } from "../components/BlueskyIcon";
+
import { useAtProto } from "../providers/AtProtoProvider";
export interface BlueskyProfileRendererProps {
record: ProfileRecord;
···
handle,
avatarUrl,
}) => {
+
const { blueskyAppBaseUrl } = useAtProto();
if (error)
return (
-
<div style={{ padding: 8, color: "crimson" }}>
+
<div role="alert" style={{ padding: 8, color: "crimson" }}>
Failed to load profile.
</div>
);
-
if (loading && !record) return <div style={{ padding: 8 }}>Loadingโ€ฆ</div>;
+
if (loading && !record) return <div role="status" aria-live="polite" style={{ padding: 8 }}>Loadingโ€ฆ</div>;
-
const profileUrl = `https://bsky.app/profile/${did}`;
+
const profileUrl = `${blueskyAppBaseUrl}/profile/${did}`;
const rawWebsite = record.website?.trim();
const websiteHref = rawWebsite
? rawWebsite.match(/^https?:\/\//i)
···
<div style={{ ...base.card, background: `var(--atproto-color-bg)`, borderColor: `var(--atproto-color-border)`, color: `var(--atproto-color-text)` }}>
<div style={base.header}>
{avatarUrl ? (
-
<img src={avatarUrl} alt="avatar" style={base.avatarImg} />
+
<img src={avatarUrl} alt={`${record.displayName || handle || did}'s profile picture`} style={base.avatarImg} />
) : (
<div
style={{ ...base.avatar, background: `var(--atproto-color-bg-elevated)` }}
-
aria-label="avatar"
+
aria-hidden="true"
/>
)}
<div style={{ flex: 1 }}>
···
{record.description}
</p>
)}
-
{record.createdAt && (
-
<div style={{ ...base.meta, color: `var(--atproto-color-text-secondary)` }}>
-
Joined {new Date(record.createdAt).toLocaleDateString()}
-
</div>
-
)}
-
<div style={base.links}>
-
{websiteHref && websiteLabel && (
+
<div style={base.bottomRow}>
+
<div style={base.bottomLeft}>
+
{record.createdAt && (
+
<div style={{ ...base.meta, color: `var(--atproto-color-text-secondary)` }}>
+
Joined {new Date(record.createdAt).toLocaleDateString()}
+
</div>
+
)}
+
{websiteHref && websiteLabel && (
+
<a
+
href={websiteHref}
+
target="_blank"
+
rel="noopener noreferrer"
+
style={{ ...base.link, color: `var(--atproto-color-link)` }}
+
>
+
{websiteLabel}
+
</a>
+
)}
<a
-
href={websiteHref}
+
href={profileUrl}
target="_blank"
rel="noopener noreferrer"
style={{ ...base.link, color: `var(--atproto-color-link)` }}
>
-
{websiteLabel}
+
View on Bluesky
</a>
-
)}
-
<a
-
href={profileUrl}
-
target="_blank"
-
rel="noopener noreferrer"
-
style={{ ...base.link, color: `var(--atproto-color-link)` }}
-
>
-
View on Bluesky
-
</a>
-
</div>
-
<div style={base.iconCorner} aria-hidden>
-
<BlueskyIcon size={18} />
+
</div>
+
<div aria-hidden>
+
<BlueskyIcon size={18} />
+
</div>
</div>
</div>
);
···
const base: Record<string, React.CSSProperties> = {
card: {
+
display: "flex",
+
flexDirection: "column",
+
height: "100%",
borderRadius: 12,
padding: 16,
fontFamily: "system-ui, sans-serif",
···
lineHeight: 1.4,
},
meta: {
-
marginTop: 12,
+
marginTop: 0,
fontSize: 12,
},
pronouns: {
···
padding: "2px 8px",
marginTop: 6,
},
-
links: {
-
display: "flex",
-
flexDirection: "column",
-
gap: 8,
-
marginTop: 12,
-
},
link: {
display: "inline-flex",
alignItems: "center",
···
fontWeight: 600,
textDecoration: "none",
},
+
bottomRow: {
+
display: "flex",
+
alignItems: "flex-end",
+
justifyContent: "space-between",
+
marginTop: "auto",
+
paddingTop: 12,
+
},
+
bottomLeft: {
+
display: "flex",
+
flexDirection: "column",
+
gap: 8,
+
},
iconCorner: {
-
position: "absolute",
-
right: 12,
-
bottom: 12,
+
// Removed absolute positioning, now in flex layout
},
};
+749
lib/renderers/CurrentlyPlayingRenderer.tsx
···
+
import React, { useState, useEffect, useRef } from "react";
+
import type { TealActorStatusRecord } from "../types/teal";
+
+
export interface CurrentlyPlayingRendererProps {
+
record: TealActorStatusRecord;
+
error?: Error;
+
loading: boolean;
+
did: string;
+
rkey: string;
+
colorScheme?: "light" | "dark" | "system";
+
/** Label to display (e.g., "CURRENTLY PLAYING", "LAST PLAYED"). Defaults to "CURRENTLY PLAYING". */
+
label?: string;
+
/** Handle to display in not listening state */
+
handle?: string;
+
}
+
+
interface SonglinkPlatform {
+
url: string;
+
entityUniqueId: string;
+
nativeAppUriMobile?: string;
+
nativeAppUriDesktop?: string;
+
}
+
+
interface SonglinkResponse {
+
linksByPlatform: {
+
[platform: string]: SonglinkPlatform;
+
};
+
entitiesByUniqueId: {
+
[id: string]: {
+
thumbnailUrl?: string;
+
title?: string;
+
artistName?: string;
+
};
+
};
+
}
+
+
export const CurrentlyPlayingRenderer: React.FC<CurrentlyPlayingRendererProps> = ({
+
record,
+
error,
+
loading,
+
label = "CURRENTLY PLAYING",
+
handle,
+
}) => {
+
const [albumArt, setAlbumArt] = useState<string | undefined>(undefined);
+
const [artworkLoading, setArtworkLoading] = useState(true);
+
const [songlinkData, setSonglinkData] = useState<SonglinkResponse | undefined>(undefined);
+
const [showPlatformModal, setShowPlatformModal] = useState(false);
+
const previousTrackIdentityRef = useRef<string>("");
+
+
// Auto-refresh interval removed - handled by AtProtoRecord
+
+
useEffect(() => {
+
if (!record) return;
+
+
const { item } = record;
+
const artistName = item.artists[0]?.artistName;
+
const trackName = item.trackName;
+
+
if (!artistName || !trackName) {
+
setArtworkLoading(false);
+
return;
+
}
+
+
// Create a unique identity for this track
+
const trackIdentity = `${trackName}::${artistName}`;
+
+
// Check if the track has actually changed
+
const trackHasChanged = trackIdentity !== previousTrackIdentityRef.current;
+
+
// Update tracked identity
+
previousTrackIdentityRef.current = trackIdentity;
+
+
// Only reset loading state and clear data when track actually changes
+
// This prevents the loading flicker when auto-refreshing the same track
+
if (trackHasChanged) {
+
console.log(`[teal.fm] ๐ŸŽต Track changed: "${trackName}" by ${artistName}`);
+
setArtworkLoading(true);
+
setAlbumArt(undefined);
+
setSonglinkData(undefined);
+
} else {
+
console.log(`[teal.fm] ๐Ÿ”„ Auto-refresh: same track still playing ("${trackName}" by ${artistName})`);
+
}
+
+
let cancelled = false;
+
+
const fetchMusicData = async () => {
+
try {
+
// Step 1: Check if we have an ISRC - Songlink supports this directly
+
if (item.isrc) {
+
console.log(`[teal.fm] Attempting ISRC lookup for ${trackName} by ${artistName}`, { isrc: item.isrc });
+
const response = await fetch(
+
`https://api.song.link/v1-alpha.1/links?platform=isrc&type=song&id=${encodeURIComponent(item.isrc)}&songIfSingle=true`
+
);
+
if (cancelled) return;
+
if (response.ok) {
+
const data = await response.json();
+
setSonglinkData(data);
+
+
// Extract album art from Songlink data
+
const entityId = data.entityUniqueId;
+
const entity = data.entitiesByUniqueId?.[entityId];
+
+
// Debug: Log the entity structure to see what fields are available
+
console.log(`[teal.fm] ISRC entity data:`, { entityId, entity });
+
+
if (entity?.thumbnailUrl) {
+
console.log(`[teal.fm] โœ“ Found album art via ISRC lookup`);
+
setAlbumArt(entity.thumbnailUrl);
+
} else {
+
console.warn(`[teal.fm] ISRC lookup succeeded but no thumbnail found`, {
+
entityId,
+
entityKeys: entity ? Object.keys(entity) : 'no entity',
+
entity
+
});
+
}
+
setArtworkLoading(false);
+
return;
+
} else {
+
console.warn(`[teal.fm] ISRC lookup failed with status ${response.status}`);
+
}
+
}
+
+
// Step 2: Search iTunes Search API to find the track (single request for both artwork and links)
+
console.log(`[teal.fm] Attempting iTunes search for: "${trackName}" by "${artistName}"`);
+
const iTunesSearchUrl = `https://itunes.apple.com/search?term=${encodeURIComponent(
+
`${trackName} ${artistName}`
+
)}&media=music&entity=song&limit=1`;
+
+
const iTunesResponse = await fetch(iTunesSearchUrl);
+
+
if (cancelled) return;
+
+
if (iTunesResponse.ok) {
+
const iTunesData = await iTunesResponse.json();
+
+
if (iTunesData.results && iTunesData.results.length > 0) {
+
const match = iTunesData.results[0];
+
const iTunesId = match.trackId;
+
+
// Set album artwork immediately (600x600 for high quality)
+
const artworkUrl = match.artworkUrl100?.replace('100x100', '600x600') || match.artworkUrl100;
+
if (artworkUrl) {
+
console.log(`[teal.fm] โœ“ Found album art via iTunes search`, { url: artworkUrl });
+
setAlbumArt(artworkUrl);
+
} else {
+
console.warn(`[teal.fm] iTunes match found but no artwork URL`);
+
}
+
setArtworkLoading(false);
+
+
// Step 3: Use iTunes ID with Songlink to get all platform links
+
console.log(`[teal.fm] Fetching platform links via Songlink (iTunes ID: ${iTunesId})`);
+
const songlinkResponse = await fetch(
+
`https://api.song.link/v1-alpha.1/links?platform=itunes&type=song&id=${iTunesId}&songIfSingle=true`
+
);
+
+
if (cancelled) return;
+
+
if (songlinkResponse.ok) {
+
const songlinkData = await songlinkResponse.json();
+
console.log(`[teal.fm] โœ“ Got platform links from Songlink`);
+
setSonglinkData(songlinkData);
+
return;
+
} else {
+
console.warn(`[teal.fm] Songlink request failed with status ${songlinkResponse.status}`);
+
}
+
} else {
+
console.warn(`[teal.fm] No iTunes results found for "${trackName}" by "${artistName}"`);
+
setArtworkLoading(false);
+
}
+
} else {
+
console.warn(`[teal.fm] iTunes search failed with status ${iTunesResponse.status}`);
+
}
+
+
// Step 4: Fallback - if originUrl is from a supported platform, try it directly
+
if (item.originUrl && (
+
item.originUrl.includes('spotify.com') ||
+
item.originUrl.includes('apple.com') ||
+
item.originUrl.includes('youtube.com') ||
+
item.originUrl.includes('tidal.com')
+
)) {
+
console.log(`[teal.fm] Attempting Songlink lookup via originUrl`, { url: item.originUrl });
+
const songlinkResponse = await fetch(
+
`https://api.song.link/v1-alpha.1/links?url=${encodeURIComponent(item.originUrl)}&songIfSingle=true`
+
);
+
+
if (cancelled) return;
+
+
if (songlinkResponse.ok) {
+
const data = await songlinkResponse.json();
+
console.log(`[teal.fm] โœ“ Got data from Songlink via originUrl`);
+
setSonglinkData(data);
+
+
// Try to get artwork from Songlink if we don't have it yet
+
if (!albumArt) {
+
const entityId = data.entityUniqueId;
+
const entity = data.entitiesByUniqueId?.[entityId];
+
+
// Debug: Log the entity structure to see what fields are available
+
console.log(`[teal.fm] Songlink originUrl entity data:`, { entityId, entity });
+
+
if (entity?.thumbnailUrl) {
+
console.log(`[teal.fm] โœ“ Found album art via Songlink originUrl lookup`);
+
setAlbumArt(entity.thumbnailUrl);
+
} else {
+
console.warn(`[teal.fm] Songlink lookup succeeded but no thumbnail found`, {
+
entityId,
+
entityKeys: entity ? Object.keys(entity) : 'no entity',
+
entity
+
});
+
}
+
}
+
} else {
+
console.warn(`[teal.fm] Songlink originUrl lookup failed with status ${songlinkResponse.status}`);
+
}
+
}
+
+
if (!albumArt) {
+
console.warn(`[teal.fm] โœ— All album art fetch methods failed for "${trackName}" by "${artistName}"`);
+
}
+
+
setArtworkLoading(false);
+
} catch (err) {
+
console.error(`[teal.fm] โœ— Error fetching music data for "${trackName}" by "${artistName}":`, err);
+
setArtworkLoading(false);
+
}
+
};
+
+
fetchMusicData();
+
+
return () => {
+
cancelled = true;
+
};
+
}, [record]); // Runs on record change
+
+
if (error)
+
return (
+
<div role="alert" style={{ padding: 8, color: "var(--atproto-color-error)" }}>
+
Failed to load status.
+
</div>
+
);
+
if (loading && !record)
+
return (
+
<div role="status" aria-live="polite" style={{ padding: 8, color: "var(--atproto-color-text-secondary)" }}>
+
Loadingโ€ฆ
+
</div>
+
);
+
+
const { item } = record;
+
+
// Check if user is not listening to anything
+
const isNotListening = !item.trackName || item.artists.length === 0;
+
+
// Show "not listening" state
+
if (isNotListening) {
+
const displayHandle = handle || "User";
+
return (
+
<div style={styles.notListeningContainer}>
+
<div style={styles.notListeningIcon}>
+
<svg
+
width="80"
+
height="80"
+
viewBox="0 0 24 24"
+
fill="none"
+
stroke="currentColor"
+
strokeWidth="1.5"
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
>
+
<path d="M9 18V5l12-2v13" />
+
<circle cx="6" cy="18" r="3" />
+
<circle cx="18" cy="16" r="3" />
+
</svg>
+
</div>
+
<div style={styles.notListeningTitle}>
+
{displayHandle} isn't listening to anything
+
</div>
+
<div style={styles.notListeningSubtitle}>Check back soon</div>
+
</div>
+
);
+
}
+
+
const artistNames = item.artists.map((a) => a.artistName).join(", ");
+
+
const platformConfig: Record<string, { name: string; svg: string; color: string }> = {
+
spotify: {
+
name: "Spotify",
+
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="#1ed760" d="M248 8C111.1 8 0 119.1 0 256s111.1 248 248 248 248-111.1 248-248S384.9 8 248 8Z"/><path d="M406.6 231.1c-5.2 0-8.4-1.3-12.9-3.9-71.2-42.5-198.5-52.7-280.9-29.7-3.6 1-8.1 2.6-12.9 2.6-13.2 0-23.3-10.3-23.3-23.6 0-13.6 8.4-21.3 17.4-23.9 35.2-10.3 74.6-15.2 117.5-15.2 73 0 149.5 15.2 205.4 47.8 7.8 4.5 12.9 10.7 12.9 22.6 0 13.6-11 23.3-23.2 23.3zm-31 76.2c-5.2 0-8.7-2.3-12.3-4.2-62.5-37-155.7-51.9-238.6-29.4-4.8 1.3-7.4 2.6-11.9 2.6-10.7 0-19.4-8.7-19.4-19.4s5.2-17.8 15.5-20.7c27.8-7.8 56.2-13.6 97.8-13.6 64.9 0 127.6 16.1 177 45.5 8.1 4.8 11.3 11 11.3 19.7-.1 10.8-8.5 19.5-19.4 19.5zm-26.9 65.6c-4.2 0-6.8-1.3-10.7-3.6-62.4-37.6-135-39.2-206.7-24.5-3.9 1-9 2.6-11.9 2.6-9.7 0-15.8-7.7-15.8-15.8 0-10.3 6.1-15.2 13.6-16.8 81.9-18.1 165.6-16.5 237 26.2 6.1 3.9 9.7 7.4 9.7 16.5s-7.1 15.4-15.2 15.4z"/></svg>',
+
color: "#1DB954"
+
},
+
appleMusic: {
+
name: "Apple Music",
+
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 361 361"><defs><linearGradient id="apple-grad" x1="180" y1="358.6" x2="180" y2="7.76" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#FA233B"/><stop offset="1" style="stop-color:#FB5C74"/></linearGradient></defs><path fill="url(#apple-grad)" d="M360 112.61V247.39c0 4.3 0 8.6-.02 12.9-.02 3.62-.06 7.24-.16 10.86-.21 7.89-.68 15.84-2.08 23.64-1.42 7.92-3.75 15.29-7.41 22.49-3.6 7.07-8.3 13.53-13.91 19.14-5.61 5.61-12.08 10.31-19.15 13.91-7.19 3.66-14.56 5.98-22.47 7.41-7.8 1.4-15.76 1.87-23.65 2.08-3.62.1-7.24.14-10.86.16-4.3.03-8.6.02-12.9.02H112.61c-4.3 0-8.6 0-12.9-.02-3.62-.02-7.24-.06-10.86-.16-7.89-.21-15.85-.68-23.65-2.08-7.92-1.42-15.28-3.75-22.47-7.41-7.07-3.6-13.54-8.3-19.15-13.91-5.61-5.61-10.31-12.07-13.91-19.14-3.66-7.2-5.99-14.57-7.41-22.49-1.4-7.8-1.87-15.76-2.08-23.64-.1-3.62-.14-7.24-.16-10.86C0 255.99 0 251.69 0 247.39V112.61c0-4.3 0-8.6.02-12.9.02-3.62.06-7.24.16-10.86.21-7.89.68-15.84 2.08-23.64 1.42-7.92 3.75-15.29 7.41-22.49 3.6-7.07 8.3-13.53 13.91-19.14 5.61-5.61 12.08-10.31 19.15-13.91 7.19-3.66 14.56-5.98 22.47-7.41 7.8-1.4 15.76-1.87 23.65-2.08 3.62-.1 7.24-.14 10.86-.16C104.01 0 108.31 0 112.61 0h134.77c4.3 0 8.6 0 12.9.02 3.62.02 7.24.06 10.86.16 7.89.21 15.85.68 23.65 2.08 7.92 1.42 15.28 3.75 22.47 7.41 7.07 3.6 13.54 8.3 19.15 13.91 5.61 5.61 10.31 12.07 13.91 19.14 3.66 7.2 5.99 14.57 7.41 22.49 1.4 7.8 1.87 15.76 2.08 23.64.1 3.62.14 7.24.16 10.86.03 4.3.02 8.6.02 12.9z"/><path fill="#FFF" d="M254.5 55c-.87.08-8.6 1.45-9.53 1.64l-107 21.59-.04.01c-2.79.59-4.98 1.58-6.67 3-2.04 1.71-3.17 4.13-3.6 6.95-.09.6-.24 1.82-.24 3.62v133.92c0 3.13-.25 6.17-2.37 8.76-2.12 2.59-4.74 3.37-7.81 3.99-2.33.47-4.66.94-6.99 1.41-8.84 1.78-14.59 2.99-19.8 5.01-4.98 1.93-8.71 4.39-11.68 7.51-5.89 6.17-8.28 14.54-7.46 22.38.7 6.69 3.71 13.09 8.88 17.82 3.49 3.2 7.85 5.63 12.99 6.66 5.33 1.07 11.01.7 19.31-.98 4.42-.89 8.56-2.28 12.5-4.61 3.9-2.3 7.24-5.37 9.85-9.11 2.62-3.75 4.31-7.92 5.24-12.35.96-4.57 1.19-8.7 1.19-13.26V128.82c0-6.22 1.76-7.86 6.78-9.08l93.09-18.75c5.79-1.11 8.52.54 8.52 6.61v79.29c0 3.14-.03 6.32-2.17 8.92-2.12 2.59-4.74 3.37-7.81 3.99-2.33.47-4.66.94-6.99 1.41-8.84 1.78-14.59 2.99-19.8 5.01-4.98 1.93-8.71 4.39-11.68 7.51-5.89 6.17-8.49 14.54-7.67 22.38.7 6.69 3.92 13.09 9.09 17.82 3.49 3.2 7.85 5.56 12.99 6.6 5.33 1.07 11.01.69 19.31-.98 4.42-.89 8.56-2.22 12.5-4.55 3.9-2.3 7.24-5.37 9.85-9.11 2.62-3.75 4.31-7.92 5.24-12.35.96-4.57 1-8.7 1-13.26V64.46c0-6.16-3.25-9.96-9.04-9.46z"/></svg>',
+
color: "#FA243C"
+
},
+
youtube: {
+
name: "YouTube",
+
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><g transform="scale(.75)"><path fill="red" d="M199.917 105.63s-84.292 0-105.448 5.497c-11.328 3.165-20.655 12.493-23.82 23.987-5.498 21.156-5.498 64.969-5.498 64.969s0 43.979 5.497 64.802c3.165 11.494 12.326 20.655 23.82 23.82 21.323 5.664 105.448 5.664 105.448 5.664s84.459 0 105.615-5.497c11.494-3.165 20.655-12.16 23.654-23.82 5.664-20.99 5.664-64.803 5.664-64.803s.166-43.98-5.664-65.135c-2.999-11.494-12.16-20.655-23.654-23.654-21.156-5.83-105.615-5.83-105.615-5.83zm-26.82 53.974 70.133 40.479-70.133 40.312v-80.79z"/><path fill="#fff" d="m173.097 159.604 70.133 40.479-70.133 40.312v-80.79z"/></g></svg>',
+
color: "#FF0000"
+
},
+
youtubeMusic: {
+
name: "YouTube Music",
+
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 176 176"><circle fill="#FF0000" cx="88" cy="88" r="88"/><path fill="#FFF" d="M88 46c23.1 0 42 18.8 42 42s-18.8 42-42 42-42-18.8-42-42 18.8-42 42-42m0-4c-25.4 0-46 20.6-46 46s20.6 46 46 46 46-20.6 46-46-20.6-46-46-46z"/><path fill="#FFF" d="m72 111 39-24-39-22z"/></svg>',
+
color: "#FF0000"
+
},
+
tidal: {
+
name: "Tidal",
+
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 0c141.385 0 256 114.615 256 256S397.385 512 256 512 0 397.385 0 256 114.615 0 256 0zm50.384 219.459-50.372 50.383 50.379 50.391-50.382 50.393-50.395-50.393 50.393-50.389-50.393-50.39 50.395-50.372 50.38 50.369 50.389-50.375 50.382 50.382-50.382 50.392-50.394-50.391zm-100.767-.001-50.392 50.392-50.385-50.392 50.385-50.382 50.392 50.382z"/></svg>',
+
color: "#000000"
+
},
+
bandcamp: {
+
name: "Bandcamp",
+
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#1DA0C3" d="M0 156v200h172l84-200z"/></svg>',
+
color: "#1DA0C3"
+
},
+
};
+
+
const availablePlatforms = songlinkData
+
? Object.keys(platformConfig).filter((platform) =>
+
songlinkData.linksByPlatform[platform]
+
)
+
: [];
+
+
return (
+
<>
+
<div style={styles.container}>
+
{/* Album Artwork */}
+
<div style={styles.artworkContainer}>
+
{artworkLoading ? (
+
<div style={styles.artworkPlaceholder}>
+
<div style={styles.loadingSpinner} />
+
</div>
+
) : albumArt ? (
+
<img
+
src={albumArt}
+
alt={`${item.releaseName || "Album"} cover`}
+
style={styles.artwork}
+
onError={(e) => {
+
console.error("Failed to load album art:", {
+
url: albumArt,
+
track: item.trackName,
+
artist: item.artists[0]?.artistName,
+
error: "Image load error"
+
});
+
e.currentTarget.style.display = "none";
+
}}
+
/>
+
) : (
+
<div style={styles.artworkPlaceholder}>
+
<svg
+
width="64"
+
height="64"
+
viewBox="0 0 24 24"
+
fill="none"
+
stroke="currentColor"
+
strokeWidth="1.5"
+
>
+
<circle cx="12" cy="12" r="10" />
+
<circle cx="12" cy="12" r="3" />
+
<path d="M12 2v3M12 19v3M2 12h3M19 12h3" />
+
</svg>
+
</div>
+
)}
+
</div>
+
+
{/* Content */}
+
<div style={styles.content}>
+
<div style={styles.label}>{label}</div>
+
<h2 style={styles.trackName}>{item.trackName}</h2>
+
<div style={styles.artistName}>{artistNames}</div>
+
{item.releaseName && (
+
<div style={styles.releaseName}>from {item.releaseName}</div>
+
)}
+
+
{/* Listen Button */}
+
{availablePlatforms.length > 0 ? (
+
<button
+
onClick={() => setShowPlatformModal(true)}
+
style={styles.listenButton}
+
data-teal-listen-button="true"
+
>
+
<span>Listen with your Streaming Client</span>
+
<svg
+
width="16"
+
height="16"
+
viewBox="0 0 24 24"
+
fill="none"
+
stroke="currentColor"
+
strokeWidth="2"
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
>
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
+
<polyline points="15 3 21 3 21 9" />
+
<line x1="10" y1="14" x2="21" y2="3" />
+
</svg>
+
</button>
+
) : item.originUrl ? (
+
<a
+
href={item.originUrl}
+
target="_blank"
+
rel="noopener noreferrer"
+
style={styles.listenButton}
+
data-teal-listen-button="true"
+
>
+
<span>Listen on Last.fm</span>
+
<svg
+
width="16"
+
height="16"
+
viewBox="0 0 24 24"
+
fill="none"
+
stroke="currentColor"
+
strokeWidth="2"
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
>
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
+
<polyline points="15 3 21 3 21 9" />
+
<line x1="10" y1="14" x2="21" y2="3" />
+
</svg>
+
</a>
+
) : null}
+
</div>
+
</div>
+
+
{/* Platform Selection Modal */}
+
{showPlatformModal && songlinkData && (
+
<div style={styles.modalOverlay} onClick={() => setShowPlatformModal(false)}>
+
<div
+
role="dialog"
+
aria-modal="true"
+
aria-labelledby="platform-modal-title"
+
style={styles.modalContent}
+
onClick={(e) => e.stopPropagation()}
+
>
+
<div style={styles.modalHeader}>
+
<h3 id="platform-modal-title" style={styles.modalTitle}>Choose your streaming service</h3>
+
<button
+
style={styles.closeButton}
+
onClick={() => setShowPlatformModal(false)}
+
data-teal-close="true"
+
>
+
ร—
+
</button>
+
</div>
+
<div style={styles.platformList}>
+
{availablePlatforms.map((platform) => {
+
const config = platformConfig[platform];
+
const link = songlinkData.linksByPlatform[platform];
+
return (
+
<a
+
key={platform}
+
href={link.url}
+
target="_blank"
+
rel="noopener noreferrer"
+
style={{
+
...styles.platformItem,
+
borderLeft: `4px solid ${config.color}`,
+
}}
+
onClick={() => setShowPlatformModal(false)}
+
data-teal-platform="true"
+
>
+
<span
+
style={styles.platformIcon}
+
dangerouslySetInnerHTML={{ __html: config.svg }}
+
/>
+
<span style={styles.platformName}>{config.name}</span>
+
<svg
+
width="20"
+
height="20"
+
viewBox="0 0 24 24"
+
fill="none"
+
stroke="currentColor"
+
strokeWidth="2"
+
style={styles.platformArrow}
+
>
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
+
<polyline points="15 3 21 3 21 9" />
+
<line x1="10" y1="14" x2="21" y2="3" />
+
</svg>
+
</a>
+
);
+
})}
+
</div>
+
</div>
+
</div>
+
)}
+
</>
+
);
+
};
+
+
const styles: Record<string, React.CSSProperties> = {
+
container: {
+
fontFamily: "system-ui, -apple-system, sans-serif",
+
display: "flex",
+
flexDirection: "column",
+
background: "var(--atproto-color-bg)",
+
borderRadius: 16,
+
overflow: "hidden",
+
maxWidth: 420,
+
color: "var(--atproto-color-text)",
+
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.4)",
+
border: "1px solid var(--atproto-color-border)",
+
},
+
artworkContainer: {
+
width: "100%",
+
aspectRatio: "1 / 1",
+
position: "relative",
+
overflow: "hidden",
+
},
+
artwork: {
+
width: "100%",
+
height: "100%",
+
objectFit: "cover",
+
display: "block",
+
},
+
artworkPlaceholder: {
+
width: "100%",
+
height: "100%",
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
+
color: "rgba(255, 255, 255, 0.5)",
+
},
+
loadingSpinner: {
+
width: 40,
+
height: 40,
+
border: "3px solid var(--atproto-color-border)",
+
borderTop: "3px solid var(--atproto-color-primary)",
+
borderRadius: "50%",
+
animation: "spin 1s linear infinite",
+
},
+
content: {
+
padding: "24px",
+
display: "flex",
+
flexDirection: "column",
+
gap: "8px",
+
},
+
label: {
+
fontSize: 11,
+
fontWeight: 600,
+
letterSpacing: "0.1em",
+
textTransform: "uppercase",
+
color: "var(--atproto-color-text-secondary)",
+
marginBottom: "4px",
+
},
+
trackName: {
+
fontSize: 28,
+
fontWeight: 700,
+
margin: 0,
+
lineHeight: 1.2,
+
color: "var(--atproto-color-text)",
+
},
+
artistName: {
+
fontSize: 16,
+
color: "var(--atproto-color-text-secondary)",
+
marginTop: "4px",
+
},
+
releaseName: {
+
fontSize: 14,
+
color: "var(--atproto-color-text-secondary)",
+
marginTop: "2px",
+
},
+
listenButton: {
+
display: "inline-flex",
+
alignItems: "center",
+
gap: "8px",
+
marginTop: "16px",
+
padding: "12px 20px",
+
background: "var(--atproto-color-bg-elevated)",
+
border: "1px solid var(--atproto-color-border)",
+
borderRadius: 24,
+
color: "var(--atproto-color-text)",
+
fontSize: 14,
+
fontWeight: 600,
+
textDecoration: "none",
+
cursor: "pointer",
+
transition: "all 0.2s ease",
+
alignSelf: "flex-start",
+
},
+
modalOverlay: {
+
position: "fixed",
+
top: 0,
+
left: 0,
+
right: 0,
+
bottom: 0,
+
backgroundColor: "rgba(0, 0, 0, 0.85)",
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
zIndex: 9999,
+
backdropFilter: "blur(4px)",
+
},
+
modalContent: {
+
background: "var(--atproto-color-bg)",
+
borderRadius: 16,
+
padding: 0,
+
maxWidth: 450,
+
width: "90%",
+
maxHeight: "80vh",
+
overflow: "auto",
+
boxShadow: "0 20px 60px rgba(0, 0, 0, 0.8)",
+
border: "1px solid var(--atproto-color-border)",
+
},
+
modalHeader: {
+
display: "flex",
+
justifyContent: "space-between",
+
alignItems: "center",
+
padding: "24px 24px 16px 24px",
+
borderBottom: "1px solid var(--atproto-color-border)",
+
},
+
modalTitle: {
+
margin: 0,
+
fontSize: 20,
+
fontWeight: 700,
+
color: "var(--atproto-color-text)",
+
},
+
closeButton: {
+
background: "transparent",
+
border: "none",
+
color: "var(--atproto-color-text-secondary)",
+
fontSize: 32,
+
cursor: "pointer",
+
padding: 0,
+
width: 32,
+
height: 32,
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
borderRadius: "50%",
+
transition: "all 0.2s ease",
+
lineHeight: 1,
+
},
+
platformList: {
+
padding: "16px",
+
display: "flex",
+
flexDirection: "column",
+
gap: "8px",
+
},
+
platformItem: {
+
display: "flex",
+
alignItems: "center",
+
gap: "16px",
+
padding: "16px",
+
background: "var(--atproto-color-bg-hover)",
+
borderRadius: 12,
+
textDecoration: "none",
+
color: "var(--atproto-color-text)",
+
transition: "all 0.2s ease",
+
cursor: "pointer",
+
border: "1px solid var(--atproto-color-border)",
+
},
+
platformIcon: {
+
fontSize: 24,
+
width: 32,
+
height: 32,
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
},
+
platformName: {
+
flex: 1,
+
fontSize: 16,
+
fontWeight: 600,
+
},
+
platformArrow: {
+
opacity: 0.5,
+
transition: "opacity 0.2s ease",
+
},
+
notListeningContainer: {
+
fontFamily: "system-ui, -apple-system, sans-serif",
+
display: "flex",
+
flexDirection: "column",
+
alignItems: "center",
+
justifyContent: "center",
+
background: "var(--atproto-color-bg)",
+
borderRadius: 16,
+
padding: "80px 40px",
+
maxWidth: 420,
+
color: "var(--atproto-color-text-secondary)",
+
border: "1px solid var(--atproto-color-border)",
+
textAlign: "center",
+
},
+
notListeningIcon: {
+
width: 120,
+
height: 120,
+
borderRadius: "50%",
+
background: "var(--atproto-color-bg-elevated)",
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
marginBottom: 24,
+
color: "var(--atproto-color-text-muted)",
+
},
+
notListeningTitle: {
+
fontSize: 18,
+
fontWeight: 600,
+
color: "var(--atproto-color-text)",
+
marginBottom: 8,
+
},
+
notListeningSubtitle: {
+
fontSize: 14,
+
color: "var(--atproto-color-text-secondary)",
+
},
+
};
+
+
// Add keyframes and hover styles
+
if (typeof document !== "undefined") {
+
const styleId = "teal-status-styles";
+
if (!document.getElementById(styleId)) {
+
const styleElement = document.createElement("style");
+
styleElement.id = styleId;
+
styleElement.textContent = `
+
@keyframes spin {
+
0% { transform: rotate(0deg); }
+
100% { transform: rotate(360deg); }
+
}
+
+
button[data-teal-listen-button]:hover:not(:disabled),
+
a[data-teal-listen-button]:hover {
+
background: var(--atproto-color-bg-pressed) !important;
+
border-color: var(--atproto-color-border-hover) !important;
+
transform: translateY(-2px);
+
}
+
+
button[data-teal-listen-button]:disabled {
+
opacity: 0.5;
+
cursor: not-allowed;
+
}
+
+
button[data-teal-close]:hover {
+
background: var(--atproto-color-bg-hover) !important;
+
color: var(--atproto-color-text) !important;
+
}
+
+
a[data-teal-platform]:hover {
+
background: var(--atproto-color-bg-pressed) !important;
+
transform: translateX(4px);
+
}
+
+
a[data-teal-platform]:hover svg {
+
opacity: 1 !important;
+
}
+
`;
+
document.head.appendChild(styleElement);
+
}
+
}
+
+
export default CurrentlyPlayingRenderer;
+971
lib/renderers/GrainGalleryRenderer.tsx
···
+
import React from "react";
+
import type { GrainGalleryRecord, GrainPhotoRecord } from "../types/grain";
+
import { useBlob } from "../hooks/useBlob";
+
import { isBlobWithCdn, extractCidFromBlob } from "../utils/blob";
+
+
export interface GrainGalleryPhoto {
+
record: GrainPhotoRecord;
+
did: string;
+
rkey: string;
+
position?: number;
+
}
+
+
export interface GrainGalleryRendererProps {
+
gallery: GrainGalleryRecord;
+
photos: GrainGalleryPhoto[];
+
loading: boolean;
+
error?: Error;
+
authorHandle?: string;
+
authorDisplayName?: string;
+
avatarUrl?: string;
+
}
+
+
export const GrainGalleryRenderer: React.FC<GrainGalleryRendererProps> = ({
+
gallery,
+
photos,
+
loading,
+
error,
+
authorDisplayName,
+
authorHandle,
+
avatarUrl,
+
}) => {
+
const [currentPage, setCurrentPage] = React.useState(0);
+
const [lightboxOpen, setLightboxOpen] = React.useState(false);
+
const [lightboxPhotoIndex, setLightboxPhotoIndex] = React.useState(0);
+
+
const createdDate = new Date(gallery.createdAt);
+
const created = createdDate.toLocaleString(undefined, {
+
dateStyle: "medium",
+
timeStyle: "short",
+
});
+
+
const primaryName = authorDisplayName || authorHandle || "โ€ฆ";
+
+
// Memoize sorted photos to prevent re-sorting on every render
+
const sortedPhotos = React.useMemo(
+
() => [...photos].sort((a, b) => (a.position ?? 0) - (b.position ?? 0)),
+
[photos]
+
);
+
+
// Open lightbox
+
const openLightbox = React.useCallback((photoIndex: number) => {
+
setLightboxPhotoIndex(photoIndex);
+
setLightboxOpen(true);
+
}, []);
+
+
// Close lightbox
+
const closeLightbox = React.useCallback(() => {
+
setLightboxOpen(false);
+
}, []);
+
+
// Navigate lightbox
+
const goToNextPhoto = React.useCallback(() => {
+
setLightboxPhotoIndex((prev) => (prev + 1) % sortedPhotos.length);
+
}, [sortedPhotos.length]);
+
+
const goToPrevPhoto = React.useCallback(() => {
+
setLightboxPhotoIndex((prev) => (prev - 1 + sortedPhotos.length) % sortedPhotos.length);
+
}, [sortedPhotos.length]);
+
+
// Keyboard navigation
+
React.useEffect(() => {
+
if (!lightboxOpen) return;
+
+
const handleKeyDown = (e: KeyboardEvent) => {
+
if (e.key === "Escape") closeLightbox();
+
if (e.key === "ArrowLeft") goToPrevPhoto();
+
if (e.key === "ArrowRight") goToNextPhoto();
+
};
+
+
window.addEventListener("keydown", handleKeyDown);
+
return () => window.removeEventListener("keydown", handleKeyDown);
+
}, [lightboxOpen, closeLightbox, goToPrevPhoto, goToNextPhoto]);
+
+
const isSinglePhoto = sortedPhotos.length === 1;
+
+
// Preload all photos to avoid loading states when paginating
+
usePreloadAllPhotos(sortedPhotos);
+
+
// Reset to first page when photos change
+
React.useEffect(() => {
+
setCurrentPage(0);
+
}, [sortedPhotos.length]);
+
+
// Memoize pagination calculations with intelligent photo count per page
+
const paginationData = React.useMemo(() => {
+
const pages = calculatePages(sortedPhotos);
+
const totalPages = pages.length;
+
const visiblePhotos = pages[currentPage] || [];
+
const hasMultiplePages = totalPages > 1;
+
const layoutPhotos = calculateLayout(visiblePhotos);
+
+
return {
+
pages,
+
totalPages,
+
visiblePhotos,
+
hasMultiplePages,
+
layoutPhotos,
+
};
+
}, [sortedPhotos, currentPage]);
+
+
const { totalPages, hasMultiplePages, layoutPhotos } = paginationData;
+
+
// Memoize navigation handlers to prevent re-creation
+
const goToNextPage = React.useCallback(() => {
+
setCurrentPage((prev) => (prev + 1) % totalPages);
+
}, [totalPages]);
+
+
const goToPrevPage = React.useCallback(() => {
+
setCurrentPage((prev) => (prev - 1 + totalPages) % totalPages);
+
}, [totalPages]);
+
+
if (error) {
+
return (
+
<div role="alert" style={{ padding: 8, color: "crimson" }}>
+
Failed to load gallery.
+
</div>
+
);
+
}
+
+
if (loading && photos.length === 0) {
+
return <div role="status" aria-live="polite" style={{ padding: 8 }}>Loading galleryโ€ฆ</div>;
+
}
+
+
return (
+
<>
+
{/* Hidden preload elements for all photos */}
+
<div style={{ display: "none" }} aria-hidden>
+
{sortedPhotos.map((photo) => (
+
<PreloadPhoto key={`${photo.did}-${photo.rkey}-preload`} photo={photo} />
+
))}
+
</div>
+
+
{/* Lightbox */}
+
{lightboxOpen && (
+
<Lightbox
+
photo={sortedPhotos[lightboxPhotoIndex]}
+
photoIndex={lightboxPhotoIndex}
+
totalPhotos={sortedPhotos.length}
+
onClose={closeLightbox}
+
onNext={goToNextPhoto}
+
onPrev={goToPrevPhoto}
+
/>
+
)}
+
+
<article style={styles.card}>
+
<header style={styles.header}>
+
{avatarUrl ? (
+
<img src={avatarUrl} alt={`${authorDisplayName || authorHandle || 'User'}'s profile picture`} style={styles.avatarImg} />
+
) : (
+
<div style={styles.avatarPlaceholder} aria-hidden />
+
)}
+
<div style={styles.authorInfo}>
+
<strong style={styles.displayName}>{primaryName}</strong>
+
{authorHandle && (
+
<span
+
style={{
+
...styles.handle,
+
color: `var(--atproto-color-text-secondary)`,
+
}}
+
>
+
@{authorHandle}
+
</span>
+
)}
+
</div>
+
</header>
+
+
<div style={styles.galleryInfo}>
+
<h2
+
style={{
+
...styles.title,
+
color: `var(--atproto-color-text)`,
+
}}
+
>
+
{gallery.title}
+
</h2>
+
{gallery.description && (
+
<p
+
style={{
+
...styles.description,
+
color: `var(--atproto-color-text-secondary)`,
+
}}
+
>
+
{gallery.description}
+
</p>
+
)}
+
</div>
+
+
{isSinglePhoto ? (
+
<div style={styles.singlePhotoContainer}>
+
<GalleryPhotoItem
+
key={`${sortedPhotos[0].did}-${sortedPhotos[0].rkey}`}
+
photo={sortedPhotos[0]}
+
isSingle={true}
+
onClick={() => openLightbox(0)}
+
/>
+
</div>
+
) : (
+
<div style={styles.carouselContainer}>
+
{hasMultiplePages && currentPage > 0 && (
+
<button
+
onClick={goToPrevPage}
+
onMouseEnter={(e) => (e.currentTarget.style.opacity = "1")}
+
onMouseLeave={(e) => (e.currentTarget.style.opacity = "0.7")}
+
style={{
+
...styles.navButton,
+
...styles.navButtonLeft,
+
color: "white",
+
background: "rgba(0, 0, 0, 0.5)",
+
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
+
}}
+
aria-label="Previous photos"
+
>
+
โ€น
+
</button>
+
)}
+
<div style={styles.photosGrid}>
+
{layoutPhotos.map((item) => {
+
const photoIndex = sortedPhotos.findIndex(p => p.did === item.did && p.rkey === item.rkey);
+
return (
+
<GalleryPhotoItem
+
key={`${item.did}-${item.rkey}`}
+
photo={item}
+
isSingle={false}
+
span={item.span}
+
onClick={() => openLightbox(photoIndex)}
+
/>
+
);
+
})}
+
</div>
+
{hasMultiplePages && currentPage < totalPages - 1 && (
+
<button
+
onClick={goToNextPage}
+
onMouseEnter={(e) => (e.currentTarget.style.opacity = "1")}
+
onMouseLeave={(e) => (e.currentTarget.style.opacity = "0.7")}
+
style={{
+
...styles.navButton,
+
...styles.navButtonRight,
+
color: "white",
+
background: "rgba(0, 0, 0, 0.5)",
+
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
+
}}
+
aria-label="Next photos"
+
>
+
โ€บ
+
</button>
+
)}
+
</div>
+
)}
+
+
<footer style={styles.footer}>
+
<time
+
style={{
+
...styles.time,
+
color: `var(--atproto-color-text-muted)`,
+
}}
+
dateTime={gallery.createdAt}
+
>
+
{created}
+
</time>
+
{hasMultiplePages && !isSinglePhoto && (
+
<div style={styles.paginationDots}>
+
{Array.from({ length: totalPages }, (_, i) => (
+
<button
+
key={i}
+
onClick={() => setCurrentPage(i)}
+
style={{
+
...styles.paginationDot,
+
background: i === currentPage
+
? `var(--atproto-color-text)`
+
: `var(--atproto-color-border)`,
+
}}
+
aria-label={`Go to page ${i + 1}`}
+
aria-current={i === currentPage ? "page" : undefined}
+
/>
+
))}
+
</div>
+
)}
+
</footer>
+
</article>
+
</>
+
);
+
};
+
+
// Component to preload a single photo's blob
+
const PreloadPhoto: React.FC<{ photo: GrainGalleryPhoto }> = ({ photo }) => {
+
const photoBlob = photo.record.photo;
+
const cdnUrl = isBlobWithCdn(photoBlob) ? photoBlob.cdnUrl : undefined;
+
const cid = cdnUrl ? undefined : extractCidFromBlob(photoBlob);
+
+
// Trigger blob loading via the hook
+
useBlob(photo.did, cid);
+
+
// Preload CDN images via Image element
+
React.useEffect(() => {
+
if (cdnUrl) {
+
const img = new Image();
+
img.src = cdnUrl;
+
}
+
}, [cdnUrl]);
+
+
return null;
+
};
+
+
// Hook to preload all photos (CDN-based)
+
const usePreloadAllPhotos = (photos: GrainGalleryPhoto[]) => {
+
React.useEffect(() => {
+
// Preload CDN images
+
photos.forEach((photo) => {
+
const photoBlob = photo.record.photo;
+
const cdnUrl = isBlobWithCdn(photoBlob) ? photoBlob.cdnUrl : undefined;
+
+
if (cdnUrl) {
+
const img = new Image();
+
img.src = cdnUrl;
+
}
+
});
+
}, [photos]);
+
};
+
+
// Calculate pages with intelligent photo count (1, 2, or 3)
+
// Only includes multiple photos when they fit well together
+
const calculatePages = (photos: GrainGalleryPhoto[]): GrainGalleryPhoto[][] => {
+
if (photos.length === 0) return [];
+
if (photos.length === 1) return [[photos[0]]];
+
+
const pages: GrainGalleryPhoto[][] = [];
+
let i = 0;
+
+
while (i < photos.length) {
+
const remaining = photos.length - i;
+
+
// Only one photo left - use it
+
if (remaining === 1) {
+
pages.push([photos[i]]);
+
break;
+
}
+
+
// Check if next 3 photos can fit well together
+
if (remaining >= 3) {
+
const nextThree = photos.slice(i, i + 3);
+
if (canFitThreePhotos(nextThree)) {
+
pages.push(nextThree);
+
i += 3;
+
continue;
+
}
+
}
+
+
// Check if next 2 photos can fit well together
+
if (remaining >= 2) {
+
const nextTwo = photos.slice(i, i + 2);
+
if (canFitTwoPhotos(nextTwo)) {
+
pages.push(nextTwo);
+
i += 2;
+
continue;
+
}
+
}
+
+
// Photos don't fit well together, use 1 per page
+
pages.push([photos[i]]);
+
i += 1;
+
}
+
+
return pages;
+
};
+
+
// Helper functions for aspect ratio classification
+
const isPortrait = (ratio: number) => ratio < 0.8;
+
const isLandscape = (ratio: number) => ratio > 1.2;
+
const isSquarish = (ratio: number) => ratio >= 0.8 && ratio <= 1.2;
+
+
// Determine if 2 photos can fit well together side by side
+
const canFitTwoPhotos = (photos: GrainGalleryPhoto[]): boolean => {
+
if (photos.length !== 2) return false;
+
+
const ratios = photos.map((p) => {
+
const ar = p.record.aspectRatio;
+
return ar ? ar.width / ar.height : 1;
+
});
+
+
const [r1, r2] = ratios;
+
+
// Two portraits side by side don't work well (too narrow)
+
if (isPortrait(r1) && isPortrait(r2)) return false;
+
+
// Portrait + landscape/square creates awkward layout
+
if (isPortrait(r1) && !isPortrait(r2)) return false;
+
if (!isPortrait(r1) && isPortrait(r2)) return false;
+
+
// Two landscape or two squarish photos work well
+
if ((isLandscape(r1) || isSquarish(r1)) && (isLandscape(r2) || isSquarish(r2))) {
+
return true;
+
}
+
+
// Default to not fitting
+
return false;
+
};
+
+
// Determine if 3 photos can fit well together in a layout
+
const canFitThreePhotos = (photos: GrainGalleryPhoto[]): boolean => {
+
if (photos.length !== 3) return false;
+
+
const ratios = photos.map((p) => {
+
const ar = p.record.aspectRatio;
+
return ar ? ar.width / ar.height : 1;
+
});
+
+
const [r1, r2, r3] = ratios;
+
+
// Good pattern: one portrait, two landscape/square
+
if (isPortrait(r1) && !isPortrait(r2) && !isPortrait(r3)) return true;
+
if (isPortrait(r3) && !isPortrait(r1) && !isPortrait(r2)) return true;
+
+
// Good pattern: all similar aspect ratios (all landscape or all squarish)
+
const allLandscape = ratios.every(isLandscape);
+
const allSquarish = ratios.every(isSquarish);
+
if (allLandscape || allSquarish) return true;
+
+
// Three portraits in a row can work
+
const allPortrait = ratios.every(isPortrait);
+
if (allPortrait) return true;
+
+
// Otherwise don't fit 3 together
+
return false;
+
};
+
+
// Layout calculator for intelligent photo grid arrangement
+
const calculateLayout = (photos: GrainGalleryPhoto[]) => {
+
if (photos.length === 0) return [];
+
if (photos.length === 1) {
+
return [{ ...photos[0], span: { row: 2, col: 2 } }];
+
}
+
+
const photosWithRatios = photos.map((photo) => {
+
const ratio = photo.record.aspectRatio
+
? photo.record.aspectRatio.width / photo.record.aspectRatio.height
+
: 1;
+
return {
+
...photo,
+
ratio,
+
isPortrait: isPortrait(ratio),
+
isLandscape: isLandscape(ratio)
+
};
+
});
+
+
// For 2 photos: side by side
+
if (photos.length === 2) {
+
return photosWithRatios.map((p) => ({ ...p, span: { row: 2, col: 1 } }));
+
}
+
+
// For 3 photos: try to create a balanced layout
+
if (photos.length === 3) {
+
const [p1, p2, p3] = photosWithRatios;
+
+
// Pattern 1: One tall on left, two stacked on right
+
if (p1.isPortrait && !p2.isPortrait && !p3.isPortrait) {
+
return [
+
{ ...p1, span: { row: 2, col: 1 } },
+
{ ...p2, span: { row: 1, col: 1 } },
+
{ ...p3, span: { row: 1, col: 1 } },
+
];
+
}
+
+
// Pattern 2: Two stacked on left, one tall on right
+
if (!p1.isPortrait && !p2.isPortrait && p3.isPortrait) {
+
return [
+
{ ...p1, span: { row: 1, col: 1 } },
+
{ ...p2, span: { row: 1, col: 1 } },
+
{ ...p3, span: { row: 2, col: 1 } },
+
];
+
}
+
+
// Pattern 3: All in a row
+
const allPortrait = photosWithRatios.every((p) => p.isPortrait);
+
if (allPortrait) {
+
// All portraits: display in a row with smaller cells
+
return photosWithRatios.map((p) => ({ ...p, span: { row: 1, col: 1 } }));
+
}
+
+
// Default: All three in a row
+
return photosWithRatios.map((p) => ({ ...p, span: { row: 1, col: 1 } }));
+
}
+
+
return photosWithRatios.map((p) => ({ ...p, span: { row: 1, col: 1 } }));
+
};
+
+
// Lightbox component for fullscreen image viewing
+
const Lightbox: React.FC<{
+
photo: GrainGalleryPhoto;
+
photoIndex: number;
+
totalPhotos: number;
+
onClose: () => void;
+
onNext: () => void;
+
onPrev: () => void;
+
}> = ({ photo, photoIndex, totalPhotos, onClose, onNext, onPrev }) => {
+
const photoBlob = photo.record.photo;
+
const cdnUrl = isBlobWithCdn(photoBlob) ? photoBlob.cdnUrl : undefined;
+
const cid = cdnUrl ? undefined : extractCidFromBlob(photoBlob);
+
const { url: urlFromBlob, loading: photoLoading, error: photoError } = useBlob(photo.did, cid);
+
const url = cdnUrl || urlFromBlob;
+
const alt = photo.record.alt?.trim() || "grain.social photo";
+
+
return (
+
<div
+
role="dialog"
+
aria-modal="true"
+
aria-label={`Photo ${photoIndex + 1} of ${totalPhotos}`}
+
style={{
+
position: "fixed",
+
top: 0,
+
left: 0,
+
right: 0,
+
bottom: 0,
+
background: "rgba(0, 0, 0, 0.95)",
+
zIndex: 9999,
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
padding: 20,
+
}}
+
onClick={onClose}
+
>
+
{/* Close button */}
+
<button
+
onClick={onClose}
+
style={{
+
position: "absolute",
+
top: 20,
+
right: 20,
+
width: 40,
+
height: 40,
+
border: "none",
+
borderRadius: "50%",
+
background: "rgba(255, 255, 255, 0.1)",
+
color: "white",
+
fontSize: 24,
+
cursor: "pointer",
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
transition: "background 200ms ease",
+
}}
+
onMouseEnter={(e) => (e.currentTarget.style.background = "rgba(255, 255, 255, 0.2)")}
+
onMouseLeave={(e) => (e.currentTarget.style.background = "rgba(255, 255, 255, 0.1)")}
+
aria-label="Close lightbox"
+
>
+
ร—
+
</button>
+
+
{/* Previous button */}
+
{totalPhotos > 1 && (
+
<button
+
onClick={(e) => {
+
e.stopPropagation();
+
onPrev();
+
}}
+
style={{
+
position: "absolute",
+
left: 20,
+
top: "50%",
+
transform: "translateY(-50%)",
+
width: 50,
+
height: 50,
+
border: "none",
+
borderRadius: "50%",
+
background: "rgba(255, 255, 255, 0.1)",
+
color: "white",
+
fontSize: 24,
+
cursor: "pointer",
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
transition: "background 200ms ease",
+
}}
+
onMouseEnter={(e) => (e.currentTarget.style.background = "rgba(255, 255, 255, 0.2)")}
+
onMouseLeave={(e) => (e.currentTarget.style.background = "rgba(255, 255, 255, 0.1)")}
+
aria-label={`Previous photo (${photoIndex} of ${totalPhotos})`}
+
>
+
โ€น
+
</button>
+
)}
+
+
{/* Next button */}
+
{totalPhotos > 1 && (
+
<button
+
onClick={(e) => {
+
e.stopPropagation();
+
onNext();
+
}}
+
style={{
+
position: "absolute",
+
right: 20,
+
top: "50%",
+
transform: "translateY(-50%)",
+
width: 50,
+
height: 50,
+
border: "none",
+
borderRadius: "50%",
+
background: "rgba(255, 255, 255, 0.1)",
+
color: "white",
+
fontSize: 24,
+
cursor: "pointer",
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
transition: "background 200ms ease",
+
}}
+
onMouseEnter={(e) => (e.currentTarget.style.background = "rgba(255, 255, 255, 0.2)")}
+
onMouseLeave={(e) => (e.currentTarget.style.background = "rgba(255, 255, 255, 0.1)")}
+
aria-label={`Next photo (${photoIndex + 2} of ${totalPhotos})`}
+
>
+
โ€บ
+
</button>
+
)}
+
+
{/* Image */}
+
<div
+
style={{
+
maxWidth: "90vw",
+
maxHeight: "90vh",
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
}}
+
onClick={(e) => e.stopPropagation()}
+
>
+
{url ? (
+
<img
+
src={url}
+
alt={alt}
+
style={{
+
maxWidth: "100%",
+
maxHeight: "100%",
+
objectFit: "contain",
+
borderRadius: 8,
+
}}
+
/>
+
) : (
+
<div
+
style={{
+
color: "white",
+
fontSize: 16,
+
textAlign: "center",
+
}}
+
>
+
{photoLoading ? "Loadingโ€ฆ" : photoError ? "Failed to load" : "Unavailable"}
+
</div>
+
)}
+
</div>
+
+
{/* Photo counter */}
+
{totalPhotos > 1 && (
+
<div
+
style={{
+
position: "absolute",
+
bottom: 20,
+
left: "50%",
+
transform: "translateX(-50%)",
+
color: "white",
+
fontSize: 14,
+
background: "rgba(0, 0, 0, 0.5)",
+
padding: "8px 16px",
+
borderRadius: 20,
+
}}
+
>
+
{photoIndex + 1} / {totalPhotos}
+
</div>
+
)}
+
</div>
+
);
+
};
+
+
const GalleryPhotoItem: React.FC<{
+
photo: GrainGalleryPhoto;
+
isSingle: boolean;
+
span?: { row: number; col: number };
+
onClick?: () => void;
+
}> = ({ photo, isSingle, span, onClick }) => {
+
const [showAltText, setShowAltText] = React.useState(false);
+
const photoBlob = photo.record.photo;
+
const cdnUrl = isBlobWithCdn(photoBlob) ? photoBlob.cdnUrl : undefined;
+
const cid = cdnUrl ? undefined : extractCidFromBlob(photoBlob);
+
const { url: urlFromBlob, loading: photoLoading, error: photoError } = useBlob(photo.did, cid);
+
const url = cdnUrl || urlFromBlob;
+
const alt = photo.record.alt?.trim() || "grain.social photo";
+
const hasAlt = photo.record.alt && photo.record.alt.trim().length > 0;
+
+
const aspect =
+
photo.record.aspectRatio && photo.record.aspectRatio.height > 0
+
? `${photo.record.aspectRatio.width} / ${photo.record.aspectRatio.height}`
+
: undefined;
+
+
const gridItemStyle = span
+
? {
+
gridRow: `span ${span.row}`,
+
gridColumn: `span ${span.col}`,
+
}
+
: {};
+
+
return (
+
<figure style={{ ...(isSingle ? styles.singlePhotoItem : styles.photoItem), ...gridItemStyle }}>
+
<button
+
onClick={onClick}
+
aria-label={hasAlt ? `View photo: ${alt}` : "View photo"}
+
style={{
+
...(isSingle ? styles.singlePhotoMedia : styles.photoContainer),
+
background: `var(--atproto-color-image-bg)`,
+
// Only apply aspect ratio for single photos; grid photos fill their cells
+
...(isSingle && aspect ? { aspectRatio: aspect } : {}),
+
cursor: onClick ? "pointer" : "default",
+
border: "none",
+
padding: 0,
+
display: "block",
+
width: "100%",
+
}}
+
>
+
{url ? (
+
<img src={url} alt={alt} style={isSingle ? styles.photo : styles.photoGrid} />
+
) : (
+
<div
+
style={{
+
...styles.placeholder,
+
color: `var(--atproto-color-text-muted)`,
+
}}
+
>
+
{photoLoading
+
? "Loadingโ€ฆ"
+
: photoError
+
? "Failed to load"
+
: "Unavailable"}
+
</div>
+
)}
+
{hasAlt && (
+
<button
+
onClick={(e) => {
+
e.stopPropagation();
+
setShowAltText(!showAltText);
+
}}
+
style={{
+
...styles.altBadge,
+
background: showAltText
+
? `var(--atproto-color-text)`
+
: `var(--atproto-color-bg-secondary)`,
+
color: showAltText
+
? `var(--atproto-color-bg)`
+
: `var(--atproto-color-text)`,
+
}}
+
title="Toggle alt text"
+
aria-label="Toggle alt text"
+
aria-pressed={showAltText}
+
>
+
ALT
+
</button>
+
)}
+
</button>
+
{hasAlt && showAltText && (
+
<figcaption
+
style={{
+
...styles.caption,
+
color: `var(--atproto-color-text-secondary)`,
+
}}
+
>
+
{photo.record.alt}
+
</figcaption>
+
)}
+
</figure>
+
);
+
};
+
+
const styles: Record<string, React.CSSProperties> = {
+
card: {
+
borderRadius: 12,
+
border: `1px solid var(--atproto-color-border)`,
+
background: `var(--atproto-color-bg)`,
+
color: `var(--atproto-color-text)`,
+
fontFamily: "system-ui, sans-serif",
+
display: "flex",
+
flexDirection: "column",
+
maxWidth: 600,
+
transition:
+
"background-color 180ms ease, border-color 180ms ease, color 180ms ease",
+
overflow: "hidden",
+
},
+
header: {
+
display: "flex",
+
alignItems: "center",
+
gap: 12,
+
padding: 12,
+
paddingBottom: 0,
+
},
+
avatarPlaceholder: {
+
width: 32,
+
height: 32,
+
borderRadius: "50%",
+
background: `var(--atproto-color-border)`,
+
},
+
avatarImg: {
+
width: 32,
+
height: 32,
+
borderRadius: "50%",
+
objectFit: "cover",
+
},
+
authorInfo: {
+
display: "flex",
+
flexDirection: "column",
+
gap: 2,
+
},
+
displayName: {
+
fontSize: 14,
+
fontWeight: 600,
+
},
+
handle: {
+
fontSize: 12,
+
},
+
galleryInfo: {
+
padding: 12,
+
paddingBottom: 8,
+
},
+
title: {
+
margin: 0,
+
fontSize: 18,
+
fontWeight: 600,
+
marginBottom: 4,
+
},
+
description: {
+
margin: 0,
+
fontSize: 14,
+
lineHeight: 1.4,
+
whiteSpace: "pre-wrap",
+
},
+
singlePhotoContainer: {
+
padding: 0,
+
},
+
carouselContainer: {
+
position: "relative",
+
padding: 4,
+
},
+
photosGrid: {
+
display: "grid",
+
gridTemplateColumns: "repeat(2, 1fr)",
+
gridTemplateRows: "repeat(2, 1fr)",
+
gap: 4,
+
minHeight: 400,
+
},
+
navButton: {
+
position: "absolute",
+
top: "50%",
+
transform: "translateY(-50%)",
+
width: 28,
+
height: 28,
+
border: "none",
+
borderRadius: "50%",
+
fontSize: 18,
+
fontWeight: "600",
+
cursor: "pointer",
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
zIndex: 10,
+
transition: "opacity 150ms ease",
+
userSelect: "none",
+
opacity: 0.7,
+
},
+
navButtonLeft: {
+
left: 8,
+
},
+
navButtonRight: {
+
right: 8,
+
},
+
photoItem: {
+
margin: 0,
+
display: "flex",
+
flexDirection: "column",
+
gap: 4,
+
},
+
singlePhotoItem: {
+
margin: 0,
+
display: "flex",
+
flexDirection: "column",
+
gap: 8,
+
},
+
photoContainer: {
+
position: "relative",
+
width: "100%",
+
height: "100%",
+
overflow: "hidden",
+
borderRadius: 4,
+
},
+
singlePhotoMedia: {
+
position: "relative",
+
width: "100%",
+
overflow: "hidden",
+
borderRadius: 0,
+
},
+
photo: {
+
width: "100%",
+
height: "100%",
+
objectFit: "cover",
+
display: "block",
+
},
+
photoGrid: {
+
width: "100%",
+
height: "100%",
+
objectFit: "cover",
+
display: "block",
+
},
+
placeholder: {
+
display: "flex",
+
alignItems: "center",
+
justifyContent: "center",
+
width: "100%",
+
height: "100%",
+
minHeight: 100,
+
fontSize: 12,
+
},
+
caption: {
+
fontSize: 12,
+
lineHeight: 1.3,
+
padding: "0 12px 8px",
+
},
+
altBadge: {
+
position: "absolute",
+
bottom: 8,
+
right: 8,
+
padding: "4px 8px",
+
fontSize: 10,
+
fontWeight: 600,
+
letterSpacing: "0.5px",
+
border: "none",
+
borderRadius: 4,
+
cursor: "pointer",
+
transition: "background 150ms ease, color 150ms ease",
+
fontFamily: "system-ui, sans-serif",
+
},
+
footer: {
+
padding: 12,
+
paddingTop: 8,
+
display: "flex",
+
justifyContent: "space-between",
+
alignItems: "center",
+
},
+
time: {
+
fontSize: 11,
+
},
+
paginationDots: {
+
display: "flex",
+
gap: 6,
+
alignItems: "center",
+
},
+
paginationDot: {
+
width: 6,
+
height: 6,
+
borderRadius: "50%",
+
border: "none",
+
padding: 0,
+
cursor: "pointer",
+
transition: "background 200ms ease, transform 150ms ease",
+
flexShrink: 0,
+
},
+
};
+
+
export default GrainGalleryRenderer;
+5 -3
lib/renderers/LeafletDocumentRenderer.tsx
···
import React, { useMemo, useRef } from "react";
import { useDidResolution } from "../hooks/useDidResolution";
import { useBlob } from "../hooks/useBlob";
+
import { useAtProto } from "../providers/AtProtoProvider";
import {
parseAtUri,
formatDidForLabel,
···
publicationBaseUrl,
publicationRecord,
}) => {
+
const { blueskyAppBaseUrl } = useAtProto();
const authorDid = record.author?.startsWith("did:")
? record.author
: undefined;
···
: undefined);
const authorLabel = resolvedPublicationLabel ?? fallbackAuthorLabel;
const authorHref = publicationUri
-
? `https://bsky.app/profile/${publicationUri.did}`
+
? `${blueskyAppBaseUrl}/profile/${publicationUri.did}`
: undefined;
if (error)
···
timeStyle: "short",
})
: undefined;
-
const fallbackLeafletUrl = `https://bsky.app/leaflet/${encodeURIComponent(did)}/${encodeURIComponent(rkey)}`;
+
const fallbackLeafletUrl = `${blueskyAppBaseUrl}/leaflet/${encodeURIComponent(did)}/${encodeURIComponent(rkey)}`;
const publicationRoot =
publicationBaseUrl ?? publicationRecord?.base_path ?? undefined;
const resolvedPublicationRoot = publicationRoot
···
publicationLeafletUrl ??
postUrl ??
(publicationUri
-
? `https://bsky.app/profile/${publicationUri.did}`
+
? `${blueskyAppBaseUrl}/profile/${publicationUri.did}`
: undefined) ??
fallbackLeafletUrl;
+330
lib/renderers/TangledRepoRenderer.tsx
···
+
import React from "react";
+
import type { TangledRepoRecord } from "../types/tangled";
+
import { useAtProto } from "../providers/AtProtoProvider";
+
import { useBacklinks } from "../hooks/useBacklinks";
+
import { useRepoLanguages } from "../hooks/useRepoLanguages";
+
+
export interface TangledRepoRendererProps {
+
record: TangledRepoRecord;
+
error?: Error;
+
loading: boolean;
+
did: string;
+
rkey: string;
+
canonicalUrl?: string;
+
showStarCount?: boolean;
+
branch?: string;
+
languages?: string[];
+
}
+
+
export const TangledRepoRenderer: React.FC<TangledRepoRendererProps> = ({
+
record,
+
error,
+
loading,
+
did,
+
rkey,
+
canonicalUrl,
+
showStarCount = true,
+
branch,
+
languages,
+
}) => {
+
const { tangledBaseUrl, constellationBaseUrl } = useAtProto();
+
+
// Construct the AT-URI for this repo record
+
const atUri = `at://${did}/sh.tangled.repo/${rkey}`;
+
+
// Fetch star backlinks
+
const {
+
count: starCount,
+
loading: starsLoading,
+
error: starsError,
+
} = useBacklinks({
+
subject: atUri,
+
source: "sh.tangled.feed.star:subject",
+
limit: 100,
+
constellationBaseUrl,
+
enabled: showStarCount,
+
});
+
+
// Extract knot server from record.knot (e.g., "knot.gaze.systems")
+
const knotUrl = record?.knot
+
? record.knot.startsWith("http://") || record.knot.startsWith("https://")
+
? new URL(record.knot).hostname
+
: record.knot
+
: undefined;
+
+
// Fetch language data from knot server only if languages not provided
+
const {
+
data: languagesData,
+
loading: _languagesLoading,
+
error: _languagesError,
+
} = useRepoLanguages({
+
knot: knotUrl,
+
did,
+
repoName: record?.name,
+
branch,
+
enabled: !languages && !!knotUrl && !!record?.name,
+
});
+
+
// Convert provided language names to the format expected by the renderer
+
const providedLanguagesData = languages
+
? {
+
languages: languages.map((name) => ({
+
name,
+
percentage: 0,
+
size: 0,
+
})),
+
ref: branch || "main",
+
totalFiles: 0,
+
totalSize: 0,
+
}
+
: undefined;
+
+
// Use provided languages or fetched languages
+
const finalLanguagesData = providedLanguagesData ?? languagesData;
+
+
if (error)
+
return (
+
<div role="alert" style={{ padding: 8, color: "crimson" }}>
+
Failed to load repository.
+
</div>
+
);
+
if (loading && !record) return <div role="status" aria-live="polite" style={{ padding: 8 }}>Loadingโ€ฆ</div>;
+
+
// Construct the canonical URL: tangled.org/[did]/[repo-name]
+
const viewUrl =
+
canonicalUrl ??
+
`${tangledBaseUrl}/${did}/${encodeURIComponent(record.name)}`;
+
+
const tangledIcon = (
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 25 25" style={{ display: "block" }}>
+
<path fill="currentColor" d="m 16.208435,23.914069 c -0.06147,-0.02273 -0.147027,-0.03034 -0.190158,-0.01691 -0.197279,0.06145 -1.31068,-0.230493 -1.388819,-0.364153 -0.01956,-0.03344 -0.163274,-0.134049 -0.319377,-0.223561 -0.550395,-0.315603 -1.010951,-0.696643 -1.428383,-1.181771 -0.264598,-0.307509 -0.597257,-0.785384 -0.597257,-0.857979 0,-0.0216 -0.02841,-0.06243 -0.06313,-0.0907 -0.04977,-0.04053 -0.160873,0.0436 -0.52488,0.397463 -0.479803,0.466432 -0.78924,0.689475 -1.355603,0.977118 -0.183693,0.0933 -0.323426,0.179989 -0.310516,0.192658 0.02801,0.02748 -0.7656391,0.270031 -1.209129,0.369517 -0.5378332,0.120647 -1.6341809,0.08626 -1.9721503,-0.06186 C 6.7977157,23.031391 6.56735,22.957551 6.3371134,22.889782 4.9717169,22.487902 3.7511914,21.481518 3.1172396,20.234838 2.6890391,19.392772 2.5582276,18.827446 2.5610489,17.831154 2.5639589,16.802192 2.7366641,16.125844 3.2142117,15.273187 3.3040457,15.112788 3.3713143,14.976533 3.3636956,14.9704 3.3560756,14.9643 3.2459634,14.90305 3.1189994,14.834381 1.7582586,14.098312 0.77760984,12.777439 0.44909837,11.23818 0.33531456,10.705039 0.33670119,9.7067968 0.45195381,9.1778795 0.72259241,7.9359287 1.3827188,6.8888436 2.4297498,6.0407205 2.6856126,5.8334648 3.2975489,5.4910878 3.6885849,5.3364049 L 4.0584319,5.190106 4.2333984,4.860432 C 4.8393906,3.7186139 5.8908314,2.7968028 7.1056396,2.3423025 7.7690673,2.0940921 8.2290216,2.0150935 9.01853,2.0137575 c 0.9625627,-0.00163 1.629181,0.1532762 2.485864,0.5776514 l 0.271744,0.1346134 0.42911,-0.3607688 c 1.082666,-0.9102346 2.185531,-1.3136811 3.578383,-1.3090327 0.916696,0.00306 1.573918,0.1517893 2.356121,0.5331927 1.465948,0.7148 2.54506,2.0625628 2.865177,3.57848 l 0.07653,0.362429 0.515095,0.2556611 c 1.022872,0.5076874 1.756122,1.1690944 2.288361,2.0641468 0.401896,0.6758594 0.537303,1.0442682 0.675505,1.8378683 0.288575,1.6570823 -0.266229,3.3548023 -1.490464,4.5608743 -0.371074,0.36557 -0.840205,0.718265 -1.203442,0.904754 -0.144112,0.07398 -0.271303,0.15826 -0.282647,0.187269 -0.01134,0.02901 0.02121,0.142764 0.07234,0.25279 0.184248,0.396467 0.451371,1.331823 0.619371,2.168779 0.463493,2.30908 -0.754646,4.693707 -2.92278,5.721632 -0.479538,0.227352 -0.717629,0.309322 -1.144194,0.39393 -0.321869,0.06383 -1.850573,0.09139 -2.000174,0.03604 z M 12.25443,18.636956 c 0.739923,-0.24652 1.382521,-0.718922 1.874623,-1.37812 0.0752,-0.100718 0.213883,-0.275851 0.308198,-0.389167 0.09432,-0.113318 0.210136,-0.271056 0.257381,-0.350531 0.416347,-0.700389 0.680936,-1.176102 0.766454,-1.378041 0.05594,-0.132087 0.114653,-0.239607 0.130477,-0.238929 0.01583,6.79e-4 0.08126,0.08531 0.145412,0.188069 0.178029,0.285173 0.614305,0.658998 0.868158,0.743878 0.259802,0.08686 0.656158,0.09598 0.911369,0.02095 0.213812,-0.06285 0.507296,-0.298016 0.645179,-0.516947 0.155165,-0.246374 0.327989,-0.989595 0.327989,-1.410501 0,-1.26718 -0.610975,-3.143405 -1.237774,-3.801045 -0.198483,-0.2082486 -0.208557,-0.2319396 -0.208557,-0.4904655 0,-0.2517771 -0.08774,-0.5704927 -0.258476,-0.938956 C 16.694963,8.50313 16.375697,8.1377479 16.135846,7.9543702 L 15.932296,7.7987471 15.683004,7.9356529 C 15.131767,8.2383821 14.435638,8.1945733 13.943459,7.8261812 L 13.782862,7.7059758 13.686773,7.8908012 C 13.338849,8.5600578 12.487087,8.8811064 11.743178,8.6233891 11.487199,8.5347109 11.358897,8.4505994 11.063189,8.1776138 L 10.69871,7.8411436 10.453484,8.0579255 C 10.318608,8.1771557 10.113778,8.3156283 9.9983037,8.3656417 9.7041488,8.4930449 9.1808299,8.5227884 8.8979004,8.4281886 8.7754792,8.3872574 8.6687415,8.3537661 8.6607053,8.3537661 c -0.03426,0 -0.3092864,0.3066098 -0.3791974,0.42275 -0.041935,0.069664 -0.1040482,0.1266636 -0.1380294,0.1266636 -0.1316419,0 -0.4197402,0.1843928 -0.6257041,0.4004735 -0.1923125,0.2017571 -0.6853701,0.9036038 -0.8926582,1.2706578 -0.042662,0.07554 -0.1803555,0.353687 -0.3059848,0.618091 -0.1256293,0.264406 -0.3270073,0.686768 -0.4475067,0.938581 -0.1204992,0.251816 -0.2469926,0.519654 -0.2810961,0.595199 -0.2592829,0.574347 -0.285919,1.391094 -0.057822,1.77304 0.1690683,0.283105 0.4224039,0.480895 0.7285507,0.568809 0.487122,0.139885 0.9109638,-0.004 1.6013422,-0.543768 l 0.4560939,-0.356568 0.0036,0.172041 c 0.01635,0.781837 0.1831084,1.813183 0.4016641,2.484154 0.1160449,0.356262 0.3781448,0.83968 0.5614081,1.035462 0.2171883,0.232025 0.7140951,0.577268 1.0100284,0.701749 0.121485,0.0511 0.351032,0.110795 0.510105,0.132647 0.396966,0.05452 1.2105,0.02265 1.448934,-0.05679 z"/>
+
</svg>
+
);
+
+
return (
+
<div
+
style={{
+
...base.container,
+
background: `var(--atproto-color-bg)`,
+
borderWidth: "1px",
+
borderStyle: "solid",
+
borderColor: `var(--atproto-color-border)`,
+
color: `var(--atproto-color-text)`,
+
}}
+
>
+
{/* Header with title and icons */}
+
<div
+
style={{
+
...base.header,
+
background: `var(--atproto-color-bg)`,
+
}}
+
>
+
<div style={base.headerTop}>
+
<strong
+
style={{
+
...base.repoName,
+
color: `var(--atproto-color-text)`,
+
}}
+
>
+
{record.name}
+
</strong>
+
<div style={base.headerRight}>
+
<a
+
href={viewUrl}
+
target="_blank"
+
rel="noopener noreferrer"
+
style={{
+
...base.iconLink,
+
color: `var(--atproto-color-text)`,
+
}}
+
title="View on Tangled"
+
>
+
{tangledIcon}
+
</a>
+
{record.source && (
+
<a
+
href={record.source}
+
target="_blank"
+
rel="noopener noreferrer"
+
style={{
+
...base.iconLink,
+
color: `var(--atproto-color-text)`,
+
}}
+
title="View source repository"
+
>
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 16 16" fill="currentColor" style={{ display: "block" }}>
+
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
+
</svg>
+
</a>
+
)}
+
</div>
+
</div>
+
</div>
+
+
{/* Description */}
+
{record.description && (
+
<div
+
style={{
+
...base.description,
+
background: `var(--atproto-color-bg)`,
+
color: `var(--atproto-color-text-secondary)`,
+
}}
+
>
+
{record.description}
+
</div>
+
)}
+
+
{/* Languages and Stars */}
+
<div
+
style={{
+
...base.languageSection,
+
background: `var(--atproto-color-bg)`,
+
}}
+
>
+
{/* Languages */}
+
{finalLanguagesData && finalLanguagesData.languages.length > 0 && (() => {
+
const topLanguages = finalLanguagesData.languages
+
.filter((lang) => lang.name && (lang.percentage > 0 || finalLanguagesData.languages.every(l => l.percentage === 0)))
+
.sort((a, b) => b.percentage - a.percentage)
+
.slice(0, 2);
+
return topLanguages.length > 0 ? (
+
<div style={base.languageTags}>
+
{topLanguages.map((lang) => (
+
<span key={lang.name} style={base.languageTag}>
+
{lang.name}
+
</span>
+
))}
+
</div>
+
) : null;
+
})()}
+
+
{/* Right side: Stars and View on Tangled link */}
+
<div style={base.rightSection}>
+
{/* Stars */}
+
{showStarCount && (
+
<div
+
style={{
+
...base.starCountContainer,
+
color: `var(--atproto-color-text-secondary)`,
+
}}
+
>
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style={{ display: "block" }}>
+
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"/>
+
</svg>
+
{starsLoading ? (
+
<span style={base.starCount}>...</span>
+
) : starsError ? (
+
<span style={base.starCount}>โ€”</span>
+
) : (
+
<span style={base.starCount}>{starCount}</span>
+
)}
+
</div>
+
)}
+
+
{/* View on Tangled link */}
+
<a
+
href={viewUrl}
+
target="_blank"
+
rel="noopener noreferrer"
+
style={{
+
...base.viewLink,
+
color: `var(--atproto-color-link)`,
+
}}
+
>
+
View on Tangled
+
</a>
+
</div>
+
</div>
+
</div>
+
);
+
};
+
+
const base: Record<string, React.CSSProperties> = {
+
container: {
+
fontFamily: "system-ui, sans-serif",
+
borderRadius: 6,
+
overflow: "hidden",
+
transition:
+
"background-color 180ms ease, border-color 180ms ease, color 180ms ease, box-shadow 180ms ease",
+
width: "100%",
+
},
+
header: {
+
padding: "16px",
+
display: "flex",
+
flexDirection: "column",
+
},
+
headerTop: {
+
display: "flex",
+
justifyContent: "space-between",
+
alignItems: "flex-start",
+
gap: 12,
+
},
+
headerRight: {
+
display: "flex",
+
alignItems: "center",
+
gap: 8,
+
},
+
repoName: {
+
fontFamily:
+
'SFMono-Regular, ui-monospace, Menlo, Monaco, "Courier New", monospace',
+
fontSize: 18,
+
fontWeight: 600,
+
wordBreak: "break-word",
+
margin: 0,
+
},
+
iconLink: {
+
display: "flex",
+
alignItems: "center",
+
textDecoration: "none",
+
opacity: 0.7,
+
transition: "opacity 150ms ease",
+
},
+
description: {
+
padding: "0 16px 16px 16px",
+
fontSize: 14,
+
lineHeight: 1.5,
+
},
+
languageSection: {
+
padding: "0 16px 16px 16px",
+
display: "flex",
+
justifyContent: "space-between",
+
alignItems: "center",
+
gap: 12,
+
flexWrap: "wrap",
+
},
+
languageTags: {
+
display: "flex",
+
gap: 8,
+
flexWrap: "wrap",
+
},
+
languageTag: {
+
fontSize: 12,
+
fontWeight: 500,
+
padding: "4px 10px",
+
background: `var(--atproto-color-bg)`,
+
borderRadius: 12,
+
border: "1px solid var(--atproto-color-border)",
+
},
+
rightSection: {
+
display: "flex",
+
alignItems: "center",
+
gap: 12,
+
},
+
starCountContainer: {
+
display: "flex",
+
alignItems: "center",
+
gap: 4,
+
fontSize: 13,
+
},
+
starCount: {
+
fontSize: 13,
+
fontWeight: 500,
+
},
+
viewLink: {
+
fontSize: 13,
+
fontWeight: 500,
+
textDecoration: "none",
+
},
+
};
+
+
export default TangledRepoRenderer;
+4 -4
lib/renderers/TangledStringRenderer.tsx
···
import React from "react";
-
import type { ShTangledString } from "@atcute/tangled";
-
-
export type TangledStringRecord = ShTangledString.Main;
+
import { useAtProto } from "../providers/AtProtoProvider";
+
import type { TangledStringRecord } from "../types/tangled";
export interface TangledStringRendererProps {
record: TangledStringRecord;
···
rkey,
canonicalUrl,
}) => {
+
const { tangledBaseUrl } = useAtProto();
if (error)
return (
···
const viewUrl =
canonicalUrl ??
-
`https://tangled.org/strings/${did}/${encodeURIComponent(rkey)}`;
+
`${tangledBaseUrl}/strings/${did}/${encodeURIComponent(rkey)}`;
const timestamp = new Date(record.createdAt).toLocaleString(undefined, {
dateStyle: "medium",
timeStyle: "short",
+59 -47
lib/styles.css
···
:root {
/* Light theme colors (default) */
-
--atproto-color-bg: #ffffff;
-
--atproto-color-bg-elevated: #f8fafc;
-
--atproto-color-bg-secondary: #f1f5f9;
+
--atproto-color-bg: #f5f7f9;
+
--atproto-color-bg-elevated: #f8f9fb;
+
--atproto-color-bg-secondary: #edf1f5;
--atproto-color-text: #0f172a;
--atproto-color-text-secondary: #475569;
--atproto-color-text-muted: #64748b;
-
--atproto-color-border: #e2e8f0;
-
--atproto-color-border-subtle: #cbd5e1;
+
--atproto-color-border: #d6dce3;
+
--atproto-color-border-subtle: #c1cad4;
+
--atproto-color-border-hover: #94a3b8;
--atproto-color-link: #2563eb;
--atproto-color-link-hover: #1d4ed8;
--atproto-color-error: #dc2626;
-
--atproto-color-button-bg: #f1f5f9;
-
--atproto-color-button-hover: #e2e8f0;
+
--atproto-color-primary: #2563eb;
+
--atproto-color-button-bg: #edf1f5;
+
--atproto-color-button-hover: #e3e9ef;
--atproto-color-button-text: #0f172a;
-
--atproto-color-code-bg: #f1f5f9;
-
--atproto-color-code-border: #e2e8f0;
-
--atproto-color-blockquote-border: #cbd5e1;
-
--atproto-color-blockquote-bg: #f8fafc;
-
--atproto-color-hr: #e2e8f0;
-
--atproto-color-image-bg: #f1f5f9;
+
--atproto-color-bg-hover: #f0f3f6;
+
--atproto-color-bg-pressed: #e3e9ef;
+
--atproto-color-code-bg: #edf1f5;
+
--atproto-color-code-border: #d6dce3;
+
--atproto-color-blockquote-border: #c1cad4;
+
--atproto-color-blockquote-bg: #f0f3f6;
+
--atproto-color-hr: #d6dce3;
+
--atproto-color-image-bg: #edf1f5;
--atproto-color-highlight: #fef08a;
}
/* Dark theme - can be applied via [data-theme="dark"] or .dark class */
[data-theme="dark"],
.dark {
-
--atproto-color-bg: #0f172a;
-
--atproto-color-bg-elevated: #1e293b;
-
--atproto-color-bg-secondary: #0b1120;
-
--atproto-color-text: #e2e8f0;
-
--atproto-color-text-secondary: #94a3b8;
-
--atproto-color-text-muted: #64748b;
-
--atproto-color-border: #1e293b;
-
--atproto-color-border-subtle: #334155;
+
--atproto-color-bg: #141b22;
+
--atproto-color-bg-elevated: #1a222a;
+
--atproto-color-bg-secondary: #0f161c;
+
--atproto-color-text: #fafafa;
+
--atproto-color-text-secondary: #a1a1aa;
+
--atproto-color-text-muted: #71717a;
+
--atproto-color-border: #1f2933;
+
--atproto-color-border-subtle: #2d3748;
+
--atproto-color-border-hover: #4a5568;
--atproto-color-link: #60a5fa;
--atproto-color-link-hover: #93c5fd;
--atproto-color-error: #ef4444;
-
--atproto-color-button-bg: #1e293b;
-
--atproto-color-button-hover: #334155;
-
--atproto-color-button-text: #e2e8f0;
-
--atproto-color-code-bg: #0b1120;
-
--atproto-color-code-border: #1e293b;
-
--atproto-color-blockquote-border: #334155;
-
--atproto-color-blockquote-bg: #1e293b;
-
--atproto-color-hr: #334155;
-
--atproto-color-image-bg: #1e293b;
+
--atproto-color-primary: #3b82f6;
+
--atproto-color-button-bg: #1a222a;
+
--atproto-color-button-hover: #243039;
+
--atproto-color-button-text: #fafafa;
+
--atproto-color-bg-hover: #1a222a;
+
--atproto-color-bg-pressed: #243039;
+
--atproto-color-code-bg: #0f161c;
+
--atproto-color-code-border: #1f2933;
+
--atproto-color-blockquote-border: #2d3748;
+
--atproto-color-blockquote-bg: #1a222a;
+
--atproto-color-hr: #243039;
+
--atproto-color-image-bg: #1a222a;
--atproto-color-highlight: #854d0e;
}
···
@media (prefers-color-scheme: dark) {
:root:not([data-theme]),
:root[data-theme="system"] {
-
--atproto-color-bg: #0f172a;
-
--atproto-color-bg-elevated: #1e293b;
-
--atproto-color-bg-secondary: #0b1120;
-
--atproto-color-text: #e2e8f0;
-
--atproto-color-text-secondary: #94a3b8;
-
--atproto-color-text-muted: #64748b;
-
--atproto-color-border: #1e293b;
-
--atproto-color-border-subtle: #334155;
+
--atproto-color-bg: #141b22;
+
--atproto-color-bg-elevated: #1a222a;
+
--atproto-color-bg-secondary: #0f161c;
+
--atproto-color-text: #fafafa;
+
--atproto-color-text-secondary: #a1a1aa;
+
--atproto-color-text-muted: #71717a;
+
--atproto-color-border: #1f2933;
+
--atproto-color-border-subtle: #2d3748;
+
--atproto-color-border-hover: #4a5568;
--atproto-color-link: #60a5fa;
--atproto-color-link-hover: #93c5fd;
--atproto-color-error: #ef4444;
-
--atproto-color-button-bg: #1e293b;
-
--atproto-color-button-hover: #334155;
-
--atproto-color-button-text: #e2e8f0;
-
--atproto-color-code-bg: #0b1120;
-
--atproto-color-code-border: #1e293b;
-
--atproto-color-blockquote-border: #334155;
-
--atproto-color-blockquote-bg: #1e293b;
-
--atproto-color-hr: #334155;
-
--atproto-color-image-bg: #1e293b;
+
--atproto-color-primary: #3b82f6;
+
--atproto-color-button-bg: #1a222a;
+
--atproto-color-button-hover: #243039;
+
--atproto-color-button-text: #fafafa;
+
--atproto-color-bg-hover: #1a222a;
+
--atproto-color-bg-pressed: #243039;
+
--atproto-color-code-bg: #0f161c;
+
--atproto-color-code-border: #1f2933;
+
--atproto-color-blockquote-border: #2d3748;
+
--atproto-color-blockquote-bg: #1a222a;
+
--atproto-color-hr: #243039;
+
--atproto-color-image-bg: #1a222a;
--atproto-color-highlight: #854d0e;
}
}
+95
lib/types/grain.ts
···
+
/**
+
* Type definitions for grain.social records
+
* Uses standard atcute blob types for compatibility
+
*/
+
import type { Blob } from "@atcute/lexicons/interfaces";
+
import type { BlobWithCdn } from "../hooks/useBlueskyAppview";
+
+
/**
+
* grain.social gallery record
+
* A container for a collection of photos
+
*/
+
export interface GrainGalleryRecord {
+
/**
+
* Record type identifier
+
*/
+
$type: "social.grain.gallery";
+
/**
+
* Gallery title
+
*/
+
title: string;
+
/**
+
* Gallery description
+
*/
+
description?: string;
+
/**
+
* Self-label values (content warnings)
+
*/
+
labels?: {
+
$type: "com.atproto.label.defs#selfLabels";
+
values: Array<{ val: string }>;
+
};
+
/**
+
* Timestamp when the gallery was created
+
*/
+
createdAt: string;
+
}
+
+
/**
+
* grain.social gallery item record
+
* Links a photo to a gallery
+
*/
+
export interface GrainGalleryItemRecord {
+
/**
+
* Record type identifier
+
*/
+
$type: "social.grain.gallery.item";
+
/**
+
* AT URI of the photo (social.grain.photo)
+
*/
+
item: string;
+
/**
+
* AT URI of the gallery this item belongs to
+
*/
+
gallery: string;
+
/**
+
* Position/order within the gallery
+
*/
+
position?: number;
+
/**
+
* Timestamp when the item was added to the gallery
+
*/
+
createdAt: string;
+
}
+
+
/**
+
* grain.social photo record
+
* Compatible with records from @atcute clients
+
*/
+
export interface GrainPhotoRecord {
+
/**
+
* Record type identifier
+
*/
+
$type: "social.grain.photo";
+
/**
+
* Alt text description of the image (required for accessibility)
+
*/
+
alt: string;
+
/**
+
* Photo blob reference - uses standard AT Proto blob format
+
* Supports any image/* mime type
+
* May include cdnUrl when fetched from appview
+
*/
+
photo: Blob<`image/${string}`> | BlobWithCdn;
+
/**
+
* Timestamp when the photo was created
+
*/
+
createdAt?: string;
+
/**
+
* Aspect ratio of the photo
+
*/
+
aspectRatio?: {
+
width: number;
+
height: number;
+
};
+
}
+22
lib/types/tangled.ts
···
+
import type { ShTangledRepo, ShTangledString } from "@atcute/tangled";
+
+
export type TangledRepoRecord = ShTangledRepo.Main;
+
export type TangledStringRecord = ShTangledString.Main;
+
+
/** Language information from sh.tangled.repo.languages endpoint */
+
export interface RepoLanguage {
+
name: string;
+
percentage: number;
+
size: number;
+
}
+
+
/**
+
* Response from sh.tangled.repo.languages endpoint from tangled knot
+
*/
+
export interface RepoLanguagesResponse {
+
languages: RepoLanguage[];
+
/** Branch name */
+
ref: string;
+
totalFiles: number;
+
totalSize: number;
+
}
+40
lib/types/teal.ts
···
+
/**
+
* teal.fm record types for music listening history
+
* Specification: fm.teal.alpha.actor.status and fm.teal.alpha.feed.play
+
*/
+
+
export interface TealArtist {
+
artistName: string;
+
artistMbId?: string;
+
}
+
+
export interface TealPlayItem {
+
artists: TealArtist[];
+
originUrl?: string;
+
trackName: string;
+
playedTime: string;
+
releaseName?: string;
+
recordingMbId?: string;
+
releaseMbId?: string;
+
submissionClientAgent?: string;
+
musicServiceBaseDomain?: string;
+
isrc?: string;
+
duration?: number;
+
}
+
+
/**
+
* fm.teal.alpha.actor.status - The last played song
+
*/
+
export interface TealActorStatusRecord {
+
$type: "fm.teal.alpha.actor.status";
+
item: TealPlayItem;
+
time: string;
+
expiry?: string;
+
}
+
+
/**
+
* fm.teal.alpha.feed.play - A single play record
+
*/
+
export interface TealFeedPlayRecord extends TealPlayItem {
+
$type: "fm.teal.alpha.feed.play";
+
}
+37 -4
lib/utils/atproto-client.ts
···
export interface ServiceResolverOptions {
plcDirectory?: string;
identityService?: string;
+
slingshotBaseUrl?: string;
fetch?: typeof fetch;
}
const DEFAULT_PLC = "https://plc.directory";
const DEFAULT_IDENTITY_SERVICE = "https://public.api.bsky.app";
+
const DEFAULT_SLINGSHOT = "https://slingshot.microcosm.blue";
+
const DEFAULT_APPVIEW = "https://public.api.bsky.app";
+
const DEFAULT_BLUESKY_APP = "https://bsky.app";
+
const DEFAULT_TANGLED = "https://tangled.org";
+
const DEFAULT_CONSTELLATION = "https://constellation.microcosm.blue";
+
const ABSOLUTE_URL_RE = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
const SUPPORTED_DID_METHODS = ["plc", "web"] as const;
type SupportedDidMethod = (typeof SUPPORTED_DID_METHODS)[number];
type SupportedDid = Did<SupportedDidMethod>;
-
export const SLINGSHOT_BASE_URL = "https://slingshot.microcosm.blue";
+
/**
+
* Default configuration values for AT Protocol services.
+
* These can be overridden via AtProtoProvider props.
+
*/
+
export const DEFAULT_CONFIG = {
+
plcDirectory: DEFAULT_PLC,
+
identityService: DEFAULT_IDENTITY_SERVICE,
+
slingshotBaseUrl: DEFAULT_SLINGSHOT,
+
blueskyAppviewService: DEFAULT_APPVIEW,
+
blueskyAppBaseUrl: DEFAULT_BLUESKY_APP,
+
tangledBaseUrl: DEFAULT_TANGLED,
+
constellationBaseUrl: DEFAULT_CONSTELLATION,
+
} as const;
+
+
export const SLINGSHOT_BASE_URL = DEFAULT_SLINGSHOT;
export const normalizeBaseUrl = (input: string): string => {
const trimmed = input.trim();
···
export class ServiceResolver {
private plc: string;
+
private slingshot: string;
private didResolver: CompositeDidDocumentResolver<SupportedDidMethod>;
private handleResolver: XrpcHandleResolver;
private fetchImpl: typeof fetch;
···
opts.identityService && opts.identityService.trim()
? opts.identityService
: DEFAULT_IDENTITY_SERVICE;
+
const slingshotSource =
+
opts.slingshotBaseUrl && opts.slingshotBaseUrl.trim()
+
? opts.slingshotBaseUrl
+
: DEFAULT_SLINGSHOT;
this.plc = normalizeBaseUrl(plcSource);
const identityBase = normalizeBaseUrl(identitySource);
+
this.slingshot = normalizeBaseUrl(slingshotSource);
this.fetchImpl = bindFetch(opts.fetch);
const plcResolver = new PlcDidDocumentResolver({
apiUrl: this.plc,
···
return svc.serviceEndpoint.replace(/\/$/, "");
}
+
getSlingshotUrl(): string {
+
return this.slingshot;
+
}
+
async resolveHandle(handle: string): Promise<string> {
const normalized = handle.trim().toLowerCase();
if (!normalized) throw new Error("Handle cannot be empty");
···
try {
const url = new URL(
"/xrpc/com.atproto.identity.resolveHandle",
-
SLINGSHOT_BASE_URL,
+
this.slingshot,
);
url.searchParams.set("handle", normalized);
const response = await this.fetchImpl(url);
···
}
if (!service) throw new Error("service or did required");
const normalizedService = normalizeBaseUrl(service);
-
const handler = createSlingshotAwareHandler(normalizedService, fetchImpl);
+
const slingshotUrl = resolver.getSlingshotUrl();
+
const handler = createSlingshotAwareHandler(normalizedService, slingshotUrl, fetchImpl);
const rpc = new Client({ handler });
return { rpc, service: normalizedService, resolver };
}
···
function createSlingshotAwareHandler(
service: string,
+
slingshotBaseUrl: string,
fetchImpl: typeof fetch,
): FetchHandler {
const primary = simpleFetchHandler({ service, fetch: fetchImpl });
const slingshot = simpleFetchHandler({
-
service: SLINGSHOT_BASE_URL,
+
service: slingshotBaseUrl,
fetch: fetchImpl,
});
return async (pathname, init) => {
+120
lib/utils/cache.ts
···
}
}
}
+
+
interface RecordCacheEntry<T = unknown> {
+
record: T;
+
timestamp: number;
+
}
+
+
interface InFlightRecordEntry<T = unknown> {
+
promise: Promise<T>;
+
abort: () => void;
+
refCount: number;
+
}
+
+
interface RecordEnsureResult<T = unknown> {
+
promise: Promise<T>;
+
release: () => void;
+
}
+
+
export class RecordCache {
+
private store = new Map<string, RecordCacheEntry>();
+
private inFlight = new Map<string, InFlightRecordEntry>();
+
// Collections that should not be cached (e.g., status records that change frequently)
+
private noCacheCollections = new Set<string>([
+
"fm.teal.alpha.actor.status",
+
"fm.teal.alpha.feed.play",
+
]);
+
+
private key(did: string, collection: string, rkey: string): string {
+
return `${did}::${collection}::${rkey}`;
+
}
+
+
private shouldCache(collection: string): boolean {
+
return !this.noCacheCollections.has(collection);
+
}
+
+
get<T = unknown>(
+
did?: string,
+
collection?: string,
+
rkey?: string,
+
): T | undefined {
+
if (!did || !collection || !rkey) return undefined;
+
// Don't return cached data for non-cacheable collections
+
if (!this.shouldCache(collection)) return undefined;
+
return this.store.get(this.key(did, collection, rkey))?.record as
+
| T
+
| undefined;
+
}
+
+
set<T = unknown>(
+
did: string,
+
collection: string,
+
rkey: string,
+
record: T,
+
): void {
+
// Don't cache records for non-cacheable collections
+
if (!this.shouldCache(collection)) return;
+
this.store.set(this.key(did, collection, rkey), {
+
record,
+
timestamp: Date.now(),
+
});
+
}
+
+
ensure<T = unknown>(
+
did: string,
+
collection: string,
+
rkey: string,
+
loader: () => { promise: Promise<T>; abort: () => void },
+
): RecordEnsureResult<T> {
+
const cached = this.get<T>(did, collection, rkey);
+
if (cached !== undefined) {
+
return { promise: Promise.resolve(cached), release: () => {} };
+
}
+
+
const key = this.key(did, collection, rkey);
+
const existing = this.inFlight.get(key) as
+
| InFlightRecordEntry<T>
+
| undefined;
+
if (existing) {
+
existing.refCount += 1;
+
return {
+
promise: existing.promise,
+
release: () => this.release(key),
+
};
+
}
+
+
const { promise, abort } = loader();
+
const wrapped = promise.then((record) => {
+
this.set(did, collection, rkey, record);
+
return record;
+
});
+
+
const entry: InFlightRecordEntry<T> = {
+
promise: wrapped,
+
abort,
+
refCount: 1,
+
};
+
+
this.inFlight.set(key, entry as InFlightRecordEntry);
+
+
wrapped
+
.catch(() => {})
+
.finally(() => {
+
this.inFlight.delete(key);
+
});
+
+
return {
+
promise: wrapped,
+
release: () => this.release(key),
+
};
+
}
+
+
private release(key: string) {
+
const entry = this.inFlight.get(key);
+
if (!entry) return;
+
entry.refCount -= 1;
+
if (entry.refCount <= 0) {
+
this.inFlight.delete(key);
+
entry.abort();
+
}
+
}
+
}
+120
lib/utils/richtext.ts
···
+
import type { AppBskyRichtextFacet } from "@atcute/bluesky";
+
+
export interface TextSegment {
+
text: string;
+
facet?: AppBskyRichtextFacet.Main;
+
}
+
+
/**
+
* Converts a text string with facets into segments that can be rendered
+
* with appropriate styling and interactivity.
+
*/
+
export function createTextSegments(
+
text: string,
+
facets?: AppBskyRichtextFacet.Main[],
+
): TextSegment[] {
+
if (!facets || facets.length === 0) {
+
return [{ text }];
+
}
+
+
// Build byte-to-char index mapping
+
const bytePrefix = buildBytePrefix(text);
+
+
// Sort facets by start position
+
const sortedFacets = [...facets].sort(
+
(a, b) => a.index.byteStart - b.index.byteStart,
+
);
+
+
const segments: TextSegment[] = [];
+
let currentPos = 0;
+
+
for (const facet of sortedFacets) {
+
const startChar = byteOffsetToCharIndex(bytePrefix, facet.index.byteStart);
+
const endChar = byteOffsetToCharIndex(bytePrefix, facet.index.byteEnd);
+
+
// Add plain text before this facet
+
if (startChar > currentPos) {
+
segments.push({
+
text: sliceByCharRange(text, currentPos, startChar),
+
});
+
}
+
+
// Add the faceted text
+
segments.push({
+
text: sliceByCharRange(text, startChar, endChar),
+
facet,
+
});
+
+
currentPos = endChar;
+
}
+
+
// Add remaining plain text
+
if (currentPos < text.length) {
+
segments.push({
+
text: sliceByCharRange(text, currentPos, text.length),
+
});
+
}
+
+
return segments;
+
}
+
+
/**
+
* Builds a byte offset prefix array for UTF-8 encoded text.
+
* This handles multi-byte characters correctly.
+
*/
+
function buildBytePrefix(text: string): number[] {
+
const encoder = new TextEncoder();
+
const prefix: number[] = [0];
+
let byteCount = 0;
+
+
for (let i = 0; i < text.length; ) {
+
const codePoint = text.codePointAt(i);
+
if (codePoint === undefined) break;
+
+
const char = String.fromCodePoint(codePoint);
+
const encoded = encoder.encode(char);
+
byteCount += encoded.length;
+
prefix.push(byteCount);
+
+
// Handle surrogate pairs (emojis, etc.)
+
i += codePoint > 0xffff ? 2 : 1;
+
}
+
+
return prefix;
+
}
+
+
/**
+
* Converts a byte offset to a character index using the byte prefix array.
+
*/
+
function byteOffsetToCharIndex(prefix: number[], byteOffset: number): number {
+
for (let i = 0; i < prefix.length; i++) {
+
if (prefix[i] === byteOffset) return i;
+
if (prefix[i] > byteOffset) return Math.max(0, i - 1);
+
}
+
return prefix.length - 1;
+
}
+
+
/**
+
* Slices text by character range, handling multi-byte characters correctly.
+
*/
+
function sliceByCharRange(text: string, start: number, end: number): string {
+
if (start <= 0 && end >= text.length) return text;
+
+
let result = "";
+
let charIndex = 0;
+
+
for (let i = 0; i < text.length && charIndex < end; ) {
+
const codePoint = text.codePointAt(i);
+
if (codePoint === undefined) break;
+
+
const char = String.fromCodePoint(codePoint);
+
if (charIndex >= start && charIndex < end) {
+
result += char;
+
}
+
+
i += codePoint > 0xffff ? 2 : 1;
+
charIndex++;
+
}
+
+
return result;
+
}
+1219 -1234
package-lock.json
···
{
"name": "atproto-ui",
-
"version": "0.5.2-beta",
+
"version": "0.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "atproto-ui",
-
"version": "0.5.2-beta",
+
"version": "0.12",
"dependencies": {
"@atcute/atproto": "^3.1.7",
"@atcute/bluesky": "^3.2.3",
"@atcute/client": "^4.0.3",
"@atcute/identity-resolver": "^1.1.3",
-
"@atcute/tangled": "^1.0.6"
+
"@atcute/tangled": "^1.0.10"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
···
}
},
"node_modules/@atcute/atproto": {
-
"version": "3.1.7",
-
"resolved": "https://registry.npmjs.org/@atcute/atproto/-/atproto-3.1.7.tgz",
-
"integrity": "sha512-3Ym8qaVZg2vf8qw0KO1aue39z/5oik5J+UDoSes1vr8ddw40UVLA5sV4bXSKmLnhzQHiLLgoVZXe4zaKfozPoQ==",
+
"version": "3.1.9",
"license": "0BSD",
"dependencies": {
"@atcute/lexicons": "^1.2.2"
}
},
"node_modules/@atcute/bluesky": {
-
"version": "3.2.3",
-
"resolved": "https://registry.npmjs.org/@atcute/bluesky/-/bluesky-3.2.3.tgz",
-
"integrity": "sha512-IdPQQ54F1BLhW5z49k81ZUC/GQl/tVygZ+CzLHYvQySHA6GJRcvPzwEf8aV21u0SZOJF+yF4CWEGNgtryyxPmg==",
+
"version": "3.2.11",
"license": "0BSD",
"dependencies": {
-
"@atcute/atproto": "^3.1.4",
-
"@atcute/lexicons": "^1.1.1"
+
"@atcute/atproto": "^3.1.9",
+
"@atcute/lexicons": "^1.2.5"
}
},
"node_modules/@atcute/client": {
-
"version": "4.0.3",
-
"resolved": "https://registry.npmjs.org/@atcute/client/-/client-4.0.3.tgz",
-
"integrity": "sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==",
-
"license": "MIT",
+
"version": "4.1.0",
+
"license": "0BSD",
"dependencies": {
-
"@atcute/identity": "^1.0.2",
-
"@atcute/lexicons": "^1.0.3"
+
"@atcute/identity": "^1.1.3",
+
"@atcute/lexicons": "^1.2.5"
}
},
"node_modules/@atcute/identity": {
-
"version": "1.1.0",
-
"resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-1.1.0.tgz",
-
"integrity": "sha512-6vRvRqJatDB+JUQsb+UswYmtBGQnSZcqC3a2y6H5DB/v5KcIh+6nFFtc17G0+3W9rxdk7k9M4KkgkdKf/YDNoQ==",
+
"version": "1.1.3",
"license": "0BSD",
+
"peer": true,
"dependencies": {
-
"@atcute/lexicons": "^1.1.1",
-
"@badrap/valita": "^0.4.5"
+
"@atcute/lexicons": "^1.2.4",
+
"@badrap/valita": "^0.4.6"
}
},
"node_modules/@atcute/identity-resolver": {
"version": "1.1.4",
-
"resolved": "https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-1.1.4.tgz",
-
"integrity": "sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA==",
"license": "0BSD",
"dependencies": {
"@atcute/lexicons": "^1.2.2",
···
}
},
"node_modules/@atcute/lexicons": {
-
"version": "1.2.2",
-
"resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.2.2.tgz",
-
"integrity": "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA==",
+
"version": "1.2.5",
"license": "0BSD",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
···
}
},
"node_modules/@atcute/tangled": {
-
"version": "1.0.6",
-
"resolved": "https://registry.npmjs.org/@atcute/tangled/-/tangled-1.0.6.tgz",
-
"integrity": "sha512-eEOtrKRbjKfeLYtb5hmkhE45w8h4sV6mT4E2CQzJmhOMGCiK31GX7Vqfh59rhNLb9AlbW72RcQTV737pxx+ksw==",
+
"version": "1.0.12",
"license": "0BSD",
"dependencies": {
-
"@atcute/atproto": "^3.1.4",
-
"@atcute/lexicons": "^1.1.1"
+
"@atcute/atproto": "^3.1.9",
+
"@atcute/lexicons": "^1.2.3"
}
},
"node_modules/@atcute/util-fetch": {
-
"version": "1.0.3",
-
"resolved": "https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-1.0.3.tgz",
-
"integrity": "sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ==",
+
"version": "1.0.4",
"license": "0BSD",
"dependencies": {
"@badrap/valita": "^0.4.6"
···
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
-
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
-
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
···
}
},
"node_modules/@babel/compat-data": {
-
"version": "7.28.4",
-
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
-
"integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
+
"version": "7.28.5",
"dev": true,
"license": "MIT",
"engines": {
···
}
},
"node_modules/@babel/core": {
-
"version": "7.28.4",
-
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
-
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
+
"version": "7.28.5",
"dev": true,
"license": "MIT",
+
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
-
"@babel/generator": "^7.28.3",
+
"@babel/generator": "^7.28.5",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
-
"@babel/parser": "^7.28.4",
+
"@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
-
"@babel/traverse": "^7.28.4",
-
"@babel/types": "^7.28.4",
+
"@babel/traverse": "^7.28.5",
+
"@babel/types": "^7.28.5",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
···
"url": "https://opencollective.com/babel"
}
},
+
"node_modules/@babel/core/node_modules/semver": {
+
"version": "6.3.1",
+
"dev": true,
+
"license": "ISC",
+
"bin": {
+
"semver": "bin/semver.js"
+
}
+
},
"node_modules/@babel/generator": {
-
"version": "7.28.3",
-
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
-
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
+
"version": "7.28.5",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@babel/parser": "^7.28.3",
-
"@babel/types": "^7.28.2",
+
"@babel/parser": "^7.28.5",
+
"@babel/types": "^7.28.5",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
···
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.27.2",
-
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
-
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"node": ">=6.9.0"
}
},
+
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+
"version": "5.1.1",
+
"dev": true,
+
"license": "ISC",
+
"dependencies": {
+
"yallist": "^3.0.2"
+
}
+
},
+
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache/node_modules/yallist": {
+
"version": "3.1.1",
+
"dev": true,
+
"license": "ISC"
+
},
+
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+
"version": "6.3.1",
+
"dev": true,
+
"license": "ISC",
+
"bin": {
+
"semver": "bin/semver.js"
+
}
+
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
-
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
-
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/@babel/helper-module-imports": {
"version": "7.27.1",
-
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
-
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.28.3",
-
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
-
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.27.1",
-
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
-
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
-
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
-
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
···
}
},
"node_modules/@babel/helper-validator-identifier": {
-
"version": "7.27.1",
-
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
-
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+
"version": "7.28.5",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/@babel/helper-validator-option": {
"version": "7.27.1",
-
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
-
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/@babel/helpers": {
"version": "7.28.4",
-
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
-
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"dev": true,
"license": "MIT",
"dependencies": {
···
}
},
"node_modules/@babel/parser": {
-
"version": "7.28.4",
-
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
-
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
+
"version": "7.28.5",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@babel/types": "^7.28.4"
+
"@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
···
},
"node_modules/@babel/plugin-transform-react-jsx-self": {
"version": "7.27.1",
-
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
-
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@babel/plugin-transform-react-jsx-source": {
"version": "7.27.1",
-
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
-
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@babel/template": {
"version": "7.27.2",
-
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
-
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
}
},
"node_modules/@babel/traverse": {
-
"version": "7.28.4",
-
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
-
"integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
+
"version": "7.28.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
-
"@babel/generator": "^7.28.3",
+
"@babel/generator": "^7.28.5",
"@babel/helper-globals": "^7.28.0",
-
"@babel/parser": "^7.28.4",
+
"@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
-
"@babel/types": "^7.28.4",
+
"@babel/types": "^7.28.5",
"debug": "^4.3.1"
},
"engines": {
···
}
},
"node_modules/@babel/types": {
-
"version": "7.28.4",
-
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
-
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
+
"version": "7.28.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
-
"@babel/helper-validator-identifier": "^7.27.1"
+
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
···
},
"node_modules/@badrap/valita": {
"version": "0.4.6",
-
"resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.4.6.tgz",
-
"integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==",
"license": "MIT",
"engines": {
"node": ">= 18"
}
},
+
"node_modules/@emnapi/core": {
+
"version": "1.7.1",
+
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
+
"integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"dependencies": {
+
"@emnapi/wasi-threads": "1.1.0",
+
"tslib": "^2.4.0"
+
}
+
},
+
"node_modules/@emnapi/runtime": {
+
"version": "1.7.1",
+
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
+
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"dependencies": {
+
"tslib": "^2.4.0"
+
}
+
},
+
"node_modules/@emnapi/wasi-threads": {
+
"version": "1.1.0",
+
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
+
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"dependencies": {
+
"tslib": "^2.4.0"
+
}
+
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
-
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
-
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
"version": "3.4.3",
-
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
-
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
···
}
},
"node_modules/@eslint-community/regexpp": {
-
"version": "4.12.1",
-
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
-
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+
"version": "4.12.2",
"dev": true,
"license": "MIT",
"engines": {
···
}
},
"node_modules/@eslint/config-array": {
-
"version": "0.21.0",
-
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
-
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
+
"version": "0.21.1",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
-
"@eslint/object-schema": "^2.1.6",
+
"@eslint/object-schema": "^2.1.7",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
},
···
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+
"node_modules/@eslint/config-array/node_modules/minimatch": {
+
"version": "3.1.2",
+
"dev": true,
+
"license": "ISC",
+
"dependencies": {
+
"brace-expansion": "^1.1.7"
+
},
+
"engines": {
+
"node": "*"
+
}
+
},
"node_modules/@eslint/config-helpers": {
-
"version": "0.4.0",
-
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
-
"integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
+
"version": "0.4.2",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
-
"@eslint/core": "^0.16.0"
+
"@eslint/core": "^0.17.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
-
"version": "0.16.0",
-
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
-
"integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
+
"version": "0.17.0",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
···
}
},
"node_modules/@eslint/eslintrc": {
-
"version": "3.3.1",
-
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
-
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+
"version": "3.3.3",
"dev": true,
"license": "MIT",
"dependencies": {
···
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
-
"js-yaml": "^4.1.0",
+
"js-yaml": "^4.1.1",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
···
"url": "https://opencollective.com/eslint"
}
},
+
"node_modules/@eslint/eslintrc/node_modules/ajv": {
+
"version": "6.12.6",
+
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+
"dev": true,
+
"license": "MIT",
+
"dependencies": {
+
"fast-deep-equal": "^3.1.1",
+
"fast-json-stable-stringify": "^2.0.0",
+
"json-schema-traverse": "^0.4.1",
+
"uri-js": "^4.2.2"
+
},
+
"funding": {
+
"type": "github",
+
"url": "https://github.com/sponsors/epoberezkin"
+
}
+
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "14.0.0",
-
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
-
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"license": "MIT",
"engines": {
···
"url": "https://github.com/sponsors/sindresorhus"
}
},
+
"node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
+
"version": "0.4.1",
+
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+
"dev": true,
+
"license": "MIT"
+
},
+
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
+
"version": "3.1.2",
+
"dev": true,
+
"license": "ISC",
+
"dependencies": {
+
"brace-expansion": "^1.1.7"
+
},
+
"engines": {
+
"node": "*"
+
}
+
},
"node_modules/@eslint/js": {
-
"version": "9.37.0",
-
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
-
"integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
+
"version": "9.39.1",
"dev": true,
"license": "MIT",
"engines": {
···
}
},
"node_modules/@eslint/object-schema": {
-
"version": "2.1.6",
-
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
-
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+
"version": "2.1.7",
"dev": true,
"license": "Apache-2.0",
"engines": {
···
}
},
"node_modules/@eslint/plugin-kit": {
-
"version": "0.4.0",
-
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
-
"integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
+
"version": "0.4.1",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
-
"@eslint/core": "^0.16.0",
+
"@eslint/core": "^0.17.0",
"levn": "^0.4.1"
},
"engines": {
···
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
-
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
-
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
···
},
"node_modules/@humanfs/node": {
"version": "0.16.7",
-
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
-
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
···
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
-
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
-
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
···
},
"node_modules/@humanwhocodes/retry": {
"version": "0.4.3",
-
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
-
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
···
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
-
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
-
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
-
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
-
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
-
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
-
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
-
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
-
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
-
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
-
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.11",
-
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
-
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"dev": true,
"license": "MIT",
"optional": true,
-
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
···
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
-
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
-
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
-
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
-
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
}
},
"node_modules/@microsoft/api-extractor": {
-
"version": "7.53.1",
-
"resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.53.1.tgz",
-
"integrity": "sha512-bul5eTNxijLdDBqLye74u9494sRmf+9QULtec9Od0uHnifahGeNt8CC4/xCdn7mVyEBrXIQyQ5+sc4Uc0QfBSA==",
+
"version": "7.55.1",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@microsoft/api-extractor-model": "7.31.1",
-
"@microsoft/tsdoc": "~0.15.1",
-
"@microsoft/tsdoc-config": "~0.17.1",
-
"@rushstack/node-core-library": "5.17.0",
+
"@microsoft/api-extractor-model": "7.32.1",
+
"@microsoft/tsdoc": "~0.16.0",
+
"@microsoft/tsdoc-config": "~0.18.0",
+
"@rushstack/node-core-library": "5.19.0",
"@rushstack/rig-package": "0.6.0",
-
"@rushstack/terminal": "0.19.1",
-
"@rushstack/ts-command-line": "5.1.1",
+
"@rushstack/terminal": "0.19.4",
+
"@rushstack/ts-command-line": "5.1.4",
+
"diff": "~8.0.2",
"lodash": "~4.17.15",
"minimatch": "10.0.3",
"resolve": "~1.22.1",
···
}
},
"node_modules/@microsoft/api-extractor-model": {
-
"version": "7.31.1",
-
"resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.31.1.tgz",
-
"integrity": "sha512-Dhnip5OFKbl85rq/ICHBFGhV4RA5UQSl8AC/P/zoGvs+CBudPkatt5kIhMGiYgVPnUWmfR6fcp38+1AFLYNtUw==",
+
"version": "7.32.1",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@microsoft/tsdoc": "~0.15.1",
-
"@microsoft/tsdoc-config": "~0.17.1",
-
"@rushstack/node-core-library": "5.17.0"
-
}
-
},
-
"node_modules/@microsoft/api-extractor/node_modules/lru-cache": {
-
"version": "6.0.0",
-
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-
"dev": true,
-
"license": "ISC",
-
"dependencies": {
-
"yallist": "^4.0.0"
-
},
-
"engines": {
-
"node": ">=10"
-
}
-
},
-
"node_modules/@microsoft/api-extractor/node_modules/minimatch": {
-
"version": "10.0.3",
-
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
-
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
-
"dev": true,
-
"license": "ISC",
-
"dependencies": {
-
"@isaacs/brace-expansion": "^5.0.0"
-
},
-
"engines": {
-
"node": "20 || >=22"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/isaacs"
-
}
-
},
-
"node_modules/@microsoft/api-extractor/node_modules/semver": {
-
"version": "7.5.4",
-
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-
"dev": true,
-
"license": "ISC",
-
"dependencies": {
-
"lru-cache": "^6.0.0"
-
},
-
"bin": {
-
"semver": "bin/semver.js"
-
},
-
"engines": {
-
"node": ">=10"
+
"@microsoft/tsdoc": "~0.16.0",
+
"@microsoft/tsdoc-config": "~0.18.0",
+
"@rushstack/node-core-library": "5.19.0"
}
},
"node_modules/@microsoft/api-extractor/node_modules/typescript": {
"version": "5.8.2",
-
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
-
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
···
"node": ">=14.17"
}
},
-
"node_modules/@microsoft/api-extractor/node_modules/yallist": {
-
"version": "4.0.0",
-
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-
"dev": true,
-
"license": "ISC"
-
},
"node_modules/@microsoft/tsdoc": {
-
"version": "0.15.1",
-
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz",
-
"integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==",
+
"version": "0.16.0",
"dev": true,
"license": "MIT"
},
"node_modules/@microsoft/tsdoc-config": {
-
"version": "0.17.1",
-
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.1.tgz",
-
"integrity": "sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==",
+
"version": "0.18.0",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@microsoft/tsdoc": "0.15.1",
+
"@microsoft/tsdoc": "0.16.0",
"ajv": "~8.12.0",
"jju": "~1.4.0",
"resolve": "~1.22.2"
···
},
"node_modules/@microsoft/tsdoc-config/node_modules/ajv": {
"version": "8.12.0",
-
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
-
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"url": "https://github.com/sponsors/epoberezkin"
}
},
-
"node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": {
-
"version": "1.0.0",
-
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
-
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+
"node_modules/@napi-rs/wasm-runtime": {
+
"version": "1.1.0",
+
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz",
+
"integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==",
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"dependencies": {
+
"@emnapi/core": "^1.7.1",
+
"@emnapi/runtime": "^1.7.1",
+
"@tybys/wasm-util": "^0.10.1"
+
}
+
},
+
"node_modules/@oxc-project/runtime": {
+
"version": "0.92.0",
+
"dev": true,
+
"license": "MIT",
+
"engines": {
+
"node": "^20.19.0 || >=22.12.0"
+
}
+
},
+
"node_modules/@oxc-project/types": {
+
"version": "0.93.0",
+
"dev": true,
+
"license": "MIT",
+
"funding": {
+
"url": "https://github.com/sponsors/Boshen"
+
}
+
},
+
"node_modules/@rolldown/binding-android-arm64": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.41.tgz",
+
"integrity": "sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"android"
+
],
+
"engines": {
+
"node": "^20.19.0 || >=22.12.0"
+
}
+
},
+
"node_modules/@rolldown/binding-darwin-arm64": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.41.tgz",
+
"integrity": "sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"darwin"
+
],
+
"engines": {
+
"node": "^20.19.0 || >=22.12.0"
+
}
+
},
+
"node_modules/@rolldown/binding-darwin-x64": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.41.tgz",
+
"integrity": "sha512-Ho6lIwGJed98zub7n0xcRKuEtnZgbxevAmO4x3zn3C3N4GVXZD5xvCvTVxSMoeBJwTcIYzkVDRTIhylQNsTgLQ==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"darwin"
+
],
+
"engines": {
+
"node": "^20.19.0 || >=22.12.0"
+
}
+
},
+
"node_modules/@rolldown/binding-freebsd-x64": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.41.tgz",
+
"integrity": "sha512-ijAZETywvL+gACjbT4zBnCp5ez1JhTRs6OxRN4J+D6AzDRbU2zb01Esl51RP5/8ZOlvB37xxsRQ3X4YRVyYb3g==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"freebsd"
+
],
+
"engines": {
+
"node": "^20.19.0 || >=22.12.0"
+
}
+
},
+
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.41.tgz",
+
"integrity": "sha512-EgIOZt7UildXKFEFvaiLNBXm+4ggQyGe3E5Z1QP9uRcJJs9omihOnm897FwOBQdCuMvI49iBgjFrkhH+wMJ2MA==",
+
"cpu": [
+
"arm"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": "^20.19.0 || >=22.12.0"
+
}
+
},
+
"node_modules/@rolldown/binding-linux-arm64-gnu": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.41.tgz",
+
"integrity": "sha512-F8bUwJq8v/JAU8HSwgF4dztoqJ+FjdyjuvX4//3+Fbe2we9UktFeZ27U4lRMXF1vxWtdV4ey6oCSqI7yUrSEeg==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": "^20.19.0 || >=22.12.0"
+
}
+
},
+
"node_modules/@rolldown/binding-linux-arm64-musl": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.41.tgz",
+
"integrity": "sha512-MioXcCIX/wB1pBnBoJx8q4OGucUAfC1+/X1ilKFsjDK05VwbLZGRgOVD5OJJpUQPK86DhQciNBrfOKDiatxNmg==",
+
"cpu": [
+
"arm64"
+
],
"dev": true,
-
"license": "MIT"
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": "^20.19.0 || >=22.12.0"
+
}
},
-
"node_modules/@nodelib/fs.scandir": {
-
"version": "2.1.5",
-
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
-
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+
"node_modules/@rolldown/binding-linux-x64-gnu": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.41.tgz",
+
"integrity": "sha512-m66M61fizvRCwt5pOEiZQMiwBL9/y0bwU/+Kc4Ce/Pef6YfoEkR28y+DzN9rMdjo8Z28NXjsDPq9nH4mXnAP0g==",
+
"cpu": [
+
"x64"
+
],
"dev": true,
"license": "MIT",
-
"dependencies": {
-
"@nodelib/fs.stat": "2.0.5",
-
"run-parallel": "^1.1.9"
-
},
+
"optional": true,
+
"os": [
+
"linux"
+
],
"engines": {
-
"node": ">= 8"
+
"node": "^20.19.0 || >=22.12.0"
}
},
-
"node_modules/@nodelib/fs.stat": {
-
"version": "2.0.5",
-
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
-
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+
"node_modules/@rolldown/binding-linux-x64-musl": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.41.tgz",
+
"integrity": "sha512-yRxlSfBvWnnfrdtJfvi9lg8xfG5mPuyoSHm0X01oiE8ArmLRvoJGHUTJydCYz+wbK2esbq5J4B4Tq9WAsOlP1Q==",
+
"cpu": [
+
"x64"
+
],
"dev": true,
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
],
"engines": {
-
"node": ">= 8"
+
"node": "^20.19.0 || >=22.12.0"
}
},
-
"node_modules/@nodelib/fs.walk": {
-
"version": "1.2.8",
-
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
-
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+
"node_modules/@rolldown/binding-openharmony-arm64": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.41.tgz",
+
"integrity": "sha512-PHVxYhBpi8UViS3/hcvQQb9RFqCtvFmFU1PvUoTRiUdBtgHA6fONNHU4x796lgzNlVSD3DO/MZNk1s5/ozSMQg==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"openharmony"
+
],
+
"engines": {
+
"node": "^20.19.0 || >=22.12.0"
+
}
+
},
+
"node_modules/@rolldown/binding-wasm32-wasi": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.41.tgz",
+
"integrity": "sha512-OAfcO37ME6GGWmj9qTaDT7jY4rM0T2z0/8ujdQIJQ2x2nl+ztO32EIwURfmXOK0U1tzkyuaKYvE34Pug/ucXlQ==",
+
"cpu": [
+
"wasm32"
+
],
"dev": true,
"license": "MIT",
+
"optional": true,
"dependencies": {
-
"@nodelib/fs.scandir": "2.1.5",
-
"fastq": "^1.6.0"
+
"@napi-rs/wasm-runtime": "^1.0.5"
},
"engines": {
-
"node": ">= 8"
+
"node": ">=14.0.0"
}
},
-
"node_modules/@oxc-project/runtime": {
-
"version": "0.92.0",
-
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.92.0.tgz",
-
"integrity": "sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw==",
+
"node_modules/@rolldown/binding-win32-arm64-msvc": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.41.tgz",
+
"integrity": "sha512-NIYGuCcuXaq5BC4Q3upbiMBvmZsTsEPG9k/8QKQdmrch+ocSy5Jv9tdpdmXJyighKqm182nh/zBt+tSJkYoNlg==",
+
"cpu": [
+
"arm64"
+
],
"dev": true,
"license": "MIT",
+
"optional": true,
+
"os": [
+
"win32"
+
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
-
"node_modules/@oxc-project/types": {
-
"version": "0.93.0",
-
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.93.0.tgz",
-
"integrity": "sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==",
+
"node_modules/@rolldown/binding-win32-ia32-msvc": {
+
"version": "1.0.0-beta.41",
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.41.tgz",
+
"integrity": "sha512-kANdsDbE5FkEOb5NrCGBJBCaZ2Sabp3D7d4PRqMYJqyLljwh9mDyYyYSv5+QNvdAmifj+f3lviNEUUuUZPEFPw==",
+
"cpu": [
+
"ia32"
+
],
"dev": true,
"license": "MIT",
-
"funding": {
-
"url": "https://github.com/sponsors/Boshen"
+
"optional": true,
+
"os": [
+
"win32"
+
],
+
"engines": {
+
"node": "^20.19.0 || >=22.12.0"
}
},
-
"node_modules/@rolldown/binding-darwin-arm64": {
+
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-beta.41",
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.41.tgz",
-
"integrity": "sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw==",
"cpu": [
-
"arm64"
+
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
-
"darwin"
+
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/pluginutils": {
-
"version": "1.0.0-beta.38",
-
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
-
"integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==",
+
"version": "1.0.0-beta.47",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/pluginutils": {
-
"version": "5.3.0",
-
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
-
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
+
"version": "4.2.1",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@types/estree": "^1.0.0",
-
"estree-walker": "^2.0.2",
-
"picomatch": "^4.0.2"
+
"estree-walker": "^2.0.1",
+
"picomatch": "^2.2.2"
},
"engines": {
-
"node": ">=14.0.0"
-
},
-
"peerDependencies": {
-
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
-
},
-
"peerDependenciesMeta": {
-
"rollup": {
-
"optional": true
-
}
+
"node": ">= 8.0.0"
}
},
"node_modules/@rollup/pluginutils/node_modules/picomatch": {
-
"version": "4.0.3",
-
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
-
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+
"version": "2.3.1",
"dev": true,
"license": "MIT",
"engines": {
-
"node": ">=12"
+
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+
"node_modules/@rollup/rollup-android-arm-eabi": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
+
"integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
+
"cpu": [
+
"arm"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"android"
+
]
+
},
+
"node_modules/@rollup/rollup-android-arm64": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
+
"integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"android"
+
]
+
},
"node_modules/@rollup/rollup-darwin-arm64": {
-
"version": "4.52.4",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
-
"integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
+
"integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
"cpu": [
"arm64"
],
···
"optional": true,
"os": [
"darwin"
+
]
+
},
+
"node_modules/@rollup/rollup-darwin-x64": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
+
"integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
+
"cpu": [
+
"x64"
],
-
"peer": true
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"darwin"
+
]
+
},
+
"node_modules/@rollup/rollup-freebsd-arm64": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
+
"integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"freebsd"
+
]
+
},
+
"node_modules/@rollup/rollup-freebsd-x64": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
+
"integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"freebsd"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
+
"integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
+
"cpu": [
+
"arm"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
+
"integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
+
"cpu": [
+
"arm"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
+
"integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
+
"integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-loong64-gnu": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
+
"integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
+
"cpu": [
+
"loong64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
+
"integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
+
"cpu": [
+
"ppc64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
+
"integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
+
"cpu": [
+
"riscv64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
+
"integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
+
"cpu": [
+
"riscv64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
+
"integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
+
"cpu": [
+
"s390x"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
+
"integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-x64-musl": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
+
"integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-openharmony-arm64": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
+
"integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"openharmony"
+
]
+
},
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
+
"integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"win32"
+
]
+
},
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
+
"version": "4.53.3",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
+
"integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
+
"cpu": [
+
"ia32"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"win32"
+
]
+
},
+
"node_modules/@rollup/rollup-win32-x64-gnu": {
+
"version": "4.53.3",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"win32"
+
]
+
},
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
+
"version": "4.53.3",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"license": "MIT",
+
"optional": true,
+
"os": [
+
"win32"
+
]
},
"node_modules/@rushstack/node-core-library": {
-
"version": "5.17.0",
-
"resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.17.0.tgz",
-
"integrity": "sha512-24vt1GbHN6kyIglRMTVpyEiNRRRJK8uZHc1XoGAhmnTDKnrWet8OmOpImMswJIe6gM78eV8cMg1HXwuUHkSSgg==",
+
"version": "5.19.0",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@rushstack/node-core-library/node_modules/ajv": {
"version": "8.13.0",
-
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz",
-
"integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"url": "https://github.com/sponsors/epoberezkin"
},
-
"node_modules/@rushstack/node-core-library/node_modules/ajv-draft-04": {
-
"version": "1.0.0",
-
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
-
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
+
"node_modules/@rushstack/node-core-library/node_modules/fs-extra": {
+
"version": "11.3.2",
"dev": true,
"license": "MIT",
-
"peerDependencies": {
-
"ajv": "^8.5.0"
-
},
-
"peerDependenciesMeta": {
-
"ajv": {
-
"optional": true
-
}
-
}
-
},
-
"node_modules/@rushstack/node-core-library/node_modules/json-schema-traverse": {
-
"version": "1.0.0",
-
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
-
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/@rushstack/node-core-library/node_modules/lru-cache": {
-
"version": "6.0.0",
-
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-
"dev": true,
-
"license": "ISC",
"dependencies": {
-
"yallist": "^4.0.0"
-
},
-
"engines": {
-
"node": ">=10"
-
}
-
},
-
"node_modules/@rushstack/node-core-library/node_modules/semver": {
-
"version": "7.5.4",
-
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-
"dev": true,
-
"license": "ISC",
-
"dependencies": {
-
"lru-cache": "^6.0.0"
-
},
-
"bin": {
-
"semver": "bin/semver.js"
+
"graceful-fs": "^4.2.0",
+
"jsonfile": "^6.0.1",
+
"universalify": "^2.0.0"
},
"engines": {
-
"node": ">=10"
+
"node": ">=14.14"
},
-
"node_modules/@rushstack/node-core-library/node_modules/yallist": {
-
"version": "4.0.0",
-
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-
"dev": true,
-
"license": "ISC"
-
},
"node_modules/@rushstack/problem-matcher": {
"version": "0.1.1",
-
"resolved": "https://registry.npmjs.org/@rushstack/problem-matcher/-/problem-matcher-0.1.1.tgz",
-
"integrity": "sha512-Fm5XtS7+G8HLcJHCWpES5VmeMyjAKaWeyZU5qPzZC+22mPlJzAsOxymHiWIfuirtPckX3aptWws+K2d0BzniJA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
···
},
"node_modules/@rushstack/rig-package": {
"version": "0.6.0",
-
"resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.6.0.tgz",
-
"integrity": "sha512-ZQmfzsLE2+Y91GF15c65L/slMRVhF6Hycq04D4TwtdGaUAbIXXg9c5pKA5KFU7M4QMaihoobp9JJYpYcaY3zOw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@rushstack/terminal": {
-
"version": "0.19.1",
-
"resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.19.1.tgz",
-
"integrity": "sha512-jsBuSad67IDVMO2yp0hDfs0OdE4z3mDIjIL2pclDT3aEJboeZXE85e1HjuD0F6JoW3XgHvDwoX+WOV+AVTDQeA==",
+
"version": "0.19.4",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@rushstack/node-core-library": "5.17.0",
+
"@rushstack/node-core-library": "5.19.0",
"@rushstack/problem-matcher": "0.1.1",
"supports-color": "~8.1.1"
},
···
},
-
"node_modules/@rushstack/terminal/node_modules/supports-color": {
-
"version": "8.1.1",
-
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
-
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"has-flag": "^4.0.0"
-
},
-
"engines": {
-
"node": ">=10"
-
},
-
"funding": {
-
"url": "https://github.com/chalk/supports-color?sponsor=1"
-
}
-
},
"node_modules/@rushstack/ts-command-line": {
-
"version": "5.1.1",
-
"resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.1.1.tgz",
-
"integrity": "sha512-HPzFsUcr+wZ3oQI08Ec/E6cuiAVHKzrXZGHhwiwIGygAFiqN5QzX+ff30n70NU2WyE26CykgMwBZZSSyHCJrzA==",
+
"version": "5.1.4",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@rushstack/terminal": "0.19.1",
+
"@rushstack/terminal": "0.19.4",
"@types/argparse": "1.0.38",
"argparse": "~1.0.9",
"string-argv": "~0.3.1"
},
-
"node_modules/@rushstack/ts-command-line/node_modules/argparse": {
-
"version": "1.0.10",
-
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
-
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+
"node_modules/@standard-schema/spec": {
+
"version": "1.0.0",
+
"license": "MIT"
+
},
+
"node_modules/@tybys/wasm-util": {
+
"version": "0.10.1",
+
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
+
"optional": true,
"dependencies": {
-
"sprintf-js": "~1.0.2"
+
"tslib": "^2.4.0"
},
-
"node_modules/@standard-schema/spec": {
-
"version": "1.0.0",
-
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
-
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
-
"license": "MIT"
-
},
"node_modules/@types/argparse": {
"version": "1.0.38",
-
"resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz",
-
"integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
-
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
-
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@types/babel__generator": {
"version": "7.27.0",
-
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
-
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@types/babel__template": {
"version": "7.4.4",
-
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
-
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@types/babel__traverse": {
"version": "7.28.0",
-
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
-
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/@types/estree": {
"version": "1.0.8",
-
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
-
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
-
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
-
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
-
"version": "24.7.0",
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz",
-
"integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==",
+
"version": "24.10.1",
"dev": true,
"license": "MIT",
+
"peer": true,
"dependencies": {
-
"undici-types": "~7.14.0"
+
"undici-types": "~7.16.0"
},
"node_modules/@types/react": {
-
"version": "19.2.2",
-
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
-
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
+
"version": "19.2.7",
"dev": true,
"license": "MIT",
+
"peer": true,
"dependencies": {
-
"csstype": "^3.0.2"
+
"csstype": "^3.2.2"
},
"node_modules/@types/react-dom": {
-
"version": "19.2.1",
-
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz",
-
"integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==",
+
"version": "19.2.3",
"dev": true,
"license": "MIT",
"peerDependencies": {
···
},
"node_modules/@typescript-eslint/eslint-plugin": {
-
"version": "8.46.0",
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz",
-
"integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==",
+
"version": "8.48.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
-
"@typescript-eslint/scope-manager": "8.46.0",
-
"@typescript-eslint/type-utils": "8.46.0",
-
"@typescript-eslint/utils": "8.46.0",
-
"@typescript-eslint/visitor-keys": "8.46.0",
+
"@typescript-eslint/scope-manager": "8.48.1",
+
"@typescript-eslint/type-utils": "8.48.1",
+
"@typescript-eslint/utils": "8.48.1",
+
"@typescript-eslint/visitor-keys": "8.48.1",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
···
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
-
"@typescript-eslint/parser": "^8.46.0",
+
"@typescript-eslint/parser": "^8.48.1",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.5",
-
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
-
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/@typescript-eslint/parser": {
-
"version": "8.46.0",
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz",
-
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
+
"version": "8.48.1",
"dev": true,
"license": "MIT",
+
"peer": true,
"dependencies": {
-
"@typescript-eslint/scope-manager": "8.46.0",
-
"@typescript-eslint/types": "8.46.0",
-
"@typescript-eslint/typescript-estree": "8.46.0",
-
"@typescript-eslint/visitor-keys": "8.46.0",
+
"@typescript-eslint/scope-manager": "8.48.1",
+
"@typescript-eslint/types": "8.48.1",
+
"@typescript-eslint/typescript-estree": "8.48.1",
+
"@typescript-eslint/visitor-keys": "8.48.1",
"debug": "^4.3.4"
},
"engines": {
···
},
"node_modules/@typescript-eslint/project-service": {
-
"version": "8.46.0",
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz",
-
"integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==",
+
"version": "8.48.1",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@typescript-eslint/tsconfig-utils": "^8.46.0",
-
"@typescript-eslint/types": "^8.46.0",
+
"@typescript-eslint/tsconfig-utils": "^8.48.1",
+
"@typescript-eslint/types": "^8.48.1",
"debug": "^4.3.4"
},
"engines": {
···
},
"node_modules/@typescript-eslint/scope-manager": {
-
"version": "8.46.0",
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz",
-
"integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==",
+
"version": "8.48.1",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@typescript-eslint/types": "8.46.0",
-
"@typescript-eslint/visitor-keys": "8.46.0"
+
"@typescript-eslint/types": "8.48.1",
+
"@typescript-eslint/visitor-keys": "8.48.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
···
},
"node_modules/@typescript-eslint/tsconfig-utils": {
-
"version": "8.46.0",
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz",
-
"integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==",
+
"version": "8.48.1",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/@typescript-eslint/type-utils": {
-
"version": "8.46.0",
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz",
-
"integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==",
+
"version": "8.48.1",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@typescript-eslint/types": "8.46.0",
-
"@typescript-eslint/typescript-estree": "8.46.0",
-
"@typescript-eslint/utils": "8.46.0",
+
"@typescript-eslint/types": "8.48.1",
+
"@typescript-eslint/typescript-estree": "8.48.1",
+
"@typescript-eslint/utils": "8.48.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
···
},
"node_modules/@typescript-eslint/types": {
-
"version": "8.46.0",
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz",
-
"integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==",
+
"version": "8.48.1",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/@typescript-eslint/typescript-estree": {
-
"version": "8.46.0",
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz",
-
"integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==",
+
"version": "8.48.1",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@typescript-eslint/project-service": "8.46.0",
-
"@typescript-eslint/tsconfig-utils": "8.46.0",
-
"@typescript-eslint/types": "8.46.0",
-
"@typescript-eslint/visitor-keys": "8.46.0",
+
"@typescript-eslint/project-service": "8.48.1",
+
"@typescript-eslint/tsconfig-utils": "8.48.1",
+
"@typescript-eslint/types": "8.48.1",
+
"@typescript-eslint/visitor-keys": "8.48.1",
"debug": "^4.3.4",
-
"fast-glob": "^3.3.2",
-
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
+
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.1.0"
},
"engines": {
···
"typescript": ">=4.8.4 <6.0.0"
},
-
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
-
"version": "2.0.2",
-
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
-
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"balanced-match": "^1.0.0"
-
}
-
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
-
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
-
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
···
"url": "https://github.com/sponsors/isaacs"
},
+
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": {
+
"version": "2.0.2",
+
"dev": true,
+
"license": "MIT",
+
"dependencies": {
+
"balanced-match": "^1.0.0"
+
}
+
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.3",
-
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
-
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
···
},
"node_modules/@typescript-eslint/utils": {
-
"version": "8.46.0",
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz",
-
"integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==",
+
"version": "8.48.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
-
"@typescript-eslint/scope-manager": "8.46.0",
-
"@typescript-eslint/types": "8.46.0",
-
"@typescript-eslint/typescript-estree": "8.46.0"
+
"@typescript-eslint/scope-manager": "8.48.1",
+
"@typescript-eslint/types": "8.48.1",
+
"@typescript-eslint/typescript-estree": "8.48.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
···
},
"node_modules/@typescript-eslint/visitor-keys": {
-
"version": "8.46.0",
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz",
-
"integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==",
+
"version": "8.48.1",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@typescript-eslint/types": "8.46.0",
+
"@typescript-eslint/types": "8.48.1",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
···
},
"node_modules/@vitejs/plugin-react": {
-
"version": "5.0.4",
-
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz",
-
"integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==",
+
"version": "5.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@babel/core": "^7.28.4",
+
"@babel/core": "^7.28.5",
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
-
"@rolldown/pluginutils": "1.0.0-beta.38",
+
"@rolldown/pluginutils": "1.0.0-beta.47",
"@types/babel__core": "^7.20.5",
-
"react-refresh": "^0.17.0"
+
"react-refresh": "^0.18.0"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
···
},
"node_modules/@volar/language-core": {
-
"version": "2.4.23",
-
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz",
-
"integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==",
+
"version": "2.4.26",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@volar/source-map": "2.4.23"
+
"@volar/source-map": "2.4.26"
},
"node_modules/@volar/source-map": {
-
"version": "2.4.23",
-
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz",
-
"integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==",
+
"version": "2.4.26",
"dev": true,
"license": "MIT"
},
"node_modules/@volar/typescript": {
-
"version": "2.4.23",
-
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz",
-
"integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==",
+
"version": "2.4.26",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@volar/language-core": "2.4.23",
+
"@volar/language-core": "2.4.26",
"path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8"
},
"node_modules/acorn": {
"version": "8.15.0",
-
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
-
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
+
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
···
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
-
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
-
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
···
},
"node_modules/ajv": {
-
"version": "6.12.6",
-
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+
"version": "8.17.1",
+
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
+
"peer": true,
"dependencies": {
-
"fast-deep-equal": "^3.1.1",
-
"fast-json-stable-stringify": "^2.0.0",
-
"json-schema-traverse": "^0.4.1",
-
"uri-js": "^4.2.2"
+
"fast-deep-equal": "^3.1.3",
+
"fast-uri": "^3.0.1",
+
"json-schema-traverse": "^1.0.0",
+
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
},
+
"node_modules/ajv-draft-04": {
+
"version": "1.0.0",
+
"dev": true,
+
"license": "MIT",
+
"peerDependencies": {
+
"ajv": "^8.5.0"
+
},
+
"peerDependenciesMeta": {
+
"ajv": {
+
"optional": true
+
}
+
}
+
},
"node_modules/ajv-formats": {
"version": "3.0.1",
-
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
-
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
-
"node_modules/ajv-formats/node_modules/ajv": {
-
"version": "8.17.1",
-
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
-
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"fast-deep-equal": "^3.1.3",
-
"fast-uri": "^3.0.1",
-
"json-schema-traverse": "^1.0.0",
-
"require-from-string": "^2.0.2"
-
},
-
"funding": {
-
"type": "github",
-
"url": "https://github.com/sponsors/epoberezkin"
-
}
-
},
-
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
-
"version": "1.0.0",
-
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
-
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-
"dev": true,
-
"license": "MIT"
-
},
"node_modules/ansi-styles": {
"version": "4.3.0",
-
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/ansis": {
"version": "4.2.0",
-
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
-
"integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==",
"dev": true,
"license": "ISC",
"engines": {
···
},
"node_modules/argparse": {
-
"version": "2.0.1",
-
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+
"version": "1.0.10",
"dev": true,
-
"license": "Python-2.0"
+
"license": "MIT",
+
"dependencies": {
+
"sprintf-js": "~1.0.2"
+
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
-
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
-
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
-
"version": "2.8.13",
-
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.13.tgz",
-
"integrity": "sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==",
+
"version": "2.8.32",
"dev": true,
"license": "Apache-2.0",
"bin": {
···
},
"node_modules/brace-expansion": {
"version": "1.1.12",
-
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
-
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"concat-map": "0.0.1"
},
-
"node_modules/braces": {
-
"version": "3.0.3",
-
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
-
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"fill-range": "^7.1.1"
-
},
-
"engines": {
-
"node": ">=8"
-
}
-
},
"node_modules/browserslist": {
-
"version": "4.26.3",
-
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
-
"integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
+
"version": "4.28.0",
"dev": true,
"funding": [
···
],
"license": "MIT",
+
"peer": true,
"dependencies": {
-
"baseline-browser-mapping": "^2.8.9",
-
"caniuse-lite": "^1.0.30001746",
-
"electron-to-chromium": "^1.5.227",
-
"node-releases": "^2.0.21",
-
"update-browserslist-db": "^1.1.3"
+
"baseline-browser-mapping": "^2.8.25",
+
"caniuse-lite": "^1.0.30001754",
+
"electron-to-chromium": "^1.5.249",
+
"node-releases": "^2.0.27",
+
"update-browserslist-db": "^1.1.4"
},
"bin": {
"browserslist": "cli.js"
···
},
"node_modules/buffer-from": {
"version": "1.1.2",
-
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
-
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT",
-
"optional": true,
-
"peer": true
+
"optional": true
},
"node_modules/callsites": {
"version": "3.1.0",
-
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
-
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/caniuse-lite": {
-
"version": "1.0.30001748",
-
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz",
-
"integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==",
+
"version": "1.0.30001759",
"dev": true,
"funding": [
···
},
"node_modules/chalk": {
"version": "4.1.2",
-
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"url": "https://github.com/chalk/chalk?sponsor=1"
},
+
"node_modules/chalk/node_modules/supports-color": {
+
"version": "7.2.0",
+
"dev": true,
+
"license": "MIT",
+
"dependencies": {
+
"has-flag": "^4.0.0"
+
},
+
"engines": {
+
"node": ">=8"
+
}
+
},
"node_modules/color-convert": {
"version": "2.0.1",
-
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/color-name": {
"version": "1.1.4",
-
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/commander": {
"version": "2.20.3",
-
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
-
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT",
-
"optional": true,
-
"peer": true
+
"optional": true
},
"node_modules/commondir": {
"version": "1.0.1",
-
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
-
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true,
"license": "MIT"
},
"node_modules/compare-versions": {
"version": "6.1.1",
-
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
-
"integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==",
"dev": true,
"license": "MIT"
},
"node_modules/concat-map": {
"version": "0.0.1",
-
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/confbox": {
"version": "0.2.2",
-
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
-
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"dev": true,
"license": "MIT"
},
"node_modules/convert-source-map": {
"version": "2.0.0",
-
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
-
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
-
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
-
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/csstype": {
-
"version": "3.1.3",
-
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
-
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+
"version": "3.2.3",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
-
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
-
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/deep-is": {
"version": "0.1.4",
-
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
-
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
-
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
-
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
},
+
"node_modules/diff": {
+
"version": "8.0.2",
+
"dev": true,
+
"license": "BSD-3-Clause",
+
"engines": {
+
"node": ">=0.3.1"
+
}
+
},
"node_modules/electron-to-chromium": {
-
"version": "1.5.232",
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.232.tgz",
-
"integrity": "sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==",
+
"version": "1.5.263",
"dev": true,
"license": "ISC"
},
"node_modules/escalade": {
"version": "3.2.0",
-
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
-
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
-
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
-
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/eslint": {
-
"version": "9.37.0",
-
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
-
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
+
"version": "9.39.1",
"dev": true,
"license": "MIT",
+
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
-
"@eslint/config-array": "^0.21.0",
-
"@eslint/config-helpers": "^0.4.0",
-
"@eslint/core": "^0.16.0",
+
"@eslint/config-array": "^0.21.1",
+
"@eslint/config-helpers": "^0.4.2",
+
"@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
-
"@eslint/js": "9.37.0",
-
"@eslint/plugin-kit": "^0.4.0",
+
"@eslint/js": "9.39.1",
+
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
-
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.6",
···
},
"node_modules/eslint-plugin-react-hooks": {
"version": "5.2.0",
-
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
-
"integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/eslint-plugin-react-refresh": {
-
"version": "0.4.23",
-
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.23.tgz",
-
"integrity": "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA==",
+
"version": "0.4.24",
"dev": true,
"license": "MIT",
"peerDependencies": {
···
},
"node_modules/eslint-scope": {
"version": "8.4.0",
-
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
-
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
···
},
"node_modules/eslint-visitor-keys": {
"version": "4.2.1",
-
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
-
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
···
"url": "https://opencollective.com/eslint"
},
+
"node_modules/eslint/node_modules/ajv": {
+
"version": "6.12.6",
+
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+
"dev": true,
+
"license": "MIT",
+
"dependencies": {
+
"fast-deep-equal": "^3.1.1",
+
"fast-json-stable-stringify": "^2.0.0",
+
"json-schema-traverse": "^0.4.1",
+
"uri-js": "^4.2.2"
+
},
+
"funding": {
+
"type": "github",
+
"url": "https://github.com/sponsors/epoberezkin"
+
}
+
},
+
"node_modules/eslint/node_modules/json-schema-traverse": {
+
"version": "0.4.1",
+
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+
"dev": true,
+
"license": "MIT"
+
},
+
"node_modules/eslint/node_modules/minimatch": {
+
"version": "3.1.2",
+
"dev": true,
+
"license": "ISC",
+
"dependencies": {
+
"brace-expansion": "^1.1.7"
+
},
+
"engines": {
+
"node": "*"
+
}
+
},
"node_modules/esm-env": {
"version": "1.2.2",
-
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
-
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"license": "MIT"
},
"node_modules/espree": {
"version": "10.4.0",
-
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
-
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
···
},
"node_modules/esquery": {
"version": "1.6.0",
-
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
-
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
···
},
"node_modules/esrecurse": {
"version": "4.3.0",
-
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
-
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
···
},
"node_modules/estraverse": {
"version": "5.3.0",
-
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
-
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
···
},
"node_modules/estree-walker": {
"version": "2.0.2",
-
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
-
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/esutils": {
"version": "2.0.3",
-
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
-
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
···
},
"node_modules/exsolve": {
-
"version": "1.0.7",
-
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
-
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
+
"version": "1.0.8",
"dev": true,
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
-
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
-
"node_modules/fast-glob": {
-
"version": "3.3.3",
-
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
-
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"@nodelib/fs.stat": "^2.0.2",
-
"@nodelib/fs.walk": "^1.2.3",
-
"glob-parent": "^5.1.2",
-
"merge2": "^1.3.0",
-
"micromatch": "^4.0.8"
-
},
-
"engines": {
-
"node": ">=8.6.0"
-
}
-
},
-
"node_modules/fast-glob/node_modules/glob-parent": {
-
"version": "5.1.2",
-
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
-
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-
"dev": true,
-
"license": "ISC",
-
"dependencies": {
-
"is-glob": "^4.0.1"
-
},
-
"engines": {
-
"node": ">= 6"
-
}
-
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
···
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
-
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
-
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.0",
-
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
-
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"dev": true,
"funding": [
···
],
"license": "BSD-3-Clause"
},
-
"node_modules/fastq": {
-
"version": "1.19.1",
-
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
-
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+
"node_modules/fdir": {
+
"version": "6.5.0",
"dev": true,
-
"license": "ISC",
-
"dependencies": {
-
"reusify": "^1.0.4"
+
"license": "MIT",
+
"engines": {
+
"node": ">=12.0.0"
+
},
+
"peerDependencies": {
+
"picomatch": "^3 || ^4"
+
},
+
"peerDependenciesMeta": {
+
"picomatch": {
+
"optional": true
+
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
-
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
-
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"node": ">=16.0.0"
},
-
"node_modules/fill-range": {
-
"version": "7.1.1",
-
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
-
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"to-regex-range": "^5.0.1"
-
},
-
"engines": {
-
"node": ">=8"
-
}
-
},
"node_modules/find-cache-dir": {
"version": "3.3.2",
-
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
-
"integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/find-up": {
"version": "5.0.0",
-
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
-
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/flat-cache": {
"version": "4.0.1",
-
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
-
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/flatted": {
"version": "3.3.3",
-
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
-
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true,
"license": "ISC"
},
"node_modules/fs-extra": {
-
"version": "11.3.2",
-
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
-
"integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==",
+
"version": "10.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
···
"universalify": "^2.0.0"
},
"engines": {
-
"node": ">=14.14"
+
"node": ">=12"
},
"node_modules/fsevents": {
···
},
"node_modules/function-bind": {
"version": "1.1.2",
-
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
-
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
···
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
-
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
-
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/glob-parent": {
"version": "6.0.2",
-
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
-
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
···
},
"node_modules/globals": {
-
"version": "16.4.0",
-
"resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
-
"integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
+
"version": "16.5.0",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/graceful-fs": {
"version": "4.2.11",
-
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
-
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/graphemer": {
"version": "1.4.0",
-
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
-
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true,
"license": "MIT"
},
"node_modules/has-flag": {
"version": "4.0.0",
-
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/hasown": {
"version": "2.0.2",
-
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
-
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/ignore": {
"version": "5.3.2",
-
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
-
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/import-fresh": {
"version": "3.3.1",
-
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
-
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/import-lazy": {
"version": "4.0.0",
-
"resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz",
-
"integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/imurmurhash": {
"version": "0.1.4",
-
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
-
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/is-core-module": {
"version": "2.16.1",
-
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
-
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/is-extglob": {
"version": "2.1.1",
-
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/is-glob": {
"version": "4.0.3",
-
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
-
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"node": ">=0.10.0"
},
-
"node_modules/is-number": {
-
"version": "7.0.0",
-
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=0.12.0"
-
}
-
},
"node_modules/isexe": {
"version": "2.0.0",
-
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/jju": {
"version": "1.4.0",
-
"resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
-
"integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==",
"dev": true,
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
-
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
-
"version": "4.1.0",
-
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+
"version": "4.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
···
"js-yaml": "bin/js-yaml.js"
},
+
"node_modules/js-yaml/node_modules/argparse": {
+
"version": "2.0.1",
+
"dev": true,
+
"license": "Python-2.0"
+
},
"node_modules/jsesc": {
"version": "3.1.0",
-
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
-
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
···
},
"node_modules/json-buffer": {
"version": "3.0.1",
-
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
-
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
-
"version": "0.4.1",
-
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+
"version": "1.0.0",
+
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
-
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
-
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/json5": {
"version": "2.2.3",
-
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
-
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"bin": {
···
},
"node_modules/jsonfile": {
"version": "6.2.0",
-
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
-
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/keyv": {
"version": "4.5.4",
-
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
-
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/kolorist": {
"version": "1.8.0",
-
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
-
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
"dev": true,
"license": "MIT"
},
"node_modules/levn": {
"version": "0.4.1",
-
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
-
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/lightningcss": {
"version": "1.30.2",
-
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
-
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
···
"lightningcss-win32-x64-msvc": "1.30.2"
},
+
"node_modules/lightningcss-android-arm64": {
+
"version": "1.30.2",
+
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
+
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MPL-2.0",
+
"optional": true,
+
"os": [
+
"android"
+
],
+
"engines": {
+
"node": ">= 12.0.0"
+
},
+
"funding": {
+
"type": "opencollective",
+
"url": "https://opencollective.com/parcel"
+
}
+
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
···
"url": "https://opencollective.com/parcel"
},
+
"node_modules/lightningcss-darwin-x64": {
+
"version": "1.30.2",
+
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
+
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"license": "MPL-2.0",
+
"optional": true,
+
"os": [
+
"darwin"
+
],
+
"engines": {
+
"node": ">= 12.0.0"
+
},
+
"funding": {
+
"type": "opencollective",
+
"url": "https://opencollective.com/parcel"
+
}
+
},
+
"node_modules/lightningcss-freebsd-x64": {
+
"version": "1.30.2",
+
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
+
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"license": "MPL-2.0",
+
"optional": true,
+
"os": [
+
"freebsd"
+
],
+
"engines": {
+
"node": ">= 12.0.0"
+
},
+
"funding": {
+
"type": "opencollective",
+
"url": "https://opencollective.com/parcel"
+
}
+
},
+
"node_modules/lightningcss-linux-arm-gnueabihf": {
+
"version": "1.30.2",
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
+
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
+
"cpu": [
+
"arm"
+
],
+
"dev": true,
+
"license": "MPL-2.0",
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">= 12.0.0"
+
},
+
"funding": {
+
"type": "opencollective",
+
"url": "https://opencollective.com/parcel"
+
}
+
},
+
"node_modules/lightningcss-linux-arm64-gnu": {
+
"version": "1.30.2",
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
+
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MPL-2.0",
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">= 12.0.0"
+
},
+
"funding": {
+
"type": "opencollective",
+
"url": "https://opencollective.com/parcel"
+
}
+
},
+
"node_modules/lightningcss-linux-arm64-musl": {
+
"version": "1.30.2",
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
+
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MPL-2.0",
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">= 12.0.0"
+
},
+
"funding": {
+
"type": "opencollective",
+
"url": "https://opencollective.com/parcel"
+
}
+
},
+
"node_modules/lightningcss-linux-x64-gnu": {
+
"version": "1.30.2",
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
+
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"license": "MPL-2.0",
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">= 12.0.0"
+
},
+
"funding": {
+
"type": "opencollective",
+
"url": "https://opencollective.com/parcel"
+
}
+
},
+
"node_modules/lightningcss-linux-x64-musl": {
+
"version": "1.30.2",
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
+
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"license": "MPL-2.0",
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">= 12.0.0"
+
},
+
"funding": {
+
"type": "opencollective",
+
"url": "https://opencollective.com/parcel"
+
}
+
},
+
"node_modules/lightningcss-win32-arm64-msvc": {
+
"version": "1.30.2",
+
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
+
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"license": "MPL-2.0",
+
"optional": true,
+
"os": [
+
"win32"
+
],
+
"engines": {
+
"node": ">= 12.0.0"
+
},
+
"funding": {
+
"type": "opencollective",
+
"url": "https://opencollective.com/parcel"
+
}
+
},
+
"node_modules/lightningcss-win32-x64-msvc": {
+
"version": "1.30.2",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"license": "MPL-2.0",
+
"optional": true,
+
"os": [
+
"win32"
+
],
+
"engines": {
+
"node": ">= 12.0.0"
+
},
+
"funding": {
+
"type": "opencollective",
+
"url": "https://opencollective.com/parcel"
+
}
+
},
"node_modules/local-pkg": {
"version": "1.1.2",
-
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
-
"integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/locate-path": {
"version": "6.0.0",
-
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
-
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/lodash": {
"version": "4.17.21",
-
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
-
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
-
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/lru-cache": {
-
"version": "5.1.1",
-
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
-
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+
"version": "6.0.0",
"dev": true,
"license": "ISC",
"dependencies": {
-
"yallist": "^3.0.2"
+
"yallist": "^4.0.0"
+
},
+
"engines": {
+
"node": ">=10"
},
"node_modules/magic-string": {
-
"version": "0.30.19",
-
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
-
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
+
"version": "0.30.21",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/make-dir": {
"version": "3.1.0",
-
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
-
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"url": "https://github.com/sponsors/sindresorhus"
},
-
"node_modules/merge2": {
-
"version": "1.4.1",
-
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
-
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+
"node_modules/make-dir/node_modules/semver": {
+
"version": "6.3.1",
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">= 8"
-
}
-
},
-
"node_modules/micromatch": {
-
"version": "4.0.8",
-
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
-
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"braces": "^3.0.3",
-
"picomatch": "^2.3.1"
-
},
-
"engines": {
-
"node": ">=8.6"
+
"license": "ISC",
+
"bin": {
+
"semver": "bin/semver.js"
},
"node_modules/minimatch": {
-
"version": "3.1.2",
-
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+
"version": "10.0.3",
"dev": true,
"license": "ISC",
"dependencies": {
-
"brace-expansion": "^1.1.7"
+
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
-
"node": "*"
+
"node": "20 || >=22"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/isaacs"
},
"node_modules/mlly": {
"version": "1.8.0",
-
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
-
"integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"ufo": "^1.6.1"
},
-
"node_modules/mlly/node_modules/confbox": {
-
"version": "0.1.8",
-
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
-
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
-
"dev": true,
-
"license": "MIT"
-
},
"node_modules/mlly/node_modules/pkg-types": {
"version": "1.3.1",
-
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
-
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"mlly": "^1.7.4",
"pathe": "^2.0.1"
+
},
+
"node_modules/mlly/node_modules/pkg-types/node_modules/confbox": {
+
"version": "0.1.8",
+
"dev": true,
+
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
-
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
-
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
-
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
···
},
"node_modules/natural-compare": {
"version": "1.4.0",
-
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
-
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
"license": "MIT"
},
"node_modules/node-releases": {
-
"version": "2.0.23",
-
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz",
-
"integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==",
+
"version": "2.0.27",
"dev": true,
"license": "MIT"
},
"node_modules/optionator": {
"version": "0.9.4",
-
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
-
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/p-limit": {
"version": "3.1.0",
-
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
-
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/p-locate": {
"version": "5.0.0",
-
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
-
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/p-try": {
"version": "2.2.0",
-
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
-
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/parent-module": {
"version": "1.0.1",
-
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
-
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/path-browserify": {
"version": "1.0.1",
-
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
-
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"dev": true,
"license": "MIT"
},
"node_modules/path-exists": {
"version": "4.0.0",
-
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
-
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/path-key": {
"version": "3.1.1",
-
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
-
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/path-parse": {
"version": "1.0.7",
-
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
-
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/pathe": {
"version": "2.0.3",
-
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
-
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
-
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
-
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
-
"version": "2.3.1",
-
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+
"version": "4.0.3",
"dev": true,
"license": "MIT",
"engines": {
-
"node": ">=8.6"
+
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
···
},
"node_modules/pkg-dir": {
"version": "4.2.0",
-
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
-
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/pkg-dir/node_modules/find-up": {
"version": "4.1.0",
-
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
-
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"node": ">=8"
},
-
"node_modules/pkg-dir/node_modules/locate-path": {
+
"node_modules/pkg-dir/node_modules/find-up/node_modules/locate-path": {
"version": "5.0.0",
-
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
-
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"node": ">=8"
},
-
"node_modules/pkg-dir/node_modules/p-limit": {
-
"version": "2.3.0",
-
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
-
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+
"node_modules/pkg-dir/node_modules/find-up/node_modules/locate-path/node_modules/p-locate": {
+
"version": "4.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
-
"p-try": "^2.0.0"
+
"p-limit": "^2.2.0"
},
"engines": {
-
"node": ">=6"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
+
"node": ">=8"
},
-
"node_modules/pkg-dir/node_modules/p-locate": {
-
"version": "4.1.0",
-
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
-
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+
"node_modules/pkg-dir/node_modules/find-up/node_modules/locate-path/node_modules/p-locate/node_modules/p-limit": {
+
"version": "2.3.0",
"dev": true,
"license": "MIT",
"dependencies": {
-
"p-limit": "^2.2.0"
+
"p-try": "^2.0.0"
},
"engines": {
-
"node": ">=8"
+
"node": ">=6"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
},
"node_modules/pkg-types": {
"version": "2.3.0",
-
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
-
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/postcss": {
"version": "8.5.6",
-
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
-
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
···
},
"node_modules/prelude-ls": {
"version": "1.2.1",
-
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
-
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/punycode": {
"version": "2.3.1",
-
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
-
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/quansync": {
"version": "0.2.11",
-
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
-
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
"dev": true,
"funding": [
···
],
"license": "MIT"
},
-
"node_modules/queue-microtask": {
-
"version": "1.2.3",
-
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
-
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-
"dev": true,
-
"funding": [
-
{
-
"type": "github",
-
"url": "https://github.com/sponsors/feross"
-
},
-
{
-
"type": "patreon",
-
"url": "https://www.patreon.com/feross"
-
},
-
{
-
"type": "consulting",
-
"url": "https://feross.org/support"
-
}
-
],
-
"license": "MIT"
-
},
"node_modules/react": {
"version": "19.2.0",
-
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
-
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"dev": true,
"license": "MIT",
+
"peer": true,
"engines": {
"node": ">=0.10.0"
},
"node_modules/react-dom": {
"version": "19.2.0",
-
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
-
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/react-refresh": {
-
"version": "0.17.0",
-
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
-
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+
"version": "0.18.0",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/require-from-string": {
"version": "2.0.2",
-
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
-
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/resolve": {
-
"version": "1.22.10",
-
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
-
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+
"version": "1.22.11",
"dev": true,
"license": "MIT",
"dependencies": {
-
"is-core-module": "^2.16.0",
+
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
···
},
"node_modules/resolve-from": {
"version": "4.0.0",
-
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
-
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
},
-
"node_modules/reusify": {
-
"version": "1.1.0",
-
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
-
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"iojs": ">=1.0.0",
-
"node": ">=0.10.0"
-
}
-
},
"node_modules/rolldown": {
"version": "1.0.0-beta.41",
-
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.41.tgz",
-
"integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==",
"dev": true,
"license": "MIT",
+
"peer": true,
"dependencies": {
"@oxc-project/types": "=0.93.0",
"@rolldown/pluginutils": "1.0.0-beta.41",
···
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.41",
-
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.41.tgz",
-
"integrity": "sha512-ycMEPrS3StOIeb87BT3/+bu+blEtyvwQ4zmo2IcJQy0Rd1DAAhKksA0iUZ3MYSpJtjlPhg0Eo6mvVS6ggPhRbw==",
"dev": true,
"license": "MIT"
},
"node_modules/rollup": {
-
"version": "4.52.4",
-
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
-
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
+
"version": "4.53.3",
"dev": true,
"license": "MIT",
"peer": true,
···
"npm": ">=8.0.0"
},
"optionalDependencies": {
-
"@rollup/rollup-android-arm-eabi": "4.52.4",
-
"@rollup/rollup-android-arm64": "4.52.4",
-
"@rollup/rollup-darwin-arm64": "4.52.4",
-
"@rollup/rollup-darwin-x64": "4.52.4",
-
"@rollup/rollup-freebsd-arm64": "4.52.4",
-
"@rollup/rollup-freebsd-x64": "4.52.4",
-
"@rollup/rollup-linux-arm-gnueabihf": "4.52.4",
-
"@rollup/rollup-linux-arm-musleabihf": "4.52.4",
-
"@rollup/rollup-linux-arm64-gnu": "4.52.4",
-
"@rollup/rollup-linux-arm64-musl": "4.52.4",
-
"@rollup/rollup-linux-loong64-gnu": "4.52.4",
-
"@rollup/rollup-linux-ppc64-gnu": "4.52.4",
-
"@rollup/rollup-linux-riscv64-gnu": "4.52.4",
-
"@rollup/rollup-linux-riscv64-musl": "4.52.4",
-
"@rollup/rollup-linux-s390x-gnu": "4.52.4",
-
"@rollup/rollup-linux-x64-gnu": "4.52.4",
-
"@rollup/rollup-linux-x64-musl": "4.52.4",
-
"@rollup/rollup-openharmony-arm64": "4.52.4",
-
"@rollup/rollup-win32-arm64-msvc": "4.52.4",
-
"@rollup/rollup-win32-ia32-msvc": "4.52.4",
-
"@rollup/rollup-win32-x64-gnu": "4.52.4",
-
"@rollup/rollup-win32-x64-msvc": "4.52.4",
+
"@rollup/rollup-android-arm-eabi": "4.53.3",
+
"@rollup/rollup-android-arm64": "4.53.3",
+
"@rollup/rollup-darwin-arm64": "4.53.3",
+
"@rollup/rollup-darwin-x64": "4.53.3",
+
"@rollup/rollup-freebsd-arm64": "4.53.3",
+
"@rollup/rollup-freebsd-x64": "4.53.3",
+
"@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
+
"@rollup/rollup-linux-arm-musleabihf": "4.53.3",
+
"@rollup/rollup-linux-arm64-gnu": "4.53.3",
+
"@rollup/rollup-linux-arm64-musl": "4.53.3",
+
"@rollup/rollup-linux-loong64-gnu": "4.53.3",
+
"@rollup/rollup-linux-ppc64-gnu": "4.53.3",
+
"@rollup/rollup-linux-riscv64-gnu": "4.53.3",
+
"@rollup/rollup-linux-riscv64-musl": "4.53.3",
+
"@rollup/rollup-linux-s390x-gnu": "4.53.3",
+
"@rollup/rollup-linux-x64-gnu": "4.53.3",
+
"@rollup/rollup-linux-x64-musl": "4.53.3",
+
"@rollup/rollup-openharmony-arm64": "4.53.3",
+
"@rollup/rollup-win32-arm64-msvc": "4.53.3",
+
"@rollup/rollup-win32-ia32-msvc": "4.53.3",
+
"@rollup/rollup-win32-x64-gnu": "4.53.3",
+
"@rollup/rollup-win32-x64-msvc": "4.53.3",
"fsevents": "~2.3.2"
},
"node_modules/rollup-plugin-typescript2": {
"version": "0.36.0",
-
"resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.36.0.tgz",
-
"integrity": "sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"typescript": ">=2.4.0"
},
-
"node_modules/rollup-plugin-typescript2/node_modules/@rollup/pluginutils": {
-
"version": "4.2.1",
-
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz",
-
"integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"estree-walker": "^2.0.1",
-
"picomatch": "^2.2.2"
-
},
-
"engines": {
-
"node": ">= 8.0.0"
-
}
-
},
-
"node_modules/rollup-plugin-typescript2/node_modules/fs-extra": {
-
"version": "10.1.0",
-
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
-
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"graceful-fs": "^4.2.0",
-
"jsonfile": "^6.0.1",
-
"universalify": "^2.0.0"
-
},
-
"engines": {
-
"node": ">=12"
-
}
-
},
"node_modules/rollup-plugin-typescript2/node_modules/semver": {
"version": "7.7.3",
-
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
-
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
···
"node": ">=10"
},
-
"node_modules/run-parallel": {
-
"version": "1.2.0",
-
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
-
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-
"dev": true,
-
"funding": [
-
{
-
"type": "github",
-
"url": "https://github.com/sponsors/feross"
-
},
-
{
-
"type": "patreon",
-
"url": "https://www.patreon.com/feross"
-
},
-
{
-
"type": "consulting",
-
"url": "https://feross.org/support"
-
}
-
],
-
"license": "MIT",
-
"dependencies": {
-
"queue-microtask": "^1.2.2"
-
}
-
},
"node_modules/scheduler": {
"version": "0.27.0",
-
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
-
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"dev": true,
"license": "MIT"
},
"node_modules/semver": {
-
"version": "6.3.1",
-
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+
"version": "7.5.4",
"dev": true,
"license": "ISC",
+
"dependencies": {
+
"lru-cache": "^6.0.0"
+
},
"bin": {
"semver": "bin/semver.js"
+
},
+
"engines": {
+
"node": ">=10"
},
"node_modules/shebang-command": {
"version": "2.0.0",
-
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
-
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/shebang-regex": {
"version": "3.0.0",
-
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
-
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/source-map": {
"version": "0.6.1",
-
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
···
},
"node_modules/source-map-js": {
"version": "1.2.1",
-
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
-
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
···
},
"node_modules/source-map-support": {
"version": "0.5.21",
-
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
-
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"optional": true,
-
"peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
···
},
"node_modules/sprintf-js": {
"version": "1.0.3",
-
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
-
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/string-argv": {
"version": "0.3.2",
-
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
-
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
-
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
-
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/supports-color": {
-
"version": "7.2.0",
-
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+
"version": "8.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
-
"node": ">=8"
+
"node": ">=10"
+
},
+
"funding": {
+
"url": "https://github.com/chalk/supports-color?sponsor=1"
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
-
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
-
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
···
"url": "https://github.com/sponsors/ljharb"
},
-
"node_modules/terser": {
-
"version": "5.44.0",
-
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
-
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
-
"dev": true,
-
"license": "BSD-2-Clause",
-
"optional": true,
-
"peer": true,
-
"dependencies": {
-
"@jridgewell/source-map": "^0.3.3",
-
"acorn": "^8.15.0",
-
"commander": "^2.20.0",
-
"source-map-support": "~0.5.20"
-
},
-
"bin": {
-
"terser": "bin/terser"
-
},
-
"engines": {
-
"node": ">=10"
-
}
-
},
"node_modules/tinyglobby": {
"version": "0.2.15",
-
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
-
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"url": "https://github.com/sponsors/SuperchupuDev"
},
-
"node_modules/tinyglobby/node_modules/fdir": {
-
"version": "6.5.0",
-
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
-
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=12.0.0"
-
},
-
"peerDependencies": {
-
"picomatch": "^3 || ^4"
-
},
-
"peerDependenciesMeta": {
-
"picomatch": {
-
"optional": true
-
}
-
}
-
},
-
"node_modules/tinyglobby/node_modules/picomatch": {
-
"version": "4.0.3",
-
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
-
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=12"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/jonschlinkert"
-
}
-
},
-
"node_modules/to-regex-range": {
-
"version": "5.0.1",
-
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"is-number": "^7.0.0"
-
},
-
"engines": {
-
"node": ">=8.0"
-
}
-
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
-
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
-
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/tslib": {
"version": "2.8.1",
-
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
-
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
-
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
-
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"license": "MIT",
"dependencies": {
···
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
···
},
"node_modules/typescript-eslint": {
-
"version": "8.46.0",
-
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.0.tgz",
-
"integrity": "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==",
+
"version": "8.48.1",
"dev": true,
"license": "MIT",
"dependencies": {
-
"@typescript-eslint/eslint-plugin": "8.46.0",
-
"@typescript-eslint/parser": "8.46.0",
-
"@typescript-eslint/typescript-estree": "8.46.0",
-
"@typescript-eslint/utils": "8.46.0"
+
"@typescript-eslint/eslint-plugin": "8.48.1",
+
"@typescript-eslint/parser": "8.48.1",
+
"@typescript-eslint/typescript-estree": "8.48.1",
+
"@typescript-eslint/utils": "8.48.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
···
},
"node_modules/ufo": {
"version": "1.6.1",
-
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
-
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
-
"version": "7.14.0",
-
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
-
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
+
"version": "7.16.0",
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
-
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
-
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/unplugin": {
-
"version": "2.3.10",
-
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz",
-
"integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==",
+
"version": "2.3.11",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
"node_modules/unplugin-dts": {
"version": "1.0.0-beta.6",
-
"resolved": "https://registry.npmjs.org/unplugin-dts/-/unplugin-dts-1.0.0-beta.6.tgz",
-
"integrity": "sha512-+xbFv5aVFtLZFNBAKI4+kXmd2h+T42/AaP8Bsp0YP/je/uOTN94Ame2Xt3e9isZS+Z7/hrLCLbsVJh+saqFMfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
···
},
-
"node_modules/unplugin/node_modules/picomatch": {
-
"version": "4.0.3",
-
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
-
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+
"node_modules/unplugin-dts/node_modules/@rollup/pluginutils": {
+
"version": "5.3.0",
"dev": true,
"license": "MIT",
+
"dependencies": {
+
"@types/estree": "^1.0.0",
+
"estree-walker": "^2.0.2",
+
"picomatch": "^4.0.2"
+
},
"engines": {
-
"node": ">=12"
+
"node": ">=14.0.0"
+
},
+
"peerDependencies": {
+
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
-
"funding": {
-
"url": "https://github.com/sponsors/jonschlinkert"
+
"peerDependenciesMeta": {
+
"rollup": {
+
"optional": true
+
}
},
"node_modules/update-browserslist-db": {
-
"version": "1.1.3",
-
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
-
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+
"version": "1.1.4",
"dev": true,
"funding": [
···
},
"node_modules/uri-js": {
"version": "4.4.1",
-
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
-
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
···
"node_modules/vite": {
"name": "rolldown-vite",
"version": "7.1.14",
-
"resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.1.14.tgz",
-
"integrity": "sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==",
"dev": true,
"license": "MIT",
+
"peer": true,
"dependencies": {
"@oxc-project/runtime": "0.92.0",
"fdir": "^6.5.0",
···
},
-
"node_modules/vite/node_modules/fdir": {
-
"version": "6.5.0",
-
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
-
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=12.0.0"
-
},
-
"peerDependencies": {
-
"picomatch": "^3 || ^4"
-
},
-
"peerDependenciesMeta": {
-
"picomatch": {
-
"optional": true
-
}
-
}
-
},
-
"node_modules/vite/node_modules/picomatch": {
-
"version": "4.0.3",
-
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
-
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=12"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/jonschlinkert"
-
}
-
},
"node_modules/vscode-uri": {
"version": "3.1.0",
-
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
-
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"dev": true,
"license": "MIT"
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
-
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
-
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
-
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
-
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
···
},
"node_modules/word-wrap": {
"version": "1.2.5",
-
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
-
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"license": "MIT",
"engines": {
···
},
"node_modules/yallist": {
-
"version": "3.1.1",
-
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+
"version": "4.0.0",
"dev": true,
"license": "ISC"
},
"node_modules/yocto-queue": {
"version": "0.1.0",
-
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
-
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"license": "MIT",
"engines": {
+3 -4
package.json
···
{
"name": "atproto-ui",
-
"version": "0.5.4",
+
"version": "0.12.0",
"type": "module",
"description": "React components and hooks for rendering AT Protocol records.",
"main": "./lib-dist/index.js",
···
"README.md"
],
"sideEffects": [
-
"./lib-dist/styles.css",
-
"./lib-dist/index.js"
+
"./lib-dist/styles.css"
],
"scripts": {
"dev": "vite",
···
"@atcute/bluesky": "^3.2.3",
"@atcute/client": "^4.0.3",
"@atcute/identity-resolver": "^1.1.3",
-
"@atcute/tangled": "^1.0.6"
+
"@atcute/tangled": "^1.0.10"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
+139 -5
src/App.tsx
···
import React, { useState, useCallback, useRef } from "react";
-
import { AtProtoProvider } from "../lib";
-
import "../lib/styles.css"
+
import { AtProtoProvider, TangledRepo } from "../lib";
+
import "../lib/styles.css";
import "./App.css";
import { TangledString } from "../lib/components/TangledString";
···
} from "../lib/components/BlueskyPost";
import { BlueskyPostList } from "../lib/components/BlueskyPostList";
import { BlueskyQuotePost } from "../lib/components/BlueskyQuotePost";
+
import { GrainGallery } from "../lib/components/GrainGallery";
+
import { CurrentlyPlaying } from "../lib/components/CurrentlyPlaying";
+
import { LastPlayed } from "../lib/components/LastPlayed";
+
import { SongHistoryList } from "../lib/components/SongHistoryList";
import { useDidResolution } from "../lib/hooks/useDidResolution";
import { useLatestRecord } from "../lib/hooks/useLatestRecord";
import type { FeedPostRecord } from "../lib/types/bluesky";
···
// Pass prefetched recordโ€”BlueskyPost won't re-fetch it
return <BlueskyPost did={did} rkey={rkey} record={record} />;
};`;
+
+
const atcuteUsageSnippet = `import { Client, simpleFetchHandler, ok } from '@atcute/client';
+
import type { AppBskyFeedPost } from '@atcute/bluesky';
+
import { BlueskyPost } from 'atproto-ui';
+
+
// Create atcute client
+
const client = new Client({
+
handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' })
+
});
+
+
// Fetch a record
+
const data = await ok(
+
client.get('com.atproto.repo.getRecord', {
+
params: {
+
repo: 'did:plc:ttdrpj45ibqunmfhdsb4zdwq',
+
collection: 'app.bsky.feed.post',
+
rkey: '3m45rq4sjes2h'
+
}
+
})
+
);
+
+
const record = data.value as AppBskyFeedPost.Main;
+
+
// Pass atcute record directly to component!
+
<BlueskyPost
+
did="did:plc:ttdrpj45ibqunmfhdsb4zdwq"
+
rkey="3m45rq4sjes2h"
+
record={record}
+
/>`;
const codeBlockBase: React.CSSProperties = {
fontFamily: 'Menlo, Consolas, "SFMono-Regular", ui-monospace, monospace',
···
<h3 style={sectionHeaderStyle}>Recent Posts</h3>
<BlueskyPostList did={did} />
</section>
+
<section style={panelStyle}>
+
<h3 style={sectionHeaderStyle}>
+
grain.social Gallery Demo
+
</h3>
+
<p
+
style={{
+
fontSize: 12,
+
color: `var(--demo-text-secondary)`,
+
margin: "0 0 8px",
+
}}
+
>
+
Instagram-style photo gallery from grain.social
+
</p>
+
<GrainGallery
+
did="kat.meangirls.online"
+
rkey="3m2e2qikseq2f"
+
/>
+
</section>
+
<section style={panelStyle}>
+
<h3 style={sectionHeaderStyle}>
+
teal.fm Currently Playing
+
</h3>
+
<p
+
style={{
+
fontSize: 12,
+
color: `var(--demo-text-secondary)`,
+
margin: "0 0 8px",
+
}}
+
>
+
Currently playing track from teal.fm (refreshes every 15s)
+
</p>
+
<CurrentlyPlaying did="nekomimi.pet" />
+
</section>
+
<section style={panelStyle}>
+
<h3 style={sectionHeaderStyle}>
+
teal.fm Last Played
+
</h3>
+
<p
+
style={{
+
fontSize: 12,
+
color: `var(--demo-text-secondary)`,
+
margin: "0 0 8px",
+
}}
+
>
+
Most recent play from teal.fm feed
+
</p>
+
<LastPlayed did="nekomimi.pet" />
+
</section>
+
<section style={panelStyle}>
+
<h3 style={sectionHeaderStyle}>
+
teal.fm Song History
+
</h3>
+
<p
+
style={{
+
fontSize: 12,
+
color: `var(--demo-text-secondary)`,
+
margin: "0 0 8px",
+
}}
+
>
+
Listening history with album art focus
+
</p>
+
<SongHistoryList did="nekomimi.pet" limit={6} />
+
</section>
</div>
<div style={columnStackStyle}>
<section style={panelStyle}>
···
Reply Post Demo
</h3>
<BlueskyPost
-
did="nekomimi.pet"
-
rkey="3m36jkng6nk22"
+
did="did:plc:xwhsmuozq3mlsp56dyd7copv"
+
rkey="3m3je5ydg4s2o"
showParent={true}
recursiveParent={true}
/>
-
<section />
+
</section>
+
<section style={panelStyle}>
+
<h3 style={sectionHeaderStyle}>
+
Rich Text Facets Demo
+
</h3>
+
<p
+
style={{
+
fontSize: 12,
+
color: `var(--demo-text-secondary)`,
+
margin: "0 0 8px",
+
}}
+
>
+
Post with mentions, links, and hashtags
+
</p>
+
<BlueskyPost
+
did="nekomimi.pet"
+
rkey="3m45s553cys22"
+
showParent={false}
+
/>
+
</section>
+
<section style={panelStyle}>
+
<TangledRepo
+
did="did:plc:ttdrpj45ibqunmfhdsb4zdwq"
+
rkey="3m2sx5zpxzs22"
+
/>
</section>
<section style={panelStyle}>
<h3 style={sectionHeaderStyle}>
···
style={codeTextStyle}
>
{prefetchedDataSnippet}
+
</code>
+
</pre>
+
<p
+
style={{
+
color: `var(--demo-text-secondary)`,
+
margin: "16px 0 8px",
+
}}
+
>
+
Use atcute directly to construct records and pass them to
+
componentsโ€”fully compatible!
+
</p>
+
<pre style={codeBlockStyle}>
+
<code className="language-tsx" style={codeTextStyle}>
+
{atcuteUsageSnippet}
</code>
</pre>
</section>
+2 -1
tsconfig.lib.json
···
"declarationDir": "dist-lib",
"sourceMap": true,
"outDir": "./lib-dist",
-
"rootDir": "./lib"
+
"rootDir": "./lib",
+
"types": ["@atcute/bluesky", "@atcute/tangled"]
},
"include": ["lib/**/*.ts", "lib/**/*.tsx"]
}
+2 -3
vite.config.ts
···
rollupOptions: {
input: resolve(__dirname, 'index.html')
},
-
sourcemap: true
+
sourcemap: false
} : {
// Library build configuration
lib: {
entry: resolve(__dirname, 'lib/index.ts'),
+
cssFileName: resolve(__dirname, 'lib/styles.css'),
name: 'atproto-ui',
formats: ['es'],
fileName: 'atproto-ui'
···
}
}
},
-
sourcemap: true,
-
minify: false
}
});