diff --git a/.github/workflows/deploy-worker.yml b/.github/workflows/deploy-particleprocessor.yml similarity index 93% rename from .github/workflows/deploy-worker.yml rename to .github/workflows/deploy-particleprocessor.yml index a190cf3..55c047b 100644 --- a/.github/workflows/deploy-worker.yml +++ b/.github/workflows/deploy-particleprocessor.yml @@ -17,6 +17,6 @@ jobs: deploy: uses: ./.github/workflows/_deploy.yml with: - module: worker + module: particleprocessor environment: ${{ inputs.environment }} secrets: inherit diff --git a/go/Dockerfile.particleprocessorworker b/go/Dockerfile.particleprocessorworker index f68a2ac..385a93c 100644 --- a/go/Dockerfile.particleprocessorworker +++ b/go/Dockerfile.particleprocessorworker @@ -14,6 +14,8 @@ RUN ls FROM alpine:latest AS second-stage +RUN apk add --no-cache ffmpeg ca-certificates + WORKDIR /app COPY --from=first-stage /app/cmd/particleprocessorworker . RUN echo "copied over binary to production stage" diff --git a/go/cmd/particleprocessorworker/main.go b/go/cmd/particleprocessorworker/main.go index 6642d42..fe58a22 100644 --- a/go/cmd/particleprocessorworker/main.go +++ b/go/cmd/particleprocessorworker/main.go @@ -1,11 +1,13 @@ package main import ( + "bytes" "context" "fmt" "log" "log/slog" "os" + "os/exec" "strings" "time" @@ -107,6 +109,7 @@ func main() { updateParentLastChildCreatedAt(ctx, change.Doc) transcribeMediaParticle(ctx, depotSvc, speechSvc, change.Doc) + transcodeMediaParticle(ctx, depotSvc, change.Doc) recordFreemiumUsage(ctx, billingSvc, change.Doc) if err := processingRepo.MarkProcessed(ctx, particleID); err != nil { @@ -162,6 +165,152 @@ func transcribeMediaParticle(ctx context.Context, depotSvc depot.Service, speech slog.Info("transcribed media particle", "particleID", doc.Ref.ID) } +// transcodeMediaParticle produces an iOS-playable MP4/m4a derivative for media +// particles whose original mime type AVPlayer can't decode (notably the WebM +// the desktop recorder emits today). Skips when the source is already in an +// iOS-playable family or when a transcoded variant has already been written. +// +// ffmpeg reads directly from the GCS signed URL and writes to a local temp +// file — `+faststart` requires seekable output, so a stdout pipe wouldn't work. +// The temp file is then streamed to GCS via depotSvc.CreateFromReader (no +// presigned-PUT round-trip — the worker has direct SDK access). +func transcodeMediaParticle(ctx context.Context, depotSvc depot.Service, doc *firestore.DocumentSnapshot) { + var mediaParticle particle.FirestoreMediaParticle + if err := doc.DataTo(&mediaParticle); err != nil { + slog.Error("transcode: unable to marshal particle data", "error", err) + return + } + + particleType, err := particle.ParseParticleType(mediaParticle.Type) + if err != nil { + slog.Error("transcode: invalid particle type", "error", err) + return + } + if particleType != particle.TypeMedia { + slog.Info("transcode: particle is not of type media") + return + } + + // Already transcoded — re-delivery within the 5-min Firestore window. + if mediaParticle.Properties.TranscodedObjectId != "" { + return + } + + // Source is already iOS-playable; nothing to do. + if isIOSPlayableMime(mediaParticle.Properties.MimeType) { + slog.Info("transcode: skipping because already playable on ios") + return + } + + sourceURL, err := depotSvc.GetDownloadURL(ctx, mediaParticle.Properties.ObjectId) + if err != nil { + slog.Error("transcode: failed to get download URL", "error", err, "object_id", mediaParticle.Properties.ObjectId) + return + } + + isAudio := strings.HasPrefix(mediaParticle.Properties.MimeType, "audio/") + var outputExt, outputMime string + if isAudio { + outputExt = ".m4a" + outputMime = "audio/mp4" + } else { + outputExt = ".mp4" + outputMime = "video/mp4" + } + + tmp, err := os.CreateTemp("", "transcode-*"+outputExt) + if err != nil { + slog.Error("transcode: failed to create temp file", "error", err) + return + } + tmpPath := tmp.Name() + tmp.Close() + defer os.Remove(tmpPath) + + transcodeCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) + defer cancel() + + var args []string + if isAudio { + args = []string{ + "-y", "-i", sourceURL, + "-vn", + "-c:a", "aac", "-b:a", "128k", + "-movflags", "+faststart", + tmpPath, + } + } else { + // Cap encoder parallelism and lookahead to keep memory bounded — screen + // recordings come in at native display resolution (often 1440p–4K) and + // libx264's per-thread lookahead/reference buffers blow past the worker's + // memory limit otherwise. Output is also downscaled to 1080p max, which + // mobile playback won't notice; the original WebM stays in GCS untouched. + args = []string{ + "-y", "-i", sourceURL, + "-vf", "scale='min(1920,iw)':-2:flags=lanczos", + "-c:v", "libx264", "-preset", "veryfast", "-crf", "23", + "-pix_fmt", "yuv420p", "-profile:v", "baseline", "-level", "3.1", + "-x264-params", "rc-lookahead=20:ref=2", + "-threads", "2", "-filter_threads", "2", + "-c:a", "aac", "-b:a", "128k", + "-movflags", "+faststart", + tmpPath, + } + } + + cmd := exec.CommandContext(transcodeCtx, "ffmpeg", args...) + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + slog.Error("transcode: ffmpeg failed", "error", err, "stderr", stderr.String(), "particleID", doc.Ref.ID) + return + } + + networkID, err := networkIDFromParticlePath(doc.Ref.Path) + if err != nil { + slog.Error("transcode: failed to derive network id", "error", err, "path", doc.Ref.Path) + return + } + + f, err := os.Open(tmpPath) + if err != nil { + slog.Error("transcode: failed to open transcoded file", "error", err, "path", tmpPath) + return + } + defer f.Close() + + newObj, err := depotSvc.CreateFromReader(ctx, depot.CreateFromReaderInput{ + Prefix: networkID, + Name: "transcoded" + outputExt, + ContentType: outputMime, + }, f) + if err != nil { + slog.Error("transcode: failed to upload transcoded object", "error", err, "particleID", doc.Ref.ID) + return + } + + _, err = doc.Ref.Set(ctx, map[string]interface{}{ + "properties": map[string]interface{}{ + "transcoded_object_id": newObj.ID, + "transcoded_mime_type": outputMime, + }, + }, firestore.MergeAll) + if err != nil { + slog.Error("transcode: failed to update particle in firestore", "error", err, "particleID", doc.Ref.ID) + return + } + + slog.Info("transcoded media particle", "particleID", doc.Ref.ID, "transcoded_object_id", newObj.ID, "mime", outputMime) +} + +func isIOSPlayableMime(mime string) bool { + switch mime { + case "video/mp4", "video/quicktime", "audio/mp4", "audio/aac", "audio/x-m4a", "audio/mpeg": + return true + } + return false +} + func toFirestoreTranscript(result *speech.TranscriptResult) particle.FirestoreTranscript { words := make([]particle.FirestoreTranscriptWord, len(result.Words)) for i, w := range result.Words { diff --git a/go/go.mod b/go/go.mod index 7a0ec0f..2f54f7f 100644 --- a/go/go.mod +++ b/go/go.mod @@ -177,6 +177,7 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.42.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect @@ -184,3 +185,5 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.110.1 // indirect ) + +tool go.uber.org/mock/mockgen diff --git a/go/go.sum b/go/go.sum index 0a93052..7ef5333 100644 --- a/go/go.sum +++ b/go/go.sum @@ -477,6 +477,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= diff --git a/go/internal/billing/service.go b/go/internal/billing/service.go index f2bfa65..fabea1a 100644 --- a/go/internal/billing/service.go +++ b/go/internal/billing/service.go @@ -15,7 +15,7 @@ import ( stripesub "github.com/stripe/stripe-go/v85/subscription" ) -//go:generate mockgen -source ./service.go -destination ./mocks/service.go +//go:generate go tool mockgen -source ./service.go -destination ./mocks/service.go type Service interface { GetStatus(ctx context.Context, networkID string) (*Status, error) diff --git a/go/internal/depot/models.go b/go/internal/depot/models.go index 3f11d1e..8042a72 100644 --- a/go/internal/depot/models.go +++ b/go/internal/depot/models.go @@ -29,6 +29,15 @@ type PrepareUploadResult struct { UploadHeaders map[string]string } +// CreateFromReaderInput is for server-side direct uploads (no presigned URL). +// Used by background workers that already have the bytes on hand and don't +// need a client round-trip. +type CreateFromReaderInput struct { + Prefix string // Optional prefix for organizing objects (e.g., network_id) + Name string + ContentType string +} + // Config holds configuration for the depot service type Config struct { GoogleServiceAccountEmail string diff --git a/go/internal/depot/service.go b/go/internal/depot/service.go index a9c1290..4026552 100644 --- a/go/internal/depot/service.go +++ b/go/internal/depot/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "log/slog" "time" @@ -20,6 +21,7 @@ const ( type Service interface { PrepareUpload(ctx context.Context, input PrepareUploadInput) (*PrepareUploadResult, error) ConfirmUpload(ctx context.Context, objectID string) (*Object, error) + CreateFromReader(ctx context.Context, input CreateFromReaderInput, body io.Reader) (*Object, error) GetByID(ctx context.Context, objectID string) (*Object, error) GetDownloadURL(ctx context.Context, objectID string) (string, error) Delete(ctx context.Context, objectID string) error @@ -155,6 +157,56 @@ func (s *serviceImpl) ConfirmUpload(ctx context.Context, objectID string) (*Obje return s.repo.getByID(ctx, objectID) } +// CreateFromReader streams bytes directly to GCS using the storage client and +// records the depot_objects row in one shot. Unlike PrepareUpload, there is no +// signed URL or client round-trip — the caller already has the bytes. Intended +// for worker-side flows (e.g. transcoded media variants). +func (s *serviceImpl) CreateFromReader(ctx context.Context, input CreateFromReaderInput, body io.Reader) (*Object, error) { + if input.Name == "" { + return nil, errors.Join(ErrInvalidInput, errors.New("name is required")) + } + if input.ContentType == "" { + return nil, errors.Join(ErrInvalidInput, errors.New("content_type is required")) + } + + objectKey := fmt.Sprintf("%s/%s/%s", input.Prefix, uuid.New().String(), input.Name) + + w := s.storageClient.Bucket(s.bucketName).Object(objectKey).NewWriter(ctx) + w.ContentType = input.ContentType + if _, err := io.Copy(w, body); err != nil { + // Close to release resources, then surface the original copy error. + if cerr := w.Close(); cerr != nil { + slog.Warn("failed to close GCS writer after copy failure", "error", cerr, "object_key", objectKey) + } + slog.Error("failed to stream object to GCS", "error", err, "bucket", s.bucketName, "object_key", objectKey) + return nil, err + } + if err := w.Close(); err != nil { + slog.Error("failed to close GCS writer", "error", err, "bucket", s.bucketName, "object_key", objectKey) + return nil, err + } + + obj := &Object{ + Name: input.Name, + ContentType: input.ContentType, + ContentLength: w.Attrs().Size, + BucketName: s.bucketName, + ObjectKey: objectKey, + ContainsContent: true, + } + + created, err := s.repo.create(ctx, obj) + if err != nil { + // Best-effort: clean up the GCS object since we can't track it in the DB. + if delErr := s.storageClient.Bucket(s.bucketName).Object(objectKey).Delete(ctx); delErr != nil { + slog.Warn("failed to clean up GCS object after db create failure", "error", delErr, "object_key", objectKey) + } + return nil, err + } + + return created, nil +} + func (s *serviceImpl) GetByID(ctx context.Context, objectID string) (*Object, error) { obj, err := s.repo.getByID(ctx, objectID) if err != nil { diff --git a/go/internal/livestore/membershippublisher.go b/go/internal/livestore/membershippublisher.go index b922a01..a9d47f3 100644 --- a/go/internal/livestore/membershippublisher.go +++ b/go/internal/livestore/membershippublisher.go @@ -6,7 +6,7 @@ import ( "cloud.google.com/go/firestore" ) -//go:generate mockgen -source ./membershippublisher.go -destination ./mocks/membershippublisher.go +//go:generate go tool mockgen -source ./membershippublisher.go -destination ./mocks/membershippublisher.go // MembershipPublisher publishes network membership changes to the live store // (Firestore) that clients subscribe to. Postgres remains the source of truth; diff --git a/go/internal/particle/firestore_types.go b/go/internal/particle/firestore_types.go index fd93ff1..16085d5 100644 --- a/go/internal/particle/firestore_types.go +++ b/go/internal/particle/firestore_types.go @@ -35,11 +35,13 @@ type FirestoreTranscript struct { } type FirestoreMediaParticleProperties struct { - ObjectId string `firestore:"object_id"` - MimeType string `firestore:"mime_type"` - DurationMs int `firestore:"duration_ms"` - SizeBytes int `firestore:"size_bytes"` - Transcript *FirestoreTranscript `firestore:"transcript,omitempty"` + ObjectId string `firestore:"object_id"` + MimeType string `firestore:"mime_type"` + DurationMs int `firestore:"duration_ms"` + SizeBytes int `firestore:"size_bytes"` + Transcript *FirestoreTranscript `firestore:"transcript,omitempty"` + TranscodedObjectId string `firestore:"transcoded_object_id,omitempty"` + TranscodedMimeType string `firestore:"transcoded_mime_type,omitempty"` } type FirestoreStreamParticle struct { diff --git a/go/internal/particle/networkmembershipchecker.go b/go/internal/particle/networkmembershipchecker.go index 3d7bd7b..7886d1e 100644 --- a/go/internal/particle/networkmembershipchecker.go +++ b/go/internal/particle/networkmembershipchecker.go @@ -2,7 +2,7 @@ package particle import "context" -//go:generate mockgen -source ./networkmembershipchecker.go -destination ./mocks/networkmembershipchecker.go +//go:generate go tool mockgen -source ./networkmembershipchecker.go -destination ./mocks/networkmembershipchecker.go type NetworkMembershipChecker interface { // IsMember returns true if the humanId is a member of the network. diff --git a/go/internal/testhelper/mocks/aero/gen.go b/go/internal/testhelper/mocks/aero/gen.go index d4de8e5..0be8b88 100644 --- a/go/internal/testhelper/mocks/aero/gen.go +++ b/go/internal/testhelper/mocks/aero/gen.go @@ -3,4 +3,4 @@ // directive lives here rather than next to the source. package aero -//go:generate mockgen -destination ./mock_aero.go -package aero github.com/flowy-live/llink/genproto/aero PrimaryClient +//go:generate go tool mockgen -destination ./mock_aero.go -package aero github.com/flowy-live/llink/genproto/aero PrimaryClient diff --git a/go/k8s/dev/particleprocessorworker.yaml b/go/k8s/dev/particleprocessorworker.yaml index 6657255..7a69497 100644 --- a/go/k8s/dev/particleprocessorworker.yaml +++ b/go/k8s/dev/particleprocessorworker.yaml @@ -21,11 +21,11 @@ spec: image: "particleprocessorworker" resources: requests: - memory: "52Mi" - cpu: 50m + memory: "1Gi" + cpu: 500m limits: - memory: "52Mi" - cpu: 50m + memory: "1Gi" + cpu: 500m env: - name: "GCP_PROJECT" value: "flowy-dev-440017" diff --git a/go/k8s/prod/particleprocessorworker.yaml b/go/k8s/prod/particleprocessorworker.yaml index 7fca618..41d7082 100644 --- a/go/k8s/prod/particleprocessorworker.yaml +++ b/go/k8s/prod/particleprocessorworker.yaml @@ -18,11 +18,11 @@ spec: image: "particleprocessorworker" resources: requests: - memory: "52Mi" - cpu: 50m + memory: "1Gi" + cpu: 500m limits: - memory: "52Mi" - cpu: 50m + memory: "1Gi" + cpu: 500m env: - name: "GCP_PROJECT" value: "flowy-prod-440017" diff --git a/go/skaffold.yaml b/go/skaffold.yaml index 1669737..613f22c 100644 --- a/go/skaffold.yaml +++ b/go/skaffold.yaml @@ -59,7 +59,7 @@ profiles: apiVersion: skaffold/v4beta11 kind: Config metadata: - name: worker + name: particleprocessor build: local: {} tagPolicy: diff --git a/js/desktop/src/api/types.ts b/js/desktop/src/api/types.ts index 42e014a..1ab02d1 100644 --- a/js/desktop/src/api/types.ts +++ b/js/desktop/src/api/types.ts @@ -123,6 +123,11 @@ export const MediaPropertiesSchema = z.object({ size_bytes: z.number(), transcript: TranscriptSchema.optional(), source: z.enum(["camera", "screen"]).optional(), + // Set by the particle processor worker once an iOS-playable MP4/m4a variant + // has been produced from a non-iOS-playable original (e.g. WebM from desktop). + // When present, clients should prefer these over object_id/mime_type for playback. + transcoded_object_id: z.string().optional(), + transcoded_mime_type: z.string().optional(), }); export type MediaProperties = z.infer; diff --git a/js/desktop/src/features/particles/media-particle-view.tsx b/js/desktop/src/features/particles/media-particle-view.tsx index d08b145..2efad22 100644 --- a/js/desktop/src/features/particles/media-particle-view.tsx +++ b/js/desktop/src/features/particles/media-particle-view.tsx @@ -33,12 +33,18 @@ export const MediaParticleView = forwardRef(null); const audioRef = useRef(null); - const isAudio = particle.properties.mime_type?.startsWith("audio/"); + const isAudio = activeMime?.startsWith("audio/"); useImperativeHandle(ref, () => ({ seek(deltaSec: number) { @@ -153,7 +159,7 @@ export const MediaParticleView = forwardRef {transcript && ( diff --git a/js/mobile/.gitignore b/js/mobile/.gitignore new file mode 100644 index 0000000..1d3894a --- /dev/null +++ b/js/mobile/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +.expo/ +dist/ +ios/ +android/ +*.log +.DS_Store +.env +.env.local +expo-env.d.ts diff --git a/js/mobile/app.config.ts b/js/mobile/app.config.ts new file mode 100644 index 0000000..6d7b634 --- /dev/null +++ b/js/mobile/app.config.ts @@ -0,0 +1,62 @@ +import type { ExpoConfig } from "expo/config"; + +const APP_ENV = (process.env.EXPO_PUBLIC_APP_ENV ?? "dev") as "dev" | "prod"; + +const config: ExpoConfig = { + name: "Flowy", + slug: "flowy", + version: "0.1.0", + orientation: "portrait", + icon: "./assets/icon.png", + scheme: "flowy", + userInterfaceStyle: "automatic", + newArchEnabled: true, + splash: { + image: "./assets/icon.png", + resizeMode: "contain", + backgroundColor: "#000000", + }, + ios: { + supportsTablet: true, + bundleIdentifier: "ai.flowylabs.llink", + infoPlist: { + ITSAppUsesNonExemptEncryption: false, + }, + }, + plugins: [ + [ + "expo-build-properties", + { + ios: { + deploymentTarget: "16.0", + }, + }, + ], + "expo-secure-store", + [ + "expo-camera", + { + cameraPermission: + "Flowy uses your camera to record video messages.", + microphonePermission: + "Flowy uses your microphone to record voice and video messages.", + recordAudioAndroid: true, + }, + ], + [ + "expo-audio", + { + microphonePermission: + "Flowy uses your microphone to record voice messages.", + }, + ], + ], + experiments: { + typedRoutes: false, + }, + extra: { + appEnv: APP_ENV, + }, +}; + +export default config; diff --git a/js/mobile/assets/flowy.icns b/js/mobile/assets/flowy.icns new file mode 100644 index 0000000..310b5ef Binary files /dev/null and b/js/mobile/assets/flowy.icns differ diff --git a/js/mobile/assets/flowy.ico b/js/mobile/assets/flowy.ico new file mode 100644 index 0000000..f097dba Binary files /dev/null and b/js/mobile/assets/flowy.ico differ diff --git a/js/mobile/assets/flowy.iconset/icon_128x128.png b/js/mobile/assets/flowy.iconset/icon_128x128.png new file mode 100644 index 0000000..48bed99 Binary files /dev/null and b/js/mobile/assets/flowy.iconset/icon_128x128.png differ diff --git a/js/mobile/assets/flowy.iconset/icon_128x128@2x.png b/js/mobile/assets/flowy.iconset/icon_128x128@2x.png new file mode 100644 index 0000000..4af3689 Binary files /dev/null and b/js/mobile/assets/flowy.iconset/icon_128x128@2x.png differ diff --git a/js/mobile/assets/flowy.iconset/icon_16x16.png b/js/mobile/assets/flowy.iconset/icon_16x16.png new file mode 100644 index 0000000..a084a02 Binary files /dev/null and b/js/mobile/assets/flowy.iconset/icon_16x16.png differ diff --git a/js/mobile/assets/flowy.iconset/icon_16x16@2x.png b/js/mobile/assets/flowy.iconset/icon_16x16@2x.png new file mode 100644 index 0000000..67ed0d3 Binary files /dev/null and b/js/mobile/assets/flowy.iconset/icon_16x16@2x.png differ diff --git a/js/mobile/assets/flowy.iconset/icon_256x256.png b/js/mobile/assets/flowy.iconset/icon_256x256.png new file mode 100644 index 0000000..4af3689 Binary files /dev/null and b/js/mobile/assets/flowy.iconset/icon_256x256.png differ diff --git a/js/mobile/assets/flowy.iconset/icon_256x256@2x.png b/js/mobile/assets/flowy.iconset/icon_256x256@2x.png new file mode 100644 index 0000000..6b338f5 Binary files /dev/null and b/js/mobile/assets/flowy.iconset/icon_256x256@2x.png differ diff --git a/js/mobile/assets/flowy.iconset/icon_32x32.png b/js/mobile/assets/flowy.iconset/icon_32x32.png new file mode 100644 index 0000000..67ed0d3 Binary files /dev/null and b/js/mobile/assets/flowy.iconset/icon_32x32.png differ diff --git a/js/mobile/assets/flowy.iconset/icon_32x32@2x.png b/js/mobile/assets/flowy.iconset/icon_32x32@2x.png new file mode 100644 index 0000000..d05c053 Binary files /dev/null and b/js/mobile/assets/flowy.iconset/icon_32x32@2x.png differ diff --git a/js/mobile/assets/flowy.iconset/icon_512x512.png b/js/mobile/assets/flowy.iconset/icon_512x512.png new file mode 100644 index 0000000..6b338f5 Binary files /dev/null and b/js/mobile/assets/flowy.iconset/icon_512x512.png differ diff --git a/js/mobile/assets/icon.png b/js/mobile/assets/icon.png new file mode 100644 index 0000000..a4e96a5 Binary files /dev/null and b/js/mobile/assets/icon.png differ diff --git a/js/mobile/assets/icon_green.png b/js/mobile/assets/icon_green.png new file mode 100644 index 0000000..009ff0c Binary files /dev/null and b/js/mobile/assets/icon_green.png differ diff --git a/js/mobile/assets/icon_red.png b/js/mobile/assets/icon_red.png new file mode 100644 index 0000000..da43505 Binary files /dev/null and b/js/mobile/assets/icon_red.png differ diff --git a/js/mobile/assets/images/FLOWY-4.svg b/js/mobile/assets/images/FLOWY-4.svg new file mode 100644 index 0000000..044b26e --- /dev/null +++ b/js/mobile/assets/images/FLOWY-4.svg @@ -0,0 +1,3 @@ + + + diff --git a/js/mobile/assets/sound.wav b/js/mobile/assets/sound.wav new file mode 100644 index 0000000..68abf03 Binary files /dev/null and b/js/mobile/assets/sound.wav differ diff --git a/js/mobile/assets/sounds/click.mp3 b/js/mobile/assets/sounds/click.mp3 new file mode 100644 index 0000000..4fd472c Binary files /dev/null and b/js/mobile/assets/sounds/click.mp3 differ diff --git a/js/mobile/babel.config.js b/js/mobile/babel.config.js new file mode 100644 index 0000000..4c12ec8 --- /dev/null +++ b/js/mobile/babel.config.js @@ -0,0 +1,10 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: [ + ["babel-preset-expo", { jsxImportSource: "nativewind" }], + "nativewind/babel", + ], + plugins: ["react-native-worklets/plugin"], + }; +}; diff --git a/js/mobile/global.css b/js/mobile/global.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/js/mobile/global.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/js/mobile/index.ts b/js/mobile/index.ts new file mode 100644 index 0000000..f45fcf4 --- /dev/null +++ b/js/mobile/index.ts @@ -0,0 +1,5 @@ +import "./global.css"; +import { registerRootComponent } from "expo"; +import App from "./src/App"; + +registerRootComponent(App); diff --git a/js/mobile/metro.config.js b/js/mobile/metro.config.js new file mode 100644 index 0000000..b0963fe --- /dev/null +++ b/js/mobile/metro.config.js @@ -0,0 +1,6 @@ +const { getDefaultConfig } = require("expo/metro-config"); +const { withNativeWind } = require("nativewind/metro"); + +const config = getDefaultConfig(__dirname); + +module.exports = withNativeWind(config, { input: "./global.css" }); diff --git a/js/mobile/nativewind-env.d.ts b/js/mobile/nativewind-env.d.ts new file mode 100644 index 0000000..a13e313 --- /dev/null +++ b/js/mobile/nativewind-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/js/mobile/package.json b/js/mobile/package.json new file mode 100644 index 0000000..b3de624 --- /dev/null +++ b/js/mobile/package.json @@ -0,0 +1,51 @@ +{ + "name": "flowy-mobile", + "version": "0.1.0", + "private": true, + "main": "index.ts", + "scripts": { + "start": "expo start", + "ios": "expo run:ios --device", + "android": "expo run:android", + "compile": "tsc --noEmit", + "lint": "expo lint" + }, + "packageManager": "yarn@1.22.22", + "dependencies": { + "@react-native-async-storage/async-storage": "2.2.0", + "@react-navigation/native": "^7.0.14", + "@react-navigation/native-stack": "^7.2.0", + "@tanstack/react-query": "^5.90.21", + "clsx": "^2.1.1", + "expo": "~54.0.0", + "expo-audio": "~1.0.13", + "expo-camera": "~17.0.10", + "expo-constants": "~18.0.13", + "expo-file-system": "~19.0.16", + "expo-haptics": "~15.0.7", + "expo-secure-store": "~15.0.8", + "expo-status-bar": "~3.0.9", + "expo-video": "~3.0.10", + "firebase": "^12.10.0", + "lucide-react-native": "^0.575.0", + "nativewind": "^4.1.23", + "react": "19.1.0", + "react-native": "0.81.5", + "react-native-gesture-handler": "~2.28.0", + "react-native-reanimated": "~4.1.1", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0", + "react-native-svg": "15.12.1", + "react-native-worklets": "0.5.1", + "sonner-native": "^0.21.0", + "tailwind-merge": "^3.5.0", + "zod": "^4.3.6", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@types/react": "~19.1.0", + "expo-build-properties": "~1.0.10", + "tailwindcss": "^3.4.17", + "typescript": "~5.9.0" + } +} diff --git a/js/mobile/src/App.tsx b/js/mobile/src/App.tsx new file mode 100644 index 0000000..f932e26 --- /dev/null +++ b/js/mobile/src/App.tsx @@ -0,0 +1,40 @@ +import { useEffect } from "react"; +import { StatusBar } from "expo-status-bar"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { NavigationContainer } from "@react-navigation/native"; +import { + initialWindowMetrics, + SafeAreaProvider, +} from "react-native-safe-area-context"; +import { Toaster } from "sonner-native"; +import { createQueryClient } from "@/lib/query-client"; +import { PusherProvider } from "@/lib/pusher-provider"; +import { RootNavigator } from "@/navigation/RootNavigator"; +import { useAuthStore } from "@/stores/auth-store"; + +const queryClient = createQueryClient(); + +export default function App() { + const restoreSession = useAuthStore((s) => s.restoreSession); + + useEffect(() => { + void restoreSession(); + }, [restoreSession]); + + return ( + + + + + + + + + + + + + + ); +} diff --git a/js/mobile/src/api/client.ts b/js/mobile/src/api/client.ts new file mode 100644 index 0000000..2095d79 --- /dev/null +++ b/js/mobile/src/api/client.ts @@ -0,0 +1,270 @@ +import { appConfig } from "@/config/env"; +import { useSessionStore } from "@/stores/session-store"; +import { ApiError } from "@/lib/errors"; +import type { z } from "zod"; +import { + BillingStatusSchema, + CheckoutSessionResponseSchema, + DepotObjectSchema, + FirebaseTokenResponseSchema, + GetLivekitTokenResponseSchema, + HumanSchema, + ListInvitationsResponseSchema, + ListNetworksResponseSchema, + NetworkSchema, + NetworkUsageSchema, + PortalSessionResponseSchema, + PrepareUploadResponseSchema, + SignInResponseSchema, +} from "./types"; +import type { + AcceptInvitationRequest, + AddMembersRequest, + BillingCadence, + CreateNetworkRequest, + PrepareUploadRequest, + RequestCodeRequest, + RevokeInvitationRequest, + SignInRequest, +} from "./types"; + +interface ApiClientConfig { + baseUrl: string; + getToken: () => string | null; + onUnauthorized: () => void; +} + +class ApiClient { + private config: ApiClientConfig; + + constructor(config: ApiClientConfig) { + this.config = config; + } + + private async fetch( + method: string, + path: string, + body?: unknown, + ): Promise { + const headers: Record = {}; + + if (body) { + headers["Content-Type"] = "application/json"; + } + + const token = this.config.getToken(); + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + + const response = await fetch(`${this.config.baseUrl}${path}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + if (response.status === 401) { + this.config.onUnauthorized(); + throw new ApiError(401, "Unauthorized"); + } + + if (!response.ok) { + const text = await response.text().catch(() => "Unknown error"); + throw new ApiError(response.status, text); + } + + return response; + } + + private async request( + schema: z.ZodType, + method: string, + path: string, + body?: unknown, + ): Promise { + const response = await this.fetch(method, path, body); + const json = await response.json(); + return schema.parse(json); + } + + private async requestVoid( + method: string, + path: string, + body?: unknown, + ): Promise { + await this.fetch(method, path, body); + } + + // --- Auth --- + + async requestCode(data: RequestCodeRequest): Promise { + await this.requestVoid("POST", "/auth/request-code", data); + } + + async signIn(data: SignInRequest) { + return this.request(SignInResponseSchema, "POST", "/auth/sign-in", data); + } + + async me() { + return this.request(HumanSchema, "GET", "/auth/me"); + } + + async signOut(): Promise { + await this.requestVoid("POST", "/auth/sign-out"); + } + + async getFirebaseToken() { + return this.request( + FirebaseTokenResponseSchema, + "POST", + "/auth/firebase-token", + ); + } + + // TODO: security: require passing in the particle id once api deprecates this + async getParticleDownloadUrl(objectId: string): Promise { + const response = await this.fetch( + "GET", + `/particles/${objectId}/download`, + ); + const data = await response.json(); + return data.url; + } + + // --- Settings --- + + async updateSettings(data: { + email_notifications_enabled?: boolean; + }): Promise { + await this.requestVoid("PATCH", "/humans/me/settings", data); + } + + // --- Depot --- + + async prepareUpload(data: PrepareUploadRequest) { + return this.request( + PrepareUploadResponseSchema, + "POST", + "/depot/upload", + data, + ); + } + + async confirmUpload(objectId: string) { + return this.request( + DepotObjectSchema, + "POST", + `/depot/objects/${objectId}/confirm`, + ); + } + + // --- Networks --- + + async listNetworks() { + return this.request(ListNetworksResponseSchema, "GET", "/networks"); + } + + async createNetwork(data: CreateNetworkRequest) { + return this.request(NetworkSchema, "POST", "/networks", data); + } + + async getNetwork(id: string) { + return this.request(NetworkSchema, "GET", `/networks/${id}`); + } + + async addMembers(networkId: string, data: AddMembersRequest): Promise { + await this.requestVoid("POST", `/networks/${networkId}/members`, data); + } + + async removeMember(networkId: string, humanId: string): Promise { + await this.requestVoid( + "DELETE", + `/networks/${networkId}/members/${humanId}`, + ); + } + + // --- Invitations --- + + async listNetworkInvitations(networkId: string) { + return this.request( + ListInvitationsResponseSchema, + "GET", + `/networks/${networkId}/invitations`, + ); + } + + async listMyInvitations() { + return this.request(ListInvitationsResponseSchema, "GET", "/invitations"); + } + + async acceptInvitation(data: AcceptInvitationRequest): Promise { + await this.requestVoid("POST", "/invitations/accept", data); + } + + async revokeInvitation( + networkId: string, + data: RevokeInvitationRequest, + ): Promise { + await this.requestVoid( + "DELETE", + `/networks/${networkId}/invitations`, + data, + ); + } + + // --- LiveKit --- + + async getLivekitToken(networkId: string, streamId: string) { + return this.request( + GetLivekitTokenResponseSchema, + "POST", + "/livekit/token", + { network_id: networkId, stream_id: streamId }, + ); + } + + // --- Billing (network admin only) --- + + async getNetworkBilling(networkId: string) { + return this.request( + BillingStatusSchema, + "GET", + `/networks/${networkId}/billing`, + ); + } + + async createCheckoutSession(networkId: string, cadence: BillingCadence) { + return this.request( + CheckoutSessionResponseSchema, + "POST", + `/networks/${networkId}/billing/checkout-session`, + { cadence }, + ); + } + + async createPortalSession(networkId: string) { + return this.request( + PortalSessionResponseSchema, + "POST", + `/networks/${networkId}/billing/portal-session`, + ); + } + + async getNetworkUsage(networkId: string) { + return this.request( + NetworkUsageSchema, + "GET", + `/networks/${networkId}/usage`, + ); + } +} + +export const apiClient = new ApiClient({ + baseUrl: appConfig.orionUrl, + getToken: () => useSessionStore.getState().token, + // SecureStore writes are async; we fire-and-forget so the throwing + // request doesn't have to wait for persistence to finish. + onUnauthorized: () => { + void useSessionStore.getState().clearToken(); + }, +}); diff --git a/js/mobile/src/api/types.ts b/js/mobile/src/api/types.ts new file mode 100644 index 0000000..1ab02d1 --- /dev/null +++ b/js/mobile/src/api/types.ts @@ -0,0 +1,324 @@ +import { z } from "zod"; + +export const HumanSchema = z.object({ + id: z.string(), + created_at: z.coerce.date(), + email: z.string().email(), + email_prefix: z.string(), + email_notifications_enabled: z.boolean(), +}); + +export type Human = z.infer; + +export const NetworkSchema = z.object({ + id: z.string(), + name: z.string(), + admin_human: HumanSchema, + humans: z.array(HumanSchema), + created_at: z.coerce.date(), +}); + +export type Network = z.infer; + +export const ListNetworksResponseSchema = z.array(NetworkSchema); +export type ListNetworksResponse = z.infer; + +// --- Network request/response types --- + +const CreateNetworkRequestSchema = z.object({ + name: z.string(), +}); +export type CreateNetworkRequest = z.infer; + +const AddMembersRequestSchema = z.object({ + email_addresses: z.array(z.string().email()), +}); +export type AddMembersRequest = z.infer; + +// --- Invitation types --- + +export const InvitationSchema = z.object({ + network_id: z.string(), + network_name: z.string(), + email: z.string(), + created_at: z.coerce.date(), +}); +export type Invitation = z.infer; + +export const ListInvitationsResponseSchema = z.array(InvitationSchema); + +export type AcceptInvitationRequest = { network_id: string }; +export type RevokeInvitationRequest = { email: string }; + +// --- Depot types --- + +const PrepareUploadRequestSchema = z.object({ + network_id: z.string(), + name: z.string(), + content_type: z.string(), + content_length: z.number(), +}); +export type PrepareUploadRequest = z.infer; + +export const PrepareUploadResponseSchema = z.object({ + object_id: z.string(), + upload_url: z.string(), + upload_headers: z.record(z.string(), z.string()), +}); +export type PrepareUploadResponse = z.infer; + +export const DepotObjectSchema = z.object({ + id: z.string(), + name: z.string(), + content_type: z.string(), + content_length: z.number(), + contains_content: z.boolean(), + created_at: z.coerce.date(), +}); +export type DepotObject = z.infer; + +// --- Particle property schemas --- + +export const StreamPropertiesSchema = z.object({ + name: z.string(), + description: z.string().optional(), +}); +export type StreamProperties = z.infer; + +export const FolderPropertiesSchema = z.object({ + name: z.string(), + color: z.string().optional(), +}); +export type FolderProperties = z.infer; + +const TranscriptWordSchema = z.object({ + word: z.string(), + start: z.number(), + end: z.number(), +}); + +const TranscriptSentenceSchema = z.object({ + text: z.string(), + start: z.number(), + end: z.number(), +}); + +const TranscriptParagraphSchema = z.object({ + sentences: z.array(TranscriptSentenceSchema), + start: z.number(), + end: z.number(), +}); + +export const TranscriptSchema = z.object({ + transcript: z.string(), + words: z.array(TranscriptWordSchema), + paragraphs: z.array(TranscriptParagraphSchema), +}); +export type Transcript = z.infer; + +export const MediaPropertiesSchema = z.object({ + object_id: z.string(), + mime_type: z.string(), + duration_ms: z.number(), + size_bytes: z.number(), + transcript: TranscriptSchema.optional(), + source: z.enum(["camera", "screen"]).optional(), + // Set by the particle processor worker once an iOS-playable MP4/m4a variant + // has been produced from a non-iOS-playable original (e.g. WebM from desktop). + // When present, clients should prefer these over object_id/mime_type for playback. + transcoded_object_id: z.string().optional(), + transcoded_mime_type: z.string().optional(), +}); +export type MediaProperties = z.infer; + +export const FilePropertiesSchema = z.object({ + object_id: z.string(), + filename: z.string(), + mime_type: z.string(), + size_bytes: z.number(), +}); +export type FileProperties = z.infer; + +export const TextPropertiesSchema = z.object({ + content: z.string(), + edited_at: z.coerce.date().optional(), +}); +export type TextProperties = z.infer; + +export const QuestPropertiesSchema = z.object({ + title: z.string(), + description: z.string(), + status: z.string().optional(), + // humanId + assigned_to: z.string().optional(), +}); +export type QuestProperties = z.infer; + +export const PaperPropertiesSchema = z.object({ + title: z.string(), + content: z.string(), +}); +export type PaperProperties = z.infer; + +// --- Reactions --- + +export const ReactionsSchema = z.record(z.string(), z.array(z.string())).optional(); +export type Reactions = z.infer; + +// --- Tombstone (soft-delete) --- + +// Fields added to non-container particles when their creator deletes them. +// We keep the doc around so concurrent viewers can see a "This particle was +// deleted" message in place, rather than being jumped to the next particle. +const TombstoneFields = { + deleted_at: z.coerce.date().optional(), + deleted_by_human_id: z.string().optional(), +}; + +export const REACTION_EMOJIS = ["\u{1F44D}", "\u{2764}\u{FE0F}", "\u{1F525}", "\u{1F440}", "\u{2705}", "\u{2753}", "\u{1F602}"] as const; + +export interface ParticlePropertiesMap { + stream: StreamProperties; + folder: FolderProperties; + media: MediaProperties; + file: FileProperties; + text: TextProperties; + quest: QuestProperties; + paper: PaperProperties; +} + +// --- Unified Particle types --- + +const ParticleBaseSchema = z.object({ + id: z.string(), + created_at: z.coerce.date(), + created_by_human_id: z.string(), + updated_at: z.coerce.date().optional(), +}); + +export const ParticleSchema = z.discriminatedUnion("type", [ + ParticleBaseSchema.extend({ + type: z.literal("stream"), + properties: StreamPropertiesSchema, + // e.g. ["human:human_xxxx", "human:human_yyyy"] - visible only to Aron and John + // e.g. ["network:xywx"] - visible to everyone in the network + visible_to: z.array(z.string()), + // Marks human_id to their `playback_position_at`: where they left off in a conversation + playback_markers: z.record(z.string(), z.coerce.date()).optional(), + // Timestamp of the most recent child particle + // used for sorting streams by recent activity without needing to query subcollections + last_child_created_at: z.coerce.date().optional(), + // Array of humanIds currently in the huddle (updated via LiveKit webhooks) + huddle_active_participants: z.array(z.string()).optional(), + status: z.enum(["open", "closed"]).optional(), + }), + ParticleBaseSchema.extend({ + type: z.literal("folder"), properties: FolderPropertiesSchema, + // e.g. ["human:human_xxxx", "human:human_yyyy"] - visible only to Aron and John + // e.g. ["network:123"] - visible to everyone in the network + visible_to: z.array(z.string()), + }), + ParticleBaseSchema.extend({ type: z.literal("media"), properties: MediaPropertiesSchema, reactions: ReactionsSchema, ...TombstoneFields }), + ParticleBaseSchema.extend({ type: z.literal("file"), properties: FilePropertiesSchema, ...TombstoneFields }), + ParticleBaseSchema.extend({ type: z.literal("text"), properties: TextPropertiesSchema, reactions: ReactionsSchema, ...TombstoneFields }), + ParticleBaseSchema.extend({ type: z.literal("quest"), properties: QuestPropertiesSchema, ...TombstoneFields }), + ParticleBaseSchema.extend({ type: z.literal("paper"), properties: PaperPropertiesSchema, ...TombstoneFields }), +]); + +export type Particle = z.infer; + +export type ParticleType = Particle["type"]; + +/** Container types can have children subcollections */ +export const CONTAINER_TYPES: ReadonlySet = new Set(["stream", "folder"]); + +export function isContainerType(type: ParticleType): boolean { + return CONTAINER_TYPES.has(type); +} + +/** True when a non-container particle has been soft-deleted (tombstoned). */ +export function isParticleDeleted(particle: Particle): boolean { + return "deleted_at" in particle && particle.deleted_at != null; +} + +// --- LiveKit types --- + +export const GetLivekitTokenResponseSchema = z.object({ + token: z.string(), + server_url: z.string(), +}); +export type GetLivekitTokenResponse = z.infer; + +// --- Auth types --- + +const RequestCodeRequestSchema = z.object({ + email: z.string().email(), +}); +export type RequestCodeRequest = z.infer; + +const SignInRequestSchema = z.object({ + email: z.string().email(), + code: z.string(), +}); +export type SignInRequest = z.infer; + +export const SignInResponseSchema = z.object({ + human: HumanSchema, + token: z.string(), +}); +export type SignInResponse = z.infer; + +export const FirebaseTokenResponseSchema = z.object({ + token: z.string(), +}); +export type FirebaseTokenResponse = z.infer; + +// --- Billing types --- + +export const BillingCadenceSchema = z.enum(["monthly", "annual"]); +export type BillingCadence = z.infer; + +export const NetworkPlanSchema = z.enum(["free", "pro"]); +export type NetworkPlan = z.infer; + +// Mirrors Stripe subscription.status plus "active" as the default free-tier value. +export const BillingPlanStatusSchema = z.enum([ + "active", + "trialing", + "past_due", + "canceled", + "incomplete", + "incomplete_expired", + "unpaid", +]); +export type BillingPlanStatus = z.infer; + +export const BillingStatusSchema = z.object({ + plan: NetworkPlanSchema, + plan_status: BillingPlanStatusSchema, + cadence: BillingCadenceSchema.nullable(), + seats: z.number().int(), + current_period_end: z.coerce.date().nullable(), + cancel_at_period_end: z.boolean(), + price_monthly_cents: z.number().int(), + price_annual_cents: z.number().int(), +}); +export type BillingStatus = z.infer; + +export const CheckoutSessionResponseSchema = z.object({ + url: z.string().url(), +}); +export type CheckoutSessionResponse = z.infer; + +export const PortalSessionResponseSchema = z.object({ + url: z.string().url(), +}); +export type PortalSessionResponse = z.infer; + +export const NetworkUsageSchema = z.object({ + plan: NetworkPlanSchema, + used: z.number().int().nonnegative(), + limit: z.number().int().nonnegative().nullable(), + reset_at: z.coerce.date(), +}); +export type NetworkUsage = z.infer; diff --git a/js/mobile/src/components/Avatar.tsx b/js/mobile/src/components/Avatar.tsx new file mode 100644 index 0000000..bced118 --- /dev/null +++ b/js/mobile/src/components/Avatar.tsx @@ -0,0 +1,60 @@ +import { Text, View } from "react-native"; +import type { Human } from "@/api/types"; +import { resolveHumanDisplay } from "@/lib/humans"; +import { cn } from "@/lib/utils"; + +type Size = "xs" | "sm" | "md"; + +interface AvatarProps { + humanId: string | null | undefined; + humans: Human[] | undefined; + size?: Size; + /** True for online presence — adds a green ring (matches desktop). */ + online?: boolean; + /** Background ring used to separate stacked avatars from the chrome. */ + stackBg?: string; + className?: string; +} + +const sizeMap: Record = { + xs: { box: "h-6 w-6", text: "text-[9px]", ring: 1.5 }, + sm: { box: "h-9 w-9", text: "text-xs", ring: 2 }, + md: { box: "h-10 w-10", text: "text-sm", ring: 2 }, +}; + +/** + * Initials avatar with optional online ring (green) and an optional outer + * stack ring used to visually separate overlapping avatars on a busy chrome. + * Matches desktop's avatar + presence pattern (`ring-2 ring-green-500`). + */ +export function Avatar({ + humanId, + humans, + size = "sm", + online = false, + stackBg, + className, +}: AvatarProps) { + const { initials } = resolveHumanDisplay(humanId, humans); + const dims = sizeMap[size]; + + return ( + + + {initials} + + + ); +} diff --git a/js/mobile/src/components/BottomSheet.tsx b/js/mobile/src/components/BottomSheet.tsx new file mode 100644 index 0000000..b6ec381 --- /dev/null +++ b/js/mobile/src/components/BottomSheet.tsx @@ -0,0 +1,174 @@ +import { useEffect, useState } from "react"; +import { + Dimensions, + KeyboardAvoidingView, + Modal, + Platform, + Pressable, + View, +} from "react-native"; +import { + initialWindowMetrics, + SafeAreaProvider, + SafeAreaView, +} from "react-native-safe-area-context"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import Animated, { + Easing, + Extrapolation, + interpolate, + runOnJS, + useAnimatedStyle, + useSharedValue, + withSpring, + withTiming, +} from "react-native-reanimated"; + +const SCREEN_HEIGHT = Dimensions.get("window").height; +const ANIMATION_MS = 240; + +interface BottomSheetProps { + open: boolean; + onClose: () => void; + /** When true, wrap content in KeyboardAvoidingView so the sheet floats above the keyboard. */ + avoidKeyboard?: boolean; + /** + * Cap on the sheet's height. Defaults to 85%; pass a string like "60%" or + * a number of px when content has a stable footprint. + */ + maxHeight?: number | `${number}%`; + children: React.ReactNode; +} + +/** + * Shared modal sheet shell. Handles slide-in animation, backdrop fade, + * drag-to-dismiss, and modal-safe SafeAreaProvider seeding so iOS modals get + * correct insets on the first frame. The drag handle at the top is rendered + * here too, so callers don't need to draw it themselves. + */ +export function BottomSheet({ + open, + onClose, + avoidKeyboard = false, + maxHeight = "85%", + children, +}: BottomSheetProps) { + // Mount slightly past `open` so the slide-in animation has its starting + // position rendered, and the slide-out animation can play before unmount. + const [mounted, setMounted] = useState(false); + const translateY = useSharedValue(SCREEN_HEIGHT); + + useEffect(() => { + if (open) { + setMounted(true); + requestAnimationFrame(() => { + translateY.value = withSpring(0, { + damping: 24, + stiffness: 260, + mass: 0.7, + }); + }); + } else if (mounted) { + translateY.value = withTiming( + SCREEN_HEIGHT, + { duration: ANIMATION_MS, easing: Easing.in(Easing.cubic) }, + (finished) => { + if (finished) runOnJS(setMounted)(false); + }, + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); + + const sheetPan = Gesture.Pan() + .activeOffsetY(10) + .failOffsetX([-25, 25]) + .onUpdate((e) => { + "worklet"; + translateY.value = Math.max(0, e.translationY); + }) + .onEnd((e) => { + "worklet"; + if (e.translationY > 120 || e.velocityY > 800) { + runOnJS(onClose)(); + } else { + translateY.value = withSpring(0, { + damping: 24, + stiffness: 260, + mass: 0.7, + }); + } + }); + + const sheetStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: translateY.value }], + })); + + const backdropStyle = useAnimatedStyle(() => { + const opacity = interpolate( + translateY.value, + [0, SCREEN_HEIGHT * 0.7], + [0.55, 0], + Extrapolation.CLAMP, + ); + return { opacity }; + }); + + if (!mounted) return null; + + const Wrapper = avoidKeyboard ? KeyboardAvoidingView : View; + const wrapperProps = avoidKeyboard + ? { behavior: Platform.OS === "ios" ? ("padding" as const) : undefined } + : {}; + + return ( + + + + + + + + + + + + + + + {children} + + + + + + + + ); +} diff --git a/js/mobile/src/components/ComposingIndicator.tsx b/js/mobile/src/components/ComposingIndicator.tsx new file mode 100644 index 0000000..fcc62ea --- /dev/null +++ b/js/mobile/src/components/ComposingIndicator.tsx @@ -0,0 +1,95 @@ +import { useEffect } from "react"; +import { Text, View } from "react-native"; +import Animated, { + Easing, + useAnimatedStyle, + useSharedValue, + withRepeat, + withTiming, +} from "react-native-reanimated"; +import type { Human } from "@/api/types"; +import { resolveHumanDisplay } from "@/lib/humans"; +import type { ComposingUser } from "@/features/stream-view/stream-presence-context"; + +interface ComposingIndicatorProps { + users: ComposingUser[]; + networkHumans?: Human[]; +} + +/** + * Slim horizontal pill stack pinned just under the metadata header. Each + * pill is the typing/recording indicator for a single user. Rendered above + * the particle canvas with a translucent background so it reads on any + * media. Mobile equivalent of desktop's vertical writing-mode indicator. + */ +export function ComposingIndicator({ + users, + networkHumans, +}: ComposingIndicatorProps) { + if (users.length === 0) return null; + + return ( + + {users.map((u) => { + const { displayName } = resolveHumanDisplay(u.humanId, networkHumans); + const label = + u.mode === "recording" + ? `${displayName} is recording` + : u.mode === "screen" + ? `${displayName} is sharing` + : `${displayName} is typing`; + + return ( + + + + {label} + + + ); + })} + + ); +} + +function BouncingDots() { + return ( + + + + + + ); +} + +function Dot({ delay }: { delay: number }) { + const y = useSharedValue(0); + + useEffect(() => { + const start = setTimeout(() => { + y.value = withRepeat( + withTiming(-3, { + duration: 360, + easing: Easing.inOut(Easing.quad), + }), + -1, + true, + ); + }, delay); + return () => clearTimeout(start); + }, [delay, y]); + + const style = useAnimatedStyle(() => ({ + transform: [{ translateY: y.value }], + })); + + return ( + + ); +} diff --git a/js/mobile/src/components/RelativeTimestamp.tsx b/js/mobile/src/components/RelativeTimestamp.tsx new file mode 100644 index 0000000..53d60f6 --- /dev/null +++ b/js/mobile/src/components/RelativeTimestamp.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react"; +import { Text, type TextProps } from "react-native"; +import { formatDistanceToNow } from "@/lib/time-utils"; + +const MINUTE_MS = 60_000; + +interface RelativeTimestampProps extends Omit { + date: Date; +} + +/** + * Re-renders once per minute so labels like "5m ago" stay accurate without + * any per-card timer wiring at the call site. + */ +export function RelativeTimestamp({ date, ...rest }: RelativeTimestampProps) { + const [, force] = useState(0); + + useEffect(() => { + const interval = setInterval(() => force((n) => n + 1), MINUTE_MS); + return () => clearInterval(interval); + }, []); + + return {formatDistanceToNow(date)}; +} diff --git a/js/mobile/src/config/env.ts b/js/mobile/src/config/env.ts new file mode 100644 index 0000000..b54fe17 --- /dev/null +++ b/js/mobile/src/config/env.ts @@ -0,0 +1,63 @@ +import Constants from "expo-constants"; + +// Expo-side equivalent of desktop's __APP_ENV__ build-time replacement +// (see js/desktop/src/config/env.ts). On mobile we read from app.config.ts +// `extra.appEnv`, which itself reads `process.env.EXPO_PUBLIC_APP_ENV` at +// build time. Defaults to "dev". +// +// Firebase web config is public by design (security is enforced via +// Firestore rules + App Check), so both configs live in source. To refresh, +// run: cd infra/gcp/{dev,prod} && terraform output -json firebase_config + +type FirebaseConfig = { + apiKey: string; + appId: string; + authDomain: string; + messagingSenderId: string; + projectId: string; + storageBucket: string; +}; + +type AppConfig = { + orionUrl: string; + pusherUrl: string; + firebase: FirebaseConfig; + /** Empty string disables Sentry. Same DSN across envs; events are split by `environment` tag. */ + sentryDsn: string; +}; + +const configs: Record<"dev" | "prod", AppConfig> = { + dev: { + orionUrl: "https://orion.dev.flowy.live", + pusherUrl: "wss://pusher.dev.flowy.live/ws", + firebase: { + apiKey: "AIzaSyDF_fbk7tDY9tNxUiOwwi--2nYZWZygMGk", + appId: "1:1006580076785:web:e2a0736d60a78e02b15950", + authDomain: "flowy-dev-440017.firebaseapp.com", + messagingSenderId: "1006580076785", + projectId: "flowy-dev-440017", + storageBucket: "flowy-dev-440017.firebasestorage.app", + }, + sentryDsn: + "https://4bb3af832825929eeec8ab4edd56930f@o4511282312118272.ingest.us.sentry.io/4511282313494528", + }, + prod: { + orionUrl: "https://orion.flowy.live", + pusherUrl: "wss://pusher.flowy.live/ws", + firebase: { + apiKey: "AIzaSyB8it3SKr9DHScHGlpu4uwWicvt8wzjdBg", + appId: "1:68063426854:web:5054f16f50898f5706e9e7", + authDomain: "flowy-prod-440017.firebaseapp.com", + messagingSenderId: "68063426854", + projectId: "flowy-prod-440017", + storageBucket: "flowy-prod-440017.firebasestorage.app", + }, + sentryDsn: + "https://4bb3af832825929eeec8ab4edd56930f@o4511282312118272.ingest.us.sentry.io/4511282313494528", + }, +}; + +const rawEnv = (Constants.expoConfig?.extra as { appEnv?: string } | undefined) + ?.appEnv; +export const appEnv: "dev" | "prod" = rawEnv === "prod" ? "prod" : "dev"; +export const appConfig: AppConfig = configs[appEnv]; diff --git a/js/mobile/src/features/auth/SignInScreen.tsx b/js/mobile/src/features/auth/SignInScreen.tsx new file mode 100644 index 0000000..ab85b2d --- /dev/null +++ b/js/mobile/src/features/auth/SignInScreen.tsx @@ -0,0 +1,193 @@ +import { useState } from "react"; +import { + KeyboardAvoidingView, + Platform, + Pressable, + Text, + TextInput, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useAuthStore } from "@/stores/auth-store"; + +type Step = "email" | "code"; + +export function SignInScreen() { + const [step, setStep] = useState("email"); + const [email, setEmail] = useState(""); + + return ( + + + + {step === "email" ? ( + { + setEmail(submittedEmail); + setStep("code"); + }} + /> + ) : ( + setStep("email")} /> + )} + + + + ); +} + +function EmailStep({ onCodeSent }: { onCodeSent: (email: string) => void }) { + const [email, setEmail] = useState(""); + const isRequestingCode = useAuthStore((s) => s.isRequestingCode); + const error = useAuthStore((s) => s.error); + const requestCode = useAuthStore((s) => s.requestCode); + const clearError = useAuthStore((s) => s.clearError); + + const submit = async () => { + try { + await requestCode(email); + onCodeSent(email); + } catch { + // Error surfaced via the store + } + }; + + const disabled = isRequestingCode || email.trim().length === 0; + + return ( + + + Sign in + + Enter your email to receive a sign-in code. + + + + + Email + { + setEmail(text); + if (error) clearError(); + }} + placeholder="you@example.com" + placeholderTextColor="#878787" + keyboardType="email-address" + autoCapitalize="none" + autoCorrect={false} + autoComplete="email" + textContentType="emailAddress" + autoFocus + returnKeyType="go" + onSubmitEditing={submit} + className="border-input text-foreground rounded-lg border bg-background px-4 py-3 text-base" + /> + + + {error ? ( + {error} + ) : null} + + + + {isRequestingCode ? "Sending..." : "Continue"} + + + + ); +} + +function CodeStep({ email, onBack }: { email: string; onBack: () => void }) { + const [code, setCode] = useState(""); + const isSigningIn = useAuthStore((s) => s.isSigningIn); + const error = useAuthStore((s) => s.error); + const signIn = useAuthStore((s) => s.signIn); + const clearError = useAuthStore((s) => s.clearError); + + const submit = async () => { + try { + await signIn(email, code); + } catch { + // Error surfaced via the store; keep the screen visible + } + }; + + const disabled = isSigningIn || code.trim().length === 0; + + return ( + + + + Check your email + + + We sent a code to{" "} + {email}. + + + + + Code + { + setCode(text); + if (error) clearError(); + }} + placeholder="Enter code" + placeholderTextColor="#878787" + keyboardType="number-pad" + autoFocus + returnKeyType="go" + textContentType="oneTimeCode" + onSubmitEditing={submit} + className="border-input text-foreground rounded-lg border bg-background px-4 py-3 text-base tracking-widest" + /> + + + {error ? ( + {error} + ) : null} + + + + + {isSigningIn ? "Signing in..." : "Sign in"} + + + + + Back + + + + + ); +} diff --git a/js/mobile/src/features/compose/AudioRecordingOverlay.tsx b/js/mobile/src/features/compose/AudioRecordingOverlay.tsx new file mode 100644 index 0000000..c0bf7db --- /dev/null +++ b/js/mobile/src/features/compose/AudioRecordingOverlay.tsx @@ -0,0 +1,125 @@ +import { useEffect, useRef } from "react"; +import { Pressable, StyleSheet, Text, View } from "react-native"; +import { Mic } from "lucide-react-native"; +import { + RecordingPresets, + setAudioModeAsync, + useAudioRecorder, + useAudioRecorderState, +} from "expo-audio"; +import { logError } from "@/lib/errors"; + +const MAX_DURATION_S = 60; + +interface AudioRecordingOverlayProps { + onComplete: (result: { uri: string; durationMs: number }) => void; + onCancel: () => void; +} + +export function AudioRecordingOverlay({ + onComplete, + onCancel, +}: AudioRecordingOverlayProps) { + const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY); + const state = useAudioRecorderState(recorder, 250); + const finalizedRef = useRef(false); + + useEffect(() => { + let active = true; + (async () => { + try { + await setAudioModeAsync({ + allowsRecording: true, + playsInSilentMode: true, + }); + await recorder.prepareToRecordAsync(); + if (!active) return; + recorder.record(); + } catch (err) { + logError(err, { scope: "compose.audio.start" }); + if (active) onCancel(); + } + })(); + + return () => { + active = false; + if (!finalizedRef.current) { + finalizedRef.current = true; + recorder.stop().catch(() => {}); + } + void setAudioModeAsync({ + allowsRecording: false, + playsInSilentMode: true, + }).catch((err) => logError(err, { scope: "compose.audio.exit" })); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const elapsedMs = state.durationMillis ?? 0; + + useEffect(() => { + if (elapsedMs >= MAX_DURATION_S * 1000 && !finalizedRef.current) { + void finish("commit"); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [elapsedMs]); + + const finish = async (kind: "commit" | "cancel") => { + if (finalizedRef.current) return; + finalizedRef.current = true; + const durationMs = state.durationMillis ?? 0; + try { + await recorder.stop(); + } catch (err) { + logError(err, { scope: "compose.audio.stop" }); + } + if (kind === "cancel") { + onCancel(); + return; + } + const uri = recorder.uri; + if (!uri) { + onCancel(); + return; + } + onComplete({ uri, durationMs }); + }; + + const elapsedSec = Math.floor(elapsedMs / 1000); + + return ( + + + + + + + + {state.isRecording ? "Recording" : "Starting…"} + + + {elapsedSec.toString().padStart(2, "0")}s · max {MAX_DURATION_S}s + + + + void finish("cancel")} + accessibilityLabel="Cancel recording" + className="rounded-full bg-white/15 px-6 py-3" + > + Cancel + + void finish("commit")} + accessibilityLabel="Stop recording" + className="rounded-full bg-white px-7 py-3" + > + Stop + + + + ); +} diff --git a/js/mobile/src/features/compose/ComposeDock.tsx b/js/mobile/src/features/compose/ComposeDock.tsx new file mode 100644 index 0000000..4b2f80a --- /dev/null +++ b/js/mobile/src/features/compose/ComposeDock.tsx @@ -0,0 +1,298 @@ +import { useCallback, useEffect, useState } from "react"; +import { Pressable, Text, View } from "react-native"; +import { Mic, Type as TypeIcon, Video as VideoIcon } from "lucide-react-native"; +import * as Haptics from "expo-haptics"; +import { + useCameraPermissions, + useMicrophonePermissions, +} from "expo-camera"; +import { toast } from "sonner-native"; +import { cn } from "@/lib/utils"; +import { useEvent } from "@/hooks/use-event"; +import { usePlaybackPauseStore } from "@/stores/playback-pause-store"; +import { useAuthStore } from "@/stores/auth-store"; +import { + createTextParticle, + uploadMediaParticle, +} from "@/lib/upload"; +import type { ParticlePath } from "@/lib/particle-path"; +import { + useStreamComposingBroadcast, + type ComposingMode, +} from "@/features/stream-view/stream-presence-context"; +import { TextComposeModal } from "./TextComposeModal"; +import { VideoRecordingOverlay } from "./VideoRecordingOverlay"; +import { AudioRecordingOverlay } from "./AudioRecordingOverlay"; +import { ReviewSheet } from "./ReviewSheet"; + +type RecordingMode = "video" | "audio"; + +type ComposeUiState = + | { kind: "idle" } + | { kind: "recording"; mode: RecordingMode } + | { + kind: "review"; + mode: RecordingMode; + uri: string; + durationMs: number; + } + | { kind: "uploading" }; + +interface SubmitMediaParams { + fileUri: string; + mimeType: string; + durationMs: number; + source: "camera" | "screen"; +} + +interface ComposeDockProps { + networkId: string; + targetPath: ParticlePath; + silentPresence?: boolean; + submitMedia?: (params: SubmitMediaParams) => Promise; + submitText?: (content: string) => Promise; +} + +export function ComposeDock({ + networkId, + targetPath, + silentPresence = false, + submitMedia, + submitText: submitTextOverride, +}: ComposeDockProps) { + const userId = useAuthStore((s) => s.user?.id); + + const [mode, setMode] = useState("video"); + const [ui, setUi] = useState({ kind: "idle" }); + const [textOpen, setTextOpen] = useState(false); + + const [camPerm, requestCamPerm] = useCameraPermissions(); + const [micPerm, requestMicPerm] = useMicrophonePermissions(); + + // Tell StreamView to fully unmount its expo-video player while we record. + // That player otherwise holds the iOS AVAudioSession and crashes the camera. + const setComposing = usePlaybackPauseStore((s) => s.setComposing); + const isComposing = ui.kind !== "idle" || textOpen; + useEffect(() => { + setComposing(isComposing); + return () => setComposing(false); + }, [isComposing, setComposing]); + + useComposingBroadcast({ ui, textOpen, silent: silentPresence }); + + const ensurePermissions = useCallback( + async (forVideo: boolean): Promise => { + if (forVideo) { + const cam = camPerm?.granted ? camPerm : await requestCamPerm(); + if (!cam.granted) { + toast.error("Camera permission is required to record video."); + return false; + } + } + const mic = micPerm?.granted ? micPerm : await requestMicPerm(); + if (!mic.granted) { + toast.error("Microphone permission is required to record."); + return false; + } + return true; + }, + [camPerm, micPerm, requestCamPerm, requestMicPerm], + ); + + const startRecording = useEvent(async () => { + if (ui.kind !== "idle") return; + const ok = await ensurePermissions(mode === "video"); + if (!ok) return; + void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + setUi({ kind: "recording", mode }); + }); + + const handleRecordingComplete = useCallback( + ({ uri, durationMs }: { uri: string; durationMs: number }) => { + void Haptics.selectionAsync(); + setUi((prev) => { + const m = "mode" in prev ? prev.mode : mode; + return { kind: "review", mode: m, uri, durationMs }; + }); + }, + [mode], + ); + + const handleRecordingCancel = useCallback(() => { + setUi({ kind: "idle" }); + }, []); + + const sendReview = useEvent(async () => { + if (ui.kind !== "review" || !userId) return; + const captured = ui; + setUi({ kind: "uploading" }); + try { + const mimeType = + captured.mode === "audio" ? "audio/mp4" : "video/mp4"; + if (submitMedia) { + await submitMedia({ + fileUri: captured.uri, + mimeType, + durationMs: captured.durationMs, + source: "camera", + }); + } else { + await uploadMediaParticle({ + networkId, + targetPath, + fileUri: captured.uri, + mimeType, + durationMs: captured.durationMs, + source: "camera", + createdByHumanId: userId, + }); + } + void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + setUi({ kind: "idle" }); + } catch (err) { + void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + setUi(captured); + throw err; + } + }); + + const retake = useCallback(() => setUi({ kind: "idle" }), []); + const cancelReview = useCallback(() => setUi({ kind: "idle" }), []); + + const submitText = useEvent(async (content: string) => { + if (!userId) throw new Error("Not signed in."); + if (submitTextOverride) { + await submitTextOverride(content); + } else { + await createTextParticle({ + networkId, + targetPath, + content, + createdByHumanId: userId, + }); + } + void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + }); + + const dockHidden = + ui.kind === "review" || + ui.kind === "uploading" || + ui.kind === "recording"; + + return ( + <> + {!dockHidden ? ( + + + + setMode((m) => (m === "video" ? "audio" : "video")) + } + disabled={ui.kind !== "idle"} + accessibilityLabel={`Switch to ${ + mode === "video" ? "audio" : "video" + } mode`} + className={cn( + "h-11 w-11 items-center justify-center rounded-full bg-white/15", + ui.kind !== "idle" && "opacity-40", + )} + > + {mode === "video" ? ( + + ) : ( + + )} + + + + + + + + Tap to record + + + + setTextOpen(true)} + disabled={ui.kind !== "idle"} + accessibilityLabel="Compose text" + className={cn( + "h-11 w-11 items-center justify-center rounded-full bg-white/15", + ui.kind !== "idle" && "opacity-40", + )} + > + + + + + ) : null} + + {ui.kind === "recording" ? ( + ui.mode === "video" ? ( + + ) : ( + + ) + ) : null} + + + + setTextOpen(false)} + onSubmit={submitText} + /> + + ); +} + +function useComposingBroadcast({ + ui, + textOpen, + silent, +}: { + ui: ComposeUiState; + textOpen: boolean; + silent: boolean; +}) { + let broadcast: ReturnType | null; + try { + broadcast = useStreamComposingBroadcast(); + } catch { + broadcast = null; + } + + const mode: ComposingMode | null = + ui.kind === "recording" ? "recording" : textOpen ? "typing" : null; + + useEffect(() => { + if (silent || !broadcast) return; + if (mode) { + broadcast.startComposing(mode); + return () => broadcast?.stopComposing(); + } + }, [mode, silent, broadcast]); +} diff --git a/js/mobile/src/features/compose/ReviewSheet.tsx b/js/mobile/src/features/compose/ReviewSheet.tsx new file mode 100644 index 0000000..47352ea --- /dev/null +++ b/js/mobile/src/features/compose/ReviewSheet.tsx @@ -0,0 +1,154 @@ +import { useEffect, useState } from "react"; +import { Modal, Pressable, Text, View } from "react-native"; +import { + initialWindowMetrics, + SafeAreaProvider, + SafeAreaView, +} from "react-native-safe-area-context"; +import { useVideoPlayer, VideoView } from "expo-video"; +import { Mic } from "lucide-react-native"; +import { toast } from "sonner-native"; +import { cn } from "@/lib/utils"; +import { toUserMessage } from "@/lib/errors"; + +interface ReviewSheetProps { + open: boolean; + /** Local file URI from the recorder. */ + uri: string | null; + mode: "video" | "audio" | null; + durationMs: number; + onSend: () => Promise; + onRetake: () => void; + onCancel: () => void; +} + +/** + * Loop-plays the just-recorded clip and offers Retake / Send. WhatsApp-style + * confirmation: any send-failure surfaces a toast and keeps the sheet open + * so the user doesn't lose their take. + */ +export function ReviewSheet({ + open, + uri, + mode, + durationMs, + onSend, + onRetake, + onCancel, +}: ReviewSheetProps) { + const player = useVideoPlayer(uri ?? "", (p) => { + p.loop = true; + p.muted = false; + p.audioMixingMode = "mixWithOthers"; + }); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + if (open && uri) { + player.play(); + } + }, [open, uri, player]); + + const handleSend = async () => { + if (submitting) return; + setSubmitting(true); + try { + await onSend(); + } catch (err) { + toast.error(toUserMessage(err)); + setSubmitting(false); + } + }; + + const seconds = Math.max(1, Math.round(durationMs / 1000)); + + return ( + + + + {uri ? ( + mode === "audio" ? ( + + + + + + Voice message · {seconds}s + + + Tap send to share, or retake. + + + + + + ) : ( + + ) + ) : null} + + + + + Cancel + + + + + + + + Retake + + + + + {submitting ? "Sending..." : "Send"} + + + + + + + + ); +} diff --git a/js/mobile/src/features/compose/TextComposeModal.tsx b/js/mobile/src/features/compose/TextComposeModal.tsx new file mode 100644 index 0000000..307a783 --- /dev/null +++ b/js/mobile/src/features/compose/TextComposeModal.tsx @@ -0,0 +1,152 @@ +import { useEffect, useRef, useState } from "react"; +import { + KeyboardAvoidingView, + Modal, + Platform, + Pressable, + Text, + TextInput, + View, +} from "react-native"; +import { + initialWindowMetrics, + SafeAreaProvider, + SafeAreaView, +} from "react-native-safe-area-context"; +import { toast } from "sonner-native"; +import { cn } from "@/lib/utils"; +import { toUserMessage } from "@/lib/errors"; + +const IMMERSIVE_CHAR_LIMIT = 120; + +function getImmersiveStyle(length: number) { + if (length === 0) + return { className: "text-3xl font-semibold leading-snug" }; + if (length < 30) + return { className: "text-5xl font-semibold leading-tight" }; + if (length < 70) + return { className: "text-3xl font-semibold leading-snug" }; + return { className: "text-2xl font-normal leading-snug" }; +} + +interface TextComposeModalProps { + open: boolean; + onClose: () => void; + /** + * Submit handler — must throw if the upload fails so the modal can re-show + * the editor and the user doesn't lose their text. + */ + onSubmit: (content: string) => Promise; +} + +/** + * Immersive full-screen text editor. Mirrors desktop's `text-editor.tsx`: + * dynamic font ramp at 30/70/130 chars, no markdown preview, no gradient. + * Long messages scroll inside the multiline TextInput. Pauses upstream + * playback (the host wraps render in a useSuspendPlayback while open). + */ +export function TextComposeModal({ + open, + onClose, + onSubmit, +}: TextComposeModalProps) { + const [content, setContent] = useState(""); + const [submitting, setSubmitting] = useState(false); + const inputRef = useRef(null); + + // Reset whenever the modal opens fresh. + useEffect(() => { + if (open) { + setContent(""); + setSubmitting(false); + // Re-focus on next tick; iOS occasionally drops the autoFocus call when + // the modal animation is mid-flight. + const t = setTimeout(() => inputRef.current?.focus(), 60); + return () => clearTimeout(t); + } + }, [open]); + + const trimmed = content.trim(); + const canSend = trimmed.length > 0 && !submitting; + + const handleSend = async () => { + if (!canSend) return; + setSubmitting(true); + try { + await onSubmit(trimmed); + onClose(); + } catch (err) { + toast.error(toUserMessage(err)); + setSubmitting(false); + } + }; + + const style = getImmersiveStyle(trimmed.length); + const isImmersive = trimmed.length < IMMERSIVE_CHAR_LIMIT; + + return ( + + + + + + + Cancel + + + + {submitting ? "Sending..." : "Send"} + + + + + + + + + + + + ); +} diff --git a/js/mobile/src/features/compose/VideoRecordingOverlay.tsx b/js/mobile/src/features/compose/VideoRecordingOverlay.tsx new file mode 100644 index 0000000..459e8a4 --- /dev/null +++ b/js/mobile/src/features/compose/VideoRecordingOverlay.tsx @@ -0,0 +1,138 @@ +import { useEffect, useRef, useState } from "react"; +import { Pressable, StyleSheet, Text, View } from "react-native"; +import { CameraView } from "expo-camera"; +import { logError } from "@/lib/errors"; + +const MAX_DURATION_S = 60; + +interface VideoRecordingOverlayProps { + onComplete: (result: { uri: string; durationMs: number }) => void; + onCancel: () => void; +} + +export function VideoRecordingOverlay({ + onComplete, + onCancel, +}: VideoRecordingOverlayProps) { + const cameraRef = useRef(null); + const [cameraReady, setCameraReady] = useState(false); + const [recording, setRecording] = useState(false); + const [elapsedMs, setElapsedMs] = useState(0); + const startedAtRef = useRef(null); + const cancelledRef = useRef(false); + + useEffect(() => { + return () => { + cancelledRef.current = true; + }; + }, []); + + const startRecording = async () => { + const cam = cameraRef.current; + if (!cam || recording || !cameraReady) return; + + setRecording(true); + startedAtRef.current = Date.now(); + + let result: { uri: string } | undefined; + try { + result = await cam.recordAsync({ maxDuration: MAX_DURATION_S }); + } catch (err) { + if (cancelledRef.current) return; + logError(err, { scope: "compose.video.recordAsync" }); + onCancel(); + return; + } + if (cancelledRef.current) return; + + const durationMs = + startedAtRef.current !== null ? Date.now() - startedAtRef.current : 0; + if (result?.uri) { + onComplete({ uri: result.uri, durationMs }); + } else { + onCancel(); + } + }; + + const stopRecording = () => { + cameraRef.current?.stopRecording(); + }; + + const cancel = () => { + cancelledRef.current = true; + if (recording) { + cameraRef.current?.stopRecording(); + } + onCancel(); + }; + + useEffect(() => { + if (!recording) return; + const interval = setInterval(() => { + if (startedAtRef.current === null) return; + setElapsedMs(Date.now() - startedAtRef.current); + }, 250); + return () => clearInterval(interval); + }, [recording]); + + const elapsedSec = Math.floor(elapsedMs / 1000); + + return ( + + setCameraReady(true)} + /> + + {recording ? ( + + + + + REC · {elapsedSec.toString().padStart(2, "0")}s + + + + ) : null} + + + + Cancel + + {recording ? ( + + Stop + + ) : ( + + + + )} + + + ); +} diff --git a/js/mobile/src/features/networks/Drawer.tsx b/js/mobile/src/features/networks/Drawer.tsx new file mode 100644 index 0000000..dee8a6f --- /dev/null +++ b/js/mobile/src/features/networks/Drawer.tsx @@ -0,0 +1,167 @@ +import { useEffect, useRef } from "react"; +import { + Animated, + Dimensions, + Easing, + Modal, + Pressable, + Text, + View, +} from "react-native"; +import { + initialWindowMetrics, + SafeAreaProvider, + SafeAreaView, +} from "react-native-safe-area-context"; +import { useAuthStore } from "@/stores/auth-store"; + +const SCREEN_WIDTH = Dimensions.get("window").width; +const DRAWER_WIDTH = Math.min(320, Math.round(SCREEN_WIDTH * 0.82)); +const ANIM_MS = 220; + +interface DrawerProps { + open: boolean; + onClose: () => void; + onNavigateAccount: () => void; + onNavigateSettings: () => void; +} + +export function Drawer({ + open, + onClose, + onNavigateAccount, +}: DrawerProps) { + const translateX = useRef(new Animated.Value(-DRAWER_WIDTH)).current; + const backdropOpacity = useRef(new Animated.Value(0)).current; + + useEffect(() => { + Animated.parallel([ + Animated.timing(translateX, { + toValue: open ? 0 : -DRAWER_WIDTH, + duration: ANIM_MS, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }), + Animated.timing(backdropOpacity, { + toValue: open ? 0.4 : 0, + duration: ANIM_MS, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }), + ]).start(); + }, [open, translateX, backdropOpacity]); + + const user = useAuthStore((s) => s.user); + const signOut = useAuthStore((s) => s.signOut); + const isSigningOut = useAuthStore((s) => s.isSigningOut); + + const initials = user?.email_prefix.slice(0, 2).toUpperCase() ?? "??"; + + return ( + + {/* Modal mounts a separate native view tree on iOS — without a fresh + SafeAreaProvider seeded with initialWindowMetrics, useSafeAreaInsets + inside reports {0,0,0,0} on the first frame and content snaps from + the status bar down to the safe area once metrics resolve. */} + + + + + + + + + + + + {initials} + + + + + {user?.email_prefix ?? ""} + + + {user?.email ?? ""} + + + + + + { + onClose(); + onNavigateAccount(); + }} + /> + + + + { + void signOut(); + }} + tone="destructive" + /> + + + + + + + ); +} + +function DrawerRow({ + label, + onPress, + disabled, + tone = "default", +}: { + label: string; + onPress: () => void; + disabled?: boolean; + tone?: "default" | "destructive"; +}) { + return ( + + + {label} + + + ); +} diff --git a/js/mobile/src/features/networks/NetworkListScreen.tsx b/js/mobile/src/features/networks/NetworkListScreen.tsx new file mode 100644 index 0000000..9a824b6 --- /dev/null +++ b/js/mobile/src/features/networks/NetworkListScreen.tsx @@ -0,0 +1,136 @@ +import { useCallback, useState } from "react"; +import { + ActivityIndicator, + FlatList, + Pressable, + RefreshControl, + Text, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import type { Network } from "@/api/types"; +import { useNetworks } from "@/hooks/use-networks"; +import { useAuthStore } from "@/stores/auth-store"; +import { toUserMessage } from "@/lib/errors"; +import type { RootStackScreenProps } from "@/navigation/types"; +import { Drawer } from "./Drawer"; + +export function NetworkListScreen({ + navigation, +}: RootStackScreenProps<"NetworkList">) { + const [drawerOpen, setDrawerOpen] = useState(false); + const { data, isLoading, refetch, error } = useNetworks(); + const user = useAuthStore((s) => s.user); + const initials = user?.email_prefix.slice(0, 2).toUpperCase() ?? "??"; + + // Local refreshing state — driving RefreshControl from react-query's + // isRefetching can leave the native spinner visually stuck after the + // screen is detached/reattached by native-stack. + const [refreshing, setRefreshing] = useState(false); + const onRefresh = useCallback(async () => { + setRefreshing(true); + try { + await refetch(); + } finally { + setRefreshing(false); + } + }, [refetch]); + + return ( + + + setDrawerOpen(true)} + accessibilityLabel="Open menu" + className="bg-muted h-9 w-9 items-center justify-center rounded-full" + > + + {initials} + + + Flowy + + + + {isLoading ? ( + + + + ) : error ? ( + + + {toUserMessage(error)} + + refetch()} className="mt-3 px-4 py-2"> + Retry + + + ) : !data || data.length === 0 ? ( + + ) : ( + item.id} + contentContainerClassName="p-4 gap-2" + refreshControl={ + + } + renderItem={({ item }) => ( + + navigation.navigate("StreamList", { networkId: item.id }) + } + /> + )} + /> + )} + + setDrawerOpen(false)} + onNavigateAccount={() => navigation.navigate("Account")} + onNavigateSettings={() => navigation.navigate("Settings")} + /> + + ); +} + +function NetworkCard({ + network, + onPress, +}: { + network: Network; + onPress: () => void; +}) { + return ( + + + + {network.name} + + + {network.humans.length}{" "} + {network.humans.length === 1 ? "member" : "members"} + + + + + ); +} + +function EmptyState() { + return ( + + + You aren't in any networks yet. + + + Ask a friend for an invite, or create one on desktop. + + + ); +} diff --git a/js/mobile/src/features/settings/AccountScreen.tsx b/js/mobile/src/features/settings/AccountScreen.tsx new file mode 100644 index 0000000..ee1a133 --- /dev/null +++ b/js/mobile/src/features/settings/AccountScreen.tsx @@ -0,0 +1,37 @@ +import { Pressable, Text, View } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useAuthStore } from "@/stores/auth-store"; +import type { RootStackScreenProps } from "@/navigation/types"; + +export function AccountScreen({ navigation }: RootStackScreenProps<"Account">) { + const user = useAuthStore((s) => s.user); + + return ( + + + navigation.goBack()} className="px-2 py-1"> + + + + Account + + + + + + + + + ); +} + +function Field({ label, value }: { label: string; value: string }) { + return ( + + + {label} + + {value} + + ); +} diff --git a/js/mobile/src/features/settings/SettingsScreen.tsx b/js/mobile/src/features/settings/SettingsScreen.tsx new file mode 100644 index 0000000..1a4b4e7 --- /dev/null +++ b/js/mobile/src/features/settings/SettingsScreen.tsx @@ -0,0 +1,27 @@ +import { Pressable, Text, View } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import type { RootStackScreenProps } from "@/navigation/types"; + +export function SettingsScreen({ + navigation, +}: RootStackScreenProps<"Settings">) { + return ( + + + navigation.goBack()} className="px-2 py-1"> + + + + Settings + + + + + + + Theme, notifications, and account preferences land here later. + + + + ); +} diff --git a/js/mobile/src/features/stream-view/DeletedParticleView.tsx b/js/mobile/src/features/stream-view/DeletedParticleView.tsx new file mode 100644 index 0000000..f90c750 --- /dev/null +++ b/js/mobile/src/features/stream-view/DeletedParticleView.tsx @@ -0,0 +1,52 @@ +import { useEffect } from "react"; +import { Text, View } from "react-native"; +import { Trash2 } from "lucide-react-native"; +import type { Particle } from "@/api/types"; +import { useNetwork } from "@/hooks/use-networks"; +import { resolveHumanDisplay } from "@/lib/humans"; + +// How long to linger on a tombstone before auto-advancing. Same cadence as +// desktop — a beat long enough to read "this was deleted," not so long it +// stalls the stream. +const TOMBSTONE_DURATION_MS = 2000; + +interface DeletedParticleViewProps { + particle: Particle; + networkId: string; + paused: boolean; + onEnded: () => void; +} + +export function DeletedParticleView({ + particle, + networkId, + paused, + onEnded, +}: DeletedParticleViewProps) { + const network = useNetwork(networkId); + const deleterId = + "deleted_by_human_id" in particle ? particle.deleted_by_human_id : undefined; + const deleter = deleterId + ? resolveHumanDisplay(deleterId, network?.humans) + : null; + + useEffect(() => { + if (paused) return; + const timeout = setTimeout(onEnded, TOMBSTONE_DURATION_MS); + return () => clearTimeout(timeout); + }, [paused, onEnded, particle.id]); + + return ( + + + + This particle was deleted + + {deleter ? ( + + by {deleter.displayName} + + ) : null} + + ); +} diff --git a/js/mobile/src/features/stream-view/FallbackParticleView.tsx b/js/mobile/src/features/stream-view/FallbackParticleView.tsx new file mode 100644 index 0000000..3a25ffc --- /dev/null +++ b/js/mobile/src/features/stream-view/FallbackParticleView.tsx @@ -0,0 +1,89 @@ +import { useEffect } from "react"; +import { Text, View } from "react-native"; +import { + FileIcon, + HelpCircle, + ScrollText, + BookOpen, + type LucideIcon, +} from "lucide-react-native"; +import type { Particle } from "@/api/types"; +import { useNetwork } from "@/hooks/use-networks"; +import { resolveHumanDisplay } from "@/lib/humans"; + +const TYPE_META: Record = { + quest: { icon: ScrollText, label: "Quest" }, + paper: { icon: BookOpen, label: "Paper" }, + file: { icon: FileIcon, label: "File" }, +}; + +const PLACEHOLDER_DURATION_MS = 5000; + +interface FallbackParticleViewProps { + particle: Particle; + networkId: string; + paused: boolean; + onEnded: () => void; +} + +export function FallbackParticleView({ + particle, + networkId, + paused, + onEnded, +}: FallbackParticleViewProps) { + const network = useNetwork(networkId); + const creator = resolveHumanDisplay( + particle.created_by_human_id, + network?.humans, + ); + const meta = TYPE_META[particle.type] ?? { + icon: HelpCircle, + label: particle.type, + }; + const Icon = meta.icon; + const title = (() => { + switch (particle.type) { + case "quest": + return particle.properties.title; + case "paper": + return particle.properties.title; + case "file": + return particle.properties.filename; + case "folder": + return particle.properties.name; + default: + return null; + } + })(); + + useEffect(() => { + if (paused) return; + const timeout = setTimeout(onEnded, PLACEHOLDER_DURATION_MS); + return () => clearTimeout(timeout); + }, [paused, onEnded, particle.id]); + + return ( + + + + + + + {meta.label} + + {title ? ( + + {title} + + ) : null} + + + + From {creator.displayName} + + View on desktop + + + ); +} diff --git a/js/mobile/src/features/stream-view/MediaParticleView.tsx b/js/mobile/src/features/stream-view/MediaParticleView.tsx new file mode 100644 index 0000000..9e8aeb8 --- /dev/null +++ b/js/mobile/src/features/stream-view/MediaParticleView.tsx @@ -0,0 +1,273 @@ +import { useEffect, useRef, useState } from "react"; +import { ActivityIndicator, Text, View } from "react-native"; +import { Mic, Video as VideoIcon } from "lucide-react-native"; +import { useEventListener } from "expo"; +import { useVideoPlayer, VideoView, type VideoPlayerStatus } from "expo-video"; +import type { Particle } from "@/api/types"; +import { apiClient } from "@/api/client"; +import { logError } from "@/lib/errors"; +import { useEvent } from "@/hooks/use-event"; + +type MediaParticle = Extract; + +interface MediaParticleViewProps { + particle: MediaParticle; + paused: boolean; + onEnded: () => void; + onProgress: (ratio: number) => void; + /** "cover" fills the screen (may crop); "contain" fits the whole frame. */ + contentFit?: "cover" | "contain"; +} + +const TICK_MS = 150; + +/** + * Plays MP4 / MOV / m4a content via expo-video. Desktop currently records + * WebM, which AVPlayer can't decode; the particle processor worker produces + * an iOS-playable MP4/m4a variant and writes `transcoded_object_id` / + * `transcoded_mime_type` to the particle. While that work is in flight, we + * show a placeholder and let the Firestore listener + * swap us into the playable state once the worker finishes. + * + * The signed download URL is fetched lazily via apiClient.getParticleDownloadUrl + * (Orion-issued, time-limited). We show a spinner while that resolves, then + * mount the player and report progress via a 150ms tick reading the player's + * currentTime — same model as desktop's MediaParticleView. + */ +export function MediaParticleView({ + particle, + paused, + onEnded, + onProgress, + contentFit = "cover", +}: MediaParticleViewProps) { + const activeObjectId = + particle.properties.transcoded_object_id ?? particle.properties.object_id; + const activeMime = + particle.properties.transcoded_mime_type ?? particle.properties.mime_type; + const isAudio = activeMime.startsWith("audio/"); + const isPlayable = isPlayableMime(activeMime); + + // Reset progress as the active particle changes — independent of playback + // state — so the segmented bar drops back to 0 immediately. + useEffect(() => { + onProgress(0); + }, [particle.id, onProgress]); + + if (!isPlayable) { + return ; + } + + return ( + + ); +} + +function PlayableMediaView({ + particle, + activeObjectId, + isAudio, + paused, + onEnded, + onProgress, + contentFit, +}: { + particle: MediaParticle; + activeObjectId: string; + isAudio: boolean; + paused: boolean; + onEnded: () => void; + onProgress: (ratio: number) => void; + contentFit: "cover" | "contain"; +}) { + const [sourceUri, setSourceUri] = useState(null); + const [resolveError, setResolveError] = useState(null); + + // Fetch the signed download URL once per active object. Orion URLs are + // time-limited — we treat the URL as one-shot for this view's lifetime. + // Re-runs when the worker writes `transcoded_object_id` and the parent + // resolves a new active object id. + useEffect(() => { + let cancelled = false; + setSourceUri(null); + setResolveError(null); + apiClient + .getParticleDownloadUrl(activeObjectId) + .then((url) => { + if (!cancelled) setSourceUri(url); + }) + .catch((err) => { + logError(err, { scope: "media.download-url" }); + if (!cancelled) setResolveError(err as Error); + }); + return () => { + cancelled = true; + }; + }, [activeObjectId, particle.id]); + + const player = useVideoPlayer(sourceUri ?? "", (p) => { + p.loop = false; + p.muted = false; + p.timeUpdateEventInterval = 0.15; + // Don't take exclusive ownership of the iOS AVAudioSession. Without this + // the player blocks expo-camera from acquiring the session for video + // recording (audio works because expo-audio deactivates other sessions + // natively before claiming the session). + p.audioMixingMode = "mixWithOthers"; + }); + + // Drive play/pause from the suspender store. The player itself is forgiving + // about extra play/pause calls so we don't gate this. + useEffect(() => { + if (!sourceUri) return; + if (paused) { + player.pause(); + } else { + player.play(); + } + }, [paused, sourceUri, player]); + + // End-of-clip → advance. We listen to status flips rather than computing + // duration ratios because video duration may be 0 for the first frame or two. + useEventListener(player, "statusChange", ({ status }) => { + if (status === ("idle" satisfies VideoPlayerStatus)) { + // ignored — happens during source swap + } + }); + + const onEndedStable = useEvent(onEnded); + const onProgressStable = useEvent(onProgress); + + // Progress tick: report currentTime / duration each TICK_MS. Bail when the + // player isn't ready yet (duration = 0). + useEffect(() => { + if (!sourceUri || paused) return; + const interval = setInterval(() => { + const duration = player.duration; + if (!duration || duration <= 0) return; + const ratio = Math.min(player.currentTime / duration, 1); + onProgressStable(ratio); + if (ratio >= 0.999) { + clearInterval(interval); + onEndedStable(); + } + }, TICK_MS); + return () => clearInterval(interval); + }, [sourceUri, paused, player, onEndedStable, onProgressStable]); + + if (resolveError) { + return ( + + + Couldn't load this {isAudio ? "voice message" : "video"}. + + + Tap forward to continue. + + + ); + } + + if (!sourceUri) { + return ( + + + + ); + } + + // Audio-only: hide the (blank) video surface and show a static face. The + // VideoView still renders 0×0 so the audio track keeps playing. + if (isAudio) { + return ( + + + + + + + + + Voice message + + + ); + } + + // Video: full-bleed. Default cover so portrait mobile captures fill the + // screen; the user can flip to contain via the top-right toggle when desktop + // captures at odd aspect ratios get cropped uncomfortably. + return ( + + + + ); +} + +// Shown while the particle processor worker is producing the iOS-playable +// MP4/m4a variant. The Firestore listener will re-render this view once +// `transcoded_object_id` lands on the particle, which swaps us into +// PlayableMediaView. We deliberately do not auto-advance — the user is here +// to consume this content; if the worker is slow they can tap forward. +function ProcessingForMobilePlaceholder({ isAudio }: { isAudio: boolean }) { + return ( + + + {isAudio ? ( + + ) : ( + + )} + + + {isAudio ? "Voice message" : "Video message"} + + + + View on desktop + + + + Please view this on desktop only. + + + ); +} + +function isPlayableMime(mime: string): boolean { + // expo-video uses AVPlayer on iOS — reliable for h264 in mp4 / mov / m4a. + // WebM/VP9 (the legacy desktop format) is not decodable. + return ( + mime === "video/mp4" || + mime === "video/quicktime" || + mime === "audio/mp4" || + mime === "audio/aac" || + mime === "audio/x-m4a" || + mime === "audio/mpeg" + ); +} diff --git a/js/mobile/src/features/stream-view/PlaybackPageIndicator.tsx b/js/mobile/src/features/stream-view/PlaybackPageIndicator.tsx new file mode 100644 index 0000000..70caa4f --- /dev/null +++ b/js/mobile/src/features/stream-view/PlaybackPageIndicator.tsx @@ -0,0 +1,101 @@ +import { useEffect } from "react"; +import { View } from "react-native"; +import Animated, { + Easing, + cancelAnimation, + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated"; + +interface PlaybackPageIndicatorProps { + total: number; + current: number; + /** 0–1 progress for the active segment. Source ticks at ~100ms. */ + progress: number; + paused: boolean; +} + +const SEGMENT_GAP = 3; +const SEGMENT_HEIGHT = 2.5; +const SMOOTHING_MS = 300; + +/** + * Snapchat-style segmented progress bar. Past segments full, future empty, + * active segment animated. The 300ms linear smoothing absorbs the 100ms + * tick from the particle view source so motion looks continuous at 60fps. + */ +export function PlaybackPageIndicator({ + total, + current, + progress, + paused, +}: PlaybackPageIndicatorProps) { + if (total === 0) return null; + + return ( + + {Array.from({ length: total }).map((_, i) => ( + + ))} + + ); +} + +interface SegmentProps { + isActive: boolean; + isPast: boolean; + progress: number; + paused: boolean; +} + +function Segment({ isActive, isPast, progress, paused }: SegmentProps) { + // Each segment owns its own width animation. Past = 1, future = 0, + // active = animated toward `progress`. Reanimated keeps the tween on the + // UI thread so JS thread stalls (e.g. the 100ms text tick re-render) + // can't drop frames here. + const fill = useSharedValue(isPast ? 1 : 0); + + useEffect(() => { + if (isPast) { + cancelAnimation(fill); + fill.value = withTiming(1, { duration: 120, easing: Easing.out(Easing.cubic) }); + return; + } + if (!isActive) { + cancelAnimation(fill); + fill.value = 0; + return; + } + if (paused) { + cancelAnimation(fill); + return; + } + fill.value = withTiming(progress, { + duration: SMOOTHING_MS, + easing: Easing.linear, + }); + }, [isPast, isActive, progress, paused, fill]); + + const fillStyle = useAnimatedStyle(() => ({ + width: `${Math.min(Math.max(fill.value, 0), 1) * 100}%`, + })); + + return ( + + + + ); +} diff --git a/js/mobile/src/features/stream-view/ReactionSheet.tsx b/js/mobile/src/features/stream-view/ReactionSheet.tsx new file mode 100644 index 0000000..609f29e --- /dev/null +++ b/js/mobile/src/features/stream-view/ReactionSheet.tsx @@ -0,0 +1,367 @@ +import { useEffect, useMemo, useState } from "react"; +import { + Dimensions, + KeyboardAvoidingView, + Modal, + Platform, + Pressable, + Text, + TextInput, + View, +} from "react-native"; +import { + initialWindowMetrics, + SafeAreaProvider, + SafeAreaView, +} from "react-native-safe-area-context"; +import { Send, X } from "lucide-react-native"; +import * as Haptics from "expo-haptics"; +import { + Gesture, + GestureDetector, +} from "react-native-gesture-handler"; +import Animated, { + Easing, + Extrapolation, + interpolate, + runOnJS, + useAnimatedStyle, + useSharedValue, + withSpring, + withTiming, +} from "react-native-reanimated"; +import { REACTION_EMOJIS, type Reactions } from "@/api/types"; +import type { Human } from "@/api/types"; +import { resolveHumanDisplay } from "@/lib/humans"; +import { cn } from "@/lib/utils"; +import { useSuspendPlayback } from "@/hooks/use-suspend-playback"; + +const TEXT_REACTION_MAX = 40; +const SCREEN_HEIGHT = Dimensions.get("window").height; +const ANIMATION_MS = 240; + +const EMOJI_SET = new Set(REACTION_EMOJIS); + +interface ReactionSheetProps { + open: boolean; + onClose: () => void; + reactions: Reactions; + currentHumanId: string; + humans: Human[] | undefined; + /** + * Toggle a reaction (emoji or text). Adds if the current human hasn't + * reacted, removes if they have. Mirrors desktop's `onToggle` exactly. + */ + onToggle: (key: string) => void; +} + +/** + * Slide-up reaction sheet — the mobile replacement for desktop's right-edge + * reaction stack. Tap an emoji to toggle, or send a custom text reaction + * (40-char cap). Existing reactions appear as toggleable pills at the top. + * + * Playback is suspended via `useSuspendPlayback` while the sheet is open so + * the active particle doesn't auto-advance under the user. Drag the sheet + * down past 30% of its travel to dismiss; everything else springs back. + */ +export function ReactionSheet({ + open, + onClose, + reactions, + currentHumanId, + humans, + onToggle, +}: ReactionSheetProps) { + // Suspend playback whenever the sheet is mounted-and-open. The Modal + // controls visibility so we tie the suspender to `open` directly. + useSuspendPlayback(open, "reactions-sheet"); + + // We mount the modal slightly delayed from `open` so the slide-up animation + // has its starting position rendered. Using local `mounted` state lets us + // play the close animation before unmounting. + const [mounted, setMounted] = useState(false); + const translateY = useSharedValue(SCREEN_HEIGHT); + + useEffect(() => { + if (open) { + setMounted(true); + // Schedule animation after the modal mounts + requestAnimationFrame(() => { + translateY.value = withSpring(0, { + damping: 24, + stiffness: 260, + mass: 0.7, + }); + }); + } else if (mounted) { + translateY.value = withTiming( + SCREEN_HEIGHT, + { duration: ANIMATION_MS, easing: Easing.in(Easing.cubic) }, + (finished) => { + if (finished) runOnJS(setMounted)(false); + }, + ); + } + // intentional: only react to `open`. Closing animation reads from `mounted`. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); + + const dismiss = () => { + onClose(); + }; + + const sheetPan = Gesture.Pan() + .activeOffsetY(10) + .failOffsetX([-25, 25]) + .onUpdate((e) => { + "worklet"; + translateY.value = Math.max(0, e.translationY); + }) + .onEnd((e) => { + "worklet"; + if (e.translationY > 120 || e.velocityY > 800) { + runOnJS(dismiss)(); + } else { + translateY.value = withSpring(0, { + damping: 24, + stiffness: 260, + mass: 0.7, + }); + } + }); + + const sheetStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: translateY.value }], + })); + + const backdropStyle = useAnimatedStyle(() => { + const opacity = interpolate( + translateY.value, + [0, SCREEN_HEIGHT * 0.7], + [0.55, 0], + Extrapolation.CLAMP, + ); + return { opacity }; + }); + + // --- Existing reaction pills --- + const activeEmojis = REACTION_EMOJIS.filter( + (e) => reactions?.[e] && (reactions[e]?.length ?? 0) > 0, + ); + const activeTextKeys = useMemo( + () => + Object.keys(reactions ?? {}).filter( + (k) => + !EMOJI_SET.has(k) && (reactions?.[k]?.length ?? 0) > 0, + ), + [reactions], + ); + + // --- Text reaction input --- + const [text, setText] = useState(""); + + useEffect(() => { + if (open) setText(""); + }, [open]); + + const submitText = () => { + const trimmed = text.trim(); + if (!trimmed) return; + void Haptics.selectionAsync(); + onToggle(trimmed.slice(0, TEXT_REACTION_MAX)); + setText(""); + onClose(); + }; + + const handleEmoji = (emoji: string) => { + void Haptics.selectionAsync(); + onToggle(emoji); + onClose(); + }; + + if (!mounted) return null; + + return ( + + + + + + + + + + + + + {/* Drag handle — affords downward dismissal at a glance. */} + + + + + React + + + + + + + + {/* Existing reactions row — tap a pill to toggle yours. */} + {activeEmojis.length > 0 || activeTextKeys.length > 0 ? ( + + {activeEmojis.map((emoji) => { + const reactors = reactions![emoji]; + const isMine = reactors.includes(currentHumanId); + return ( + handleEmoji(emoji)} + className={cn( + "flex-row items-center gap-1.5 rounded-full px-3 py-1.5", + isMine + ? "bg-white/25 border border-white/40" + : "bg-white/10", + )} + > + {emoji} + + {reactors.length} + + + ); + })} + + {activeTextKeys.map((key) => { + const reactors = reactions![key]; + const isMine = reactors.includes(currentHumanId); + const firstReactor = resolveHumanDisplay( + reactors[0], + humans, + ); + return ( + handleEmoji(key)} + className={cn( + "flex-row items-center gap-1.5 rounded-full px-2.5 py-1.5 max-w-[220px]", + isMine + ? "bg-white/25 border border-white/40" + : "bg-white/10", + )} + > + + + {firstReactor.initials} + + + + {key} + + {reactors.length > 1 ? ( + + {reactors.length} + + ) : null} + + ); + })} + + ) : null} + + {/* Quick-pick emoji palette — six big tappable buttons. */} + + {REACTION_EMOJIS.slice(0, 6).map((emoji) => { + const isMine = + reactions?.[emoji]?.includes(currentHumanId) ?? false; + return ( + handleEmoji(emoji)} + accessibilityLabel={`React with ${emoji}`} + className={cn( + "h-14 w-14 items-center justify-center rounded-full", + isMine ? "bg-white/25" : "bg-white/10", + )} + > + {emoji} + + ); + })} + + + {/* Text reaction input — 40-char cap matches desktop. */} + + + setText(v.slice(0, TEXT_REACTION_MAX))} + placeholder="Send a quick reply..." + placeholderTextColor="rgba(255,255,255,0.4)" + maxLength={TEXT_REACTION_MAX} + autoCapitalize="none" + autoCorrect={false} + onSubmitEditing={submitText} + returnKeyType="send" + className="text-white text-base" + /> + + + + + + + + + + + + + ); +} diff --git a/js/mobile/src/features/stream-view/ReactionStack.tsx b/js/mobile/src/features/stream-view/ReactionStack.tsx new file mode 100644 index 0000000..3b64714 --- /dev/null +++ b/js/mobile/src/features/stream-view/ReactionStack.tsx @@ -0,0 +1,124 @@ +import { useMemo } from "react"; +import { Pressable, Text, View } from "react-native"; +import { Plus } from "lucide-react-native"; +import * as Haptics from "expo-haptics"; +import { REACTION_EMOJIS, type Reactions } from "@/api/types"; +import type { Human } from "@/api/types"; +import { resolveHumanDisplay } from "@/lib/humans"; +import { cn } from "@/lib/utils"; + +const EMOJI_SET = new Set(REACTION_EMOJIS); + +interface ReactionStackProps { + reactions: Reactions; + currentHumanId: string; + humans: Human[] | undefined; + /** Toggle a reaction (emoji or text) — same contract as ReactionSheet's onToggle. */ + onToggle: (key: string) => void; + /** Open the full reaction sheet for emoji + custom-text picking. */ + onOpenSheet: () => void; +} + +/** + * Right-edge reaction stack — mobile counterpart of desktop's ReactionBar. + * Sits vertically centered on the right side of the canvas so the user can + * see existing reactions at a glance and tap to toggle their own. The "+" + * affordance opens the ReactionSheet for the full picker (emoji or text). + */ +export function ReactionStack({ + reactions, + currentHumanId, + humans, + onToggle, + onOpenSheet, +}: ReactionStackProps) { + const activeEmojis = REACTION_EMOJIS.filter( + (emoji) => reactions?.[emoji] && (reactions[emoji]?.length ?? 0) > 0, + ); + const activeTextKeys = useMemo( + () => + Object.keys(reactions ?? {}).filter( + (k) => !EMOJI_SET.has(k) && (reactions?.[k]?.length ?? 0) > 0, + ), + [reactions], + ); + + const handleToggle = (key: string) => { + void Haptics.selectionAsync(); + onToggle(key); + }; + + return ( + + {activeEmojis.map((emoji) => { + const reactors = reactions?.[emoji] ?? []; + const isMine = reactors.includes(currentHumanId); + return ( + handleToggle(emoji)} + className={cn( + "flex-row items-center gap-1 rounded-full px-2 py-1", + isMine ? "bg-white/25" : "bg-black/45", + )} + style={ + isMine + ? { borderWidth: 1, borderColor: "rgba(255,255,255,0.4)" } + : undefined + } + > + {emoji} + + {reactors.length} + + + ); + })} + + {activeTextKeys.map((text) => { + const reactors = reactions?.[text] ?? []; + const isMine = reactors.includes(currentHumanId); + const firstReactor = resolveHumanDisplay(reactors[0], humans); + return ( + handleToggle(text)} + className={cn( + "flex-row items-center gap-1.5 rounded-full py-1 pl-1 pr-2.5", + isMine ? "bg-white/25" : "bg-black/45", + )} + style={[ + { maxWidth: 200 }, + isMine + ? { borderWidth: 1, borderColor: "rgba(255,255,255,0.4)" } + : null, + ]} + > + + + {firstReactor.initials} + + + + {text} + + {reactors.length > 1 ? ( + {reactors.length} + ) : null} + + ); + })} + + + + + + ); +} diff --git a/js/mobile/src/features/stream-view/RenameStreamSheet.tsx b/js/mobile/src/features/stream-view/RenameStreamSheet.tsx new file mode 100644 index 0000000..6a94afb --- /dev/null +++ b/js/mobile/src/features/stream-view/RenameStreamSheet.tsx @@ -0,0 +1,89 @@ +import { useEffect, useState } from "react"; +import { Pressable, Text, TextInput, View } from "react-native"; +import { toast } from "sonner-native"; +import { cn } from "@/lib/utils"; +import { toUserMessage } from "@/lib/errors"; +import { updateParticleProperties } from "@/lib/firestore-particles"; +import { particlePath, toFirestoreDocPath } from "@/lib/particle-path"; +import { useSuspendPlayback } from "@/hooks/use-suspend-playback"; +import { BottomSheet } from "@/components/BottomSheet"; + +interface RenameStreamSheetProps { + open: boolean; + onClose: () => void; + networkId: string; + streamId: string; + currentName: string; +} + +export function RenameStreamSheet({ + open, + onClose, + networkId, + streamId, + currentName, +}: RenameStreamSheetProps) { + useSuspendPlayback(open, "rename-stream"); + + const [name, setName] = useState(currentName); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (open) { + setName(currentName); + setSaving(false); + } + }, [open, currentName]); + + const trimmed = name.trim(); + const canSave = !saving && trimmed.length > 0 && trimmed !== currentName; + + const handleSave = async () => { + if (!canSave) return; + setSaving(true); + try { + const docPath = toFirestoreDocPath(particlePath(networkId, [streamId])); + await updateParticleProperties<"stream">(docPath, { name: trimmed }); + onClose(); + } catch (err) { + toast.error(toUserMessage(err)); + setSaving(false); + } + }; + + return ( + + + + Cancel + + Rename + + + {saving ? "Saving..." : "Save"} + + + + + + + + ); +} diff --git a/js/mobile/src/features/stream-view/StreamActionsSheet.tsx b/js/mobile/src/features/stream-view/StreamActionsSheet.tsx new file mode 100644 index 0000000..84d083a --- /dev/null +++ b/js/mobile/src/features/stream-view/StreamActionsSheet.tsx @@ -0,0 +1,118 @@ +import { Pressable, Text, View } from "react-native"; +import { + CircleCheckBig, + CircleDot, + Pencil, + Trash2, + Users, +} from "lucide-react-native"; +import { cn } from "@/lib/utils"; +import { BottomSheet } from "@/components/BottomSheet"; + +export type StreamActionId = + | "toggle-status" + | "rename" + | "members" + | "delete-particle"; + +interface StreamActionsSheetProps { + open: boolean; + onClose: () => void; + onSelect: (action: StreamActionId) => void; + streamStatus: "open" | "closed"; + isCreator: boolean; + /** True when the *current* particle is one this user can soft-delete. */ + canDeleteParticle: boolean; +} + +export function StreamActionsSheet({ + open, + onClose, + onSelect, + streamStatus, + isCreator, + canDeleteParticle, +}: StreamActionsSheetProps) { + const choose = (id: StreamActionId) => { + onClose(); + onSelect(id); + }; + + return ( + + + + ) : ( + + ) + } + label={ + streamStatus === "open" ? "Close stream" : "Reopen stream" + } + onPress={() => choose("toggle-status")} + /> + } + label="Members" + onPress={() => choose("members")} + /> + {isCreator ? ( + } + label="Rename stream" + onPress={() => choose("rename")} + /> + ) : null} + {canDeleteParticle ? ( + } + label="Delete particle" + tone="destructive" + onPress={() => choose("delete-particle")} + /> + ) : null} + + + + + Cancel + + + + ); +} + +function ActionRow({ + icon, + label, + onPress, + tone = "default", +}: { + icon: React.ReactNode; + label: string; + onPress: () => void; + tone?: "default" | "destructive"; +}) { + return ( + + {icon} + + {label} + + + ); +} diff --git a/js/mobile/src/features/stream-view/StreamMembersSheet.tsx b/js/mobile/src/features/stream-view/StreamMembersSheet.tsx new file mode 100644 index 0000000..bcbeeae --- /dev/null +++ b/js/mobile/src/features/stream-view/StreamMembersSheet.tsx @@ -0,0 +1,273 @@ +import { useMemo } from "react"; +import { Pressable, ScrollView, Text, View } from "react-native"; +import { Globe, Lock, X } from "lucide-react-native"; +import { toast } from "sonner-native"; +import type { Particle } from "@/api/types"; +import { resolveHumanDisplay } from "@/lib/humans"; +import { useNetwork } from "@/hooks/use-networks"; +import { particlePath, toFirestoreDocPath } from "@/lib/particle-path"; +import { updateParticleVisibleTo } from "@/lib/firestore-particles"; +import { + buildCustomVisibility, + buildNetworkVisibility, + parseVisibleTo, +} from "@/lib/stream-visibility"; +import { toUserMessage } from "@/lib/errors"; +import { useSuspendPlayback } from "@/hooks/use-suspend-playback"; +import { BottomSheet } from "@/components/BottomSheet"; +import { Avatar } from "@/components/Avatar"; +import { useStreamPresence } from "./stream-presence-context"; + +interface StreamMembersSheetProps { + open: boolean; + onClose: () => void; + networkId: string; + streamParticle: Particle & { type: "stream" }; + isCreator: boolean; +} + +/** + * Read-only-for-non-creators view of who can see the stream, plus an inline + * editor for creators to flip between network-wide and per-person and to + * add/remove people. Mobile counterpart of stream-members-overlay.tsx. + */ +export function StreamMembersSheet({ + open, + onClose, + networkId, + streamParticle, + isCreator, +}: StreamMembersSheetProps) { + useSuspendPlayback(open, "stream-members"); + + const { onlineHumanIds } = useStreamPresence(); + const network = useNetwork(networkId); + const humans = network?.humans ?? []; + const creatorId = streamParticle.created_by_human_id; + const visibility = parseVisibleTo(streamParticle.visible_to, networkId); + + const docPath = useMemo( + () => toFirestoreDocPath(particlePath(networkId, [streamParticle.id])), + [networkId, streamParticle.id], + ); + + const memberIds = + visibility.mode === "network" + ? humans.map((h) => h.id) + : visibility.humanIds; + const memberSet = new Set(memberIds); + const availableToAdd = humans.filter((h) => !memberSet.has(h.id)); + + const apply = async (next: string[]) => { + try { + await updateParticleVisibleTo(docPath, next); + } catch (err) { + toast.error(toUserMessage(err)); + } + }; + + const setNetworkWide = () => apply(buildNetworkVisibility(networkId)); + const setCustomOnlyCreator = () => apply(buildCustomVisibility([creatorId])); + + const removeMember = (id: string) => { + if (visibility.mode !== "custom") return; + if (id === creatorId) return; + const next = visibility.humanIds.filter((x) => x !== id); + if (next.length === 0) return; + void apply(buildCustomVisibility(next)); + }; + + const addMember = (id: string) => { + if (visibility.mode !== "custom") return; + void apply(buildCustomVisibility([...visibility.humanIds, id])); + }; + + return ( + + + + Members + + + + + + + + Visibility + + {isCreator ? ( + + } + label="Network-wide" + onPress={setNetworkWide} + /> + } + label="Specific people" + onPress={setCustomOnlyCreator} + /> + + ) : ( + + {visibility.mode === "network" ? ( + <> + + + Everyone in {network?.name ?? "network"} + + + ) : ( + <> + + + {memberIds.length} specific{" "} + {memberIds.length === 1 ? "person" : "people"} + + + )} + + )} + + + + + + {visibility.mode === "network" ? "Has access" : "People"} ·{" "} + {memberIds.length} + + {memberIds.map((id) => { + const display = resolveHumanDisplay(id, humans); + const isCreatorRow = id === creatorId; + const canRemove = + isCreator && visibility.mode === "custom" && !isCreatorRow; + return ( + + + + + {display.displayName} + + {display.exists ? ( + + {display.email} + + ) : null} + + {isCreatorRow ? ( + + Creator + + ) : canRemove ? ( + removeMember(id)} + hitSlop={10} + accessibilityLabel={`Remove ${display.displayName}`} + > + + + ) : null} + + ); + })} + + + {isCreator && + visibility.mode === "custom" && + availableToAdd.length > 0 ? ( + + + Add people + + {availableToAdd.map((human) => { + const display = resolveHumanDisplay(human.id, humans); + return ( + addMember(human.id)} + className="flex-row items-center gap-3 py-2.5 active:bg-white/5 rounded-lg" + > + + + + {display.displayName} + + + {display.email} + + + Add + + ); + })} + + ) : null} + + + ); +} + +function ModePill({ + active, + icon, + label, + onPress, +}: { + active: boolean; + icon: React.ReactNode; + label: string; + onPress: () => void; +}) { + return ( + + {icon} + + {label} + + + ); +} diff --git a/js/mobile/src/features/stream-view/StreamMetadataHeader.tsx b/js/mobile/src/features/stream-view/StreamMetadataHeader.tsx new file mode 100644 index 0000000..d756cdd --- /dev/null +++ b/js/mobile/src/features/stream-view/StreamMetadataHeader.tsx @@ -0,0 +1,64 @@ +import { Text, View } from "react-native"; +import type { Network, Particle } from "@/api/types"; +import { resolveHumanDisplay } from "@/lib/humans"; +import { RelativeTimestamp } from "@/components/RelativeTimestamp"; +import { Avatar } from "@/components/Avatar"; +import { useStreamPresence } from "./stream-presence-context"; + +interface StreamMetadataHeaderProps { + particle: Particle | null; + network: Network | null; +} + +/** + * Avatar + display name + relative time. Sits below the segmented bar so the + * "who/when" answer is always one glance away — Snapchat-style. + */ +export function StreamMetadataHeader({ + particle, + network, +}: StreamMetadataHeaderProps) { + const { onlineHumanIds } = useStreamPresence(); + if (!particle) return null; + const display = resolveHumanDisplay( + particle.created_by_human_id, + network?.humans, + ); + + const editedAt = + particle.type === "text" ? particle.properties.edited_at : undefined; + const isOnline = particle.created_by_human_id + ? onlineHumanIds.has(particle.created_by_human_id) + : false; + + return ( + + + + + {display.displayName} + + + + {editedAt ? ( + + · edited{" "} + + + ) : null} + + + + ); +} diff --git a/js/mobile/src/features/stream-view/StreamTopActions.tsx b/js/mobile/src/features/stream-view/StreamTopActions.tsx new file mode 100644 index 0000000..c335658 --- /dev/null +++ b/js/mobile/src/features/stream-view/StreamTopActions.tsx @@ -0,0 +1,110 @@ +import { Pressable, Text, View } from "react-native"; +import { EllipsisVertical, Globe, Maximize2, Minimize2 } from "lucide-react-native"; +import type { Human, Particle } from "@/api/types"; +import { parseVisibleTo } from "@/lib/stream-visibility"; +import { cn } from "@/lib/utils"; +import { Avatar } from "@/components/Avatar"; +import { useStreamPresence } from "./stream-presence-context"; + +interface StreamTopActionsProps { + networkId: string; + streamParticle: Particle & { type: "stream" }; + humans: Human[]; + videoFit: "cover" | "contain"; + onToggleVideoFit: () => void; + onOpenMembers: () => void; + onOpenActions: () => void; + /** True when current particle is a video — fit toggle hidden otherwise. */ + showFitToggle: boolean; +} + +const MAX_AVATARS = 3; + +/** + * Top-right cluster on StreamView: visibility avatars (with presence ring), + * fit/fill toggle, and actions menu trigger. Mirrors desktop's stream-top-bar + * but compact for the mobile chrome. + */ +export function StreamTopActions({ + networkId, + streamParticle, + humans, + videoFit, + onToggleVideoFit, + onOpenMembers, + onOpenActions, + showFitToggle, +}: StreamTopActionsProps) { + const { onlineHumanIds } = useStreamPresence(); + const visibility = parseVisibleTo(streamParticle.visible_to, networkId); + const memberIds = + visibility.mode === "network" + ? humans.map((h) => h.id) + : visibility.humanIds; + const shown = memberIds.slice(0, MAX_AVATARS); + const overflow = memberIds.length - shown.length; + + return ( + + + {visibility.mode === "network" && memberIds.length === 0 ? ( + + ) : ( + + {shown.map((id, idx) => ( + + {/* The stack ring matches the chrome's translucent bg so it + reads as a separator without painting hard black halos. */} + + + ))} + + )} + {overflow > 0 ? ( + + +{overflow} + + ) : null} + + + {showFitToggle ? ( + + {videoFit === "cover" ? ( + + ) : ( + + )} + + ) : null} + + + + + + ); +} diff --git a/js/mobile/src/features/stream-view/StreamView.tsx b/js/mobile/src/features/stream-view/StreamView.tsx new file mode 100644 index 0000000..c35d364 --- /dev/null +++ b/js/mobile/src/features/stream-view/StreamView.tsx @@ -0,0 +1,646 @@ +import { useCallback, useEffect, useState } from "react"; +import { Alert, Dimensions, Pressable, Text, View } from "react-native"; +import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context"; +import { StatusBar } from "expo-status-bar"; +import * as Haptics from "expo-haptics"; +import { + Gesture, + GestureDetector, +} from "react-native-gesture-handler"; +import Animated, { + Extrapolation, + interpolate, + runOnJS, + useAnimatedStyle, + useSharedValue, + withSpring, + withTiming, +} from "react-native-reanimated"; +import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; +import { isParticleDeleted, type Particle } from "@/api/types"; +import { + parseParticlePath, + particlePath, + toFirestoreDocPath, + type ParticlePath, +} from "@/lib/particle-path"; +import { + softDeleteParticle, + toggleParticleReaction, + updateStreamStatus, +} from "@/lib/firestore-particles"; +import { toast } from "sonner-native"; +import { toUserMessage } from "@/lib/errors"; +import { useNetwork } from "@/hooks/use-networks"; +import { useStreamPlayback } from "@/hooks/use-stream-playback"; +import { useSuspendPlayback } from "@/hooks/use-suspend-playback"; +import { + selectIsComposing, + selectIsPaused, + usePlaybackPauseStore, +} from "@/stores/playback-pause-store"; +import { useAuthStore } from "@/stores/auth-store"; +import { ComposeDock } from "@/features/compose/ComposeDock"; +import { ComposingIndicator } from "@/components/ComposingIndicator"; +import { PlaybackPageIndicator } from "./PlaybackPageIndicator"; +import { ReactionSheet } from "./ReactionSheet"; +import { StreamMetadataHeader } from "./StreamMetadataHeader"; +import { StreamSafeAreaProvider } from "./stream-safe-area"; +import { + StreamPresenceProvider, + useStreamComposing, +} from "./stream-presence-context"; +import { TextParticleView } from "./TextParticleView"; +import { MediaParticleView } from "./MediaParticleView"; +import { DeletedParticleView } from "./DeletedParticleView"; +import { FallbackParticleView } from "./FallbackParticleView"; +import { useExitCountdown } from "./use-exit-countdown"; +import { StreamTopActions } from "./StreamTopActions"; +import { StreamActionsSheet, type StreamActionId } from "./StreamActionsSheet"; +import { StreamMembersSheet } from "./StreamMembersSheet"; +import { RenameStreamSheet } from "./RenameStreamSheet"; +import { ReactionStack } from "./ReactionStack"; + +const SCREEN_HEIGHT = Dimensions.get("window").height; +// Tap-zone split: left 28% goes back, right 72% goes forward — matching the +// asymmetric "Snapchat thumb-zone" so right-handed taps default to forward. +const PREV_ZONE_RATIO = 0.28; +// Swipe-down dismiss commit thresholds — either move 1/4 of the screen, or +// flick downward fast enough. +const DISMISS_DISTANCE = SCREEN_HEIGHT * 0.25; +const DISMISS_VELOCITY = 900; +// Swipe-up reactions commit thresholds — flick up ~80px or with enough velocity. +const REACTIONS_DISTANCE = 80; +const REACTIONS_VELOCITY = 600; +// Approx height of the ComposeDock from the screen bottom (record button stack +// + pb-10). Status pills sit just above this so they aren't hidden behind it. +const COMPOSE_DOCK_HEIGHT = 50; + +interface StreamViewProps { + streamParticle: Particle & { type: "stream" }; + path: ParticlePath; + onExit: () => void; +} + +export function StreamView(props: StreamViewProps) { + const { networkId } = parseParticlePath(props.path); + // The presence provider wraps the inner view so any descendant can broadcast + // composing state without re-deriving the channel id. + return ( + + + + ); +} + +function StreamViewInner({ streamParticle, path, onExit }: StreamViewProps) { + const { networkId } = parseParticlePath(path); + const network = useNetwork(networkId); + const insets = useSafeAreaInsets(); + const { composingUsers } = useStreamComposing(); + + const { children, currentParticle, currentIndex, status, next, prev } = + useStreamPlayback(streamParticle, path); + + const paused = usePlaybackPauseStore(selectIsPaused); + const composing = usePlaybackPauseStore(selectIsComposing); + const [progress, setProgress] = useState(0); + const userId = useAuthStore((s) => s.user?.id) ?? ""; + + // Local hold state drives the "touch-hold" pause suspender. We wrap the JS + // setter inside a runOnJS callback dispatched from the worklet thread. + const [holdActive, setHoldActive] = useState(false); + useSuspendPlayback(holdActive, "touch-hold"); + + // Reaction sheet — opens via swipe-up on the canvas. + const [reactionsOpen, setReactionsOpen] = useState(false); + + // Top-right cluster sheet state. `videoFit` lets the user toggle expo-video's + // contentFit for the active media particle when desktop captures of unusual + // aspect ratios get cropped uncomfortably under the default `cover` mode. + const [actionsOpen, setActionsOpen] = useState(false); + const [membersOpen, setMembersOpen] = useState(false); + const [renameOpen, setRenameOpen] = useState(false); + const [videoFit, setVideoFit] = useState<"cover" | "contain">("cover"); + + const isCreator = !!userId && userId === streamParticle.created_by_human_id; + const canDeleteCurrentParticle = + !!currentParticle && + !!userId && + currentParticle.created_by_human_id === userId && + currentParticle.type !== "stream" && + currentParticle.type !== "folder" && + !isParticleDeleted(currentParticle); + + const showFitToggle = + !!currentParticle && + !isParticleDeleted(currentParticle) && + currentParticle.type === "media" && + !currentParticle.properties.mime_type.startsWith("audio/"); + + const handleStreamAction = useCallback( + async (action: StreamActionId) => { + const streamDocPath = toFirestoreDocPath( + particlePath(networkId, [streamParticle.id]), + ); + switch (action) { + case "toggle-status": { + try { + await updateStreamStatus( + streamDocPath, + streamParticle.status === "open" ? "closed" : "open", + ); + } catch (err) { + toast.error(toUserMessage(err)); + } + return; + } + case "rename": + setRenameOpen(true); + return; + case "members": + setMembersOpen(true); + return; + case "delete-particle": { + if (!currentParticle || !userId) return; + if (!canDeleteCurrentParticle) return; + Alert.alert( + "Delete this particle?", + "This cannot be undone. Other viewers will see a \"deleted\" message in its place.", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + style: "destructive", + onPress: async () => { + try { + const docPath = toFirestoreDocPath( + particlePath(networkId, [ + streamParticle.id, + currentParticle.id, + ]), + ); + await softDeleteParticle(docPath, userId); + } catch (err) { + toast.error(toUserMessage(err)); + } + }, + }, + ], + ); + return; + } + } + }, + [ + networkId, + streamParticle.id, + streamParticle.status, + currentParticle, + userId, + canDeleteCurrentParticle, + ], + ); + + const reactionsOnCurrent = + currentParticle && !isParticleDeleted(currentParticle) + ? currentParticle.type === "media" || currentParticle.type === "text" + ? currentParticle.reactions + : undefined + : undefined; + + const handleToggleReaction = useCallback( + (key: string) => { + if (!userId || !currentParticle) return; + if (isParticleDeleted(currentParticle)) return; + const docPath = toFirestoreDocPath( + particlePath(networkId, [streamParticle.id, currentParticle.id]), + ); + void toggleParticleReaction( + docPath, + key, + userId, + reactionsOnCurrent, + ); + }, + [userId, currentParticle, networkId, streamParticle.id, reactionsOnCurrent], + ); + + const openReactions = useCallback(() => setReactionsOpen(true), []); + + // Reset progress whenever the active particle changes. + useEffect(() => { + setProgress(0); + }, [currentParticle?.id]); + + const handleTap = useCallback( + (xRatio: number) => { + if (xRatio < PREV_ZONE_RATIO) { + if (currentIndex <= 0) { + // Soft "thud" — nothing to go back to. + void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + return; + } + prev(); + } else { + next(); + } + }, + [currentIndex, next, prev], + ); + + // --- Swipe-down dismiss --- + const translateY = useSharedValue(0); + const screenWidth = Dimensions.get("window").width; + + const exit = useCallback(() => { + onExit(); + }, [onExit]); + + const panDown = Gesture.Pan() + .activeOffsetY(15) + .failOffsetX([-30, 30]) + .failOffsetY(-20) + .onUpdate((e) => { + "worklet"; + translateY.value = Math.max(0, e.translationY); + }) + .onEnd((e) => { + "worklet"; + if ( + e.translationY > DISMISS_DISTANCE || + e.velocityY > DISMISS_VELOCITY + ) { + translateY.value = withTiming(SCREEN_HEIGHT, { duration: 220 }); + runOnJS(exit)(); + } else { + translateY.value = withSpring(0, { + damping: 22, + stiffness: 220, + mass: 0.6, + }); + } + }); + + // Swipe-up opens the reaction sheet. Mirror the down pan's discipline — + // fail on horizontal motion so it doesn't fight the tap-zones. + const panUp = Gesture.Pan() + .activeOffsetY(-15) + .failOffsetX([-30, 30]) + .failOffsetY(20) + .onEnd((e) => { + "worklet"; + if ( + e.translationY < -REACTIONS_DISTANCE || + e.velocityY < -REACTIONS_VELOCITY + ) { + runOnJS(openReactions)(); + } + }); + + // --- Tap (advance / regress) --- + const tap = Gesture.Tap() + .maxDuration(180) + .maxDistance(15) + .onEnd((e, success) => { + "worklet"; + if (!success) return; + const ratio = e.x / screenWidth; + runOnJS(handleTap)(ratio); + }); + + // --- Long-press (hold-to-pause) --- + const longPress = Gesture.LongPress() + .minDuration(180) + .maxDistance(15) + .onStart(() => { + "worklet"; + runOnJS(setHoldActive)(true); + }) + .onTouchesUp(() => { + "worklet"; + runOnJS(setHoldActive)(false); + }) + .onFinalize(() => { + "worklet"; + runOnJS(setHoldActive)(false); + }); + + // Pan-down (dismiss), pan-up (reactions), and tap+longPress race against + // each other. The first to clear its activeOffsetY wins; the others fail. + const composed = Gesture.Race( + panDown, + panUp, + Gesture.Simultaneous(tap, longPress), + ); + + const containerStyle = useAnimatedStyle(() => { + const opacity = interpolate( + translateY.value, + [0, SCREEN_HEIGHT * 0.5], + [1, 0.4], + Extrapolation.CLAMP, + ); + const scale = interpolate( + translateY.value, + [0, SCREEN_HEIGHT], + [1, 0.85], + Extrapolation.CLAMP, + ); + return { + transform: [{ translateY: translateY.value }, { scale }], + opacity, + }; + }); + + const backdropStyle = useAnimatedStyle(() => { + const opacity = interpolate( + translateY.value, + [0, SCREEN_HEIGHT * 0.5], + [1, 0.6], + Extrapolation.CLAMP, + ); + return { opacity }; + }); + + // --- End-of-stream countdown --- + const exitRemainingMs = useExitCountdown(status, paused, exit); + + // Chrome reservations: top = safe-area + segmented bar (3) + gap (12) + + // metadata row (~38) + breathing room (12). Bottom = safe-area + room for + // pause / countdown pills + the compose dock that lands in this same step. + const chromeTop = insets.top + 65; + const chromeBottom = insets.bottom + 96; + + // --- Render the active particle --- + const renderParticle = (particle: Particle) => { + if (isParticleDeleted(particle)) { + return ( + + ); + } + switch (particle.type) { + case "text": + return ( + + ); + case "media": + return ( + + ); + default: + return ( + + ); + } + }; + + // --- Content guards --- + if (children.length === 0) { + return ( + + + ); + } + + return ( + + + ); +} diff --git a/js/mobile/src/features/stream-view/StreamViewScreen.tsx b/js/mobile/src/features/stream-view/StreamViewScreen.tsx new file mode 100644 index 0000000..e400596 --- /dev/null +++ b/js/mobile/src/features/stream-view/StreamViewScreen.tsx @@ -0,0 +1,48 @@ +import { ActivityIndicator, Pressable, Text, View } from "react-native"; +import { StatusBar } from "expo-status-bar"; +import type { RootStackScreenProps } from "@/navigation/types"; +import { particlePath } from "@/lib/particle-path"; +import { useLiveParticle } from "@/hooks/use-particle"; +import { StreamView } from "./StreamView"; + +export function StreamViewScreen({ + navigation, + route, +}: RootStackScreenProps<"StreamView">) { + const { networkId, streamId } = route.params; + const streamPath = particlePath(networkId, [streamId]); + const { particle, isLoading, error } = useLiveParticle(streamPath); + + if (isLoading && !particle) { + return ( + + + ); + } + + if (error || !particle || particle.type !== "stream") { + return ( + + + ); + } + + return ( + navigation.goBack()} + /> + ); +} diff --git a/js/mobile/src/features/stream-view/TextParticleView.tsx b/js/mobile/src/features/stream-view/TextParticleView.tsx new file mode 100644 index 0000000..f4aead1 --- /dev/null +++ b/js/mobile/src/features/stream-view/TextParticleView.tsx @@ -0,0 +1,114 @@ +import { useEffect, useRef } from "react"; +import { ScrollView, Text, View } from "react-native"; +import type { Particle } from "@/api/types"; +import { cn } from "@/lib/utils"; +import { useStreamSafeArea } from "./stream-safe-area"; + +type TextParticle = Extract; + +interface TextParticleViewProps { + particle: TextParticle; + paused: boolean; + onEnded: () => void; + onProgress: (ratio: number) => void; +} + +// Mirrors desktop's read-duration math (chars/min ≈ 1000, plus +2s per +// link/attachment, clamped 3–15s). Mobile v1 has no attachments and we +// don't extract link previews mid-render, so the formula collapses to +// a length-only base. +const CHARS_PER_MINUTE = 1000; +const MIN_DURATION_S = 3; +const MAX_DURATION_S = 15; +const TICK_MS = 100; +const IMMERSIVE_CHAR_LIMIT = 120; + +function computeReadDuration(text: string): number { + const base = (text.length / CHARS_PER_MINUTE) * 60; + return Math.min(Math.max(base, MIN_DURATION_S), MAX_DURATION_S); +} + +function getImmersiveStyle(length: number) { + if (length < 30) + return { className: "text-5xl font-semibold leading-tight" }; + if (length < 70) + return { className: "text-3xl font-semibold leading-snug" }; + return { className: "text-2xl font-normal leading-snug" }; +} + +export function TextParticleView({ + particle, + paused, + onEnded, + onProgress, +}: TextParticleViewProps) { + const content = particle.properties.content; + const durationS = computeReadDuration(content); + const elapsedRef = useRef(0); + const safe = useStreamSafeArea(); + + // Reset when the particle changes. + useEffect(() => { + elapsedRef.current = 0; + onProgress(0); + }, [particle.id, onProgress]); + + useEffect(() => { + if (paused) return; + + const interval = setInterval(() => { + elapsedRef.current += TICK_MS / 1000; + const ratio = Math.min(elapsedRef.current / durationS, 1); + onProgress(ratio); + if (ratio >= 1) { + clearInterval(interval); + onEnded(); + } + }, TICK_MS); + + return () => clearInterval(interval); + }, [paused, durationS, onEnded, onProgress, particle.id]); + + // Immersive (short, plain): centered, large type — feels like a lock-screen note. + if (content.length < IMMERSIVE_CHAR_LIMIT) { + const style = getImmersiveStyle(content.length); + return ( + + + {content} + + + ); + } + + // Long text: scrollable card so the reader can pace themselves; the + // duration timer keeps ticking either way, which is intentional — + // long messages should still auto-advance at the 15s cap. Padding is + // pulled from the StreamSafeArea so the card never slips under chrome. + return ( + + + {content} + + + ); +} diff --git a/js/mobile/src/features/stream-view/stream-presence-context.tsx b/js/mobile/src/features/stream-view/stream-presence-context.tsx new file mode 100644 index 0000000..9b8e1df --- /dev/null +++ b/js/mobile/src/features/stream-view/stream-presence-context.tsx @@ -0,0 +1,203 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; +import { useChannel } from "@/hooks/use-channel"; +import { useAuthStore } from "@/stores/auth-store"; + +export type ComposingMode = "recording" | "typing" | "screen"; + +export interface ComposingUser { + humanId: string; + mode: ComposingMode; + lastSeen: number; +} + +interface StreamPresenceContextValue { + onlineHumanIds: Set; + composingUsers: ComposingUser[]; + startComposing: (mode: ComposingMode) => void; + stopComposing: () => void; +} + +const COMPOSING_TIMEOUT_MS = 10_000; +const COMPOSING_HEARTBEAT_MS = 5_000; +const COMPOSING_CLEANUP_INTERVAL_MS = 2_000; + +const StreamPresenceContext = createContext( + null, +); + +interface StreamPresenceProviderProps { + networkId: string; + streamId: string; + children: ReactNode; +} + +export function StreamPresenceProvider({ + networkId, + streamId, + children, +}: StreamPresenceProviderProps) { + const channelId = `stream:${networkId}:${streamId}`; + const { presence, messages, sendMessage } = useChannel(channelId); + const currentUserId = useAuthStore((s) => s.user?.id); + + const onlineHumanIds = useMemo(() => new Set(presence), [presence]); + + // --- Composing state --- + const [composingUsers, setComposingUsers] = useState([]); + const composingMapRef = useRef(new Map()); + const processedCountRef = useRef(0); + + // Process new messages incrementally — slicing the messages array means + // we don't re-scan the whole history every render. + useEffect(() => { + if (messages.length <= processedCountRef.current) return; + + const newMessages = messages.slice(processedCountRef.current); + processedCountRef.current = messages.length; + + let changed = false; + const map = composingMapRef.current; + + for (const msg of newMessages) { + const payload = msg.payload as + | { type: string; mode?: string } + | undefined; + if (!payload?.type) continue; + if (msg.humanId === currentUserId) continue; + + if (payload.type === "composing_start" && payload.mode) { + map.set(msg.humanId, { + humanId: msg.humanId, + mode: payload.mode as ComposingMode, + lastSeen: Date.now(), + }); + changed = true; + } else if (payload.type === "composing_stop") { + if (map.delete(msg.humanId)) changed = true; + } + } + + if (changed) { + setComposingUsers(Array.from(map.values())); + } + }, [messages, currentUserId]); + + // Drop composing entries when a user leaves the channel — covers the + // "they backgrounded the app without sending stop" case. + useEffect(() => { + const map = composingMapRef.current; + const onlineSet = new Set(presence); + let changed = false; + + for (const humanId of map.keys()) { + if (!onlineSet.has(humanId)) { + map.delete(humanId); + changed = true; + } + } + + if (changed) { + setComposingUsers(Array.from(map.values())); + } + }, [presence]); + + // Sweep stale composing entries (last heartbeat > 10s ago). + useEffect(() => { + const interval = setInterval(() => { + const map = composingMapRef.current; + const now = Date.now(); + let changed = false; + + for (const [humanId, entry] of map) { + if (now - entry.lastSeen > COMPOSING_TIMEOUT_MS) { + map.delete(humanId); + changed = true; + } + } + + if (changed) { + setComposingUsers(Array.from(map.values())); + } + }, COMPOSING_CLEANUP_INTERVAL_MS); + + return () => clearInterval(interval); + }, []); + + // --- Composing broadcast --- + const heartbeatRef = useRef | undefined>( + undefined, + ); + + const startComposing = useCallback( + (mode: ComposingMode) => { + sendMessage({ type: "composing_start", mode }); + if (heartbeatRef.current) clearInterval(heartbeatRef.current); + heartbeatRef.current = setInterval(() => { + sendMessage({ type: "composing_start", mode }); + }, COMPOSING_HEARTBEAT_MS); + }, + [sendMessage], + ); + + const stopComposing = useCallback(() => { + if (heartbeatRef.current) clearInterval(heartbeatRef.current); + heartbeatRef.current = undefined; + sendMessage({ type: "composing_stop" }); + }, [sendMessage]); + + useEffect(() => { + return () => { + if (heartbeatRef.current) clearInterval(heartbeatRef.current); + }; + }, []); + + const value = useMemo( + () => ({ + onlineHumanIds, + composingUsers, + startComposing, + stopComposing, + }), + [onlineHumanIds, composingUsers, startComposing, stopComposing], + ); + + return ( + + {children} + + ); +} + +function useStreamPresenceContext() { + const ctx = useContext(StreamPresenceContext); + if (!ctx) { + throw new Error( + "useStreamPresence must be used within a StreamPresenceProvider", + ); + } + return ctx; +} + +export function useStreamPresence() { + const { onlineHumanIds } = useStreamPresenceContext(); + return { onlineHumanIds }; +} + +export function useStreamComposing() { + const { composingUsers } = useStreamPresenceContext(); + return { composingUsers }; +} + +export function useStreamComposingBroadcast() { + const { startComposing, stopComposing } = useStreamPresenceContext(); + return { startComposing, stopComposing }; +} diff --git a/js/mobile/src/features/stream-view/stream-safe-area.tsx b/js/mobile/src/features/stream-view/stream-safe-area.tsx new file mode 100644 index 0000000..6e27287 --- /dev/null +++ b/js/mobile/src/features/stream-view/stream-safe-area.tsx @@ -0,0 +1,28 @@ +import { createContext, useContext, type ReactNode } from "react"; + +interface StreamSafeArea { + /** Pixels from the screen top reserved for the segmented bar + metadata. */ + top: number; + /** Pixels from the screen bottom reserved for compose dock + pills. */ + bottom: number; +} + +const Ctx = createContext({ top: 0, bottom: 0 }); + +/** + * Lets particle views know how much vertical space the chrome reserves so + * scrollable content (long text, future inboxes) doesn't slip under the + * segmented bar / compose dock. Defaults to 0/0 so views work outside the + * StreamView shell (e.g. in a preview). + */ +export function StreamSafeAreaProvider({ + top, + bottom, + children, +}: StreamSafeArea & { children: ReactNode }) { + return {children}; +} + +export function useStreamSafeArea(): StreamSafeArea { + return useContext(Ctx); +} diff --git a/js/mobile/src/features/stream-view/use-exit-countdown.ts b/js/mobile/src/features/stream-view/use-exit-countdown.ts new file mode 100644 index 0000000..2d60735 --- /dev/null +++ b/js/mobile/src/features/stream-view/use-exit-countdown.ts @@ -0,0 +1,48 @@ +import { useEffect, useState } from "react"; +import { useEvent } from "@/hooks/use-event"; + +export const EXIT_DELAY_MS = 5000; +export const EXIT_TICK_MS = 100; + +type PlaybackStatus = "idle" | "playing" | "ended"; + +/** + * Returns the remaining ms when the stream has ended, or null otherwise. + * Pauses while `paused` is true (compose, hold-to-pause, swipe-down…). + */ +export function useExitCountdown( + status: PlaybackStatus, + paused: boolean, + onExit: () => void, +): number | null { + const [remainingMs, setRemainingMs] = useState(null); + const handleExit = useEvent(onExit); + + useEffect(() => { + if (status === "ended") { + setRemainingMs(EXIT_DELAY_MS); + } else { + setRemainingMs(null); + } + }, [status]); + + useEffect(() => { + if (remainingMs === null || remainingMs <= 0 || paused) return; + const interval = setInterval(() => { + setRemainingMs((prev) => { + if (prev === null) return null; + const next = prev - EXIT_TICK_MS; + return next <= 0 ? 0 : next; + }); + }, EXIT_TICK_MS); + return () => clearInterval(interval); + }, [remainingMs !== null && remainingMs > 0, paused, remainingMs]); + + useEffect(() => { + if (remainingMs !== null && remainingMs <= 0) { + handleExit(); + } + }, [remainingMs, handleExit]); + + return remainingMs; +} diff --git a/js/mobile/src/features/streams/NewStreamScreen.tsx b/js/mobile/src/features/streams/NewStreamScreen.tsx new file mode 100644 index 0000000..2be3492 --- /dev/null +++ b/js/mobile/src/features/streams/NewStreamScreen.tsx @@ -0,0 +1,206 @@ +import { useMemo, useState } from "react"; +import { + KeyboardAvoidingView, + Platform, + Pressable, + Text, + TextInput, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { StatusBar } from "expo-status-bar"; +import { ChevronRight, Globe, Lock, X } from "lucide-react-native"; +import { toast } from "sonner-native"; +import { ComposeDock } from "@/features/compose/ComposeDock"; +import { useNetwork } from "@/hooks/use-networks"; +import { particlePath } from "@/lib/particle-path"; +import { generateRandomName } from "@/lib/random-name"; +import { createStreamWithFirstParticle } from "@/lib/upload"; +import { toUserMessage } from "@/lib/errors"; +import { + buildNetworkVisibility, + parseVisibleTo, +} from "@/lib/stream-visibility"; +import { useAuthStore } from "@/stores/auth-store"; +import type { RootStackScreenProps } from "@/navigation/types"; +import { VisibilityPickerSheet } from "./VisibilityPickerSheet"; + +const STREAM_NAME_MAX = 60; + +/** + * Top-level stream creation. The user names the stream, picks visibility, and + * composes the first particle on one screen — desktop's compose-overlay flow + * collapsed into a touch-native single page. + */ +export function NewStreamScreen({ + route, + navigation, +}: RootStackScreenProps<"NewStream">) { + const { networkId } = route.params; + const network = useNetwork(networkId); + const userId = useAuthStore((s) => s.user?.id); + + const suggestion = useMemo(() => generateRandomName(), []); + const [name, setName] = useState(""); + const [visibleTo, setVisibleTo] = useState(() => + buildNetworkVisibility(networkId), + ); + const [pickerOpen, setPickerOpen] = useState(false); + + const effectiveName = name.trim() || suggestion; + + const handleStreamCreated = (streamId: string) => { + navigation.replace("StreamView", { networkId, streamId }); + }; + + const submitText = async (content: string) => { + if (!userId) throw new Error("Not signed in."); + try { + const { streamId } = await createStreamWithFirstParticle({ + networkId, + name: effectiveName, + visibleTo, + createdByHumanId: userId, + firstParticle: { type: "text", content }, + }); + handleStreamCreated(streamId); + } catch (err) { + toast.error(toUserMessage(err)); + throw err; + } + }; + + const submitMedia = async ({ + fileUri, + mimeType, + durationMs, + source, + }: { + fileUri: string; + mimeType: string; + durationMs: number; + source: "camera" | "screen"; + }) => { + if (!userId) throw new Error("Not signed in."); + try { + const { streamId } = await createStreamWithFirstParticle({ + networkId, + name: effectiveName, + visibleTo, + createdByHumanId: userId, + firstParticle: { + type: "media", + fileUri, + mimeType, + durationMs, + source, + }, + }); + handleStreamCreated(streamId); + } catch (err) { + toast.error(toUserMessage(err)); + throw err; + } + }; + + const placeholderPath = particlePath(networkId, []); + + const visibility = parseVisibleTo(visibleTo, networkId); + const visibleSummary = + visibility.mode === "network" + ? `Everyone in ${network?.name ?? "this network"}` + : `${visibility.humanIds.length} ${ + visibility.humanIds.length === 1 ? "person" : "people" + }`; + + return ( + + + + + + navigation.goBack()} + hitSlop={12} + accessibilityLabel="Cancel" + > + + + + New stream + + + + + + + + + Name + + setName(v.slice(0, STREAM_NAME_MAX))} + placeholder={suggestion} + placeholderTextColor="rgba(255,255,255,0.35)" + autoCapitalize="none" + autoCorrect={false} + maxLength={STREAM_NAME_MAX} + className="bg-white/10 rounded-xl px-4 py-3 text-white text-lg" + /> + + + Visible to + + setPickerOpen(true)} + className="bg-white/10 active:bg-white/15 rounded-xl px-4 py-3 flex-row items-center gap-3" + > + {visibility.mode === "network" ? ( + + ) : ( + + )} + + {visibleSummary} + + + + + + + Hold the button below to record a voice or video message — that's + the first particle in your new stream. + + + + + + + + setPickerOpen(false)} + networkId={networkId} + networkName={network?.name} + humans={network?.humans ?? []} + selfHumanId={userId} + visibleTo={visibleTo} + onChange={setVisibleTo} + /> + + ); +} diff --git a/js/mobile/src/features/streams/StreamCard.tsx b/js/mobile/src/features/streams/StreamCard.tsx new file mode 100644 index 0000000..782d979 --- /dev/null +++ b/js/mobile/src/features/streams/StreamCard.tsx @@ -0,0 +1,156 @@ +import { memo, useMemo } from "react"; +import { Pressable, Text, View } from "react-native"; +import type { Particle, StreamProperties } from "@/api/types"; +import { isParticleDeleted } from "@/api/types"; +import { RelativeTimestamp } from "@/components/RelativeTimestamp"; +import { useLiveLatestChild } from "@/hooks/use-particle"; +import { useNetwork } from "@/hooks/use-networks"; +import { particlePath } from "@/lib/particle-path"; +import { cn, getInitials } from "@/lib/utils"; +import { useAuthStore } from "@/stores/auth-store"; + +interface StreamCardProps { + particle: Particle & { type: "stream"; properties: StreamProperties }; + networkId: string; + onPress: () => void; +} + +/** + * Mobile counterpart of js/desktop/src/features/particles/stream-card.tsx — + * same data wiring (subscribe to the latest child for unread + initials), + * touch-tuned layout (single row, no preview thumbnail in v1). + */ +export const StreamCard = memo(function StreamCard({ + particle, + networkId, + onPress, +}: StreamCardProps) { + const streamPath = particlePath(networkId, [particle.id]); + const { latestChild } = useLiveLatestChild(streamPath); + const userId = useAuthStore((s) => s.user?.id) ?? ""; + const network = useNetwork(networkId); + + const isDM = + particle.visible_to.length === 2 && + particle.visible_to.every((v) => v.startsWith("human:")); + + const initials = useMemo(() => { + if (isDM) { + const otherEntry = particle.visible_to.find( + (v) => v !== `human:${userId}`, + ); + if (otherEntry) { + const otherId = otherEntry.replace("human:", ""); + const otherHuman = network?.humans?.find((h) => h.id === otherId); + if (otherHuman) return getInitials(otherHuman.email); + } + } + + if (latestChild) { + const creator = network?.humans?.find( + (h) => h.id === latestChild.created_by_human_id, + ); + if (creator) return getInitials(creator.email); + } + + return particle.properties.name.slice(0, 2).toUpperCase(); + }, [ + isDM, + particle.visible_to, + particle.properties.name, + userId, + latestChild, + network, + ]); + + const isUnseen = useMemo(() => { + if (!latestChild) return false; + const latestChildTimestamp = latestChild.created_at.getTime(); + const userPlaybackPosition = + particle.playback_markers?.[userId]?.getTime() ?? 0; + return latestChildTimestamp > userPlaybackPosition; + }, [latestChild, particle.playback_markers, userId]); + + const previewLabel = useMemo(() => { + if (!latestChild) return "No messages yet"; + if (isParticleDeleted(latestChild)) return "Message deleted"; + switch (latestChild.type) { + case "media": + return latestChild.properties.mime_type.startsWith("audio/") + ? "Voice message" + : "Video message"; + case "text": + return latestChild.properties.content; + case "file": + return latestChild.properties.filename; + case "quest": + return latestChild.properties.title; + case "paper": + return latestChild.properties.title; + default: + return "Update"; + } + }, [latestChild]); + + return ( + + + + {initials} + + + + + + {particle.properties.name} + + + {previewLabel} + + + + + {latestChild ? ( + + ) : null} + {isUnseen ? ( + + ) : null} + + + ); +}); diff --git a/js/mobile/src/features/streams/StreamListScreen.tsx b/js/mobile/src/features/streams/StreamListScreen.tsx new file mode 100644 index 0000000..2f64a9f --- /dev/null +++ b/js/mobile/src/features/streams/StreamListScreen.tsx @@ -0,0 +1,140 @@ +import { + ActivityIndicator, + FlatList, + Pressable, + Text, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { toUserMessage } from "@/lib/errors"; +import { particlePath } from "@/lib/particle-path"; +import { useNetwork } from "@/hooks/use-networks"; +import { useStreamParticles } from "@/hooks/use-stream-particles"; +import type { RootStackScreenProps } from "@/navigation/types"; +import { StreamCard } from "./StreamCard"; + +export function StreamListScreen({ + route, + navigation, +}: RootStackScreenProps<"StreamList">) { + const { networkId } = route.params; + const network = useNetwork(networkId); + + const path = particlePath(networkId, []); + + const { streams, isLoading, error } = useStreamParticles(path, { + status: "open", + }); + + return ( + +
navigation.goBack()} + /> + + {error ? ( + + ) : isLoading && streams.length === 0 ? ( + + ) : streams.length === 0 ? ( + + ) : ( + s.id} + contentContainerClassName="" + renderItem={({ item }) => ( + + navigation.navigate("StreamView", { + networkId, + streamId: item.id, + }) + } + /> + )} + /> + )} + + navigation.navigate("NewStream", { networkId })} + /> + + ); +} + +function Header({ + title, + onBack, +}: { + title: string; + onBack: () => void; +}) { + return ( + + + + + + {title} + + + + ); +} + +function LoadingState() { + return ( + + + + ); +} + +function EmptyState() { + return ( + + + No streams yet. + + + Tap the button below to start one — voice, video, or text. + + + ); +} + +function ErrorState({ message }: { message: string }) { + return ( + + {message} + + Streams reconnect automatically once the network is back. + + + ); +} + +function ComposeFab({ onPress }: { onPress: () => void }) { + return ( + + + + + + + ); +} diff --git a/js/mobile/src/features/streams/VisibilityPickerSheet.tsx b/js/mobile/src/features/streams/VisibilityPickerSheet.tsx new file mode 100644 index 0000000..4bbc9b1 --- /dev/null +++ b/js/mobile/src/features/streams/VisibilityPickerSheet.tsx @@ -0,0 +1,218 @@ +import { useEffect, useMemo, useState } from "react"; +import { Pressable, ScrollView, Text, View } from "react-native"; +import { Check, Globe, Lock, X } from "lucide-react-native"; +import type { Human } from "@/api/types"; +import { cn } from "@/lib/utils"; +import { resolveHumanDisplay } from "@/lib/humans"; +import { + buildCustomVisibility, + buildNetworkVisibility, + parseVisibleTo, +} from "@/lib/stream-visibility"; +import { BottomSheet } from "@/components/BottomSheet"; +import { Avatar } from "@/components/Avatar"; + +interface VisibilityPickerSheetProps { + open: boolean; + onClose: () => void; + networkId: string; + networkName: string | undefined; + humans: Human[]; + selfHumanId: string | undefined; + visibleTo: string[]; + onChange: (visibleTo: string[]) => void; +} + +/** + * Touch-native visibility picker. Mirrors desktop's stream-members-overlay + * (network-wide vs. specific people) but as a bottom sheet that commits on + * close — the parent's `visibleTo` only updates when the user taps Done. + */ +export function VisibilityPickerSheet({ + open, + onClose, + networkId, + networkName, + humans, + selfHumanId, + visibleTo, + onChange, +}: VisibilityPickerSheetProps) { + const initial = useMemo( + () => parseVisibleTo(visibleTo, networkId), + [visibleTo, networkId], + ); + + const [mode, setMode] = useState<"network" | "custom">(initial.mode); + const [selected, setSelected] = useState>( + () => new Set(initial.mode === "custom" ? initial.humanIds : []), + ); + + useEffect(() => { + if (!open) return; + setMode(initial.mode); + setSelected( + new Set(initial.mode === "custom" ? initial.humanIds : []), + ); + }, [open, initial]); + + const others = humans.filter((h) => h.id !== selfHumanId); + + const toggle = (id: string) => { + setSelected((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + const commit = () => { + if (mode === "network") { + onChange(buildNetworkVisibility(networkId)); + } else { + const ids = selfHumanId + ? [selfHumanId, ...Array.from(selected)] + : Array.from(selected); + onChange(buildCustomVisibility(ids)); + } + onClose(); + }; + + const customCount = selected.size + (selfHumanId ? 1 : 0); + const canCommit = mode === "network" || customCount >= 2; + + return ( + + + + + + Visible to + + + Done + + + + + + + } + label="Everyone" + onPress={() => setMode("network")} + /> + } + label="Specific people" + onPress={() => setMode("custom")} + /> + + + + {mode === "network" ? ( + + + Everyone in {networkName ?? "this network"} can see this stream. + + + ) : ( + + {others.length === 0 ? ( + + You're the only member of this network. Invite people on desktop, + then come back to choose specific viewers. + + ) : ( + others.map((human) => { + const display = resolveHumanDisplay(human.id, humans); + const isSelected = selected.has(human.id); + return ( + toggle(human.id)} + className={cn( + "flex-row items-center gap-3 px-3 py-2.5 rounded-lg", + isSelected ? "bg-white/10" : "active:bg-white/5", + )} + > + + + + {display.displayName} + + + {display.email} + + + + {isSelected ? ( + + ) : null} + + + ); + }) + )} + + )} + + ); +} + +function ModePill({ + active, + icon, + label, + onPress, +}: { + active: boolean; + icon: React.ReactNode; + label: string; + onPress: () => void; +}) { + return ( + + {icon} + + {label} + + + ); +} diff --git a/js/mobile/src/firebase.ts b/js/mobile/src/firebase.ts new file mode 100644 index 0000000..a5bdba1 --- /dev/null +++ b/js/mobile/src/firebase.ts @@ -0,0 +1,30 @@ +import { initializeApp } from "firebase/app"; +import { + initializeAuth, + // `getReactNativePersistence` is documented Firebase RN setup but Firebase + // intentionally omits it from `firebase/auth`'s public type bundle (it + // would pollute web autocomplete). The runtime export exists on every + // platform; this is the workaround the Firebase docs themselves use. + // @ts-expect-error — RN-only symbol missing from public Firebase types. + getReactNativePersistence, +} from "firebase/auth"; +import { initializeFirestore } from "firebase/firestore"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { appConfig } from "@/config/env"; + +export const firebaseApp = initializeApp(appConfig.firebase); + +// AsyncStorage persists the Firebase auth token across cold starts. Without +// it, the user would have to re-sign-in to Firestore every launch even though +// the Orion bearer token is in SecureStore. +export const firebaseAuth = initializeAuth(firebaseApp, { + persistence: getReactNativePersistence(AsyncStorage), +}); + +// Firestore's default WebChannel transport often fails on cellular networks +// and behind aggressive proxies on iOS. Long-polling is the documented +// remedy for React Native and is also what the Firebase team recommends +// for mobile apps using the JS SDK. +export const firestoreDb = initializeFirestore(firebaseApp, { + experimentalAutoDetectLongPolling: true, +}); diff --git a/js/mobile/src/hooks/use-channel.ts b/js/mobile/src/hooks/use-channel.ts new file mode 100644 index 0000000..3cbc608 --- /dev/null +++ b/js/mobile/src/hooks/use-channel.ts @@ -0,0 +1,85 @@ +import { useCallback, useEffect, useState } from "react"; +import { usePusherClient } from "@/lib/pusher-provider"; +import type { ChannelMessage } from "@/lib/pusher-client"; + +interface UseChannelResult { + /** Current set of humanIds present in the channel */ + presence: string[]; + /** Messages received on this channel (since the hook mounted) */ + messages: ChannelMessage[]; + /** Send a message to the channel */ + sendMessage: (payload: unknown) => void; +} + +/** + * Subscribe to a pusher channel. Manages presence tracking and message delivery. + * Subscribes on mount, unsubscribes on unmount. + * + * @param channelId - The channel to subscribe to, or null to skip. + */ +export function useChannel(channelId: string | null): UseChannelResult { + const client = usePusherClient(); + const [presence, setPresence] = useState([]); + const [messages, setMessages] = useState([]); + + useEffect(() => { + if (!client || !channelId) { + setPresence([]); + setMessages([]); + return; + } + + client.subscribe(channelId); + + const onSubscribed = (msg: { presence?: string[] }) => { + setPresence(msg.presence ?? []); + }; + + const onJoin = (msg: { humanId?: string }) => { + if (msg.humanId) { + setPresence((prev) => + prev.includes(msg.humanId!) ? prev : [...prev, msg.humanId!], + ); + } + }; + + const onLeave = (msg: { humanId?: string }) => { + if (msg.humanId) { + setPresence((prev) => prev.filter((id) => id !== msg.humanId)); + } + }; + + const onMessage = (msg: { humanId?: string; payload?: unknown }) => { + if (msg.humanId) { + setMessages((prev) => [ + ...prev, + { humanId: msg.humanId!, payload: msg.payload }, + ]); + } + }; + + client.on(channelId, "subscribed", onSubscribed); + client.on(channelId, "join", onJoin); + client.on(channelId, "leave", onLeave); + client.on(channelId, "message", onMessage); + + return () => { + client.off(channelId, "subscribed", onSubscribed); + client.off(channelId, "join", onJoin); + client.off(channelId, "leave", onLeave); + client.off(channelId, "message", onMessage); + client.unsubscribe(channelId); + }; + }, [client, channelId]); + + const sendMessage = useCallback( + (payload: unknown) => { + if (client && channelId) { + client.sendMessage(channelId, payload); + } + }, + [client, channelId], + ); + + return { presence, messages, sendMessage }; +} diff --git a/js/mobile/src/hooks/use-event.ts b/js/mobile/src/hooks/use-event.ts new file mode 100644 index 0000000..a3ae51f --- /dev/null +++ b/js/mobile/src/hooks/use-event.ts @@ -0,0 +1,18 @@ +import { useCallback, useLayoutEffect, useRef } from "react"; + +// Polyfill for React's `useEffectEvent` (canary). The returned function has a +// stable identity but always sees the latest closure — exactly what +// `useEffectEvent` provides. Stable enough that we use it everywhere we'd +// otherwise reach for a ref + .current dance inside an effect. +// +// Replace with `useEffectEvent` once it ships in stable React. Call sites +// don't need to change. +export function useEvent( + fn: (...args: TArgs) => TReturn, +): (...args: TArgs) => TReturn { + const ref = useRef(fn); + useLayoutEffect(() => { + ref.current = fn; + }); + return useCallback((...args: TArgs) => ref.current(...args), []); +} diff --git a/js/mobile/src/hooks/use-networks.ts b/js/mobile/src/hooks/use-networks.ts new file mode 100644 index 0000000..1ea1f38 --- /dev/null +++ b/js/mobile/src/hooks/use-networks.ts @@ -0,0 +1,23 @@ +import { useQuery } from "@tanstack/react-query"; +import { apiClient } from "@/api/client"; +import { useAuthStore } from "@/stores/auth-store"; + +export function useNetworks() { + return useQuery({ + queryKey: ["networks"], + queryFn: () => apiClient.listNetworks(), + meta: { toastOnError: true }, + }); +} + +export function useNetwork(networkId: string) { + const { data: networks } = useNetworks(); + return networks?.find((n) => n.id === networkId) ?? null; +} + +export function useIsNetworkAdmin(networkId: string): boolean { + const network = useNetwork(networkId); + const userId = useAuthStore((s) => s.user?.id); + if (!network || !userId) return false; + return network.admin_human.id === userId; +} diff --git a/js/mobile/src/hooks/use-particle.ts b/js/mobile/src/hooks/use-particle.ts new file mode 100644 index 0000000..f66c91d --- /dev/null +++ b/js/mobile/src/hooks/use-particle.ts @@ -0,0 +1,186 @@ +import { useState, useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; +import type { QueryFieldFilterConstraint } from "firebase/firestore"; +import { + subscribeToParticle, + subscribeToParticleChildren, + subscribeToLatestChild, + getParticle, + getParticleChildren, +} from "@/lib/firestore-particles"; +import type { Particle } from "@/api/types"; +import { + type ParticlePath, + toFirestoreDocPath, + toFirestoreChildrenPath, +} from "@/lib/particle-path"; +import { logError } from "@/lib/errors"; + +interface UseLiveParticleResult { + particle: Particle | null; + isLoading: boolean; + error: Error | null; +} + +export function useLiveParticle(path: ParticlePath): UseLiveParticleResult { + const [particle, setParticle] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + setIsLoading(true); + setError(null); + setParticle(null); + + const docPath = toFirestoreDocPath(path); + const unsubscribe = subscribeToParticle( + docPath, + (data) => { + setParticle(data); + setIsLoading(false); + }, + (err) => { + setError(err); + setIsLoading(false); + }, + ); + + return unsubscribe; + }, [path]); + + return { particle, isLoading, error }; +} + +interface UseLiveParticleChildrenResult { + children: Particle[]; + isLoading: boolean; + error: Error | null; +} + +interface UseLiveParticleChildrenParams { + orderByField?: string; + orderDirection?: "asc" | "desc"; + visibilityScopes?: string[]; + onAdded?: (child: Particle) => void; + onRemoved?: (child: Particle, updatedChildren: Particle[]) => void; + whereFilter?: QueryFieldFilterConstraint; + /** Optional cap on results. Changes trigger a re-subscription. */ + limit?: number; +} + +export function useLiveParticleChildren( + path: ParticlePath | undefined, + { + orderByField = "created_at", + orderDirection = "desc", + visibilityScopes, + onAdded, + onRemoved, + whereFilter, + limit, + }: UseLiveParticleChildrenParams = {}, +): UseLiveParticleChildrenResult { + const [children, setChildren] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!path) { + setChildren([]); + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + setChildren([]); + + const collectionPath = toFirestoreChildrenPath(path); + + const unsubscribe = subscribeToParticleChildren(collectionPath, { + onData: (data) => { + setChildren(data); + setIsLoading(false); + }, + onError: (err) => { + logError(err, { scope: "firestore.particle-children", path }); + setError(err); + setIsLoading(false); + }, + visibilityScopes, + orderByField, + orderDirection, + onAdded, + onRemoved, + whereFilter, + limit, + }); + + return unsubscribe; + // The hook intentionally keys only on path/whereFilter/limit — desktop + // does the same. Visibility scope changes are absorbed by the active + // listener; reordering causes a re-subscription. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [path, whereFilter, limit]); + + return { children, isLoading, error }; +} + +interface UseLiveLatestChildResult { + latestChild: Particle | null; + isLoading: boolean; +} + +export function useLiveLatestChild( + path: ParticlePath, +): UseLiveLatestChildResult { + const [latestChild, setLatestChild] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + setLatestChild(null); + + const unsubscribe = subscribeToLatestChild( + toFirestoreChildrenPath(path), + (data) => { + setLatestChild(data); + setIsLoading(false); + }, + (err) => { + logError(err, { scope: "firestore.latest-child", path }); + setIsLoading(false); + }, + ); + + return unsubscribe; + }, [path]); + + return { latestChild, isLoading }; +} + +export function useParticle(path?: ParticlePath) { + return useQuery({ + queryKey: ["particle", path], + queryFn: async () => { + if (!path) return null; + const docPath = toFirestoreDocPath(path); + const particle = await getParticle(docPath); + return particle; + }, + enabled: !!path, + }); +} + +export function useParticleChildren(path?: ParticlePath) { + return useQuery({ + queryKey: ["particle-children", path], + queryFn: async () => { + if (!path) return []; + const collectionPath = toFirestoreChildrenPath(path); + return getParticleChildren(collectionPath); + }, + enabled: !!path, + staleTime: 1000 * 60 * 5, // 5 min — attachments don't change + }); +} diff --git a/js/mobile/src/hooks/use-stream-particles.ts b/js/mobile/src/hooks/use-stream-particles.ts new file mode 100644 index 0000000..7085197 --- /dev/null +++ b/js/mobile/src/hooks/use-stream-particles.ts @@ -0,0 +1,95 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { where, type QueryFieldFilterConstraint } from "firebase/firestore"; +import { useLiveParticleChildren } from "@/hooks/use-particle"; +import { useAuthStore } from "@/stores/auth-store"; +import { parseParticlePath, type ParticlePath } from "@/lib/particle-path"; +import type { Particle, StreamProperties } from "@/api/types"; + +export type StreamParticle = Particle & { + type: "stream"; + properties: StreamProperties; +}; + +const CLOSED_INITIAL_PAGE_SIZE = 50; +const CLOSED_PAGE_INCREMENT = 50; + +// Stable where-constraint references so the Firestore subscription only +// re-attaches when the tab actually changes, not on every render. +const OPEN_STATUS_FILTER = where("status", "==", "open"); +const CLOSED_STATUS_FILTER = where("status", "==", "closed"); + +function useVisibilityScopes(userId?: string, networkId?: string) { + return useMemo(() => { + const scopes: string[] = []; + if (userId) scopes.push(`human:${userId}`); + if (networkId) scopes.push(`network:${networkId}`); + return scopes; + }, [userId, networkId]); +} + +interface UseStreamParticlesOptions { + /** + * Which streams to subscribe to. Open streams are loaded in full (bounded + * by active work — full realtime coverage is needed for autoplay/huddles). + * Closed streams are paginated via `loadMore`. + */ + status: "open" | "closed"; +} + +interface UseStreamParticlesResult { + streams: StreamParticle[]; + isLoading: boolean; + error: Error | null; + networkId: string; + /** True when more closed streams may exist beyond the current window. */ + canLoadMore: boolean; + /** Extend the pagination window. No-op on the open tab. */ + loadMore: () => void; +} + +export function useStreamParticles( + path: ParticlePath, + { status }: UseStreamParticlesOptions, +): UseStreamParticlesResult { + const { networkId } = parseParticlePath(path); + const user = useAuthStore((s) => s.user); + const visibilityScopes = useVisibilityScopes(user?.id, networkId); + + const [closedLimit, setClosedLimit] = useState(CLOSED_INITIAL_PAGE_SIZE); + + // Every time the user switches back to the closed tab, start with a fresh + // window. Avoids an ever-growing subscription across a long session. + useEffect(() => { + if (status === "closed") { + setClosedLimit(CLOSED_INITIAL_PAGE_SIZE); + } + }, [status]); + + const whereFilter: QueryFieldFilterConstraint = + status === "open" ? OPEN_STATUS_FILTER : CLOSED_STATUS_FILTER; + const limit = status === "closed" ? closedLimit : undefined; + + const { children, isLoading, error } = useLiveParticleChildren(path, { + orderByField: "last_child_created_at", + orderDirection: "desc", + visibilityScopes, + whereFilter, + limit, + }); + + const streams = useMemo( + () => children.filter((c): c is StreamParticle => c.type === "stream"), + [children], + ); + + // Heuristic: if we got back as many items as we asked for, assume there + // might be more. Clicking load-more when there are no more is a no-op. + const canLoadMore = status === "closed" && streams.length >= closedLimit; + + const loadMore = useCallback(() => { + if (status !== "closed") return; + setClosedLimit((prev) => prev + CLOSED_PAGE_INCREMENT); + }, [status]); + + return { streams, isLoading, error, networkId, canLoadMore, loadMore }; +} diff --git a/js/mobile/src/hooks/use-stream-playback.ts b/js/mobile/src/hooks/use-stream-playback.ts new file mode 100644 index 0000000..5bfe03b --- /dev/null +++ b/js/mobile/src/hooks/use-stream-playback.ts @@ -0,0 +1,258 @@ +import { useCallback, useEffect, useMemo, useReducer, useRef } from "react"; +import { useAuthStore } from "@/stores/auth-store"; +import type { Particle } from "@/api/types"; +import { useLiveParticleChildren } from "@/hooks/use-particle"; +import { toFirestoreDocPath, type ParticlePath } from "@/lib/particle-path"; +import { updateStreamPlaybackMarker } from "@/lib/firestore-particles"; +import { logError } from "@/lib/errors"; +import { useEvent } from "@/hooks/use-event"; + +// --- Playback reducer (ID-based) --- + +type PlaybackStatus = "idle" | "playing" | "ended"; + +interface PlaybackState { + currentParticleId: string | null; + status: PlaybackStatus; + initialized: boolean; +} + +type PlaybackAction = + | { type: "INIT"; particleId: string } + | { type: "SET_PARTICLE"; particleId: string } + | { type: "END" } + | { type: "PARTICLE_ADDED"; particleId: string } + | { + type: "PARTICLE_REMOVED"; + removedParticleId: string; + fallbackParticleId: string | null; + }; + +const initialState: PlaybackState = { + currentParticleId: null, + status: "idle", + initialized: false, +}; + +function playbackReducer( + state: PlaybackState, + action: PlaybackAction, +): PlaybackState { + switch (action.type) { + case "INIT": + return { + currentParticleId: action.particleId, + status: "playing", + initialized: true, + }; + case "SET_PARTICLE": + return { + ...state, + currentParticleId: action.particleId, + status: "playing", + }; + case "END": + return { ...state, status: "ended" }; + case "PARTICLE_ADDED": + if (state.status === "ended") { + return { + ...state, + currentParticleId: action.particleId, + status: "playing", + }; + } + return state; + case "PARTICLE_REMOVED": + if (action.removedParticleId !== state.currentParticleId) return state; + if (action.fallbackParticleId) { + return { + ...state, + currentParticleId: action.fallbackParticleId, + status: "playing", + }; + } + return { ...state, currentParticleId: null, status: "idle" }; + } +} + +const INIT_FALLBACK_TIMEOUT_MS = 5000; + +interface UseStreamPlaybackResult { + children: Particle[]; + currentParticle: Particle | null; + currentIndex: number; + status: PlaybackStatus; + initialized: boolean; + next: () => void; + prev: () => void; + goTo: (index: number) => void; + goToParticle: (particleId: string) => void; +} + +export function useStreamPlayback( + streamParticle: Particle & { type: "stream" }, + path: ParticlePath, +): UseStreamPlaybackResult { + const userId = useAuthStore((s) => s.user?.id); + const [state, dispatch] = useReducer(playbackReducer, initialState); + // Track which stream we initialized for, so navigating to a sibling resets cleanly. + const initializedForRef = useRef(null); + + const onParticleAdded = useCallback((particle: Particle) => { + dispatch({ type: "PARTICLE_ADDED", particleId: particle.id }); + }, []); + + const onParticleRemoved = useEvent( + (removed: Particle, updatedChildren: Particle[]) => { + const fallbackIndex = Math.min(currentIndex, updatedChildren.length - 1); + const fallback = updatedChildren[Math.max(0, fallbackIndex)]; + dispatch({ + type: "PARTICLE_REMOVED", + removedParticleId: removed.id, + fallbackParticleId: fallback?.id ?? null, + }); + }, + ); + + const { children } = useLiveParticleChildren(path, { + orderByField: "created_at", + orderDirection: "asc", + onAdded: onParticleAdded, + onRemoved: onParticleRemoved, + }); + + // Derive current index and particle from ID + const currentIndex = useMemo(() => { + if (!state.currentParticleId) return -1; + return children.findIndex((c) => c.id === state.currentParticleId); + }, [children, state.currentParticleId]); + + const currentParticle = currentIndex !== -1 ? children[currentIndex] : null; + + const initFallback = useEvent(() => { + if (state.initialized || children.length === 0) return; + initializedForRef.current = streamParticle.id; + dispatch({ type: "INIT", particleId: children[0].id }); + }); + + // --- Init logic: runs on every children change until initialized --- + useEffect(() => { + if ( + initializedForRef.current !== null && + initializedForRef.current !== streamParticle.id + ) { + initializedForRef.current = null; + } + + if (state.initialized && initializedForRef.current === streamParticle.id) + return; + + if (children.length === 0) return; + + const playbackPosition = streamParticle.playback_markers?.[userId ?? ""]; + + if (!playbackPosition) { + initializedForRef.current = streamParticle.id; + dispatch({ type: "INIT", particleId: children[0].id }); + return; + } + + const found = children.find( + (c) => c.created_at.getTime() > playbackPosition.getTime(), + ); + + if (found) { + initializedForRef.current = streamParticle.id; + dispatch({ type: "INIT", particleId: found.id }); + return; + } else { + initializedForRef.current = streamParticle.id; + dispatch({ + type: "INIT", + particleId: children[children.length - 1].id, + }); + } + + const timeout = setTimeout(initFallback, INIT_FALLBACK_TIMEOUT_MS); + return () => clearTimeout(timeout); + }, [ + children, + streamParticle.id, + streamParticle.playback_markers, + userId, + state.initialized, + initFallback, + ]); + + // --- Persist playback marker (only advance forward, never backwards) --- + const lastPersistedMarkerRef = useRef(null); + + useEffect(() => { + if (!userId || !state.initialized || !currentParticle) return; + + const currentTime = currentParticle.created_at; + const existingMarker = + lastPersistedMarkerRef.current ?? + streamParticle.playback_markers?.[userId]; + + if (existingMarker && currentTime.getTime() <= existingMarker.getTime()) + return; + + lastPersistedMarkerRef.current = currentTime; + const streamDocPath = toFirestoreDocPath(path); + updateStreamPlaybackMarker(streamDocPath, userId, currentTime).catch( + (err) => logError(err, { scope: "playback.marker", path }), + ); + // streamParticle.playback_markers is read at effect time; not in deps to + // avoid double-writes when the snapshot we just persisted echoes back. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentParticle?.id, state.initialized, userId, path]); + + // --- Navigation callbacks --- + const next = useCallback(() => { + if (currentIndex === -1) return; + if (currentIndex < children.length - 1) { + dispatch({ + type: "SET_PARTICLE", + particleId: children[currentIndex + 1].id, + }); + } else { + dispatch({ type: "END" }); + } + }, [children, currentIndex]); + + const prev = useCallback(() => { + if (currentIndex <= 0) return; + dispatch({ + type: "SET_PARTICLE", + particleId: children[currentIndex - 1].id, + }); + }, [children, currentIndex]); + + const goTo = useCallback( + (index: number) => { + if (index >= 0 && index < children.length) { + dispatch({ type: "SET_PARTICLE", particleId: children[index].id }); + } + }, + [children], + ); + + // If the particle isn't in `children` yet (e.g. just-created), the live + // query will resolve it shortly and the derived index/particle will catch up. + const goToParticle = useCallback((particleId: string) => { + dispatch({ type: "SET_PARTICLE", particleId }); + }, []); + + return { + children, + currentParticle, + currentIndex, + status: state.status, + initialized: state.initialized, + next, + prev, + goTo, + goToParticle, + }; +} diff --git a/js/mobile/src/hooks/use-suspend-playback.ts b/js/mobile/src/hooks/use-suspend-playback.ts new file mode 100644 index 0000000..8ca363f --- /dev/null +++ b/js/mobile/src/hooks/use-suspend-playback.ts @@ -0,0 +1,17 @@ +import { useEffect, useId } from "react"; +import { usePlaybackPauseStore } from "@/stores/playback-pause-store"; + +/** + * Suspend stream playback while `active` is true. The hook owns its own + * registration id; multiple instances compose. `label` is for devtools only. + */ +export function useSuspendPlayback(active: boolean, label: string) { + const id = useId(); + + useEffect(() => { + if (!active) return; + const { add, remove } = usePlaybackPauseStore.getState(); + add(id, label); + return () => remove(id); + }, [active, id, label]); +} diff --git a/js/mobile/src/lib/errors.ts b/js/mobile/src/lib/errors.ts new file mode 100644 index 0000000..d3ac284 --- /dev/null +++ b/js/mobile/src/lib/errors.ts @@ -0,0 +1,90 @@ +import { z } from "zod"; +import { appEnv } from "@/config/env"; + +export class ApiError extends Error { + constructor( + public status: number, + message: string, + ) { + super(message); + this.name = "ApiError"; + } +} + +/** + * Thrown when a free-plan network attempts to create a non-container particle + * after hitting its daily message limit. Compose UI also disables triggers + * proactively via `useNetworkUsage` — this throw is a last-line defense. + */ +export class QuotaExceededError extends Error { + constructor(public readonly networkId: string) { + super("Daily message limit reached"); + this.name = "QuotaExceededError"; + } +} + +function normalizeMessage(message: string): string { + return message.replace(/^Error:\s*/, "").trim(); +} + +export function toUserMessage(err: unknown): string { + if (err instanceof ApiError) { + if (err.status === 401) return "Please sign in again."; + if (err.status === 403) return "You don't have permission to do that."; + if (err.status === 404) return "Not found."; + if (err.status === 408 || err.status === 429) { + return "Please try again in a moment."; + } + if (err.status >= 500) { + return "Something went wrong on our end. Please try again."; + } + return normalizeMessage(err.message) || "Request failed."; + } + + if (err instanceof z.ZodError) { + return "Received unexpected data from the server."; + } + + if (err instanceof TypeError && /fetch|network/i.test(err.message)) { + return "Network error. Check your connection."; + } + + if (err instanceof Error) { + return normalizeMessage(err.message) || "Something went wrong."; + } + + return "Something went wrong."; +} + +type ErrorContext = Record; +type ErrorSink = (err: unknown, context?: ErrorContext) => void; + +// Sentry (or any observability backend) installs itself via `installErrorSinks` +// from App.tsx. Until then, logError is a dev-only console call and reportError +// always prints — no call site needs to know. +let captureSink: ErrorSink | null = null; +let breadcrumbSink: ErrorSink | null = null; + +export function installErrorSinks(sinks: { + capture: ErrorSink; + breadcrumb: ErrorSink; +}): void { + captureSink = sinks.capture; + breadcrumbSink = sinks.breadcrumb; +} + +/** Expected-but-recordable failures. Breadcrumb only — never pages anyone. */ +export function logError(err: unknown, context?: ErrorContext): void { + if (appEnv === "dev") { + // eslint-disable-next-line no-console + console.error("[error]", err, context ?? {}); + } + breadcrumbSink?.(err, context); +} + +/** Unexpected failures the user may not see. Always captured. */ +export function reportError(err: unknown, context?: ErrorContext): void { + // eslint-disable-next-line no-console + console.error("[error]", err, context ?? {}); + captureSink?.(err, context); +} diff --git a/js/mobile/src/lib/firestore-particles.ts b/js/mobile/src/lib/firestore-particles.ts new file mode 100644 index 0000000..ce1d19e --- /dev/null +++ b/js/mobile/src/lib/firestore-particles.ts @@ -0,0 +1,435 @@ +import { + collection, + doc, + onSnapshot, + addDoc, + getDoc, + getDocs, + updateDoc, + query, + orderBy, + limit, + serverTimestamp, + where, + Timestamp, + arrayUnion, + arrayRemove, + type DocumentData, + type FirestoreDataConverter, + type QueryDocumentSnapshot, + type SnapshotOptions, + type Unsubscribe, + type QueryFieldFilterConstraint, +} from "firebase/firestore"; +import { firestoreDb } from "@/firebase"; +import { isContainerType, ParticleSchema } from "@/api/types"; +import type { + Particle, + ParticleType, + ParticlePropertiesMap, + Reactions, +} from "@/api/types"; + +// --- Converter --- + +const particleConverter: FirestoreDataConverter = { + toFirestore(particle: Particle): DocumentData { + const { id: _id, created_at, updated_at, ...rest } = particle; + const deletedAt = + "deleted_at" in particle ? particle.deleted_at : undefined; + return { + ...rest, + created_at: Timestamp.fromDate(created_at), + ...(updated_at && { updated_at: Timestamp.fromDate(updated_at) }), + ...(deletedAt && { deleted_at: Timestamp.fromDate(deletedAt) }), + }; + }, + fromFirestore( + snap: QueryDocumentSnapshot, + options?: SnapshotOptions, + ): Particle { + const raw = snap.data(options); + if (typeof raw.type !== "string") { + throw new Error(`Invalid particle type: ${raw.type}`); + } + const type = raw.type as ParticleType; + switch (type) { + case "stream": + return ParticleSchema.parse({ + id: snap.id, + type: raw.type, + properties: raw.properties, + created_at: (raw.created_at as Timestamp).toDate(), + created_by_human_id: raw.created_by_human_id, + updated_at: raw.updated_at + ? (raw.updated_at as Timestamp).toDate() + : undefined, + visible_to: raw.visible_to, + playback_markers: raw.playback_markers + ? Object.fromEntries( + Object.entries(raw.playback_markers).map(([key, value]) => [ + key, + (value as Timestamp).toDate(), + ]), + ) + : undefined, + last_child_created_at: raw.last_child_created_at + ? (raw.last_child_created_at as Timestamp).toDate() + : undefined, + huddle_active_participants: + raw.huddle_active_participants ?? undefined, + status: raw.status ?? undefined, + }); + case "folder": + return ParticleSchema.parse({ + id: snap.id, + type: raw.type, + properties: raw.properties, + created_at: (raw.created_at as Timestamp).toDate(), + created_by_human_id: raw.created_by_human_id, + updated_at: raw.updated_at + ? (raw.updated_at as Timestamp).toDate() + : undefined, + visible_to: raw.visible_to, + }); + case "media": + case "file": + case "text": + case "quest": + case "paper": { + // Firestore stores timestamps as `Timestamp`; zod expects `Date`. Text + // particles carry `properties.edited_at`, so coerce it if present. + const properties = + type === "text" && raw.properties?.edited_at + ? { + ...raw.properties, + edited_at: (raw.properties.edited_at as Timestamp).toDate(), + } + : raw.properties; + return ParticleSchema.parse({ + id: snap.id, + type: raw.type, + properties, + created_at: (raw.created_at as Timestamp).toDate(), + created_by_human_id: raw.created_by_human_id, + updated_at: raw.updated_at + ? (raw.updated_at as Timestamp).toDate() + : undefined, + reactions: raw.reactions ?? undefined, + deleted_at: raw.deleted_at + ? (raw.deleted_at as Timestamp).toDate() + : undefined, + deleted_by_human_id: raw.deleted_by_human_id ?? undefined, + }); + } + default: + throw new Error(`Unknown particle type: ${type}`); + } + }, +}; + +// --- Typed reference helpers --- + +function typedDoc(path: string) { + return doc(firestoreDb, path).withConverter(particleConverter); +} + +function typedCollection(path: string) { + return collection(firestoreDb, path).withConverter(particleConverter); +} + +// --- Exported operations --- + +export function subscribeToParticle( + docPath: string, + onData: (particle: Particle | null) => void, + onError: (error: Error) => void, +): Unsubscribe { + return onSnapshot( + typedDoc(docPath), + (snap) => { + onData(snap.exists() ? snap.data() : null); + }, + onError, + ); +} + +export async function getParticle(docPath: string): Promise { + const docSnap = await getDoc(typedDoc(docPath)); + if (!docSnap.exists()) { + return null; + } + return docSnap.data(); +} + +export interface GetParticleChildrenOptions { + orderByField: string; + orderDirection: "asc" | "desc"; +} + +export async function getParticleChildren( + collectionPath: string, + { + orderByField = "created_at", + orderDirection = "asc", + }: GetParticleChildrenOptions = { + orderByField: "created_at", + orderDirection: "asc", + }, +): Promise { + const q = query( + typedCollection(collectionPath), + orderBy(orderByField, orderDirection), + ); + const snap = await getDocs(q); + return snap.docs.map((d) => d.data()); +} + +export interface SubscribeToParticleChildrenOptions { + onData: (children: Particle[]) => void; + onError: (error: Error) => void; + visibilityScopes?: string[]; + orderByField?: string; + orderDirection?: "asc" | "desc"; + onAdded?: (child: Particle) => void; + onRemoved?: (child: Particle, updatedChildren: Particle[]) => void; + whereFilter?: QueryFieldFilterConstraint; + /** Optional cap on results. Applied after order/where constraints. */ + limit?: number; +} + +export function subscribeToParticleChildren( + collectionPath: string, + { + onData, + onError, + visibilityScopes = [], + orderByField = "created_at", + orderDirection = "desc", + onAdded, + onRemoved, + whereFilter, + limit: limitValue, + }: SubscribeToParticleChildrenOptions, +): Unsubscribe { + let q = query( + typedCollection(collectionPath), + orderBy(orderByField, orderDirection), + ); + if (visibilityScopes.length > 0) { + q = query(q, where("visible_to", "array-contains-any", visibilityScopes)); + } + if (whereFilter) { + q = query(q, whereFilter); + } + if (limitValue !== undefined) { + q = query(q, limit(limitValue)); + } + return onSnapshot( + q, + (snap) => { + const updatedChildren = snap.docs.map((d) => d.data()); + onData(updatedChildren); + + if (onAdded || onRemoved) { + for (const change of snap.docChanges()) { + if (change.type === "added" && onAdded) onAdded(change.doc.data()); + if (change.type === "removed" && onRemoved) + onRemoved(change.doc.data(), updatedChildren); + } + } + }, + onError, + ); +} + +export function subscribeToLatestChild( + collectionPath: string, + onData: (child: Particle | null) => void, + onError: (error: Error) => void, +): Unsubscribe { + const q = query( + typedCollection(collectionPath), + orderBy("created_at", "desc"), + limit(1), + ); + return onSnapshot( + q, + (snap) => { + onData(snap.empty ? null : snap.docs[0].data()); + }, + onError, + ); +} + +// This creates a new particle document with the given properties and returns its ID. +export async function createParticle( + collectionPath: string, + type: T, + properties: ParticlePropertiesMap[T], + createdByHumanId: string, + // Must be passed for container types + visibleTo?: string[], +): Promise { + if (isContainerType(type) && (!visibleTo || visibleTo.length === 0)) { + throw new Error( + `visibleTo is required for container type ${type} and cannot be empty`, + ); + } + + const particle: Particle = ParticleSchema.parse({ + id: "", // ignored by toFirestore, but needed to satisfy the type + type, + properties, + created_at: new Date(), + created_by_human_id: createdByHumanId, + ...(visibleTo ? { visible_to: visibleTo } : {}), + }); + const ref = await addDoc(typedCollection(collectionPath), particle); + return ref.id; +} + +export async function createStreamParticle( + collectionPath: string, + properties: ParticlePropertiesMap["stream"], + createdByHumanId: string, + visibleTo?: string[], +): Promise { + if (!visibleTo || visibleTo.length === 0) { + throw new Error("visibleTo is required for streams and cannot be empty"); + } + + const particle: Particle = ParticleSchema.parse({ + id: "", + type: "stream", + properties, + created_at: new Date(), + created_by_human_id: createdByHumanId, + visible_to: visibleTo, + status: "open", + }); + const ref = await addDoc(typedCollection(collectionPath), particle); + return ref.id; +} + +// This allows updating properties without overwriting the entire properties object +export async function updateParticleProperties( + docPath: string, + properties: Partial, +): Promise { + const particleRef = typedDoc(docPath); + // Take the partial and create a new object with dot notation + // e.g. { title: "New Title" } becomes { "properties.title": "New Title" } + const updatedProperties: Record = {}; + for (const key in properties) { + updatedProperties[`properties.${key}`] = properties[key]; + } + await updateDoc(particleRef, { + ...updatedProperties, + updated_at: serverTimestamp(), + }); +} + +// Edits the body of a text particle and stamps `properties.edited_at` so +// readers can see that the message was edited (distinct from `updated_at`, +// which is bumped by any write — visibility, reactions, etc.). +export async function editTextParticleContent( + docPath: string, + content: string, +): Promise { + const particleRef = typedDoc(docPath); + await updateDoc(particleRef, { + "properties.content": content, + "properties.edited_at": serverTimestamp(), + updated_at: serverTimestamp(), + }); +} + +export async function updateParticleVisibleTo( + docPath: string, + visibleTo: string[], +): Promise { + const particleRef = typedDoc(docPath); + const particle = await getParticle(docPath); + if (!particle) { + throw new Error(`Particle not found at path: ${docPath}`); + } + if (!isContainerType(particle.type)) { + throw new Error( + `Only container particles can have visible_to field. Particle at ${docPath} is of type ${particle.type}`, + ); + } + + await updateDoc(particleRef, { + visible_to: visibleTo, + updated_at: serverTimestamp(), + }); +} + +// CAUTION: use the other type safe update functions in most cases +// There is no checking whether this field actually exists on the particle type, so it can lead to inconsistent data if used incorrectly +export async function updateParticle( + docPath: string, + fieldName: string, + value: unknown, +): Promise { + const particleRef = typedDoc(docPath); + await updateDoc(particleRef, { + [fieldName]: value, + updated_at: serverTimestamp(), + }); +} + +export async function updateStreamStatus( + docPath: string, + status: "open" | "closed", +): Promise { + const particleRef = typedDoc(docPath); + await updateDoc(particleRef, { status, updated_at: serverTimestamp() }); +} + +/** + * Soft-delete (tombstone) a non-container particle. The Firestore doc stays + * in place so concurrent viewers see the deletion inline rather than being + * bumped to an adjacent particle. Idempotent — re-calling on an already + * tombstoned doc just refreshes the timestamp. + */ +export async function softDeleteParticle( + docPath: string, + humanId: string, +): Promise { + const particleRef = typedDoc(docPath); + await updateDoc(particleRef, { + deleted_at: serverTimestamp(), + deleted_by_human_id: humanId, + updated_at: serverTimestamp(), + }); +} + +export async function updateStreamPlaybackMarker( + docPath: string, + humanId: string, + playbackPositionAt: Date, +): Promise { + const particleRef = typedDoc(docPath); + const markerField = `playback_markers.${humanId}`; + await updateDoc(particleRef, { + [markerField]: Timestamp.fromDate(playbackPositionAt), + updated_at: serverTimestamp(), + }); +} + +export async function toggleParticleReaction( + docPath: string, + emoji: string, + humanId: string, + currentReactions?: Reactions, +): Promise { + const particleRef = typedDoc(docPath); + const field = `reactions.${emoji}`; + const alreadyReacted = currentReactions?.[emoji]?.includes(humanId) ?? false; + await updateDoc(particleRef, { + [field]: alreadyReacted ? arrayRemove(humanId) : arrayUnion(humanId), + updated_at: serverTimestamp(), + }); +} diff --git a/js/mobile/src/lib/humans.ts b/js/mobile/src/lib/humans.ts new file mode 100644 index 0000000..543fdb6 --- /dev/null +++ b/js/mobile/src/lib/humans.ts @@ -0,0 +1,43 @@ +import type { Human } from "@/api/types"; +import { getInitials } from "@/lib/utils"; + +export const REMOVED_MEMBER_LABEL = "Removed member"; +export const REMOVED_MEMBER_INITIALS = "–"; + +export interface HumanDisplay { + /** True when the human was found in the provided list. */ + exists: boolean; + /** Short name for inline text (e.g. message sender). */ + displayName: string; + /** Full email or fallback label for tooltips. */ + email: string; + /** Initials for avatar fallback. */ + initials: string; +} + +/** + * Resolve a human's display info by id, falling back consistently when the + * human has been removed from the network. Member content (particles, reactions, + * etc.) is retained after removal, so every render path needs a graceful fallback + * instead of leaking raw ids into the UI. + */ +export function resolveHumanDisplay( + humanId: string | null | undefined, + humans: Human[] | undefined, +): HumanDisplay { + const human = humanId ? humans?.find((h) => h.id === humanId) : undefined; + if (!human) { + return { + exists: false, + displayName: REMOVED_MEMBER_LABEL, + email: REMOVED_MEMBER_LABEL, + initials: REMOVED_MEMBER_INITIALS, + }; + } + return { + exists: true, + displayName: human.email_prefix, + email: human.email, + initials: getInitials(human.email), + }; +} diff --git a/js/mobile/src/lib/particle-path.ts b/js/mobile/src/lib/particle-path.ts new file mode 100644 index 0000000..905e1fd --- /dev/null +++ b/js/mobile/src/lib/particle-path.ts @@ -0,0 +1,70 @@ +/** + * ParticlePath is a branded string type representing a URL-style path + * to a particle in the hierarchy: /{networkId}/{segment1}/{segment2}/... + * + * Using a branded type prevents accidentally passing raw strings where + * a validated particle path is expected. + */ +declare const __brand: unique symbol; +export type ParticlePath = string & { readonly [__brand]: true }; + +/** + * Construct a ParticlePath from a network ID and optional particle segments. + * + * @example + * particlePath("net1", []) // => "/net1" + * particlePath("net1", ["p1"]) // => "/net1/p1" + * particlePath("net1", ["p1","p2"])// => "/net1/p1/p2" + */ +export function particlePath( + networkId: string, + segments: string[] = [], +): ParticlePath { + return `/${[networkId, ...segments].join("/")}` as ParticlePath; +} + +/** + * Parse a ParticlePath back into its network ID and particle segments. + */ +export function parseParticlePath(path: ParticlePath): { + networkId: string; + segments: string[]; +} { + const parts = path.split("/").filter(Boolean); + return { networkId: parts[0], segments: parts.slice(1) }; +} + +/** + * Convert a ParticlePath to the Firestore document path for that particle. + * + * Firestore structure: + * /net1 → networks/net1/children (collection) + * /net1/p1 → networks/net1/children/p1 (document) + * /net1/p1/p2 → networks/net1/children/p1/children/p2 (document) + */ +export function toFirestoreDocPath(path: ParticlePath): string { + const { networkId, segments } = parseParticlePath(path); + const base = `networks/${networkId}/children`; + if (segments.length === 0) return base; + + const parts: string[] = [base, segments[0]]; + for (let i = 1; i < segments.length; i++) { + parts.push("children", segments[i]); + } + return parts.join("/"); +} + +/** + * Convert a ParticlePath to the Firestore collection path for its children. + * + * /net1 → networks/net1/children (root particles) + * /net1/p1 → networks/net1/children/p1/children + * /net1/p1/p2 → networks/net1/children/p1/children/p2/children + */ +export function toFirestoreChildrenPath(path: ParticlePath): string { + const { segments } = parseParticlePath(path); + if (segments.length === 0) { + return toFirestoreDocPath(path); + } + return `${toFirestoreDocPath(path)}/children`; +} diff --git a/js/mobile/src/lib/pusher-client.ts b/js/mobile/src/lib/pusher-client.ts new file mode 100644 index 0000000..99ce04a --- /dev/null +++ b/js/mobile/src/lib/pusher-client.ts @@ -0,0 +1,286 @@ +/** + * PusherClient manages a WebSocket connection to the pusher service. + * Handles authentication, reconnection with exponential backoff, channel + * subscriptions, and event dispatching. + * + * Identical behavior to the desktop client (js/desktop/src/lib/pusher-client.ts). + * React Native ships a WebSocket polyfill, so this code runs unchanged. + */ + +import { logError, reportError } from "@/lib/errors"; + +export type ConnectionState = + | "disconnected" + | "connecting" + | "connected" + | "reconnecting"; + +export interface ChannelMessage { + humanId: string; + payload: unknown; +} + +interface ServerMessage { + type: "subscribed" | "join" | "leave" | "message" | "error"; + channel?: string; + humanId?: string; + presence?: string[]; + payload?: unknown; + message?: string; +} + +type ChannelEventType = "subscribed" | "join" | "leave" | "message"; +type ChannelEventCallback = (msg: ServerMessage) => void; + +interface PusherClientConfig { + url: string; + getToken: () => string | null; +} + +const INITIAL_RECONNECT_DELAY = 1000; +const MAX_RECONNECT_DELAY = 30000; +const PING_INTERVAL = 20000; // 20s — keeps alive through GKE gateway timeout + +export class PusherClient { + private config: PusherClientConfig; + private ws: WebSocket | null = null; + private state: ConnectionState = "disconnected"; + private stateListeners = new Set<(state: ConnectionState) => void>(); + + private listeners = new Map< + string, + Map> + >(); + + private activeSubscriptions = new Set(); + + private reconnectDelay = INITIAL_RECONNECT_DELAY; + private reconnectTimer: ReturnType | null = null; + private shouldReconnect = false; + + private pingTimer: ReturnType | null = null; + + constructor(config: PusherClientConfig) { + this.config = config; + } + + get connectionState(): ConnectionState { + return this.state; + } + + connect(): void { + if (this.ws) return; + + const token = this.config.getToken(); + if (!token) { + console.warn("[pusher] no token available, cannot connect"); + return; + } + + this.shouldReconnect = true; + this.setState( + this.state === "reconnecting" ? "reconnecting" : "connecting", + ); + + const url = `${this.config.url}?token=${encodeURIComponent(token)}`; + this.ws = new WebSocket(url); + + this.ws.onopen = () => { + this.setState("connected"); + this.reconnectDelay = INITIAL_RECONNECT_DELAY; + this.startPing(); + this.resubscribeAll(); + }; + + this.ws.onclose = () => { + this.cleanup(); + if (this.shouldReconnect) { + this.scheduleReconnect(); + } + }; + + this.ws.onerror = (event) => { + // onclose fires after onerror — reconnection is handled there. + logError(event, { scope: "pusher.ws" }); + }; + + this.ws.onmessage = (event) => { + this.handleMessage(event.data as string); + }; + } + + disconnect(): void { + this.shouldReconnect = false; + this.clearReconnectTimer(); + this.cleanup(); + this.activeSubscriptions.clear(); + this.setState("disconnected"); + } + + subscribe(channelId: string): void { + this.activeSubscriptions.add(channelId); + this.send({ type: "subscribe", channel: channelId }); + } + + unsubscribe(channelId: string): void { + this.activeSubscriptions.delete(channelId); + this.send({ type: "unsubscribe", channel: channelId }); + } + + sendMessage(channelId: string, payload: unknown): void { + this.send({ type: "message", channel: channelId, payload }); + } + + on( + channelId: string, + event: ChannelEventType, + callback: ChannelEventCallback, + ): void { + if (!this.listeners.has(channelId)) { + this.listeners.set(channelId, new Map()); + } + const channelListeners = this.listeners.get(channelId)!; + if (!channelListeners.has(event)) { + channelListeners.set(event, new Set()); + } + channelListeners.get(event)!.add(callback); + } + + off( + channelId: string, + event: ChannelEventType, + callback: ChannelEventCallback, + ): void { + const channelListeners = this.listeners.get(channelId); + if (!channelListeners) return; + const eventListeners = channelListeners.get(event); + if (!eventListeners) return; + eventListeners.delete(callback); + + if (eventListeners.size === 0) channelListeners.delete(event); + if (channelListeners.size === 0) this.listeners.delete(channelId); + } + + onStateChange(callback: (state: ConnectionState) => void): () => void { + this.stateListeners.add(callback); + return () => { + this.stateListeners.delete(callback); + }; + } + + // --- Private --- + + private send(msg: { + type: string; + channel?: string; + payload?: unknown; + }): void { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(msg)); + } + } + + private handleMessage(data: string): void { + if (data === "pong") return; + + let msg: ServerMessage; + try { + msg = JSON.parse(data); + } catch (err) { + logError(err, { scope: "pusher.parse", data }); + return; + } + + if (msg.type === "error") { + logError(new Error(msg.message ?? "pusher server error"), { + scope: "pusher.server", + }); + return; + } + + if (!msg.channel) return; + + const channelListeners = this.listeners.get(msg.channel); + if (!channelListeners) return; + + const eventListeners = channelListeners.get(msg.type as ChannelEventType); + if (!eventListeners) return; + + for (const cb of eventListeners) { + try { + cb(msg); + } catch (err) { + reportError(err, { scope: "pusher.listener", channel: msg.channel }); + } + } + } + + private resubscribeAll(): void { + for (const channelId of this.activeSubscriptions) { + this.send({ type: "subscribe", channel: channelId }); + } + } + + private scheduleReconnect(): void { + this.setState("reconnecting"); + + const jitter = Math.random() * 0.5 + 0.75; + const delay = Math.min(this.reconnectDelay * jitter, MAX_RECONNECT_DELAY); + + this.reconnectTimer = setTimeout(() => { + this.reconnectDelay = Math.min( + this.reconnectDelay * 2, + MAX_RECONNECT_DELAY, + ); + this.connect(); + }, delay); + } + + private cleanup(): void { + this.stopPing(); + if (this.ws) { + this.ws.onopen = null; + this.ws.onclose = null; + this.ws.onerror = null; + this.ws.onmessage = null; + if ( + this.ws.readyState === WebSocket.OPEN || + this.ws.readyState === WebSocket.CONNECTING + ) { + this.ws.close(); + } + this.ws = null; + } + } + + private clearReconnectTimer(): void { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + } + + private startPing(): void { + this.stopPing(); + this.pingTimer = setInterval(() => { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send("ping"); + } + }, PING_INTERVAL); + } + + private stopPing(): void { + if (this.pingTimer) { + clearInterval(this.pingTimer); + this.pingTimer = null; + } + } + + private setState(state: ConnectionState): void { + if (this.state === state) return; + this.state = state; + for (const cb of this.stateListeners) { + cb(state); + } + } +} diff --git a/js/mobile/src/lib/pusher-provider.tsx b/js/mobile/src/lib/pusher-provider.tsx new file mode 100644 index 0000000..d458a2e --- /dev/null +++ b/js/mobile/src/lib/pusher-provider.tsx @@ -0,0 +1,67 @@ +import { + createContext, + useContext, + useEffect, + useRef, + useState, + type ReactNode, +} from "react"; +import { PusherClient, type ConnectionState } from "./pusher-client"; +import { useSessionStore } from "@/stores/session-store"; +import { appConfig } from "@/config/env"; + +const PusherContext = createContext(null); +const PusherStateContext = createContext("disconnected"); + +export function PusherProvider({ children }: { children: ReactNode }) { + const token = useSessionStore((s) => s.token); + const clientRef = useRef(null); + const [connectionState, setConnectionState] = + useState("disconnected"); + + useEffect(() => { + if (!token) { + if (clientRef.current) { + clientRef.current.disconnect(); + clientRef.current = null; + setConnectionState("disconnected"); + } + return; + } + + const client = new PusherClient({ + url: appConfig.pusherUrl, + getToken: () => useSessionStore.getState().token, + }); + + clientRef.current = client; + + const unsubscribeState = client.onStateChange((state) => { + setConnectionState(state); + }); + + client.connect(); + + return () => { + unsubscribeState(); + client.disconnect(); + clientRef.current = null; + }; + }, [token]); + + return ( + + + {children} + + + ); +} + +export function usePusherClient(): PusherClient | null { + return useContext(PusherContext); +} + +export function usePusherConnectionState(): ConnectionState { + return useContext(PusherStateContext); +} diff --git a/js/mobile/src/lib/query-client.ts b/js/mobile/src/lib/query-client.ts new file mode 100644 index 0000000..6776b63 --- /dev/null +++ b/js/mobile/src/lib/query-client.ts @@ -0,0 +1,56 @@ +import { + MutationCache, + QueryCache, + QueryClient, +} from "@tanstack/react-query"; +import { toast } from "sonner-native"; +import { ApiError, logError, reportError, toUserMessage } from "@/lib/errors"; + +declare module "@tanstack/react-query" { + interface Register { + queryMeta: { toastOnError?: boolean }; + mutationMeta: { suppressToast?: boolean }; + } +} + +function shouldRetryQuery(failureCount: number, err: unknown): boolean { + if (err instanceof ApiError) { + // Retry only on transient status codes; 4xx generally won't succeed on retry. + if (err.status === 408 || err.status === 429) return failureCount < 2; + if (err.status >= 400 && err.status < 500) return false; + } + return failureCount < 2; +} + +export function createQueryClient(): QueryClient { + return new QueryClient({ + defaultOptions: { + queries: { + retry: shouldRetryQuery, + refetchOnWindowFocus: false, + }, + mutations: { + // Mutations have side effects — never auto-retry. + retry: 0, + }, + }, + queryCache: new QueryCache({ + onError: (err, query) => { + logError(err, { scope: "query", queryKey: query.queryKey }); + if (query.meta?.toastOnError) { + toast.error(toUserMessage(err)); + } + }, + }), + mutationCache: new MutationCache({ + onError: (err, _variables, _context, mutation) => { + reportError(err, { + scope: "mutation", + mutationKey: mutation.options.mutationKey, + }); + if (mutation.meta?.suppressToast) return; + toast.error(toUserMessage(err)); + }, + }), + }); +} diff --git a/js/mobile/src/lib/random-name.ts b/js/mobile/src/lib/random-name.ts new file mode 100644 index 0000000..7053df5 --- /dev/null +++ b/js/mobile/src/lib/random-name.ts @@ -0,0 +1,19 @@ +const ADJECTIVES = [ + "amber", "bold", "calm", "crisp", "dark", "eager", "faint", "gentle", + "hasty", "icy", "jade", "keen", "lush", "misty", "nimble", "opal", + "pale", "quiet", "rapid", "sharp", "taut", "vivid", "warm", "zesty", + "bright", "clear", "deep", "fresh", "grand", "swift", +]; + +const NOUNS = [ + "arrow", "bloom", "cedar", "drift", "ember", "flint", "grove", "harbor", + "iris", "jewel", "knoll", "lake", "moss", "nova", "orbit", "petal", + "quartz", "ridge", "spark", "trail", "vale", "wave", "yarn", "zenith", + "brook", "cliff", "delta", "frost", "glow", "reef", +]; + +export function generateRandomName(): string { + const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]; + const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]; + return `${adj}-${noun}`; +} diff --git a/js/mobile/src/lib/stream-visibility.ts b/js/mobile/src/lib/stream-visibility.ts new file mode 100644 index 0000000..c748769 --- /dev/null +++ b/js/mobile/src/lib/stream-visibility.ts @@ -0,0 +1,29 @@ +import { removeDuplicates } from "@/lib/utils"; + +const HUMAN_PREFIX = "human:"; +const NETWORK_PREFIX = "network:"; + +export type StreamVisibility = + | { mode: "network" } + | { mode: "custom"; humanIds: string[] }; + +export function parseVisibleTo( + visibleTo: string[], + networkId: string, +): StreamVisibility { + if (visibleTo.includes(`${NETWORK_PREFIX}${networkId}`)) { + return { mode: "network" }; + } + const humanIds = visibleTo + .filter((v) => v.startsWith(HUMAN_PREFIX)) + .map((v) => v.slice(HUMAN_PREFIX.length)); + return { mode: "custom", humanIds }; +} + +export function buildNetworkVisibility(networkId: string): string[] { + return [`${NETWORK_PREFIX}${networkId}`]; +} + +export function buildCustomVisibility(humanIds: string[]): string[] { + return removeDuplicates(humanIds).map((id) => `${HUMAN_PREFIX}${id}`); +} diff --git a/js/mobile/src/lib/time-utils.ts b/js/mobile/src/lib/time-utils.ts new file mode 100644 index 0000000..a4fd4a9 --- /dev/null +++ b/js/mobile/src/lib/time-utils.ts @@ -0,0 +1,20 @@ +const MINUTE = 60; +const HOUR = 3600; +const DAY = 86400; +const WEEK = 604800; +const MONTH = 2592000; +const YEAR = 31536000; + +export function formatDistanceToNow(date: Date | string): string { + const ms = typeof date === "string" ? new Date(date).getTime() : date.getTime(); + const seconds = Math.floor((Date.now() - ms) / 1000); + + if (seconds < 5) return "just now"; + if (seconds < MINUTE) return `${seconds}s ago`; + if (seconds < HOUR) return `${Math.floor(seconds / MINUTE)}m ago`; + if (seconds < DAY) return `${Math.floor(seconds / HOUR)}h ago`; + if (seconds < WEEK) return `${Math.floor(seconds / DAY)}d ago`; + if (seconds < MONTH) return `${Math.floor(seconds / WEEK)}w ago`; + if (seconds < YEAR) return `${Math.floor(seconds / MONTH)}mo ago`; + return `${Math.floor(seconds / YEAR)}y ago`; +} diff --git a/js/mobile/src/lib/upload.ts b/js/mobile/src/lib/upload.ts new file mode 100644 index 0000000..a6a571b --- /dev/null +++ b/js/mobile/src/lib/upload.ts @@ -0,0 +1,201 @@ +import { + FileSystemUploadType, + getInfoAsync, + uploadAsync, +} from "expo-file-system/legacy"; +import { apiClient } from "@/api/client"; +import { + createParticle, + createStreamParticle, +} from "@/lib/firestore-particles"; +import { + particlePath, + toFirestoreChildrenPath, + type ParticlePath, +} from "@/lib/particle-path"; + +interface UploadMediaParticleParams { + networkId: string; + /** Path of the destination container (stream — possibly with sub-segments). */ + targetPath: ParticlePath; + fileUri: string; + mimeType: string; + durationMs: number; + source: "camera" | "screen"; + createdByHumanId: string; +} + +/** + * Upload a recorded file and create the corresponding `media` particle in + * Firestore. Order matches desktop's `use-recorder` flow exactly: + * prepareUpload → PUT → confirmUpload → createParticle. + * + * Returns the new particle's id, or throws on any failure (no half-states — + * if any step fails the caller cancels and reports). + */ +export async function uploadMediaParticle({ + networkId, + targetPath, + fileUri, + mimeType, + durationMs, + source, + createdByHumanId, +}: UploadMediaParticleParams): Promise { + const info = await getInfoAsync(fileUri); + if (!info.exists || info.size === undefined) { + throw new Error("Recording file disappeared before upload."); + } + const sizeBytes = info.size; + + const namePrefix = mimeType.startsWith("audio/") ? "voice" : "video"; + const ext = extensionFromMime(mimeType); + const name = `${namePrefix}-${Date.now()}${ext}`; + + const { object_id, upload_url, upload_headers } = + await apiClient.prepareUpload({ + network_id: networkId, + name, + content_type: mimeType, + content_length: sizeBytes, + }); + + const uploadResult = await uploadAsync(upload_url, fileUri, { + httpMethod: "PUT", + uploadType: FileSystemUploadType.BINARY_CONTENT, + headers: upload_headers, + }); + + if (uploadResult.status < 200 || uploadResult.status >= 300) { + throw new Error( + `Upload to depot failed (HTTP ${uploadResult.status}).`, + ); + } + + await apiClient.confirmUpload(object_id); + + const collectionPath = toFirestoreChildrenPath(targetPath); + return createParticle( + collectionPath, + "media", + { + object_id, + mime_type: mimeType, + duration_ms: durationMs, + size_bytes: sizeBytes, + source, + }, + createdByHumanId, + ); +} + +interface CreateTextParticleParams { + networkId: string; + targetPath: ParticlePath; + content: string; + createdByHumanId: string; +} + +export async function createTextParticle({ + targetPath, + content, + createdByHumanId, +}: CreateTextParticleParams): Promise { + const collectionPath = toFirestoreChildrenPath(targetPath); + return createParticle( + collectionPath, + "text", + { content }, + createdByHumanId, + ); +} + +function extensionFromMime(mime: string): string { + if (mime === "video/mp4") return ".mp4"; + if (mime === "video/quicktime") return ".mov"; + if (mime === "audio/mp4") return ".m4a"; + if (mime === "audio/webm") return ".webm"; + return ""; +} + +// Helper kept here so callers can construct a fresh stream's child-path before +// the stream particle has been written. +export function streamChildrenPath( + networkId: string, + streamId: string, +): ParticlePath { + return particlePath(networkId, [streamId]); +} + +// --- New-stream flow --- + +interface CreateStreamWithFirstParticleParams { + networkId: string; + name: string; + /** ["network:{id}"] for everyone; ["human:{id}", ...] for specific people. */ + visibleTo: string[]; + createdByHumanId: string; + /** First particle to write into the new stream. Required — empty streams are not useful. */ + firstParticle: + | { type: "text"; content: string } + | { + type: "media"; + fileUri: string; + mimeType: string; + durationMs: number; + source: "camera" | "screen"; + }; +} + +interface CreateStreamWithFirstParticleResult { + streamId: string; +} + +/** + * Create a top-level stream particle plus its first child particle, in that + * order. Mirrors desktop's "create new stream" submit path (compose-overlay + * §handleStreamSubmit). On any failure the caller is responsible for retry — + * we don't roll back the stream particle on child failure because Firestore + * doesn't expose a multi-write transaction across these subcollections, and + * an empty stream is harmless (the user can retry composing into it). + */ +export async function createStreamWithFirstParticle({ + networkId, + name, + visibleTo, + createdByHumanId, + firstParticle, +}: CreateStreamWithFirstParticleParams): Promise { + // 1. The stream particle goes at the network root. + const rootChildrenPath = toFirestoreChildrenPath(particlePath(networkId, [])); + const streamId = await createStreamParticle( + rootChildrenPath, + { name }, + createdByHumanId, + visibleTo, + ); + + const streamPath = particlePath(networkId, [streamId]); + + // 2. The first child goes inside the new stream. + if (firstParticle.type === "text") { + await createTextParticle({ + networkId, + targetPath: streamPath, + content: firstParticle.content, + createdByHumanId, + }); + } else { + await uploadMediaParticle({ + networkId, + targetPath: streamPath, + fileUri: firstParticle.fileUri, + mimeType: firstParticle.mimeType, + durationMs: firstParticle.durationMs, + source: firstParticle.source, + createdByHumanId, + }); + } + + return { streamId }; +} diff --git a/js/mobile/src/lib/utils.ts b/js/mobile/src/lib/utils.ts new file mode 100644 index 0000000..b3229a2 --- /dev/null +++ b/js/mobile/src/lib/utils.ts @@ -0,0 +1,15 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function getInitials(email: string): string { + const prefix = email.split("@")[0] ?? ""; + return prefix.slice(0, 2).toUpperCase(); +} + +export function removeDuplicates(array: T[]): T[] { + return [...new Set(array)]; +} diff --git a/js/mobile/src/navigation/RootNavigator.tsx b/js/mobile/src/navigation/RootNavigator.tsx new file mode 100644 index 0000000..0bbf270 --- /dev/null +++ b/js/mobile/src/navigation/RootNavigator.tsx @@ -0,0 +1,55 @@ +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import { ActivityIndicator, View } from "react-native"; +import { useAuthStore } from "@/stores/auth-store"; +import { SignInScreen } from "@/features/auth/SignInScreen"; +import { NetworkListScreen } from "@/features/networks/NetworkListScreen"; +import { StreamListScreen } from "@/features/streams/StreamListScreen"; +import { NewStreamScreen } from "@/features/streams/NewStreamScreen"; +import { StreamViewScreen } from "@/features/stream-view/StreamViewScreen"; +import { SettingsScreen } from "@/features/settings/SettingsScreen"; +import { AccountScreen } from "@/features/settings/AccountScreen"; +import type { RootStackParamList } from "./types"; + +const Stack = createNativeStackNavigator(); + +export function RootNavigator() { + const status = useAuthStore((s) => s.status); + + if (status === "idle" || status === "restoring") { + return ( + + + + ); + } + + if (status === "unauthenticated") { + return ( + + + + ); + } + + return ( + + + + + + + + + ); +} diff --git a/js/mobile/src/navigation/types.ts b/js/mobile/src/navigation/types.ts new file mode 100644 index 0000000..dd11343 --- /dev/null +++ b/js/mobile/src/navigation/types.ts @@ -0,0 +1,22 @@ +import type { NativeStackScreenProps } from "@react-navigation/native-stack"; + +// Pure stack model from PRD §5. Drawer affordance lives inside the +// NetworkList screen itself, not the navigator — see Drawer.tsx. +export type RootStackParamList = { + SignIn: undefined; + NetworkList: undefined; + StreamList: { networkId: string }; + StreamView: { networkId: string; streamId: string }; + NewStream: { networkId: string }; + Settings: undefined; + Account: undefined; +}; + +export type RootStackScreenProps = + NativeStackScreenProps; + +declare global { + namespace ReactNavigation { + interface RootParamList extends RootStackParamList {} + } +} diff --git a/js/mobile/src/stores/auth-store.ts b/js/mobile/src/stores/auth-store.ts new file mode 100644 index 0000000..c291498 --- /dev/null +++ b/js/mobile/src/stores/auth-store.ts @@ -0,0 +1,137 @@ +import { create } from "zustand"; +import { + signInWithCustomToken, + signOut as firebaseSignOut, +} from "firebase/auth"; +import { apiClient } from "@/api/client"; +import type { Human } from "@/api/types"; +import { firebaseAuth } from "@/firebase"; +import { logError, ApiError } from "@/lib/errors"; +import { hydrateSession, useSessionStore } from "./session-store"; + +async function signInToFirebase(): Promise { + try { + const { token } = await apiClient.getFirebaseToken(); + await signInWithCustomToken(firebaseAuth, token); + } catch (err) { + // Firestore subscriptions will fail until the next successful sign-in; the + // rest of the app keeps working against Orion. Sentry catches the failure. + logError(err, { scope: "auth.firebase" }); + } +} + +type AuthStatus = "idle" | "restoring" | "unauthenticated" | "authenticated"; + +interface AuthState { + status: AuthStatus; + user: Human | null; + isRequestingCode: boolean; + isSigningIn: boolean; + isSigningOut: boolean; + error: string | null; + restoreSession: () => Promise; + requestCode: (email: string) => Promise; + signIn: (email: string, code: string) => Promise; + signOut: () => Promise; + clearError: () => void; +} + +export const useAuthStore = create((set) => ({ + status: "idle", + user: null, + isRequestingCode: false, + isSigningIn: false, + isSigningOut: false, + error: null, + + restoreSession: async () => { + set({ status: "restoring" }); + if (!useSessionStore.getState().hydrated) { + await hydrateSession(); + } + + const token = useSessionStore.getState().token; + if (!token) { + set({ status: "unauthenticated" }); + return; + } + + try { + const user = await apiClient.me(); + await signInToFirebase(); + set({ status: "authenticated", user }); + } catch (err) { + // Expected on expired/invalid tokens — fall back to the login screen. + logError(err, { scope: "auth.restore" }); + await useSessionStore.getState().clearToken(); + set({ status: "unauthenticated", user: null }); + } + }, + + requestCode: async (email: string) => { + set({ isRequestingCode: true, error: null }); + try { + await apiClient.requestCode({ email }); + } catch (e) { + const message = + e instanceof ApiError ? e.message : "Failed to send code"; + set({ error: message }); + throw e; + } finally { + set({ isRequestingCode: false }); + } + }, + + signIn: async (email: string, code: string) => { + set({ isSigningIn: true, error: null }); + try { + const { human, token } = await apiClient.signIn({ email, code }); + await useSessionStore.getState().setToken(token); + await signInToFirebase(); + set({ status: "authenticated", user: human }); + } catch (e) { + const message = e instanceof ApiError ? e.message : "Failed to sign in"; + set({ error: message }); + throw e; + } finally { + set({ isSigningIn: false }); + } + }, + + signOut: async () => { + set({ isSigningOut: true }); + try { + await apiClient.signOut(); + } catch (err) { + // Best-effort — sign out locally regardless. + logError(err, { scope: "auth.signOut" }); + } finally { + await firebaseSignOut(firebaseAuth).catch((err) => + logError(err, { scope: "auth.firebaseSignOut" }), + ); + await useSessionStore.getState().clearToken(); + set({ + status: "unauthenticated", + user: null, + isSigningOut: false, + error: null, + }); + } + }, + + clearError: () => set({ error: null }), +})); + +// React to token being cleared externally (e.g. 401 from API client). +useSessionStore.subscribe((state, prevState) => { + if (prevState.token && !state.token) { + const authState = useAuthStore.getState(); + if (authState.status === "authenticated") { + useAuthStore.setState({ + status: "unauthenticated", + user: null, + error: null, + }); + } + } +}); diff --git a/js/mobile/src/stores/playback-pause-store.ts b/js/mobile/src/stores/playback-pause-store.ts new file mode 100644 index 0000000..8885adf --- /dev/null +++ b/js/mobile/src/stores/playback-pause-store.ts @@ -0,0 +1,33 @@ +import { create } from "zustand"; + +/** + * Single source of truth for "is stream playback paused." Each component that + * wants to pause playback registers a unique id via `useSuspendPlayback`; the + * label is for devtools only. Playback is paused while any id is registered. + */ +interface PlaybackPauseState { + activeIds: Record; + composing: boolean; + add: (id: string, label: string) => void; + remove: (id: string) => void; + setComposing: (composing: boolean) => void; +} + +export const usePlaybackPauseStore = create((set) => ({ + activeIds: {}, + composing: false, + add: (id, label) => + set((s) => ({ activeIds: { ...s.activeIds, [id]: label } })), + remove: (id) => + set((s) => { + if (!(id in s.activeIds)) return s; + const { [id]: _, ...rest } = s.activeIds; + return { activeIds: rest }; + }), + setComposing: (composing) => set({ composing }), +})); + +export const selectIsPaused = (s: PlaybackPauseState) => + Object.keys(s.activeIds).length > 0 || s.composing; + +export const selectIsComposing = (s: PlaybackPauseState) => s.composing; diff --git a/js/mobile/src/stores/session-store.ts b/js/mobile/src/stores/session-store.ts new file mode 100644 index 0000000..2d24993 --- /dev/null +++ b/js/mobile/src/stores/session-store.ts @@ -0,0 +1,46 @@ +import * as SecureStore from "expo-secure-store"; +import { create } from "zustand"; + +const AUTH_TOKEN_KEY = "auth_token"; + +interface SessionState { + token: string | null; + /** + * False until SecureStore returns the persisted token (or confirms absence). + * The API client should treat requests as unauthenticated until this flips — + * see `useSessionStore.subscribe` in App.tsx for the bootstrap. + */ + hydrated: boolean; + setToken: (token: string) => Promise; + clearToken: () => Promise; +} + +export const useSessionStore = create((set) => ({ + token: null, + hydrated: false, + + setToken: async (token: string) => { + await SecureStore.setItemAsync(AUTH_TOKEN_KEY, token); + set({ token }); + }, + + clearToken: async () => { + await SecureStore.deleteItemAsync(AUTH_TOKEN_KEY); + set({ token: null }); + }, +})); + +/** + * Bootstrap the session by reading SecureStore once. Call from App.tsx before + * mounting the navigator. Resolves after the store reflects whatever was in + * persistent storage. + */ +export async function hydrateSession(): Promise { + try { + const token = await SecureStore.getItemAsync(AUTH_TOKEN_KEY); + useSessionStore.setState({ token: token ?? null, hydrated: true }); + } catch { + // SecureStore failures are non-fatal — proceed unauthenticated. + useSessionStore.setState({ token: null, hydrated: true }); + } +} diff --git a/js/mobile/tailwind.config.js b/js/mobile/tailwind.config.js new file mode 100644 index 0000000..e39e396 --- /dev/null +++ b/js/mobile/tailwind.config.js @@ -0,0 +1,53 @@ +/** @type {import('tailwindcss').Config} */ +// Tokens mirror js/desktop/src/styles/globals.css. Desktop authors values in +// OKLCH; React Native's color parser is not guaranteed to handle oklch(), so +// we ship hex equivalents here and keep the OKLCH source in comments. +// +// Names match desktop 1:1 — components written against `bg-background`, +// `text-foreground`, etc. behave identically. +module.exports = { + content: ["./index.ts", "./src/**/*.{ts,tsx}"], + presets: [require("nativewind/preset")], + darkMode: "class", + theme: { + extend: { + colors: { + // ---- Light tokens (desktop :root) ---- + background: { DEFAULT: "#ffffff", dark: "#252525" }, // oklch(1 0 0) / oklch(0.145 0 0) + foreground: { DEFAULT: "#252525", dark: "#fafafa" }, // oklch(0.145 0 0) / oklch(0.985 0 0) + card: { DEFAULT: "#ffffff", dark: "#363636" }, // oklch(1 0 0) / oklch(0.205 0 0) + "card-foreground": { DEFAULT: "#252525", dark: "#fafafa" }, + popover: { DEFAULT: "#ffffff", dark: "#363636" }, + "popover-foreground": { DEFAULT: "#252525", dark: "#fafafa" }, + primary: { DEFAULT: "#363636", dark: "#ebebeb" }, // oklch(0.205 0 0) / oklch(0.922 0 0) + "primary-foreground": { DEFAULT: "#fafafa", dark: "#363636" }, + secondary: { DEFAULT: "#f4f4f4", dark: "#454545" }, // oklch(0.97 0 0) / oklch(0.269 0 0) + "secondary-foreground": { DEFAULT: "#363636", dark: "#fafafa" }, + muted: { DEFAULT: "#f4f4f4", dark: "#454545" }, + "muted-foreground": { DEFAULT: "#878787", dark: "#a6a6a6" }, // oklch(0.556 0 0) / oklch(0.708 0 0) + accent: { DEFAULT: "#f4f4f4", dark: "#454545" }, + "accent-foreground": { DEFAULT: "#363636", dark: "#fafafa" }, + destructive: { DEFAULT: "#dc2626", dark: "#ef4444" }, // oklch(0.577 0.245 27.325) / oklch(0.704 0.191 22.216) + border: { DEFAULT: "#dcdcdc", dark: "rgba(255,255,255,0.1)" }, + input: { DEFAULT: "#dcdcdc", dark: "rgba(255,255,255,0.15)" }, + ring: { DEFAULT: "#a6a6a6", dark: "#878787" }, // oklch(0.708 0 0) / oklch(0.556 0 0) + // Sidebar (drawer) tokens — desktop also defines these + sidebar: { DEFAULT: "#fafafa", dark: "#363636" }, + "sidebar-foreground": { DEFAULT: "#252525", dark: "#fafafa" }, + "sidebar-primary": { DEFAULT: "#363636", dark: "#6366f1" }, // chart-1 dark = oklch(0.488 0.243 264.376) + "sidebar-primary-foreground": { DEFAULT: "#fafafa", dark: "#fafafa" }, + "sidebar-accent": { DEFAULT: "#f4f4f4", dark: "#454545" }, + "sidebar-accent-foreground": { DEFAULT: "#363636", dark: "#fafafa" }, + "sidebar-border": { DEFAULT: "#dcdcdc", dark: "rgba(255,255,255,0.1)" }, + "sidebar-ring": { DEFAULT: "#a6a6a6", dark: "#878787" }, + }, + borderRadius: { + lg: "10px", // --radius: 0.625rem + md: "8px", + sm: "6px", + xl: "14px", + }, + }, + }, + plugins: [], +}; diff --git a/js/mobile/tsconfig.json b/js/mobile/tsconfig.json new file mode 100644 index 0000000..270bd74 --- /dev/null +++ b/js/mobile/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "baseUrl": ".", + "paths": { + "@/*": [ + "src/*" + ] + }, + "types": [ + "nativewind/types" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + "nativewind-env.d.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/js/mobile/yarn.lock b/js/mobile/yarn.lock new file mode 100644 index 0000000..2945124 --- /dev/null +++ b/js/mobile/yarn.lock @@ -0,0 +1,5878 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@0no-co/graphql.web@^1.0.13", "@0no-co/graphql.web@^1.0.8": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@0no-co/graphql.web/-/graphql.web-1.2.0.tgz#296d00581bfaaabfda1e976849d927824aaea81b" + integrity sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw== + +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + +"@babel/code-frame@^7.12.13", "@babel/code-frame@^7.20.0", "@babel/code-frame@^7.24.7", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/code-frame@~7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/compat-data@^7.28.6": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.20.0", "@babel/core@^7.25.2": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + "@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" + +"@babel/generator@^7.20.5", "@babel/generator@^7.25.0", "@babel/generator@^7.29.0", "@babel/generator@^7.29.1": + version "7.29.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" + integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== + dependencies: + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== + dependencies: + "@babel/types" "^7.27.3" + +"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25" + integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz#611ff5482da9ef0db6291bcd24303400bca170fb" + integrity sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.28.6" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.27.1", "@babel/helper-create-regexp-features-plugin@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz#7c1ddd64b2065c7f78034b25b43346a7e19ed997" + integrity sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + regexpu-core "^6.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.5", "@babel/helper-define-polyfill-provider@^0.6.8": + version "0.6.8" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz#cf1e4462b613f2b54c41e6ff758d5dfcaa2c85d1" + integrity sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA== + dependencies: + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + debug "^4.4.3" + lodash.debounce "^4.0.8" + resolve "^1.22.11" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-member-expression-to-functions@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150" + integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== + dependencies: + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + +"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.25.9", "@babel/helper-module-imports@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== + dependencies: + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helper-module-transforms@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" + integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.6" + +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.28.6", "@babel/helper-plugin-utils@^7.8.0": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" + integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== + +"@babel/helper-remap-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-wrap-function" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-replace-supers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz#94aa9a1d7423a00aead3f204f78834ce7d53fe44" + integrity sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.28.6" + +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.25.9", "@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helper-wrap-function@^7.27.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz#4e349ff9222dab69a93a019cc296cdd8442e279a" + integrity sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ== + dependencies: + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helpers@^7.28.6": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.2.tgz#9cfbccb02b8e229892c0b07038052cc1a8709c49" + integrity sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw== + dependencies: + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" + +"@babel/highlight@^7.10.4": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.9.tgz#8141ce68fc73757946f983b343f1231f4691acc6" + integrity sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.25.3", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.2.tgz#58bd50b9a7951d134988a1ae177a35ef9a703ba1" + integrity sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA== + dependencies: + "@babel/types" "^7.29.0" + +"@babel/plugin-proposal-decorators@^7.12.9": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz#d159f26f78740e47bf3ef075882b155b2d54ca81" + integrity sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-syntax-decorators" "^7.28.6" + +"@babel/plugin-proposal-export-default-from@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz#59b050b0e5fdc366162ab01af4fcbac06ea40919" + integrity sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-decorators@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz#8c3293a0fef033e4c786b35ce1e159fc1d676153" + integrity sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-default-from@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.28.6.tgz#8e19047560a8a48b11f1f5b46881f445f8692830" + integrity sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-flow@^7.12.1", "@babel/plugin-syntax-flow@^7.27.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz#447559a225e66c4cd477a3ffb1a74d8c1fe25a62" + integrity sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-import-attributes@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz#b71d5914665f60124e133696f17cd7669062c503" + integrity sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.27.1", "@babel/plugin-syntax-jsx@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz#f8ca28bbd84883b5fea0e447c635b81ba73997ee" + integrity sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz#c7b2ddf1d0a811145b1de800d1abd146af92e3a2" + integrity sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-arrow-functions@^7.0.0-0", "@babel/plugin-transform-arrow-functions@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-async-generator-functions@^7.25.4": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz#63ed829820298f0bf143d5a4a68fb8c06ffd742f" + integrity sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/traverse" "^7.29.0" + +"@babel/plugin-transform-async-to-generator@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz#bd97b42237b2d1bc90d74bcb486c39be5b4d7e77" + integrity sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-remap-async-to-generator" "^7.27.1" + +"@babel/plugin-transform-block-scoping@^7.25.0": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz#e1ef5633448c24e76346125c2534eeb359699a99" + integrity sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-class-properties@^7.0.0-0", "@babel/plugin-transform-class-properties@^7.25.4": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz#d274a4478b6e782d9ea987fda09bdb6d28d66b72" + integrity sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-class-static-block@^7.27.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz#1257491e8259c6d125ac4d9a6f39f9d2bf3dba70" + integrity sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-classes@^7.0.0-0", "@babel/plugin-transform-classes@^7.25.4": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz#8f6fb79ba3703978e701ce2a97e373aae7dda4b7" + integrity sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-globals" "^7.28.0" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-replace-supers" "^7.28.6" + "@babel/traverse" "^7.28.6" + +"@babel/plugin-transform-computed-properties@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz#936824fc71c26cb5c433485776d79c8e7b0202d2" + integrity sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/template" "^7.28.6" + +"@babel/plugin-transform-destructuring@^7.24.8", "@babel/plugin-transform-destructuring@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz#b8402764df96179a2070bb7b501a1586cf8ad7a7" + integrity sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.5" + +"@babel/plugin-transform-export-namespace-from@^7.25.9": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz#71ca69d3471edd6daa711cf4dfc3400415df9c23" + integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-flow-strip-types@^7.25.2": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz#5def3e1e7730f008d683144fb79b724f92c5cdf9" + integrity sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-flow" "^7.27.1" + +"@babel/plugin-transform-for-of@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-function-name@^7.25.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== + dependencies: + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-transform-literals@^7.25.2": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-logical-assignment-operators@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz#53028a3d77e33c50ef30a8fce5ca17065936e605" + integrity sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-modules-commonjs@^7.24.8", "@babel/plugin-transform-modules-commonjs@^7.27.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz#c0232e0dfe66a734cc4ad0d5e75fc3321b6fdef1" + integrity sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA== + dependencies: + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz#a26cd51e09c4718588fc4cce1c5d1c0152102d6a" + integrity sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz#9bc62096e90ab7a887f3ca9c469f6adec5679757" + integrity sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-numeric-separator@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz#1310b0292762e7a4a335df5f580c3320ee7d9e9f" + integrity sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-object-rest-spread@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz#fdd4bc2d72480db6ca42aed5c051f148d7b067f7" + integrity sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA== + dependencies: + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-transform-destructuring" "^7.28.5" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/traverse" "^7.28.6" + +"@babel/plugin-transform-optional-catch-binding@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz#75107be14c78385978201a49c86414a150a20b4c" + integrity sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-optional-chaining@^7.0.0-0", "@babel/plugin-transform-optional-chaining@^7.24.8": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz#926cf150bd421fc8362753e911b4a1b1ce4356cd" + integrity sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-parameters@^7.24.7", "@babel/plugin-transform-parameters@^7.27.7": + version "7.27.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz#1fd2febb7c74e7d21cf3b05f7aebc907940af53a" + integrity sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-private-methods@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz#c76fbfef3b86c775db7f7c106fff544610bdb411" + integrity sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-private-property-in-object@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz#4fafef1e13129d79f1d75ac180c52aafefdb2811" + integrity sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-react-display-name@^7.24.7", "@babel/plugin-transform-react-display-name@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz#6f20a7295fea7df42eb42fed8f896813f5b934de" + integrity sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx-development@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz#47ff95940e20a3a70e68ad3d4fcb657b647f6c98" + integrity sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.27.1" + +"@babel/plugin-transform-react-jsx-self@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz#af678d8506acf52c577cac73ff7fe6615c85fc92" + integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx-source@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz#dcfe2c24094bb757bf73960374e7c55e434f19f0" + integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx@^7.25.2", "@babel/plugin-transform-react-jsx@^7.27.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz#f51cb70a90b9529fbb71ee1f75ea27b7078eed62" + integrity sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-syntax-jsx" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/plugin-transform-react-pure-annotations@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz#339f1ce355eae242e0649f232b1c68907c02e879" + integrity sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-regenerator@^7.24.7": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz#dec237cec1b93330876d6da9992c4abd42c9d18b" + integrity sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-runtime@^7.24.7": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz#a5fded13cc656700804bfd6e5ebd7fffd5266803" + integrity sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + babel-plugin-polyfill-corejs2 "^0.4.14" + babel-plugin-polyfill-corejs3 "^0.13.0" + babel-plugin-polyfill-regenerator "^0.6.5" + semver "^6.3.1" + +"@babel/plugin-transform-shorthand-properties@^7.0.0-0", "@babel/plugin-transform-shorthand-properties@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-spread@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz#40a2b423f6db7b70f043ad027a58bcb44a9757b6" + integrity sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-sticky-regex@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-template-literals@^7.0.0-0": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8" + integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-typescript@^7.25.2", "@babel/plugin-transform-typescript@^7.28.5": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz#1e93d96da8adbefdfdade1d4956f73afa201a158" + integrity sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-syntax-typescript" "^7.28.6" + +"@babel/plugin-transform-unicode-regex@^7.0.0-0", "@babel/plugin-transform-unicode-regex@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/preset-react@^7.22.15": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.28.5.tgz#6fcc0400fa79698433d653092c3919bb4b0878d9" + integrity sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-transform-react-display-name" "^7.28.0" + "@babel/plugin-transform-react-jsx" "^7.27.1" + "@babel/plugin-transform-react-jsx-development" "^7.27.1" + "@babel/plugin-transform-react-pure-annotations" "^7.27.1" + +"@babel/preset-typescript@^7.16.7", "@babel/preset-typescript@^7.23.0": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz#540359efa3028236958466342967522fd8f2a60c" + integrity sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-typescript" "^7.28.5" + +"@babel/runtime@^7.20.0", "@babel/runtime@^7.25.0": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e" + integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g== + +"@babel/template@^7.25.0", "@babel/template@^7.28.6", "@babel/template@^7.3.3": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" + debug "^4.3.1" + +"@babel/traverse@^7.23.0", "@babel/traverse@^7.25.3", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" + debug "^4.3.1" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.23.0", "@babel/types@^7.25.2", "@babel/types@^7.26.0", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.29.0", "@babel/types@^7.3.3": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@egjs/hammerjs@^2.0.17": + version "2.0.17" + resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124" + integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A== + dependencies: + "@types/hammerjs" "^2.0.36" + +"@expo/cli@54.0.24": + version "54.0.24" + resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-54.0.24.tgz#7225d99e019f6eb85fd5ef018d8d82391be5dc82" + integrity sha512-5xse1bEgnVUBhOrtttc6xTNJVvjyTRavpzuF0/0nuj+312vfSbk7EiRbG+xJ2pW/iZxnhLPJkFCrPYG0nmheAQ== + dependencies: + "@0no-co/graphql.web" "^1.0.8" + "@expo/code-signing-certificates" "^0.0.6" + "@expo/config" "~12.0.13" + "@expo/config-plugins" "~54.0.4" + "@expo/devcert" "^1.2.1" + "@expo/env" "~2.0.8" + "@expo/image-utils" "^0.8.8" + "@expo/json-file" "^10.0.8" + "@expo/metro" "~54.2.0" + "@expo/metro-config" "~54.0.15" + "@expo/osascript" "^2.3.8" + "@expo/package-manager" "^1.9.10" + "@expo/plist" "^0.4.8" + "@expo/prebuild-config" "^54.0.8" + "@expo/schema-utils" "^0.1.8" + "@expo/spawn-async" "^1.7.2" + "@expo/ws-tunnel" "^1.0.1" + "@expo/xcpretty" "^4.3.0" + "@react-native/dev-middleware" "0.81.5" + "@urql/core" "^5.0.6" + "@urql/exchange-retry" "^1.3.0" + accepts "^1.3.8" + arg "^5.0.2" + better-opn "~3.0.2" + bplist-creator "0.1.0" + bplist-parser "^0.3.1" + chalk "^4.0.0" + ci-info "^3.3.0" + compression "^1.7.4" + connect "^3.7.0" + debug "^4.3.4" + env-editor "^0.4.1" + expo-server "^1.0.6" + freeport-async "^2.0.0" + getenv "^2.0.0" + glob "^13.0.0" + lan-network "^0.2.1" + minimatch "^9.0.0" + node-forge "^1.3.3" + npm-package-arg "^11.0.0" + ora "^3.4.0" + picomatch "^4.0.3" + pretty-bytes "^5.6.0" + pretty-format "^29.7.0" + progress "^2.0.3" + prompts "^2.3.2" + qrcode-terminal "0.11.0" + require-from-string "^2.0.2" + requireg "^0.2.2" + resolve "^1.22.2" + resolve-from "^5.0.0" + resolve.exports "^2.0.3" + semver "^7.6.0" + send "^0.19.0" + slugify "^1.3.4" + source-map-support "~0.5.21" + stacktrace-parser "^0.1.10" + structured-headers "^0.4.1" + tar "^7.5.2" + terminal-link "^2.1.1" + undici "^6.18.2" + wrap-ansi "^7.0.0" + ws "^8.12.1" + +"@expo/code-signing-certificates@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz#6b7b22830cb69c77a45e357c2f3aa7ab436ac772" + integrity sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w== + dependencies: + node-forge "^1.3.3" + +"@expo/config-plugins@~54.0.4": + version "54.0.4" + resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-54.0.4.tgz#b31cb16f6651342abcdafba600118245ecd9fb00" + integrity sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q== + dependencies: + "@expo/config-types" "^54.0.10" + "@expo/json-file" "~10.0.8" + "@expo/plist" "^0.4.8" + "@expo/sdk-runtime-versions" "^1.0.0" + chalk "^4.1.2" + debug "^4.3.5" + getenv "^2.0.0" + glob "^13.0.0" + resolve-from "^5.0.0" + semver "^7.5.4" + slash "^3.0.0" + slugify "^1.6.6" + xcode "^3.0.1" + xml2js "0.6.0" + +"@expo/config-types@^54.0.10": + version "54.0.10" + resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-54.0.10.tgz#688f4338255d2fdea970f44e2dfd8e8d37dec292" + integrity sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA== + +"@expo/config@~12.0.13": + version "12.0.13" + resolved "https://registry.yarnpkg.com/@expo/config/-/config-12.0.13.tgz#8e696e6121c3c364e1dd527f595cf0a1d9386828" + integrity sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ== + dependencies: + "@babel/code-frame" "~7.10.4" + "@expo/config-plugins" "~54.0.4" + "@expo/config-types" "^54.0.10" + "@expo/json-file" "^10.0.8" + deepmerge "^4.3.1" + getenv "^2.0.0" + glob "^13.0.0" + require-from-string "^2.0.2" + resolve-from "^5.0.0" + resolve-workspace-root "^2.0.0" + semver "^7.6.0" + slugify "^1.3.4" + sucrase "~3.35.1" + +"@expo/devcert@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@expo/devcert/-/devcert-1.2.1.tgz#1a687985bea1670866e54d5ba7c0ced963c354f4" + integrity sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA== + dependencies: + "@expo/sudo-prompt" "^9.3.1" + debug "^3.1.0" + +"@expo/devtools@0.1.8": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@expo/devtools/-/devtools-0.1.8.tgz#bc5b297698f78b3b67037f04593a31e688330a7a" + integrity sha512-SVLxbuanDjJPgc0sy3EfXUMLb/tXzp6XIHkhtPVmTWJAp+FOr6+5SeiCfJrCzZFet0Ifyke2vX3sFcKwEvCXwQ== + dependencies: + chalk "^4.1.2" + +"@expo/env@~2.0.8": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@expo/env/-/env-2.0.11.tgz#3a10d9142b1833566bdfb39de1c062f7a8b8ac38" + integrity sha512-xV+ps6YCW7XIPVUwFVCRN2nox09dnRwy8uIjwHWTODu0zFw4kp4omnVkl0OOjuu2XOe7tdgAHxikrkJt9xB/7Q== + dependencies: + chalk "^4.0.0" + debug "^4.3.4" + dotenv "~16.4.5" + dotenv-expand "~11.0.6" + getenv "^2.0.0" + +"@expo/fingerprint@0.15.5": + version "0.15.5" + resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.15.5.tgz#76968191026346657d22cea10d957aa8be97777e" + integrity sha512-mdVoAMcux1WlM6kd1RoWiHRNqKqS+J6mKmWQ/BKgeh937S/fcW58EE68O6nc4KDXtWi3PBeNHskOFcgyIuD4hw== + dependencies: + "@expo/spawn-async" "^1.7.2" + arg "^5.0.2" + chalk "^4.1.2" + debug "^4.3.4" + getenv "^2.0.0" + glob "^13.0.0" + ignore "^5.3.1" + minimatch "^10.2.2" + p-limit "^3.1.0" + resolve-from "^5.0.0" + semver "^7.6.0" + +"@expo/image-utils@^0.8.8": + version "0.8.13" + resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.8.13.tgz#c7476352af9f576440e5ec8201c2f75f090a4804" + integrity sha512-1I//yBQeTY6p0u1ihqGNDAr35EbSG8uFEupFrIF0jd++h9EWH33521yZJU1yE+mwGlzCb61g3ehu78siMhXBlA== + dependencies: + "@expo/require-utils" "^55.0.4" + "@expo/spawn-async" "^1.7.2" + chalk "^4.0.0" + getenv "^2.0.0" + jimp-compact "0.16.1" + parse-png "^2.1.0" + semver "^7.6.0" + +"@expo/json-file@^10.0.13", "@expo/json-file@^10.0.8", "@expo/json-file@~10.0.8": + version "10.0.13" + resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-10.0.13.tgz#1a9ac56333786e8672181b0b95aab08f8255a548" + integrity sha512-pX/XjQn7tgNw6zuuV2ikmegmwe/S7uiwhrs2wXrANMkq7ozrA+JcZwgW9Q/8WZgciBzfAhNp5hnackHcrmapQA== + dependencies: + "@babel/code-frame" "^7.20.0" + json5 "^2.2.3" + +"@expo/metro-config@54.0.15", "@expo/metro-config@~54.0.15": + version "54.0.15" + resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-54.0.15.tgz#aafdd2c2627fa60927e2d307f4d8cd303b6c5169" + integrity sha512-SqIya4VZ9KHM1S9g+xR0A+QKw1Tfs7Gacx6bQNJ98vs4+O7I5+QP5mHZIB0QSZLUV8opiXebHYTiTu+0OAsIUw== + dependencies: + "@babel/code-frame" "^7.20.0" + "@babel/core" "^7.20.0" + "@babel/generator" "^7.20.5" + "@expo/config" "~12.0.13" + "@expo/env" "~2.0.8" + "@expo/json-file" "~10.0.8" + "@expo/metro" "~54.2.0" + "@expo/spawn-async" "^1.7.2" + browserslist "^4.25.0" + chalk "^4.1.0" + debug "^4.3.2" + dotenv "~16.4.5" + dotenv-expand "~11.0.6" + getenv "^2.0.0" + glob "^13.0.0" + hermes-parser "^0.29.1" + jsc-safe-url "^0.2.4" + lightningcss "^1.30.1" + picomatch "^4.0.3" + postcss "~8.4.32" + resolve-from "^5.0.0" + +"@expo/metro@~54.2.0": + version "54.2.0" + resolved "https://registry.yarnpkg.com/@expo/metro/-/metro-54.2.0.tgz#6ecf4a77ae7553b73daca4206854728de76c854d" + integrity sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w== + dependencies: + metro "0.83.3" + metro-babel-transformer "0.83.3" + metro-cache "0.83.3" + metro-cache-key "0.83.3" + metro-config "0.83.3" + metro-core "0.83.3" + metro-file-map "0.83.3" + metro-minify-terser "0.83.3" + metro-resolver "0.83.3" + metro-runtime "0.83.3" + metro-source-map "0.83.3" + metro-symbolicate "0.83.3" + metro-transform-plugins "0.83.3" + metro-transform-worker "0.83.3" + +"@expo/osascript@^2.3.8": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@expo/osascript/-/osascript-2.4.2.tgz#fe341cff1eb2c939da43cf58ade5504c8a5d77ca" + integrity sha512-/XP7PSYF2hzOZzqfjgkoWtllyeTN8dW3aM4P6YgKcmmPikKL5FdoyQhti4eh6RK5a5VrUXJTOlTNIpIHsfB5Iw== + dependencies: + "@expo/spawn-async" "^1.7.2" + +"@expo/package-manager@^1.9.10": + version "1.10.4" + resolved "https://registry.yarnpkg.com/@expo/package-manager/-/package-manager-1.10.4.tgz#1a16bd2ccf85a23865dd98392c11b9f75f9bbf7a" + integrity sha512-y9Mr4Kmpk4abAVZrNNPCdzOZr8nLLyi18p1SXr0RCVA8IfzqZX/eY4H+50a0HTmXqIsPZrQdcdb4I3ekMS9GvQ== + dependencies: + "@expo/json-file" "^10.0.13" + "@expo/spawn-async" "^1.7.2" + chalk "^4.0.0" + npm-package-arg "^11.0.0" + ora "^3.4.0" + resolve-workspace-root "^2.0.0" + +"@expo/plist@^0.4.8": + version "0.4.8" + resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.4.8.tgz#e014511a4a5008cf2b832b91caa8e9f2704127cc" + integrity sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ== + dependencies: + "@xmldom/xmldom" "^0.8.8" + base64-js "^1.2.3" + xmlbuilder "^15.1.1" + +"@expo/prebuild-config@^54.0.8": + version "54.0.8" + resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-54.0.8.tgz#509410345489cc52d1e6ece52742384efe7ad7c6" + integrity sha512-EA7N4dloty2t5Rde+HP0IEE+nkAQiu4A/+QGZGT9mFnZ5KKjPPkqSyYcRvP5bhQE10D+tvz6X0ngZpulbMdbsg== + dependencies: + "@expo/config" "~12.0.13" + "@expo/config-plugins" "~54.0.4" + "@expo/config-types" "^54.0.10" + "@expo/image-utils" "^0.8.8" + "@expo/json-file" "^10.0.8" + "@react-native/normalize-colors" "0.81.5" + debug "^4.3.1" + resolve-from "^5.0.0" + semver "^7.6.0" + xml2js "0.6.0" + +"@expo/require-utils@^55.0.4": + version "55.0.4" + resolved "https://registry.yarnpkg.com/@expo/require-utils/-/require-utils-55.0.4.tgz#cd474a8997ba6ecfa43d084a7f17bde0cb854179" + integrity sha512-JAANvXqV7MOysWeVWgaiDzikoyDjJWOV/ulOW60Zb3kXJfrx2oZOtGtDXDFKD1mXuahQgoM5QOjuZhF7gFRNjA== + dependencies: + "@babel/code-frame" "^7.20.0" + "@babel/core" "^7.25.2" + "@babel/plugin-transform-modules-commonjs" "^7.24.8" + +"@expo/schema-utils@^0.1.8": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@expo/schema-utils/-/schema-utils-0.1.8.tgz#8b9543d77fc4ac4954196e3fa00f8fcedd71426a" + integrity sha512-9I6ZqvnAvKKDiO+ZF8BpQQFYWXOJvTAL5L/227RUbWG1OVZDInFifzCBiqAZ3b67NRfeAgpgvbA7rejsqhY62A== + +"@expo/sdk-runtime-versions@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz#d7ebd21b19f1c6b0395e50d78da4416941c57f7c" + integrity sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ== + +"@expo/spawn-async@^1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.7.2.tgz#fcfe66c3e387245e72154b1a7eae8cada6a47f58" + integrity sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew== + dependencies: + cross-spawn "^7.0.3" + +"@expo/sudo-prompt@^9.3.1": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@expo/sudo-prompt/-/sudo-prompt-9.3.2.tgz#0fd2813402a42988e49145cab220e25bea74b308" + integrity sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw== + +"@expo/vector-icons@^15.0.3": + version "15.1.1" + resolved "https://registry.yarnpkg.com/@expo/vector-icons/-/vector-icons-15.1.1.tgz#4b1d2c60493c0b0536972f0a5babd5f5c85b48f4" + integrity sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw== + +"@expo/ws-tunnel@^1.0.1": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@expo/ws-tunnel/-/ws-tunnel-1.0.6.tgz#92b70e7264ad42ea07f28a20f2f540b91d07bdd9" + integrity sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q== + +"@expo/xcpretty@^4.3.0": + version "4.4.3" + resolved "https://registry.yarnpkg.com/@expo/xcpretty/-/xcpretty-4.4.3.tgz#c49a052591cc672a9a032760c0262c76d46e08b6" + integrity sha512-wC562eD3gS6vO2tWHToFhlFnmHKfKHgF1oyvojeSkLK/ZYop1bMU+7cOMiF9Sq70CzcsLy/EMRy/uRc76QmNRw== + dependencies: + "@babel/code-frame" "^7.20.0" + chalk "^4.1.0" + js-yaml "^4.1.0" + +"@firebase/ai@2.11.1": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@firebase/ai/-/ai-2.11.1.tgz#a78c8d8a8acc5261fb2e0fa0216209b43a57c6dc" + integrity sha512-WGTF81W3WBKJY+c7xqTzO15OGAkCAs8cpADqflAI0skhTZjIkhF0qyf55rq4Ctt6jKygkv99rPfMrjAHTgXaVQ== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/component" "0.7.2" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/analytics-compat@0.2.27": + version "0.2.27" + resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.27.tgz#58ef74a91930267923577f07ca371182d9f7ce2e" + integrity sha512-ZObpYpAxL6JfgH7GnvlDD0sbzGZ0o4nijV8skatV9ZX49hJtCYbFqaEcPYptT94rgX1KUoKEderC7/fa7hybtw== + dependencies: + "@firebase/analytics" "0.10.21" + "@firebase/analytics-types" "0.8.3" + "@firebase/component" "0.7.2" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/analytics-types@0.8.3": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.8.3.tgz#d08cd39a6209693ca2039ba7a81570dfa6c1518f" + integrity sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg== + +"@firebase/analytics@0.10.21": + version "0.10.21" + resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.10.21.tgz#109d95d287acefe3d8276835291dbbcf4688c18c" + integrity sha512-j2y2q65BlgLGB5Pwjhv/Jopw2X/TBTzvAtI5z/DSp56U4wBj7LfhBfzbdCtFPges+Wz0g55GdoawXibOH5jGng== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/installations" "0.6.21" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/app-check-compat@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@firebase/app-check-compat/-/app-check-compat-0.4.2.tgz#c0b3808ebe9a366d3b2cba295eb7a1587de66314" + integrity sha512-M91NhxqbSkI0ChkJWy69blC+rPr6HEgaeRllddSaU1pQ/7IiegeCQM9pPDIgvWnwnBSzKhUHpe6ro/jhJ+cvzw== + dependencies: + "@firebase/app-check" "0.11.2" + "@firebase/app-check-types" "0.5.3" + "@firebase/component" "0.7.2" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/app-check-interop-types@0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz#ed9c4a4f48d1395ef378f007476db3940aa5351a" + integrity sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A== + +"@firebase/app-check-types@0.5.3": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@firebase/app-check-types/-/app-check-types-0.5.3.tgz#38ba954acf4bffe451581a32fffa20337f11d8e5" + integrity sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng== + +"@firebase/app-check@0.11.2": + version "0.11.2" + resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.11.2.tgz#c7f771c5222d77a810978081e7b493d3f5e8968f" + integrity sha512-jcXQVMHAQ5AEKzVD5C7s5fmAYeFOuN6lAJeNTgZK2B9aLnofWaJt8u1A8Idm8gpsBBYSaY3cVyeH5SWMOVPBLQ== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/app-compat@0.5.11": + version "0.5.11" + resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.5.11.tgz#4cb54f447cb03446465a05d00a71509b5f2ec620" + integrity sha512-KaACDjXkK5VLpI01vEs592R7/8s5DjFdIXfKoR385ly1SmK3Tu+jMHCIB4MsiY5jsez6v7VlEX/3rJ90dVkHyA== + dependencies: + "@firebase/app" "0.14.11" + "@firebase/component" "0.7.2" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/app-types@0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.4.tgz#e85864332b591db95a10620668405bef6906947b" + integrity sha512-crX9TA5SVYZwLPG7/R16IsH8FLlgkPXjJUVhsVpHVDSqJiq3D/NuFTM5ctxGTExXAOeIn//69tQw47CPerM8MQ== + dependencies: + "@firebase/logger" "0.5.0" + +"@firebase/app@0.14.11": + version "0.14.11" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.14.11.tgz#150f5f98299c24569fab8fbbffb7295075f526d7" + integrity sha512-yxADFW35LYkP8oSGobGsYIrI42I+GPCvKTNHx4meT9Yq3C950IVz1eANoBk822I9tbKv1wyv9P4Bv1G5TpucFw== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/auth-compat@0.6.5": + version "0.6.5" + resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.6.5.tgz#d12d27a4c2230d3ee32ef5c200ebd3b9108528ca" + integrity sha512-IfVsafZ3QiXbsydXTP/XMI0wVYbJLI1rkb8Qqf03/h5FnL+upbbPOb+6Yj3RpcX+Y1iP5Uh18lxTHlXfbiyAow== + dependencies: + "@firebase/auth" "1.13.0" + "@firebase/auth-types" "0.13.0" + "@firebase/component" "0.7.2" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/auth-interop-types@0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz#176a08686b0685596ff03d7879b7e4115af53de0" + integrity sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA== + +"@firebase/auth-types@0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.13.0.tgz#ae6e0015e3bd4bfe18edd0942b48a0a118a098d9" + integrity sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg== + +"@firebase/auth@1.13.0": + version "1.13.0" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.13.0.tgz#da83853b465c1ab4b638e542d78df6b3c4855a15" + integrity sha512-mKkSLNym3UbnnZ06dAmtqzp5EpPGCANGCZDJbkoR135aoUdKG6Aizwcnp29RzsQpwH0nmy5nay17Sfbsh9oY8A== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/component@0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.7.2.tgz#82a87848ad389d6037019745c973e4f2779a6983" + integrity sha512-iyVDGc6Vjx7Rm0cAdccLH/NG6fADsgJak/XW9IA2lPf8AjIlsemOpFGKczYyPHxm4rnKdR8z6sK4+KEC7NwmEg== + dependencies: + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/data-connect@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@firebase/data-connect/-/data-connect-0.6.0.tgz#c4581d13685eccac724895b8c7292df4a24cd334" + integrity sha512-OiugPRcdlhqXF97oR9CjVObILmsWU0dFUS0gXNYEe4bDfpW8pZmQ5GqhIPPtLWbT/0W2lMJJD7VILFMk+xuHPg== + dependencies: + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.7.2" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/database-compat@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-2.1.3.tgz#19a512209afbba9710d7febc52cd875e1b239e3c" + integrity sha512-GMyfWjD8mehjg/QpNkY/tl9G/MoeugPeg91n9D0atggxbWuKF/2KhVPHZDH+XmoP0EKYqMWYTtKxBsaBaNKLYQ== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/database" "1.1.2" + "@firebase/database-types" "1.0.19" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/database-types@1.0.19": + version "1.0.19" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.19.tgz#0e59454ea764aa2617fb2a8ea94104c4cb1605ac" + integrity sha512-FqewjUZmV9LqFfuEnmgdcUpiOUz7qwLXxnm/H8BcMFEzQXtd1yyUDm8ex5VRad2nuTE+ahOuCjUAM/cyDncO+g== + dependencies: + "@firebase/app-types" "0.9.4" + "@firebase/util" "1.15.0" + +"@firebase/database@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.1.2.tgz#cd99d7f205d7eee121e2c327bf89b42c3afa3776" + integrity sha512-lP96CMjMPy/+d1d9qaaHjHHdzdwvEOuyyLq9ehX89e2XMKwS1jHNzYBO+42bdSumuj5ukPbmnFtViZu8YOMT+w== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.7.2" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + faye-websocket "0.11.4" + tslib "^2.1.0" + +"@firebase/firestore-compat@0.4.8": + version "0.4.8" + resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.4.8.tgz#00651c9d01f940d9906b34ae2cc4229527f4b7f8" + integrity sha512-WK9NJRpnosGD2nuyjdr7K+Ht7AxRYJlTF62myI4rRA7ibJOosbecvjacR5oirJ7s1BgNS6qzcBw7n4fD3a5w1w== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/firestore" "4.14.0" + "@firebase/firestore-types" "3.0.3" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/firestore-types@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.3.tgz#7d0c3dd8850c0193d8f5ee0cc8f11961407742c1" + integrity sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q== + +"@firebase/firestore@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.14.0.tgz#a17801fe2e8a0095fc655d38cd791c4a48058cb4" + integrity sha512-bZc6YOjRkMBVA16527tgzi6iN9n//xRB3Mmx/R+Gr6UAP/+xrIKOejQIcn1hh+tCzNT8jO0jI+kWox5J4tB/qQ== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + "@firebase/webchannel-wrapper" "1.0.5" + "@grpc/grpc-js" "~1.9.0" + "@grpc/proto-loader" "^0.7.8" + tslib "^2.1.0" + +"@firebase/functions-compat@0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@firebase/functions-compat/-/functions-compat-0.4.3.tgz#bb7e5b8330143db594466c1140267799cf41680d" + integrity sha512-BxkEwWgx1of0tKaao/r2VR6WBLk/RAiyztatiONPrPE8gkitFkOnOCxf8i9cUyA5hX5RGt5H30uNn25Q6QNEmQ== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/functions" "0.13.3" + "@firebase/functions-types" "0.6.3" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/functions-types@0.6.3": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.6.3.tgz#f5faf770248b13f45d256f614230da6a11bfb654" + integrity sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg== + +"@firebase/functions@0.13.3": + version "0.13.3" + resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.13.3.tgz#e923cb6f6763531cbe909253155920abb4105f04" + integrity sha512-csO7ckK3SSs+NUZW1nms9EK7ckHe/1QOjiP8uAkCYa7ND18s44vjE9g3KxEeIUpyEPqZaX1EhJuFyZjHigAcYw== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.7.2" + "@firebase/messaging-interop-types" "0.2.3" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/installations-compat@0.2.21": + version "0.2.21" + resolved "https://registry.yarnpkg.com/@firebase/installations-compat/-/installations-compat-0.2.21.tgz#c00e1e1b3957aff0957cff80a776109895328c54" + integrity sha512-zahIUkaVKbR8zmTeBHkdfaVl6JGWlhVoSjF7CVH33nFqD3SlPEpEEegn2GNT5iAfsVdtlCyJJ9GW4YKjq+RJKQ== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/installations" "0.6.21" + "@firebase/installations-types" "0.5.3" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/installations-types@0.5.3": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.5.3.tgz#cac8a14dd49f09174da9df8ae453f9b359c3ef2f" + integrity sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA== + +"@firebase/installations@0.6.21": + version "0.6.21" + resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.6.21.tgz#38c9a5487c7ccc7dd4328736556afad8eebf453e" + integrity sha512-xGFGTeICJZ5vhrmmDukeczIcFULFXybojML2+QSDFoKj5A7zbGN7KzFGSKNhDkIxpjzsYG9IleJyUebuAcmqWA== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/util" "1.15.0" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/logger@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.5.0.tgz#a9e55b1c669a0983dc67127fa4a5964ce8ed5e1b" + integrity sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g== + dependencies: + tslib "^2.1.0" + +"@firebase/messaging-compat@0.2.25": + version "0.2.25" + resolved "https://registry.yarnpkg.com/@firebase/messaging-compat/-/messaging-compat-0.2.25.tgz#1fd6f317d303dfab57ec51409eb41c89080f2dd6" + integrity sha512-eoOQqGLtRlseTdiemTN44LlHZpltK5gnhq8XVUuLgtIOG+odtDzrz2UoTpcJWSzaJQVxNLb/x9f39tHdDM4N4w== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/messaging" "0.12.25" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/messaging-interop-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz#e647c9cd1beecfe6a6e82018a6eec37555e4da3e" + integrity sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q== + +"@firebase/messaging@0.12.25": + version "0.12.25" + resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.12.25.tgz#465135006a5d728efaeea1d35790f0e6e20a3e54" + integrity sha512-7RhDwoDHlOK1/ou0/LeubxmjcngsTjDdrY/ssg2vwAVpUuVAhQzQvuCAOYxcX5wNC1zCgQ54AP1vdngBwbCmOQ== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/installations" "0.6.21" + "@firebase/messaging-interop-types" "0.2.3" + "@firebase/util" "1.15.0" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/performance-compat@0.2.24": + version "0.2.24" + resolved "https://registry.yarnpkg.com/@firebase/performance-compat/-/performance-compat-0.2.24.tgz#1c970640119a8839f7447f624de365f150f21399" + integrity sha512-YRlejH8wLt7ThWao+HXoKUHUrZKGYq+otxkPS+8nuE5PeN1cBXX7NAJl9ueuUkBwMIrnKdnDqL/voHXxDAAt3g== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/logger" "0.5.0" + "@firebase/performance" "0.7.11" + "@firebase/performance-types" "0.2.3" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/performance-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.2.3.tgz#5ce64e90fa20ab5561f8b62a305010cf9fab86fb" + integrity sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ== + +"@firebase/performance@0.7.11": + version "0.7.11" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.7.11.tgz#238a805f78c5411f1d41c4cdba7854ca4115a1b7" + integrity sha512-V3uAhrz7IYJuji+OgT3qYTGKxpek/TViXti9OSsUJ4AexZ3jQjYH5Yrn7JvBxk8MGiSLsC872hh+BxQiPZsm7g== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/installations" "0.6.21" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + web-vitals "^4.2.4" + +"@firebase/remote-config-compat@0.2.23": + version "0.2.23" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-compat/-/remote-config-compat-0.2.23.tgz#3614d83c1f22c68152793bbbf6965715e1716d07" + integrity sha512-4+KqRRHEUUmKT6tFmnpWATOsaFfmSuBs1jXH8JzVtMLEYqq/WS9IDM92OdefFDSrAA2xGd0WN004z8mKeIIscw== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/logger" "0.5.0" + "@firebase/remote-config" "0.8.2" + "@firebase/remote-config-types" "0.5.0" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/remote-config-types@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz#f0f503b32edda3384f5252f9900cd9613adbb99c" + integrity sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg== + +"@firebase/remote-config@0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.8.2.tgz#389d2b01d4d877c6cb13bf85692dfda45d61fe6d" + integrity sha512-5EXqOThV4upjK9D38d/qOSVwOqRhemlaOFk9vCkMNNALeIlwr+4pLjtLNo4qoY8etQmU/1q4aIATE9N8PFqg0g== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/installations" "0.6.21" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/storage-compat@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@firebase/storage-compat/-/storage-compat-0.4.2.tgz#866e3bfc4533510bc1d6207d10e9ef5748359cf4" + integrity sha512-R+aB38wxCH5zjIO/xu9KznI7fgiPuZAG98uVm1NcidHyyupGgIDLKigGmRGBZMnxibe/m2oxNKoZpfEbUX2aQQ== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/storage" "0.14.2" + "@firebase/storage-types" "0.8.3" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/storage-types@0.8.3": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.8.3.tgz#2531ef593a3452fc12c59117195d6485c6632d3d" + integrity sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg== + +"@firebase/storage@0.14.2": + version "0.14.2" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.14.2.tgz#476bad2594ad26487b232620f0a23991d65ab69a" + integrity sha512-o/culaTeJ8GRpKXRJov21rux/n9dRaSOWLebyatFP2sqEdCxQPjVA1H9Z2fzYwQxMIU0JVmC7SPPmU11v7L6vQ== + dependencies: + "@firebase/component" "0.7.2" + "@firebase/util" "1.15.0" + tslib "^2.1.0" + +"@firebase/util@1.15.0": + version "1.15.0" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.15.0.tgz#783c1a67dc0690dbe3afca668174c11368843008" + integrity sha512-AmWf3cHAOMbrCPG4xdPKQaj5iHnyYfyLKZxwz+Xf55bqKbpAmcYifB4jQinT2W9XhDRHISOoPyBOariJpCG6FA== + dependencies: + tslib "^2.1.0" + +"@firebase/webchannel-wrapper@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz#39cf5a600450cb42f1f0b507cc385459bf103b27" + integrity sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw== + +"@grpc/grpc-js@~1.9.0": + version "1.9.15" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.15.tgz#433d7ac19b1754af690ea650ab72190bd700739b" + integrity sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.7.8": + version "0.7.15" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz#4cdfbf35a35461fc843abe8b9e2c0770b5095e60" + integrity sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + +"@isaacs/fs-minipass@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32" + integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== + dependencies: + minipass "^7.0.4" + +"@isaacs/ttlcache@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2" + integrity sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.6.tgz#8dc9afa2ac1506cb1a58f89940f1c124446c8df3" + integrity sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw== + +"@jest/create-cache-key-function@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz#793be38148fab78e65f40ae30c36785f4ad859f0" + integrity sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA== + dependencies: + "@jest/types" "^29.6.3" + +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.11" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba" + integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.5.tgz#d9315ad7cf3f30aac70bda3c068443dc6f143659" + integrity sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0", "@protobufjs/inquire@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.1.tgz#6cb936f4ac50965230af1e9d0bbfd57ea3675aa4" + integrity sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.1.tgz#eaee5900122c110a3dbcb728c0597014a2621774" + integrity sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg== + +"@react-native-async-storage/async-storage@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz#a3aa565253e46286655560172f4e366e8969f5ad" + integrity sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw== + dependencies: + merge-options "^3.0.4" + +"@react-native/assets-registry@0.81.5": + version "0.81.5" + resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.81.5.tgz#d22c924fa6f6d4a463c5af34ce91f38756c0fa7d" + integrity sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w== + +"@react-native/babel-plugin-codegen@0.81.5": + version "0.81.5" + resolved "https://registry.yarnpkg.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.81.5.tgz#328d03f42c32b5a8cc2dee1aa84a7c48dddc5f18" + integrity sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ== + dependencies: + "@babel/traverse" "^7.25.3" + "@react-native/codegen" "0.81.5" + +"@react-native/babel-preset@0.81.5": + version "0.81.5" + resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.81.5.tgz#e8b7969d21f87ef4e41e603248e8a70c44b4a5bb" + integrity sha512-UoI/x/5tCmi+pZ3c1+Ypr1DaRMDLI3y+Q70pVLLVgrnC3DHsHRIbHcCHIeG/IJvoeFqFM2sTdhSOLJrf8lOPrA== + dependencies: + "@babel/core" "^7.25.2" + "@babel/plugin-proposal-export-default-from" "^7.24.7" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-default-from" "^7.24.7" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-transform-arrow-functions" "^7.24.7" + "@babel/plugin-transform-async-generator-functions" "^7.25.4" + "@babel/plugin-transform-async-to-generator" "^7.24.7" + "@babel/plugin-transform-block-scoping" "^7.25.0" + "@babel/plugin-transform-class-properties" "^7.25.4" + "@babel/plugin-transform-classes" "^7.25.4" + "@babel/plugin-transform-computed-properties" "^7.24.7" + "@babel/plugin-transform-destructuring" "^7.24.8" + "@babel/plugin-transform-flow-strip-types" "^7.25.2" + "@babel/plugin-transform-for-of" "^7.24.7" + "@babel/plugin-transform-function-name" "^7.25.1" + "@babel/plugin-transform-literals" "^7.25.2" + "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.8" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.7" + "@babel/plugin-transform-numeric-separator" "^7.24.7" + "@babel/plugin-transform-object-rest-spread" "^7.24.7" + "@babel/plugin-transform-optional-catch-binding" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.8" + "@babel/plugin-transform-parameters" "^7.24.7" + "@babel/plugin-transform-private-methods" "^7.24.7" + "@babel/plugin-transform-private-property-in-object" "^7.24.7" + "@babel/plugin-transform-react-display-name" "^7.24.7" + "@babel/plugin-transform-react-jsx" "^7.25.2" + "@babel/plugin-transform-react-jsx-self" "^7.24.7" + "@babel/plugin-transform-react-jsx-source" "^7.24.7" + "@babel/plugin-transform-regenerator" "^7.24.7" + "@babel/plugin-transform-runtime" "^7.24.7" + "@babel/plugin-transform-shorthand-properties" "^7.24.7" + "@babel/plugin-transform-spread" "^7.24.7" + "@babel/plugin-transform-sticky-regex" "^7.24.7" + "@babel/plugin-transform-typescript" "^7.25.2" + "@babel/plugin-transform-unicode-regex" "^7.24.7" + "@babel/template" "^7.25.0" + "@react-native/babel-plugin-codegen" "0.81.5" + babel-plugin-syntax-hermes-parser "0.29.1" + babel-plugin-transform-flow-enums "^0.0.2" + react-refresh "^0.14.0" + +"@react-native/codegen@0.81.5": + version "0.81.5" + resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.81.5.tgz#d4dec668c94b9d58a5c2dbdbf026db331e1b6b27" + integrity sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g== + dependencies: + "@babel/core" "^7.25.2" + "@babel/parser" "^7.25.3" + glob "^7.1.1" + hermes-parser "0.29.1" + invariant "^2.2.4" + nullthrows "^1.1.1" + yargs "^17.6.2" + +"@react-native/community-cli-plugin@0.81.5": + version "0.81.5" + resolved "https://registry.yarnpkg.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.5.tgz#617789cda4da419d03dda00e2a78c36188b4391e" + integrity sha512-yWRlmEOtcyvSZ4+OvqPabt+NS36vg0K/WADTQLhrYrm9qdZSuXmq8PmdJWz/68wAqKQ+4KTILiq2kjRQwnyhQw== + dependencies: + "@react-native/dev-middleware" "0.81.5" + debug "^4.4.0" + invariant "^2.2.4" + metro "^0.83.1" + metro-config "^0.83.1" + metro-core "^0.83.1" + semver "^7.1.3" + +"@react-native/debugger-frontend@0.81.5": + version "0.81.5" + resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.81.5.tgz#82ece0181e9a7a3dcbdfa86cf9decd654e13f81f" + integrity sha512-bnd9FSdWKx2ncklOetCgrlwqSGhMHP2zOxObJbOWXoj7GHEmih4MKarBo5/a8gX8EfA1EwRATdfNBQ81DY+h+w== + +"@react-native/dev-middleware@0.81.5": + version "0.81.5" + resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.81.5.tgz#81e8ac545d7736ef6ebb2e59fdbaebc5cf9aedec" + integrity sha512-WfPfZzboYgo/TUtysuD5xyANzzfka8Ebni6RIb2wDxhb56ERi7qDrE4xGhtPsjCL4pQBXSVxyIlCy0d8I6EgGA== + dependencies: + "@isaacs/ttlcache" "^1.4.1" + "@react-native/debugger-frontend" "0.81.5" + chrome-launcher "^0.15.2" + chromium-edge-launcher "^0.2.0" + connect "^3.6.5" + debug "^4.4.0" + invariant "^2.2.4" + nullthrows "^1.1.1" + open "^7.0.3" + serve-static "^1.16.2" + ws "^6.2.3" + +"@react-native/gradle-plugin@0.81.5": + version "0.81.5" + resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.81.5.tgz#a58830f38789f6254b64449a17fe57455b589d00" + integrity sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg== + +"@react-native/js-polyfills@0.81.5": + version "0.81.5" + resolved "https://registry.yarnpkg.com/@react-native/js-polyfills/-/js-polyfills-0.81.5.tgz#2ca68188c8fff9b951f507b1dec7efe928848274" + integrity sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w== + +"@react-native/normalize-colors@0.81.5": + version "0.81.5" + resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz#1ca6cb6772bb7324df2b11aab35227eacd6bdfe7" + integrity sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g== + +"@react-native/virtualized-lists@0.81.5": + version "0.81.5" + resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz#24123fded16992d7e46ecc4ccd473be939ea8c1b" + integrity sha512-UVXgV/db25OPIvwZySeToXD/9sKKhOdkcWmmf4Jh8iBZuyfML+/5CasaZ1E7Lqg6g3uqVQq75NqIwkYmORJMPw== + dependencies: + invariant "^2.2.4" + nullthrows "^1.1.1" + +"@react-navigation/core@^7.17.2": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.17.2.tgz#8a17b73faf7c0688a4749dcac8c7350d8f93e943" + integrity sha512-Rt2OZwcgOmjv401uLGAKaRM6xo0fiBce/A7LfRHI1oe5FV+KooWcgAoZ2XOtgKj6UzVMuQWt3b2e6rxo/mDJRA== + dependencies: + "@react-navigation/routers" "^7.5.3" + escape-string-regexp "^4.0.0" + fast-deep-equal "^3.1.3" + nanoid "^3.3.11" + query-string "^7.1.3" + react-is "^19.1.0" + use-latest-callback "^0.2.4" + use-sync-external-store "^1.5.0" + +"@react-navigation/elements@^2.9.15": + version "2.9.15" + resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.9.15.tgz#384380f8d7f0c5b35e960aa5a6bdf4cc2bcb78b4" + integrity sha512-cyz/pPiyyC6gaTVLsGFc1g0MYgrmuCFqklAWGXMWPscr5YU3ui94vPI4vnZwcsEy0T758TQWLzmS5XudZeRKcA== + dependencies: + color "^4.2.3" + use-latest-callback "^0.2.4" + use-sync-external-store "^1.5.0" + +"@react-navigation/native-stack@^7.2.0": + version "7.14.12" + resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.14.12.tgz#b22faf70271056a4648c4601707d4b33f0e2e88d" + integrity sha512-dUfpkrVeVKKV8iqXsmoUp3Rv0iH3YaB3eZwScru/FlcqAp/r3/qA6zEXkGX9hZK+/ziWAPFrf1frBSNbgOYSFQ== + dependencies: + "@react-navigation/elements" "^2.9.15" + color "^4.2.3" + sf-symbols-typescript "^2.1.0" + warn-once "^0.1.1" + +"@react-navigation/native@^7.0.14": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.2.2.tgz#c9438fe8393454d74fdb7f959ac9abede52b1f8e" + integrity sha512-kem1Ko2BcbAjmbQIv66dNmr6EtfDut3QU0qjsVhMnLLhktwyXb6FzZYp8gTrUb6AvkAbaJoi+BF5Pl55pAUa5w== + dependencies: + "@react-navigation/core" "^7.17.2" + escape-string-regexp "^4.0.0" + fast-deep-equal "^3.1.3" + nanoid "^3.3.11" + use-latest-callback "^0.2.4" + +"@react-navigation/routers@^7.5.3": + version "7.5.3" + resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-7.5.3.tgz#8002930ef5f62351be2475d0dffde3ffaee174d7" + integrity sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg== + dependencies: + nanoid "^3.3.11" + +"@sinclair/typebox@^0.27.8": + version "0.27.10" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.10.tgz#beefe675f1853f73676aecc915b2bd2ac98c4fc6" + integrity sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA== + +"@sinonjs/commons@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + +"@tanstack/query-core@5.100.6": + version "5.100.6" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.100.6.tgz#2f6dc032aaae1f327d85d3b977e6d530c948a91f" + integrity sha512-Os2CPUr98to98RYm+D4qGqGkiffn7MGSyl2547a4MljVkHE30AMJRqTiyCqBfMwzAx/I91vCkAxp5tHSla6Twg== + +"@tanstack/react-query@^5.90.21": + version "5.100.6" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.100.6.tgz#f69aefc2136b1e7126e630e0d7881cf7b7e25c77" + integrity sha512-uVSrps0PV16Cxmcn2rvL+dUhwTpTUtiRW347AEeYxMZXO2pZe9ja7E24PAMGoQ5u2g89DD8u4QhOviBk+RN8RA== + dependencies: + "@tanstack/query-core" "5.100.6" + +"@types/babel__core@^7.1.14": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== + dependencies: + "@babel/types" "^7.28.2" + +"@types/graceful-fs@^4.1.3": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== + dependencies: + "@types/node" "*" + +"@types/hammerjs@^2.0.36": + version "2.0.46" + resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.46.tgz#381daaca1360ff8a7c8dff63f32e69745b9fb1e1" + integrity sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw== + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/istanbul-lib-report@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "25.6.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca" + integrity sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ== + dependencies: + undici-types "~7.19.0" + +"@types/react@~19.1.0": + version "19.1.17" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.17.tgz#8be0b9c546cede389b930a98eb3fad1897f209c3" + integrity sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA== + dependencies: + csstype "^3.0.2" + +"@types/stack-utils@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== + +"@types/yargs-parser@*": + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + +"@types/yargs@^17.0.8": + version "17.0.35" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.35.tgz#07013e46aa4d7d7d50a49e15604c1c5340d4eb24" + integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== + dependencies: + "@types/yargs-parser" "*" + +"@ungap/structured-clone@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +"@urql/core@^5.0.6", "@urql/core@^5.1.2": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@urql/core/-/core-5.2.0.tgz#77ee41e192e261fea30c2ca6c2f340410b45d214" + integrity sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A== + dependencies: + "@0no-co/graphql.web" "^1.0.13" + wonka "^6.3.2" + +"@urql/exchange-retry@^1.3.0": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@urql/exchange-retry/-/exchange-retry-1.3.2.tgz#042ff5f3512a062651ec7257f1b07f9db2f6fefd" + integrity sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg== + dependencies: + "@urql/core" "^5.1.2" + wonka "^6.3.2" + +"@xmldom/xmldom@^0.8.8": + version "0.8.13" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.13.tgz#00d1dd940b218dff2e49309d410d8bb212159225" + integrity sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw== + +"@xmldom/xmldom@^0.9.10": + version "0.9.10" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz#a0ad5a26fe8aa996310870726e1704977f769dee" + integrity sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@^1.3.7, accepts@^1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + +acorn@^8.15.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== + +agent-base@^7.1.2: + version "7.1.4" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== + +ajv@^8.11.0: + version "8.20.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.20.0.tgz#304b3636add88ba7d936760dd50ece006dea95f9" + integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +anser@^1.4.9: + version "1.4.10" + resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" + integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww== + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +ansi-regex@^5.0.0, ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-timsort@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926" + integrity sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ== + +asap@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + dependencies: + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-plugin-polyfill-corejs2@^0.4.14: + version "0.4.17" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz#198f970f1c99a856b466d1187e88ce30bd199d91" + integrity sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-define-polyfill-provider" "^0.6.8" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz#bb7f6aeef7addff17f7602a08a6d19a128c30164" + integrity sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.5" + core-js-compat "^3.43.0" + +babel-plugin-polyfill-regenerator@^0.6.5: + version "0.6.8" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz#8a6bfd5dd54239362b3d06ce47ac52b2d95d7721" + integrity sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.8" + +babel-plugin-react-compiler@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz#bdf7360a23a4d5ebfca090255da3893efd07425f" + integrity sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw== + dependencies: + "@babel/types" "^7.26.0" + +babel-plugin-react-native-web@~0.21.0: + version "0.21.2" + resolved "https://registry.yarnpkg.com/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz#d2f7fd673278da82577aa583457edb55d9cccbe0" + integrity sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA== + +babel-plugin-syntax-hermes-parser@0.29.1, babel-plugin-syntax-hermes-parser@^0.29.1: + version "0.29.1" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.29.1.tgz#09ca9ecb0330eba1ef939b6d3f1f55bb06a9dc33" + integrity sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA== + dependencies: + hermes-parser "0.29.1" + +babel-plugin-transform-flow-enums@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz#d1d0cc9bdc799c850ca110d0ddc9f21b9ec3ef25" + integrity sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ== + dependencies: + "@babel/plugin-syntax-flow" "^7.12.1" + +babel-preset-current-node-syntax@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz#20730d6cdc7dda5d89401cab10ac6a32067acde6" + integrity sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-import-attributes" "^7.24.7" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + +babel-preset-expo@~54.0.10: + version "54.0.10" + resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-54.0.10.tgz#3b70f4af3a5f65f945d7957ef511ee016e8f2fd6" + integrity sha512-wTt7POavLFypLcPW/uC5v8y+mtQKDJiyGLzYCjqr9tx0Qc3vCXcDKk1iCFIj/++Iy5CWhhTflEa7VvVPNWeCfw== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/plugin-proposal-decorators" "^7.12.9" + "@babel/plugin-proposal-export-default-from" "^7.24.7" + "@babel/plugin-syntax-export-default-from" "^7.24.7" + "@babel/plugin-transform-class-static-block" "^7.27.1" + "@babel/plugin-transform-export-namespace-from" "^7.25.9" + "@babel/plugin-transform-flow-strip-types" "^7.25.2" + "@babel/plugin-transform-modules-commonjs" "^7.24.8" + "@babel/plugin-transform-object-rest-spread" "^7.24.7" + "@babel/plugin-transform-parameters" "^7.24.7" + "@babel/plugin-transform-private-methods" "^7.24.7" + "@babel/plugin-transform-private-property-in-object" "^7.24.7" + "@babel/plugin-transform-runtime" "^7.24.7" + "@babel/preset-react" "^7.22.15" + "@babel/preset-typescript" "^7.23.0" + "@react-native/babel-preset" "0.81.5" + babel-plugin-react-compiler "^1.0.0" + babel-plugin-react-native-web "~0.21.0" + babel-plugin-syntax-hermes-parser "^0.29.1" + babel-plugin-transform-flow-enums "^0.0.2" + debug "^4.3.4" + resolve-from "^5.0.0" + +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +balanced-match@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" + integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== + +base64-js@^1.2.3, base64-js@^1.3.1, base64-js@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +baseline-browser-mapping@^2.10.12: + version "2.10.24" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz#6dc320c7bf53859ec2bf55d54db6d2e5c078df16" + integrity sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA== + +better-opn@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-3.0.2.tgz#f96f35deaaf8f34144a4102651babcf00d1d8817" + integrity sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ== + dependencies: + open "^8.0.4" + +big-integer@1.6.x: + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +bplist-creator@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.1.0.tgz#018a2d1b587f769e379ef5519103730f8963ba1e" + integrity sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg== + dependencies: + stream-buffers "2.2.x" + +bplist-parser@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.3.1.tgz#e1c90b2ca2a9f9474cc72f6862bbf3fee8341fd1" + integrity sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA== + dependencies: + big-integer "1.6.x" + +bplist-parser@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.3.2.tgz#3ac79d67ec52c4c107893e0237eb787cbacbced7" + integrity sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ== + dependencies: + big-integer "1.6.x" + +brace-expansion@^1.1.7: + version "1.1.14" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.14.tgz#d9de602370d91347cd9ddad1224d4fd701eb348b" + integrity sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.2: + version "2.1.0" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.0.tgz#4f41a41190216ee36067ec381526fe9539c4f0ae" + integrity sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w== + dependencies: + balanced-match "^1.0.0" + +brace-expansion@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb" + integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ== + dependencies: + balanced-match "^4.0.2" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.24.0, browserslist@^4.25.0, browserslist@^4.28.1: + version "4.28.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.2.tgz#f50b65362ef48974ca9f50b3680566d786b811d2" + integrity sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg== + dependencies: + baseline-browser-mapping "^2.10.12" + caniuse-lite "^1.0.30001782" + electron-to-chromium "^1.5.328" + node-releases "^2.0.36" + update-browserslist-db "^1.2.3" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.4.3: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001782: + version "1.0.30001791" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz#dfb93d85c40ad380c57123e72e10f3c575786b51" + integrity sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ== + +chalk@^2.0.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" + integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + +chrome-launcher@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.2.tgz#4e6404e32200095fdce7f6a1e1004f9bd36fa5da" + integrity sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ== + dependencies: + "@types/node" "*" + escape-string-regexp "^4.0.0" + is-wsl "^2.2.0" + lighthouse-logger "^1.0.0" + +chromium-edge-launcher@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz#0c378f28c99aefc360705fa155de0113997f62fc" + integrity sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg== + dependencies: + "@types/node" "*" + escape-string-regexp "^4.0.0" + is-wsl "^2.2.0" + lighthouse-logger "^1.0.0" + mkdirp "^1.0.4" + rimraf "^3.0.2" + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +ci-info@^3.2.0, ci-info@^3.3.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw== + dependencies: + restore-cursor "^2.0.0" + +cli-spinners@^2.0.0: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + +commander@^12.0.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +comment-json@^4.2.5: + version "4.6.2" + resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.6.2.tgz#235d8a908e211855b0068248a794afddb87670af" + integrity sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w== + dependencies: + array-timsort "^1.0.3" + esprima "^4.0.1" + +compressible@~2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.8.1" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" + integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== + dependencies: + bytes "3.1.2" + compressible "~2.0.18" + debug "2.6.9" + negotiator "~0.6.4" + on-headers "~1.1.0" + safe-buffer "5.2.1" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect@^3.6.5, connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +core-js-compat@^3.43.0: + version "3.49.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.49.0.tgz#06145447d92f4aaf258a0c44f24b47afaeaffef6" + integrity sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA== + dependencies: + browserslist "^4.28.1" + +cross-spawn@^7.0.3: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-select@^5.1.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.2.2.tgz#01b6e8d163637bb2dd6c982ca4ed65863682786e" + integrity sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^6.1.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea" + integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +csstype@^3.0.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + +debug@2.6.9, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7, debug@^4.4.0, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +decode-uri-component@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deepmerge@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + +detect-libc@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +dotenv-expand@~11.0.6: + version "11.0.7" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-11.0.7.tgz#af695aea007d6fdc84c86cd8d0ad7beb40a0bd08" + integrity sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA== + dependencies: + dotenv "^16.4.5" + +dotenv@^16.4.5: + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== + +dotenv@~16.4.5: + version "16.4.7" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" + integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.328: + version "1.5.344" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz#6437cc08a7d9b914a98120e182f37793c9eaffd4" + integrity sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +env-editor@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/env-editor/-/env-editor-0.4.2.tgz#4e76568d0bd8f5c2b6d314a9412c8fe9aa3ae861" + integrity sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA== + +error-stack-parser@^2.0.6: + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== + dependencies: + stackframe "^1.3.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +expo-asset@~12.0.13: + version "12.0.13" + resolved "https://registry.yarnpkg.com/expo-asset/-/expo-asset-12.0.13.tgz#1974ed7abee2ad987a519dbdcbf7f0c647dddf5b" + integrity sha512-x/p7WvQUnkn6K43b9eL6SPeq5Vnf1E8BDe9bDrWrvMqzyUvJnUFvl+ctg3034s/+UHe7Ne2pAmc0+yzbl8CrDQ== + dependencies: + "@expo/image-utils" "^0.8.8" + expo-constants "~18.0.13" + +expo-audio@~1.0.13: + version "1.0.16" + resolved "https://registry.yarnpkg.com/expo-audio/-/expo-audio-1.0.16.tgz#6cee98909c5c8b641832e73d944cf0be1c16d4de" + integrity sha512-j7otyjtO+8PVbemoCoRBr2Em0Kv9to3bfz5UpI5tDVVb5gD1dkn7sjv6/W6tWNqx14WLR1Wijh/ACecVv1Py+g== + +expo-build-properties@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/expo-build-properties/-/expo-build-properties-1.0.10.tgz#2c3fb4248f78828e952defa636635a653e3ad546" + integrity sha512-mFCZbrbrv0AP5RB151tAoRzwRJelqM7bCJzCkxpu+owOyH+p/rFC/q7H5q8B9EpVWj8etaIuszR+gKwohpmu1Q== + dependencies: + ajv "^8.11.0" + semver "^7.6.0" + +expo-camera@~17.0.10: + version "17.0.10" + resolved "https://registry.yarnpkg.com/expo-camera/-/expo-camera-17.0.10.tgz#b3a217f0eb811a6e3522c2aff9f42be578aa6456" + integrity sha512-w1RBw83mAGVk4BPPwNrCZyFop0VLiVSRE3c2V9onWbdFwonpRhzmB4drygG8YOUTl1H3wQvALJHyMPTbgsK1Jg== + dependencies: + invariant "^2.2.4" + +expo-constants@~18.0.13: + version "18.0.13" + resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-18.0.13.tgz#0117f1f3d43be7b645192c0f4f431fb4efc4803d" + integrity sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ== + dependencies: + "@expo/config" "~12.0.13" + "@expo/env" "~2.0.8" + +expo-file-system@~19.0.16, expo-file-system@~19.0.22: + version "19.0.22" + resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-19.0.22.tgz#8e8f892b2e89a78102b2b90fc1af5bb6bad4f21b" + integrity sha512-l9pgahSc7sJD0bP9vBNeXvZjy8QKDpVHVxWmei/ESQOrzmoj5BidziqLVsyZdxsi+PfdbTtttLTAmddH/JafYA== + +expo-font@~14.0.11: + version "14.0.11" + resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-14.0.11.tgz#198743d17332520545107df026d8a261e6b2732f" + integrity sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg== + dependencies: + fontfaceobserver "^2.1.0" + +expo-haptics@~15.0.7: + version "15.0.8" + resolved "https://registry.yarnpkg.com/expo-haptics/-/expo-haptics-15.0.8.tgz#f93f895ac5d76fe0c5ac26b3644e1dbb097833f3" + integrity sha512-lftutojy8Qs8zaDzzjwM3gKHFZ8bOOEZDCkmh2Ddpe95Ra6kt2izeOfOfKuP/QEh0MZ1j9TfqippyHdRd1ZM9g== + +expo-keep-awake@~15.0.8: + version "15.0.8" + resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz#911c5effeba9baff2ccde79ef0ff5bf856215f8d" + integrity sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ== + +expo-modules-autolinking@3.0.25: + version "3.0.25" + resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-3.0.25.tgz#3cb6e88fb491f15e3152c03eb64964e0b22fb707" + integrity sha512-YmHWctJlwvOuLZccg3cOXvSiXVJrPMKl7g2YR0YHWoGL9v2RvcmgaPJWPSLVW+voNEgEPsbo5UmUrAqbnYcBeg== + dependencies: + "@expo/spawn-async" "^1.7.2" + chalk "^4.1.0" + commander "^7.2.0" + require-from-string "^2.0.2" + resolve-from "^5.0.0" + +expo-modules-core@3.0.30: + version "3.0.30" + resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-3.0.30.tgz#ec4e216a33083afb0ddf314615d6d31a741984d6" + integrity sha512-a6IrpAn/Jbmwxi9L+hMmXKpNqnkUpoF7WHOpn02rVLyax2J0gB1vvCVE5rNydplEnt41Q6WxQwvcOjZaIkcSUg== + dependencies: + invariant "^2.2.4" + +expo-secure-store@~15.0.8: + version "15.0.8" + resolved "https://registry.yarnpkg.com/expo-secure-store/-/expo-secure-store-15.0.8.tgz#678065599bb76061b5a85b15b9426bf7a11089ae" + integrity sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw== + +expo-server@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/expo-server/-/expo-server-1.0.6.tgz#a821c55dd3f9f3f9bce0ba70d81bbc9fcc360ee8" + integrity sha512-vb5TBtskvEdzYuW79lATXutOEBfW5m6U4EFpNjCVZTnI7S//SAsLQkYEpn+EDfn84m6VQfzSGkIVR6YPaScKFA== + +expo-status-bar@~3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-3.0.9.tgz#87cfc803fa614f09a985b8e75e3dd7abd51ce2cb" + integrity sha512-xyYyVg6V1/SSOZWh4Ni3U129XHCnFHBTcUo0dhWtFDrZbNp/duw5AGsQfb2sVeU0gxWHXSY1+5F0jnKYC7WuOw== + dependencies: + react-native-is-edge-to-edge "^1.2.1" + +expo-video@~3.0.10: + version "3.0.16" + resolved "https://registry.yarnpkg.com/expo-video/-/expo-video-3.0.16.tgz#8160bd33fe2e898519d3c18a404567a30d81d4f2" + integrity sha512-H1HlxcHGomZItqisGfW3YL/G9BHtNBfVSimDJcLuyxyU87wFnV8loO9tCjuhufkfh/aTa2sW5BYAjLjg9DvnBQ== + +expo@~54.0.0: + version "54.0.34" + resolved "https://registry.yarnpkg.com/expo/-/expo-54.0.34.tgz#fb1c90ff9d65d58978198622808c66a2d3b66fcc" + integrity sha512-XkVHguZZDC8BcTQxHAd14/TQFbDp1Wt0Z/KApO9t68Ll5A127hLCPzU+a9gytfCIiyL/V1IpF1vIcOLKEVAoNQ== + dependencies: + "@babel/runtime" "^7.20.0" + "@expo/cli" "54.0.24" + "@expo/config" "~12.0.13" + "@expo/config-plugins" "~54.0.4" + "@expo/devtools" "0.1.8" + "@expo/fingerprint" "0.15.5" + "@expo/metro" "~54.2.0" + "@expo/metro-config" "54.0.15" + "@expo/vector-icons" "^15.0.3" + "@ungap/structured-clone" "^1.3.0" + babel-preset-expo "~54.0.10" + expo-asset "~12.0.13" + expo-constants "~18.0.13" + expo-file-system "~19.0.22" + expo-font "~14.0.11" + expo-keep-awake "~15.0.8" + expo-modules-autolinking "3.0.25" + expo-modules-core "3.0.30" + pretty-format "^29.7.0" + react-refresh "^0.14.2" + whatwg-url-without-unicode "8.0.0-3" + +exponential-backoff@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz#51cf92c1c0493c766053f9d3abee4434c244d2f6" + integrity sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA== + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + 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" + +fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-uri@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" + integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== + +fastq@^1.6.0: + version "1.20.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675" + integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== + dependencies: + reusify "^1.0.4" + +faye-websocket@0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +firebase@^12.10.0: + version "12.12.1" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-12.12.1.tgz#4c5145ce819509b1e547d27aef584ab719809d29" + integrity sha512-ee7xA+bTJLfjB9BP/8FQr3EkxmpAAGc1lNc5QkWgTDpUw24HYXFPm7FEWRdLtGnygxIdYpFmepSc5VjkI6NHhw== + dependencies: + "@firebase/ai" "2.11.1" + "@firebase/analytics" "0.10.21" + "@firebase/analytics-compat" "0.2.27" + "@firebase/app" "0.14.11" + "@firebase/app-check" "0.11.2" + "@firebase/app-check-compat" "0.4.2" + "@firebase/app-compat" "0.5.11" + "@firebase/app-types" "0.9.4" + "@firebase/auth" "1.13.0" + "@firebase/auth-compat" "0.6.5" + "@firebase/data-connect" "0.6.0" + "@firebase/database" "1.1.2" + "@firebase/database-compat" "2.1.3" + "@firebase/firestore" "4.14.0" + "@firebase/firestore-compat" "0.4.8" + "@firebase/functions" "0.13.3" + "@firebase/functions-compat" "0.4.3" + "@firebase/installations" "0.6.21" + "@firebase/installations-compat" "0.2.21" + "@firebase/messaging" "0.12.25" + "@firebase/messaging-compat" "0.2.25" + "@firebase/performance" "0.7.11" + "@firebase/performance-compat" "0.2.24" + "@firebase/remote-config" "0.8.2" + "@firebase/remote-config-compat" "0.2.23" + "@firebase/storage" "0.14.2" + "@firebase/storage-compat" "0.4.2" + "@firebase/util" "1.15.0" + +flow-enums-runtime@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz#5bb0cd1b0a3e471330f4d109039b7eba5cb3e787" + integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw== + +fontfaceobserver@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz#5fb392116e75d5024b7ec8e4f2ce92106d1488c8" + integrity sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg== + +freeport-async@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/freeport-async/-/freeport-async-2.0.0.tgz#6adf2ec0c629d11abff92836acd04b399135bab4" + integrity sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ== + +fresh@~0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +getenv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/getenv/-/getenv-2.0.0.tgz#b1698c7b0f29588f4577d06c42c73a5b475c69e0" + integrity sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^13.0.0: + version "13.0.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.6.tgz#078666566a425147ccacfbd2e332deb66a2be71d" + integrity sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw== + dependencies: + minimatch "^10.2.2" + minipass "^7.1.3" + path-scurry "^2.0.2" + +glob@^7.1.1, glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +hasown@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.3.tgz#5e5c2b15b60370a4c7930c383dfb76bf17bc403c" + integrity sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg== + dependencies: + function-bind "^1.1.2" + +hermes-estree@0.29.1: + version "0.29.1" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.29.1.tgz#043c7db076e0e8ef8c5f6ed23828d1ba463ebcc5" + integrity sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ== + +hermes-estree@0.32.0: + version "0.32.0" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.32.0.tgz#bb7da6613ab8e67e334a1854ea1e209f487d307b" + integrity sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ== + +hermes-estree@0.35.0: + version "0.35.0" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.35.0.tgz#767cce0b14a68b4bc06cd5db7efe889f6188c565" + integrity sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg== + +hermes-parser@0.29.1, hermes-parser@^0.29.1: + version "0.29.1" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.29.1.tgz#436b24bcd7bb1e71f92a04c396ccc0716c288d56" + integrity sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA== + dependencies: + hermes-estree "0.29.1" + +hermes-parser@0.32.0: + version "0.32.0" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.32.0.tgz#7916984ef6fdce62e7415d354cf35392061cd303" + integrity sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw== + dependencies: + hermes-estree "0.32.0" + +hermes-parser@0.35.0: + version "0.35.0" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.35.0.tgz#7625ec2f34ab897c2a17a7bea9788d136d5fd8c9" + integrity sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA== + dependencies: + hermes-estree "0.35.0" + +hoist-non-react-statics@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +hosted-git-info@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-7.0.2.tgz#9b751acac097757667f30114607ef7b661ff4f17" + integrity sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w== + dependencies: + lru-cache "^10.0.1" + +http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + +http-parser-js@>=0.5.1: + version "0.5.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075" + integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== + +https-proxy-agent@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + +idb@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" + integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +image-size@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.2.1.tgz#ee118aedfe666db1a6ee12bed5821cde3740276d" + integrity sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw== + dependencies: + queue "6.0.2" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@~2.0.3, inherits@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +is-arrayish@^0.3.1: + version "0.3.4" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.4.tgz#1ee5553818511915685d33bb13d31bf854e5059d" + integrity sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-wsl@^2.1.1, is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +istanbul-lib-coverage@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + dependencies: + "@jest/types" "^29.6.3" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + dependencies: + "@jest/types" "^29.6.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" + +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jimp-compact@0.16.1: + version "0.16.1" + resolved "https://registry.yarnpkg.com/jimp-compact/-/jimp-compact-0.16.1.tgz#9582aea06548a2c1e04dd148d7c3ab92075aefa3" + integrity sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww== + +jiti@^1.21.7: + version "1.21.7" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.7.tgz#9dd81043424a3d28458b193d965f0d18a2300ba9" + integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0" + integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + dependencies: + argparse "^2.0.1" + +jsc-safe-url@^0.2.2, jsc-safe-url@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz#141c14fbb43791e88d5dc64e85a374575a83477a" + integrity sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q== + +jsesc@^3.0.2, jsesc@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +lan-network@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/lan-network/-/lan-network-0.2.1.tgz#e4764a0d17f6bd1f2794c838fa219526a1b756f8" + integrity sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +lighthouse-logger@^1.0.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz#aef90f9e97cd81db367c7634292ee22079280aaa" + integrity sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g== + dependencies: + debug "^2.6.9" + marky "^1.2.2" + +lightningcss-android-arm64@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968" + integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg== + +lightningcss-darwin-arm64@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.27.0.tgz#565bd610533941cba648a70e105987578d82f996" + integrity sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ== + +lightningcss-darwin-arm64@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5" + integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ== + +lightningcss-darwin-x64@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.27.0.tgz#c906a267237b1c7fe08bff6c5ac032c099bc9482" + integrity sha512-0+mZa54IlcNAoQS9E0+niovhyjjQWEMrwW0p2sSdLRhLDc8LMQ/b67z7+B5q4VmjYCMSfnFi3djAAQFIDuj/Tg== + +lightningcss-darwin-x64@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e" + integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w== + +lightningcss-freebsd-x64@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.27.0.tgz#a7c3c4d6ee18dffeb8fa69f14f8f9267f7dc0c34" + integrity sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA== + +lightningcss-freebsd-x64@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575" + integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig== + +lightningcss-linux-arm-gnueabihf@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.27.0.tgz#c7c16432a571ec877bf734fe500e4a43d48c2814" + integrity sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA== + +lightningcss-linux-arm-gnueabihf@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d" + integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw== + +lightningcss-linux-arm64-gnu@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.27.0.tgz#cfd9e18df1cd65131da286ddacfa3aee6862a752" + integrity sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A== + +lightningcss-linux-arm64-gnu@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335" + integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ== + +lightningcss-linux-arm64-musl@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.27.0.tgz#6682ff6b9165acef9a6796bd9127a8e1247bb0ed" + integrity sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg== + +lightningcss-linux-arm64-musl@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133" + integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg== + +lightningcss-linux-x64-gnu@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.27.0.tgz#714221212ad184ddfe974bbb7dbe9300dfde4bc0" + integrity sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A== + +lightningcss-linux-x64-gnu@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz#0b7803af4eb21cfd38dd39fe2abbb53c7dd091f6" + integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA== + +lightningcss-linux-x64-musl@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.27.0.tgz#247958daf622a030a6dc2285afa16b7184bdf21e" + integrity sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA== + +lightningcss-linux-x64-musl@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz#88dc8ba865ddddb1ac5ef04b0f161804418c163b" + integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg== + +lightningcss-win32-arm64-msvc@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.27.0.tgz#64cfe473c264ef5dc275a4d57a516d77fcac6bc9" + integrity sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ== + +lightningcss-win32-arm64-msvc@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38" + integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw== + +lightningcss-win32-x64-msvc@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.27.0.tgz#237d0dc87d9cdc9cf82536bcbc07426fa9f3f422" + integrity sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw== + +lightningcss-win32-x64-msvc@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a" + integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q== + +lightningcss@^1.30.1: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9" + integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-android-arm64 "1.32.0" + lightningcss-darwin-arm64 "1.32.0" + lightningcss-darwin-x64 "1.32.0" + lightningcss-freebsd-x64 "1.32.0" + lightningcss-linux-arm-gnueabihf "1.32.0" + lightningcss-linux-arm64-gnu "1.32.0" + lightningcss-linux-arm64-musl "1.32.0" + lightningcss-linux-x64-gnu "1.32.0" + lightningcss-linux-x64-musl "1.32.0" + lightningcss-win32-arm64-msvc "1.32.0" + lightningcss-win32-x64-msvc "1.32.0" + +lightningcss@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.27.0.tgz#d4608e63044343836dd9769f6c8b5d607867649a" + integrity sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ== + dependencies: + detect-libc "^1.0.3" + optionalDependencies: + lightningcss-darwin-arm64 "1.27.0" + lightningcss-darwin-x64 "1.27.0" + lightningcss-freebsd-x64 "1.27.0" + lightningcss-linux-arm-gnueabihf "1.27.0" + lightningcss-linux-arm64-gnu "1.27.0" + lightningcss-linux-arm64-musl "1.27.0" + lightningcss-linux-x64-gnu "1.27.0" + lightningcss-linux-x64-musl "1.27.0" + lightningcss-win32-arm64-msvc "1.27.0" + lightningcss-win32-x64-msvc "1.27.0" + +lilconfig@^3.1.1, lilconfig@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" + integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== + +log-symbols@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" + integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== + dependencies: + chalk "^2.0.1" + +long@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== + +loose-envify@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^10.0.1: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^11.0.0: + version "11.3.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.3.5.tgz#29047d348c0b2793e3112a01c739bb7c6d855637" + integrity sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lucide-react-native@^0.575.0: + version "0.575.0" + resolved "https://registry.yarnpkg.com/lucide-react-native/-/lucide-react-native-0.575.0.tgz#8ce8e555d7c0ebb88cd529966256f803125b74ce" + integrity sha512-kdGcjF4Rm1YKuNs3IaW5lDAqVKn9RBj1Fmjt3JBr08PMIXpVV7iL0ICNF/awiPZQicHlx/v9xgyZZS4TAFxDNg== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +marky@^1.2.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/marky/-/marky-1.3.0.tgz#422b63b0baf65022f02eda61a238eccdbbc14997" + integrity sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ== + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +memoize-one@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + +merge-options@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" + integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== + dependencies: + is-plain-obj "^2.1.0" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +metro-babel-transformer@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.83.3.tgz#d8c134615530c9ee61364526d44ca4bb0c5343ea" + integrity sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g== + dependencies: + "@babel/core" "^7.25.2" + flow-enums-runtime "^0.0.6" + hermes-parser "0.32.0" + nullthrows "^1.1.1" + +metro-babel-transformer@0.83.7: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.83.7.tgz#8448c7a550571de87d00e97c1a7139c6b6900e4a" + integrity sha512-sBqBkt6kNut/88bv+Ucvm4yqdPetbvAEsHzi3MAgJEifOSYYzX5Z5Kgw3TFOrwf/mHJTOBG2ONlaMHoyfP15TA== + dependencies: + "@babel/core" "^7.25.2" + flow-enums-runtime "^0.0.6" + hermes-parser "0.35.0" + metro-cache-key "0.83.7" + nullthrows "^1.1.1" + +metro-cache-key@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.83.3.tgz#ae6c5d4eb1ad8d06a92bf7294ca730a8d880b573" + integrity sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw== + dependencies: + flow-enums-runtime "^0.0.6" + +metro-cache-key@0.83.7: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.83.7.tgz#59e647cf6dd6297e43d0bd7ab927db48c6ba80b5" + integrity sha512-W1c2Nmx8MiJTJt+eWhMO08z9VKi3kZOaz99IYGdqeqDgY9j+yZjXl62rUav4Di0heZfh4/n2s722PqRL1OODeg== + dependencies: + flow-enums-runtime "^0.0.6" + +metro-cache@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.83.3.tgz#f1245cc48570c47d8944495e61d67f0228f10172" + integrity sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q== + dependencies: + exponential-backoff "^3.1.1" + flow-enums-runtime "^0.0.6" + https-proxy-agent "^7.0.5" + metro-core "0.83.3" + +metro-cache@0.83.7: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.83.7.tgz#73ab7857ba6267b78f6374396829d660a67deccf" + integrity sha512-E9SRePXQ1Zvlj79VcOk57q7VC7rMHMFQ+jhmPHBiq+dJ0bJB5BL87lWZF6oh5X76Cci5tpDuQNaDwwuSCToEeg== + dependencies: + exponential-backoff "^3.1.1" + flow-enums-runtime "^0.0.6" + https-proxy-agent "^7.0.5" + metro-core "0.83.7" + +metro-config@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.83.3.tgz#a30e7a69b5cf8c4ac4c4b68b1b4c33649ae129a2" + integrity sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA== + dependencies: + connect "^3.6.5" + flow-enums-runtime "^0.0.6" + jest-validate "^29.7.0" + metro "0.83.3" + metro-cache "0.83.3" + metro-core "0.83.3" + metro-runtime "0.83.3" + yaml "^2.6.1" + +metro-config@0.83.7, metro-config@^0.83.1: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.83.7.tgz#fc88f4f75992744d6d64bf27c6a2f11b10e9fcfb" + integrity sha512-83mjWFbFOt2GeJ6pFIum5mSnc1uTsZJAtD8o4ej0s4NVsYsA7fB+pHvTfHhFrpeMONaobu2riKavkPei05Er/Q== + dependencies: + connect "^3.6.5" + flow-enums-runtime "^0.0.6" + jest-validate "^29.7.0" + metro "0.83.7" + metro-cache "0.83.7" + metro-core "0.83.7" + metro-runtime "0.83.7" + yaml "^2.6.1" + +metro-core@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.83.3.tgz#007e93f7d1983777da8988dfb103ad897c9835b8" + integrity sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw== + dependencies: + flow-enums-runtime "^0.0.6" + lodash.throttle "^4.1.1" + metro-resolver "0.83.3" + +metro-core@0.83.7, metro-core@^0.83.1: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.83.7.tgz#45cc9eebd979c75021015f190dbd023e833bdd16" + integrity sha512-6yn3w1wnltT6RQl7p7YES2l95ArC+mWrOssEiH8p5/DDrJS65/szf9LsC9JrBv8c5DdvSY3V3f0GRYg0Ox7hCg== + dependencies: + flow-enums-runtime "^0.0.6" + lodash.throttle "^4.1.1" + metro-resolver "0.83.7" + +metro-file-map@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.83.3.tgz#3d79fbb1d379ab178dd895ce54cb5ecb183d74a2" + integrity sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA== + dependencies: + debug "^4.4.0" + fb-watchman "^2.0.0" + flow-enums-runtime "^0.0.6" + graceful-fs "^4.2.4" + invariant "^2.2.4" + jest-worker "^29.7.0" + micromatch "^4.0.4" + nullthrows "^1.1.1" + walker "^1.0.7" + +metro-file-map@0.83.7: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.83.7.tgz#1d0d47db8a76631f0fed2112edad5fec462aec50" + integrity sha512-+j0F1m+FQYVAQ6syf+mwhIPV5GoFQrkInX8bppuc50IzNsZbMrp8R5H/Sx/K2daQ3YEa9F/XwkeZT8gzJfgeCw== + dependencies: + debug "^4.4.0" + fb-watchman "^2.0.0" + flow-enums-runtime "^0.0.6" + graceful-fs "^4.2.4" + invariant "^2.2.4" + jest-worker "^29.7.0" + micromatch "^4.0.4" + nullthrows "^1.1.1" + walker "^1.0.7" + +metro-minify-terser@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.83.3.tgz#c1c70929c86b14c8bf03e6321b4f9310bc8dbe87" + integrity sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ== + dependencies: + flow-enums-runtime "^0.0.6" + terser "^5.15.0" + +metro-minify-terser@0.83.7: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.83.7.tgz#2de0b70f8cd58e9383b014313a1d9e7babe8d878" + integrity sha512-MfJar2IS4tBRuLb9svwb0Gu5l9BsH+pcRm8eGcEi/wy8MzZinfinh5dFLt2nWkocnulIgtGB5NkFDdbXqMXKhQ== + dependencies: + flow-enums-runtime "^0.0.6" + terser "^5.15.0" + +metro-resolver@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.83.3.tgz#06207bdddc280b9335722a8c992aeec017413942" + integrity sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ== + dependencies: + flow-enums-runtime "^0.0.6" + +metro-resolver@0.83.7: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.83.7.tgz#84b2e2749f0dba0e1c204764d01c21892157cf1c" + integrity sha512-WSJIENlMcoSsuz66IfBHOkgfp3KJt2UW2TnEHPf1b8pIG2eEXNOVmo2+03A0H17WY2XGXWgxL0CG7FAopqgB1A== + dependencies: + flow-enums-runtime "^0.0.6" + +metro-runtime@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.83.3.tgz#ff504df5d93f38b1af396715b327e589ba8d184d" + integrity sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw== + dependencies: + "@babel/runtime" "^7.25.0" + flow-enums-runtime "^0.0.6" + +metro-runtime@0.83.7, metro-runtime@^0.83.1: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.83.7.tgz#3606a71c94a4ef862b7a0e43156b35f7a4e4cf17" + integrity sha512-9GKkJURaB2iyYoEExKnedzAHzxmKtSi+k0tsZUvMoU27tBZJElchYt7JH/Ai/XzYAI9lCAaV7u5HZSI8J5Z+wQ== + dependencies: + "@babel/runtime" "^7.25.0" + flow-enums-runtime "^0.0.6" + +metro-source-map@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.83.3.tgz#04bb464f7928ea48bcdfd18912c8607cf317c898" + integrity sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg== + dependencies: + "@babel/traverse" "^7.25.3" + "@babel/traverse--for-generate-function-map" "npm:@babel/traverse@^7.25.3" + "@babel/types" "^7.25.2" + flow-enums-runtime "^0.0.6" + invariant "^2.2.4" + metro-symbolicate "0.83.3" + nullthrows "^1.1.1" + ob1 "0.83.3" + source-map "^0.5.6" + vlq "^1.0.0" + +metro-source-map@0.83.7, metro-source-map@^0.83.1: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.83.7.tgz#6208e72427c987e66a9ec23ebaee0b3cc76dd16c" + integrity sha512-JgA1h7oc1a1jydBe1GhVFsUoMYo3wLPk7oRA32rjlDsq+sP2JLt9x2p2lWbNSxTm/u8NV4VRid3hvEJgcX8tKw== + dependencies: + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + flow-enums-runtime "^0.0.6" + invariant "^2.2.4" + metro-symbolicate "0.83.7" + nullthrows "^1.1.1" + ob1 "0.83.7" + source-map "^0.5.6" + vlq "^1.0.0" + +metro-symbolicate@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.83.3.tgz#67af03950f0dfe19a7c059e3983e39a31e95d03a" + integrity sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw== + dependencies: + flow-enums-runtime "^0.0.6" + invariant "^2.2.4" + metro-source-map "0.83.3" + nullthrows "^1.1.1" + source-map "^0.5.6" + vlq "^1.0.0" + +metro-symbolicate@0.83.7: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.83.7.tgz#ee10cfbafb5ed5ae5f04a565c496fd772afb3f5c" + integrity sha512-g4suyxw20WOHWI680c+Kq4wC/NF+Hx5pRH9afrMp+sMTxqLeKcPR1Xf4wMhsjlbvx7LbIREdke6q928jEjvJWw== + dependencies: + flow-enums-runtime "^0.0.6" + invariant "^2.2.4" + metro-source-map "0.83.7" + nullthrows "^1.1.1" + source-map "^0.5.6" + vlq "^1.0.0" + +metro-transform-plugins@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.83.3.tgz#2c59ba841e269363cf3acb13138cb992f0c75013" + integrity sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A== + dependencies: + "@babel/core" "^7.25.2" + "@babel/generator" "^7.25.0" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.3" + flow-enums-runtime "^0.0.6" + nullthrows "^1.1.1" + +metro-transform-plugins@0.83.7: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.83.7.tgz#9fc9b4c209ca2fdc1e720a9fefde7c20f32ab5ad" + integrity sha512-Ss0FpBiZDjX2kwhukMDl5sNdYK8T/06IPqxNE4H6PTlRlfs9q11cef13c/xESY/Pm4VCkp1yJUZO3kXzvMxQFA== + dependencies: + "@babel/core" "^7.25.2" + "@babel/generator" "^7.29.1" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + flow-enums-runtime "^0.0.6" + nullthrows "^1.1.1" + +metro-transform-worker@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.83.3.tgz#ca6ae4a02b0f61b33299e6e56bacaba32dcd607f" + integrity sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA== + dependencies: + "@babel/core" "^7.25.2" + "@babel/generator" "^7.25.0" + "@babel/parser" "^7.25.3" + "@babel/types" "^7.25.2" + flow-enums-runtime "^0.0.6" + metro "0.83.3" + metro-babel-transformer "0.83.3" + metro-cache "0.83.3" + metro-cache-key "0.83.3" + metro-minify-terser "0.83.3" + metro-source-map "0.83.3" + metro-transform-plugins "0.83.3" + nullthrows "^1.1.1" + +metro-transform-worker@0.83.7: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.83.7.tgz#1ba8980f660630f7dfbf52fcf2b7bd6e4ae4528b" + integrity sha512-UegCo7ygB2fT64mRK2nbAjQVJ1zSwIIHy8d96jJv2nKZFDaViYBiughEdu5HM/Ceq0WN3LZrZk3zhl9aoiLYFw== + dependencies: + "@babel/core" "^7.25.2" + "@babel/generator" "^7.29.1" + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" + flow-enums-runtime "^0.0.6" + metro "0.83.7" + metro-babel-transformer "0.83.7" + metro-cache "0.83.7" + metro-cache-key "0.83.7" + metro-minify-terser "0.83.7" + metro-source-map "0.83.7" + metro-transform-plugins "0.83.7" + nullthrows "^1.1.1" + +metro@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/metro/-/metro-0.83.3.tgz#1e7e04c15519af746f8932c7f9c553d92c39e922" + integrity sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/core" "^7.25.2" + "@babel/generator" "^7.25.0" + "@babel/parser" "^7.25.3" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.3" + "@babel/types" "^7.25.2" + accepts "^1.3.7" + chalk "^4.0.0" + ci-info "^2.0.0" + connect "^3.6.5" + debug "^4.4.0" + error-stack-parser "^2.0.6" + flow-enums-runtime "^0.0.6" + graceful-fs "^4.2.4" + hermes-parser "0.32.0" + image-size "^1.0.2" + invariant "^2.2.4" + jest-worker "^29.7.0" + jsc-safe-url "^0.2.2" + lodash.throttle "^4.1.1" + metro-babel-transformer "0.83.3" + metro-cache "0.83.3" + metro-cache-key "0.83.3" + metro-config "0.83.3" + metro-core "0.83.3" + metro-file-map "0.83.3" + metro-resolver "0.83.3" + metro-runtime "0.83.3" + metro-source-map "0.83.3" + metro-symbolicate "0.83.3" + metro-transform-plugins "0.83.3" + metro-transform-worker "0.83.3" + mime-types "^2.1.27" + nullthrows "^1.1.1" + serialize-error "^2.1.0" + source-map "^0.5.6" + throat "^5.0.0" + ws "^7.5.10" + yargs "^17.6.2" + +metro@0.83.7, metro@^0.83.1: + version "0.83.7" + resolved "https://registry.yarnpkg.com/metro/-/metro-0.83.7.tgz#8357495dfea9e11d34ea15c0a1e2acd508ee5559" + integrity sha512-SPaPEyvTsTmd0LpT7RaZciQyDw2i/JB7+iY9L5VfBo72+psescFxBqpI1TL9dnL+pmnfkU+l/J1mEEGLeF65EQ== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/core" "^7.25.2" + "@babel/generator" "^7.29.1" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + accepts "^2.0.0" + ci-info "^2.0.0" + connect "^3.6.5" + debug "^4.4.0" + error-stack-parser "^2.0.6" + flow-enums-runtime "^0.0.6" + graceful-fs "^4.2.4" + hermes-parser "0.35.0" + image-size "^1.0.2" + invariant "^2.2.4" + jest-worker "^29.7.0" + jsc-safe-url "^0.2.2" + lodash.throttle "^4.1.1" + metro-babel-transformer "0.83.7" + metro-cache "0.83.7" + metro-cache-key "0.83.7" + metro-config "0.83.7" + metro-core "0.83.7" + metro-file-map "0.83.7" + metro-resolver "0.83.7" + metro-runtime "0.83.7" + metro-source-map "0.83.7" + metro-symbolicate "0.83.7" + metro-transform-plugins "0.83.7" + metro-transform-worker "0.83.7" + mime-types "^3.0.1" + nullthrows "^1.1.1" + serialize-error "^2.1.0" + source-map "^0.5.6" + throat "^5.0.0" + ws "^7.5.10" + yargs "^17.6.2" + +micromatch@^4.0.4, micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2", mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^2.1.27, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime-types@^3.0.0, mime-types@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + dependencies: + mime-db "^1.54.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + +minimatch@^10.2.2: + version "10.2.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1" + integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== + dependencies: + brace-expansion "^5.0.5" + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.0: + version "9.0.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e" + integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== + dependencies: + brace-expansion "^2.0.2" + +minimist@^1.2.0: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^7.0.4, minipass@^7.1.2, minipass@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b" + integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== + +minizlib@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.1.0.tgz#6ad76c3a8f10227c9b51d1c9ac8e30b27f5a251c" + integrity sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw== + dependencies: + minipass "^7.1.2" + +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nanoid@^3.3.11, nanoid@^3.3.7: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +nativewind@^4.1.23: + version "4.2.3" + resolved "https://registry.yarnpkg.com/nativewind/-/nativewind-4.2.3.tgz#ad7880bd2b5ac55f041d34918b9c67b00340088c" + integrity sha512-HglF1v6A8CqBFpXWs0d3yf4qQGurrreLuyE8FTRI/VDH8b0npZa2SDG5tviTkLiBg0s5j09mQALZOjxuocgMLA== + dependencies: + comment-json "^4.2.5" + debug "^4.3.7" + react-native-css-interop "0.2.3" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + +negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + +nested-error-stacks@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz#d2cc9fc5235ddb371fc44d506234339c8e4b0a4b" + integrity sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A== + +node-forge@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.4.0.tgz#1c7b7d8bdc2d078739f58287d589d903a11b2fc2" + integrity sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.36: + version "2.0.38" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.38.tgz#791569b9e4424a044e12c3abfad418ed83ce9947" + integrity sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-package-arg@^11.0.0: + version "11.0.3" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-11.0.3.tgz#dae0c21199a99feca39ee4bfb074df3adac87e2d" + integrity sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw== + dependencies: + hosted-git-info "^7.0.0" + proc-log "^4.0.0" + semver "^7.3.5" + validate-npm-package-name "^5.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +nullthrows@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" + integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== + +ob1@0.83.3: + version "0.83.3" + resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.83.3.tgz#2208e20c9070e9beff3ad067f2db458fa6b07014" + integrity sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA== + dependencies: + flow-enums-runtime "^0.0.6" + +ob1@0.83.7: + version "0.83.7" + resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.83.7.tgz#0f9a9461ee3c3048eacd40893fee3385d42d8731" + integrity sha512-9M5kpuOLyTPogMtZiQUIxdAZxl7Dxs6tVBbJErSumsqGMuhVSoUbkfeZ3XNPpLpwBBtqY5QDUzGwggLHX3slQg== + dependencies: + flow-enums-runtime "^0.0.6" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-finished@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ== + dependencies: + mimic-fn "^1.0.0" + +open@^7.0.3: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + +open@^8.0.4: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +ora@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318" + integrity sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg== + dependencies: + chalk "^2.4.2" + cli-cursor "^2.1.0" + cli-spinners "^2.0.0" + log-symbols "^2.2.0" + strip-ansi "^5.2.0" + wcwidth "^1.0.1" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parse-png@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/parse-png/-/parse-png-2.1.0.tgz#2a42ad719fedf90f81c59ebee7ae59b280d6b338" + integrity sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ== + dependencies: + pngjs "^3.3.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.5, path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.2.tgz#6be0d0ee02a10d9e0de7a98bae65e182c9061f85" + integrity sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + +picocolors@^1.0.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" + integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== + +picomatch@^4.0.3, picomatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" + integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pirates@^4.0.1, pirates@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" + integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== + +plist@^3.0.5: + version "3.1.1" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.1.tgz#fa6099e1e3cf6ea180258ebe6378ea3878c2c841" + integrity sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA== + dependencies: + "@xmldom/xmldom" "^0.9.10" + base64-js "^1.5.1" + xmlbuilder "^15.1.1" + +pngjs@^3.3.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" + integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== + +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.1.0.tgz#003b63c6edde948766e40f3daf7e997ae43a5ce6" + integrity sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw== + dependencies: + camelcase-css "^2.0.1" + +"postcss-load-config@^4.0.2 || ^5.0 || ^6.0": + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz#6fd7dcd8ae89badcf1b2d644489cbabf83aa8096" + integrity sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g== + dependencies: + lilconfig "^3.1.1" + +postcss-nested@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" + integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== + dependencies: + postcss-selector-parser "^6.1.1" + +postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.47: + version "8.5.12" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.12.tgz#cd0c0f667f7cb0521e2313234ea6e707a9ec1ddb" + integrity sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +postcss@~8.4.32: + version "8.4.49" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" + integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +proc-log@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-4.2.0.tgz#b6f461e4026e75fdfe228b265e9f7a00779d7034" + integrity sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA== + +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +promise@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a" + integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg== + dependencies: + asap "~2.0.6" + +prompts@^2.3.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +protobufjs@^7.2.5: + version "7.5.6" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.6.tgz#11af832ebc4b4326f658a5b1308e6141eb57edfd" + integrity sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.5" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.1" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.1" + "@types/node" ">=13.7.0" + long "^5.0.0" + +punycode@^2.1.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qrcode-terminal@0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz#ffc6c28a2fc0bfb47052b47e23f4f446a5fbdb9e" + integrity sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ== + +query-string@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" + integrity sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg== + dependencies: + decode-uri-component "^0.2.2" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +queue@6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" + integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== + dependencies: + inherits "~2.0.3" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +rc@~1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-devtools-core@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-6.1.5.tgz#c5eca79209dab853a03b2158c034c5166975feee" + integrity sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA== + dependencies: + shell-quote "^1.6.1" + ws "^7" + +react-freeze@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad" + integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA== + +react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^18.0.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +react-is@^19.1.0: + version "19.2.5" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.5.tgz#7e7b54143e9313fed787b23fd4295d5a23872ad9" + integrity sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ== + +react-native-css-interop@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/react-native-css-interop/-/react-native-css-interop-0.2.3.tgz#9efef674deffd8a0c70ddc9cedd1fc40c043e61b" + integrity sha512-wc+JI7iUfdFBqnE18HhMTtD0q9vkhuMczToA87UdHGWwMyxdT5sCcNy+i4KInPCE855IY0Ic8kLQqecAIBWz7w== + dependencies: + "@babel/helper-module-imports" "^7.22.15" + "@babel/traverse" "^7.23.0" + "@babel/types" "^7.23.0" + debug "^4.3.7" + lightningcss "~1.27.0" + semver "^7.6.3" + +react-native-gesture-handler@~2.28.0: + version "2.28.0" + resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz#07fb4f5eae72f810aac3019b060d26c1835bfd0c" + integrity sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A== + dependencies: + "@egjs/hammerjs" "^2.0.17" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + +react-native-is-edge-to-edge@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz#feb9a6a8faf0874298947edd556e5af22044e139" + integrity sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA== + +react-native-reanimated@~4.1.1: + version "4.1.7" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-4.1.7.tgz#b4e8524503a1b6ec1b5a40c460ee807a6a9fd2cf" + integrity sha512-Q4H6xA3Tn7QL0/E/KjI86I1KK4tcf+ErRE04LH34Etka2oVQhW6oXQ+Q8ZcDCVxiWp5vgbBH6XcH8BOo4w/Rhg== + dependencies: + react-native-is-edge-to-edge "^1.2.1" + semver "^7.7.2" + +react-native-safe-area-context@~5.6.0: + version "5.6.2" + resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz#283e006f5b434fb247fcb4be0971ad7473d5c560" + integrity sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg== + +react-native-screens@~4.16.0: + version "4.16.0" + resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.16.0.tgz#efa42e77a092aa0b5277c9ae41391ea0240e0870" + integrity sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q== + dependencies: + react-freeze "^1.0.0" + react-native-is-edge-to-edge "^1.2.1" + warn-once "^0.1.0" + +react-native-svg@15.12.1: + version "15.12.1" + resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.12.1.tgz#7ba756dd6a235f86a2c312a1e7911f9b0d18ad3a" + integrity sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g== + dependencies: + css-select "^5.1.0" + css-tree "^1.1.3" + warn-once "0.1.1" + +react-native-worklets@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.5.1.tgz#d153242655e3757b6c62a474768831157316ad33" + integrity sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w== + dependencies: + "@babel/plugin-transform-arrow-functions" "^7.0.0-0" + "@babel/plugin-transform-class-properties" "^7.0.0-0" + "@babel/plugin-transform-classes" "^7.0.0-0" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.0.0-0" + "@babel/plugin-transform-optional-chaining" "^7.0.0-0" + "@babel/plugin-transform-shorthand-properties" "^7.0.0-0" + "@babel/plugin-transform-template-literals" "^7.0.0-0" + "@babel/plugin-transform-unicode-regex" "^7.0.0-0" + "@babel/preset-typescript" "^7.16.7" + convert-source-map "^2.0.0" + semver "7.7.2" + +react-native@0.81.5: + version "0.81.5" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.81.5.tgz#6c963f137d3979b22aef2d8482067775c8fe2fed" + integrity sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw== + dependencies: + "@jest/create-cache-key-function" "^29.7.0" + "@react-native/assets-registry" "0.81.5" + "@react-native/codegen" "0.81.5" + "@react-native/community-cli-plugin" "0.81.5" + "@react-native/gradle-plugin" "0.81.5" + "@react-native/js-polyfills" "0.81.5" + "@react-native/normalize-colors" "0.81.5" + "@react-native/virtualized-lists" "0.81.5" + abort-controller "^3.0.0" + anser "^1.4.9" + ansi-regex "^5.0.0" + babel-jest "^29.7.0" + babel-plugin-syntax-hermes-parser "0.29.1" + base64-js "^1.5.1" + commander "^12.0.0" + flow-enums-runtime "^0.0.6" + glob "^7.1.1" + invariant "^2.2.4" + jest-environment-node "^29.7.0" + memoize-one "^5.0.0" + metro-runtime "^0.83.1" + metro-source-map "^0.83.1" + nullthrows "^1.1.1" + pretty-format "^29.7.0" + promise "^8.3.0" + react-devtools-core "^6.1.5" + react-refresh "^0.14.0" + regenerator-runtime "^0.13.2" + scheduler "0.26.0" + semver "^7.1.3" + stacktrace-parser "^0.1.10" + whatwg-fetch "^3.0.0" + ws "^6.2.3" + yargs "^17.6.2" + +react-refresh@^0.14.0, react-refresh@^0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" + integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== + +react@19.1.0: + version "19.1.0" + resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75" + integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regenerate-unicode-properties@^10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz#aa113812ba899b630658c7623466be71e1f86f66" + integrity sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.13.2: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regexpu-core@^6.3.1: + version "6.4.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.4.0.tgz#3580ce0c4faedef599eccb146612436b62a176e5" + integrity sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.2" + regjsgen "^0.8.0" + regjsparser "^0.13.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.2.1" + +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.13.0: + version "0.13.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.13.1.tgz#0593cbacb27527927692030928ae4d3b878d6f8d" + integrity sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw== + dependencies: + jsesc "~3.1.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requireg@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/requireg/-/requireg-0.2.2.tgz#437e77a5316a54c9bcdbbf5d1f755fe093089830" + integrity sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg== + dependencies: + nested-error-stacks "~2.0.1" + rc "~1.2.7" + resolve "~1.7.1" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-workspace-root@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/resolve-workspace-root/-/resolve-workspace-root-2.0.1.tgz#9cbbf8321ebccaaf0e4ffea5274aa26b611ccd62" + integrity sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w== + +resolve.exports@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.3.tgz#41955e6f1b4013b7586f873749a635dea07ebe3f" + integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== + +resolve@^1.1.7, resolve@^1.22.11, resolve@^1.22.2, resolve@^1.22.8: + version "1.22.12" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f" + integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@~1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" + integrity sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw== + dependencies: + path-parse "^1.0.5" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q== + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@5.2.1, safe-buffer@>=5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +sax@>=0.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.6.0.tgz#da59637629307b97e7c4cb28e080a7bc38560d5b" + integrity sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA== + +scheduler@0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337" + integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA== + +semver@7.7.2: + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + +semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.1.3, semver@^7.3.5, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3, semver@^7.7.2: + version "7.7.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + +send@^0.19.0, send@~0.19.1: + version "0.19.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29" + integrity sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "~0.5.2" + http-errors "~2.0.1" + mime "1.6.0" + ms "2.1.3" + on-finished "~2.4.1" + range-parser "~1.2.1" + statuses "~2.0.2" + +serialize-error@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" + integrity sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw== + +serve-static@^1.16.2: + version "1.16.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9" + integrity sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "~0.19.1" + +setprototypeof@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +sf-symbols-typescript@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz#926d6e0715e3d8784cadf7658431e36581254208" + integrity sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.6.1: + version "1.8.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" + integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== + +signal-exit@^3.0.2, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +simple-plist@^1.1.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/simple-plist/-/simple-plist-1.3.1.tgz#16e1d8f62c6c9b691b8383127663d834112fb017" + integrity sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw== + dependencies: + bplist-creator "0.1.0" + bplist-parser "0.3.1" + plist "^3.0.5" + +simple-swizzle@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz#a8d11a45a11600d6a1ecdff6363329e3648c3667" + integrity sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw== + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slugify@^1.3.4, slugify@^1.6.6: + version "1.6.9" + resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.9.tgz#610957dea21e56b65e3a153215ef7b265715c8e8" + integrity sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg== + +sonner-native@^0.21.0: + version "0.21.2" + resolved "https://registry.yarnpkg.com/sonner-native/-/sonner-native-0.21.2.tgz#dec1b214cf91e54f750737e784226651ba1d08f2" + integrity sha512-LnGPmfgzrNIwcc+FvcLJqx8aH1dEHePRzvNR8aIR4kl9spySRkXK160GmQIazjfm6mSMlPqZwRa5eycvrzg/eQ== + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-support@~0.5.20, source-map-support@~0.5.21: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + +stacktrace-parser@^0.1.10: + version "0.1.11" + resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz#c7c08f9b29ef566b9a6f7b255d7db572f66fabc4" + integrity sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg== + dependencies: + type-fest "^0.7.1" + +statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + +stream-buffers@2.2.x: + version "2.2.0" + resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4" + integrity sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg== + +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +structured-headers@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/structured-headers/-/structured-headers-0.4.1.tgz#77abd9410622c6926261c09b9d16cf10592694d1" + integrity sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg== + +sucrase@^3.35.0, sucrase@~3.35.1: + version "3.35.1" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.1.tgz#4619ea50393fe8bd0ae5071c26abd9b2e346bfe1" + integrity sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.2" + commander "^4.0.0" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + tinyglobby "^0.2.11" + ts-interface-checker "^0.1.9" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-hyperlinks@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" + integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tailwind-merge@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz#06502f4496ba15151445d97d916a26564d50d1ca" + integrity sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A== + +tailwindcss@^3.4.17: + version "3.4.19" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.19.tgz#af2a0a4ae302d52ebe078b6775e799e132500ee2" + integrity sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ== + dependencies: + "@alloc/quick-lru" "^5.2.0" + arg "^5.0.2" + chokidar "^3.6.0" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.3.2" + glob-parent "^6.0.2" + is-glob "^4.0.3" + jiti "^1.21.7" + lilconfig "^3.1.3" + micromatch "^4.0.8" + normalize-path "^3.0.0" + object-hash "^3.0.0" + picocolors "^1.1.1" + postcss "^8.4.47" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.2 || ^5.0 || ^6.0" + postcss-nested "^6.2.0" + postcss-selector-parser "^6.1.2" + resolve "^1.22.8" + sucrase "^3.35.0" + +tar@^7.5.2: + version "7.5.13" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.13.tgz#0d214ed56781a26edc313581c0e2d929ceeb866d" + integrity sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng== + dependencies: + "@isaacs/fs-minipass" "^4.0.0" + chownr "^3.0.0" + minipass "^7.1.2" + minizlib "^3.1.0" + yallist "^5.0.0" + +terminal-link@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + +terser@^5.15.0: + version "5.46.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.46.2.tgz#b9529672d5b0024c7959571c83b82f65077b2a4f" + integrity sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.15.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +throat@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" + integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== + +tinyglobby@^0.2.11: + version "0.2.16" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" + integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +tslib@^2.1.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" + integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== + +typescript@~5.9.0: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +undici-types@~7.19.0: + version "7.19.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.19.2.tgz#1b67fc26d0f157a0cba3a58a5b5c1e2276b8ba2a" + integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg== + +undici@^6.18.2: + version "6.25.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.25.0.tgz#8c4efb8c998dc187fc1cfb5dde1ef19a211849fb" + integrity sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz#65a7adfad8574c219890e219285ce4c64ed67eaa" + integrity sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz#301d4f8a43d2b75c97adfad87c9dd5350c9475d1" + integrity sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ== + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" + integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +use-latest-callback@^0.2.4: + version "0.2.6" + resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.2.6.tgz#e5ea752808c86219acc179ace0ae3c1203255e77" + integrity sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg== + +use-sync-external-store@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" + integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== + +validate-npm-package-name@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz#a316573e9b49f3ccd90dbb6eb52b3f06c6d604e8" + integrity sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vlq@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468" + integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w== + +walker@^1.0.7, walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +warn-once@0.1.1, warn-once@^0.1.0, warn-once@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/warn-once/-/warn-once-0.1.1.tgz#952088f4fb56896e73fd4e6a3767272a3fccce43" + integrity sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q== + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +web-vitals@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.4.tgz#1d20bc8590a37769bd0902b289550936069184b7" + integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw== + +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + +websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-fetch@^3.0.0: + version "3.6.20" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" + integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== + +whatwg-url-without-unicode@8.0.0-3: + version "8.0.0-3" + resolved "https://registry.yarnpkg.com/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz#ab6df4bf6caaa6c85a59f6e82c026151d4bb376b" + integrity sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig== + dependencies: + buffer "^5.4.3" + punycode "^2.1.1" + webidl-conversions "^5.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wonka@^6.3.2: + version "6.3.6" + resolved "https://registry.yarnpkg.com/wonka/-/wonka-6.3.6.tgz#a70e2e54ed6aaa8e20bb57d916166cdc3d385f2d" + integrity sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +ws@^6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.3.tgz#ccc96e4add5fd6fedbc491903075c85c5a11d9ee" + integrity sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA== + dependencies: + async-limiter "~1.0.0" + +ws@^7, ws@^7.5.10: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + +ws@^8.12.1: + version "8.20.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.20.0.tgz#4cd9532358eba60bc863aad1623dfb045a4d4af8" + integrity sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA== + +xcode@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/xcode/-/xcode-3.0.1.tgz#3efb62aac641ab2c702458f9a0302696146aa53c" + integrity sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA== + dependencies: + simple-plist "^1.1.0" + uuid "^7.0.3" + +xml2js@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282" + integrity sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" + integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== + +yaml@^2.6.1: + version "2.8.3" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d" + integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.6.2, yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a" + integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg== + +zustand@^5.0.11: + version "5.0.12" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.12.tgz#ed36f647aa89965c4019b671dfc23ef6c6e3af8c" + integrity sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==