diff --git a/internal/activityprocessor/policycache.go b/internal/activityprocessor/policycache.go index 8de25fd1..67b9b208 100644 --- a/internal/activityprocessor/policycache.go +++ b/internal/activityprocessor/policycache.go @@ -464,11 +464,27 @@ func (r *CompiledRule) EvaluateEventMatch(eventMap map[string]any) (bool, error) // EvaluateCompiledAuditRules evaluates pre-compiled audit rules against an audit event. // Returns the generated Activity, the matching rule index, and any error. // Returns (nil, -1, nil) if no rule matched. +// +// Use EvaluateCompiledAuditRulesWithResolver to also enrich the resulting +// activity with user display names; this convenience wrapper passes nil. func EvaluateCompiledAuditRules( policy *CompiledPolicy, auditMap map[string]any, audit *auditv1.Event, resolveKind processor.KindResolver, +) (*v1alpha1.Activity, int, error) { + return EvaluateCompiledAuditRulesWithResolver(policy, auditMap, audit, resolveKind, nil) +} + +// EvaluateCompiledAuditRulesWithResolver is like EvaluateCompiledAuditRules +// but enriches the activity actor and any User-typed link targets with +// display names looked up via resolver. +func EvaluateCompiledAuditRulesWithResolver( + policy *CompiledPolicy, + auditMap map[string]any, + audit *auditv1.Event, + resolveKind processor.KindResolver, + resolver processor.UserResolver, ) (*v1alpha1.Activity, int, error) { for i := range policy.AuditRules { rule := &policy.AuditRules[i] @@ -491,8 +507,9 @@ func EvaluateCompiledAuditRules( } builder := &processor.ActivityBuilder{ - APIGroup: policy.APIGroup, - Kind: policy.Kind, + APIGroup: policy.APIGroup, + Kind: policy.Kind, + UserResolver: resolver, } activity, err := builder.BuildFromAudit(audit, summary, links, resolveKind) if err != nil { diff --git a/internal/activityprocessor/processor.go b/internal/activityprocessor/processor.go index 22a3a154..59ee1ee8 100644 --- a/internal/activityprocessor/processor.go +++ b/internal/activityprocessor/processor.go @@ -294,6 +294,10 @@ type Processor struct { // dlqRetryController handles automatic retry of DLQ events. dlqRetryController *DLQRetryController + // userResolver enriches activities with iam User display names. nil + // disables enrichment; activities are emitted with raw emails/IDs. + userResolver processor.UserResolver + wg sync.WaitGroup ctx context.Context cancel context.CancelFunc @@ -393,6 +397,10 @@ func (p *Processor) Start(ctx context.Context) error { // Create event emitter for health reporting p.eventEmitter = NewEventEmitter(k8sClient, recorder) + // Wire a cached iam User resolver so activities are enriched with + // human-readable display names. Failures fall back to email/UID. + p.userResolver = processor.NewCachedUserResolver(NewIAMUserResolver(k8sClient), 0, 0) + // Build NATS connection options natsOpts := []nats.Option{ nats.Name("activity-processor"), @@ -1034,7 +1042,7 @@ func (p *Processor) processMessage(msg *nats.Msg) error { // evaluateCompiledAuditRules evaluates audit rules using pre-compiled CEL programs. func (p *Processor) evaluateCompiledAuditRules(policy *CompiledPolicy, auditMap map[string]any, audit *auditv1.Event) (*v1alpha1.Activity, int, error) { - return EvaluateCompiledAuditRules(policy, auditMap, audit, p.resourceToKind) + return EvaluateCompiledAuditRulesWithResolver(policy, auditMap, audit, p.resourceToKind, p.userResolver) } // auditToMap converts an audit event to a map for CEL evaluation. diff --git a/internal/activityprocessor/userresolver.go b/internal/activityprocessor/userresolver.go new file mode 100644 index 00000000..e1980730 --- /dev/null +++ b/internal/activityprocessor/userresolver.go @@ -0,0 +1,101 @@ +package activityprocessor + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + + "go.miloapis.com/activity/internal/processor" +) + +// userGVK identifies the iam User custom resource we resolve display names +// from. The activity processor never serves these objects, so we query them +// as Unstructured to avoid pulling in the milo iam Go types. +var userGVK = schema.GroupVersionKind{ + Group: "iam.miloapis.com", + Version: "v1alpha1", + Kind: "User", +} + +// IAMUserResolver implements processor.UserResolver against an iam User CR +// store reached through a controller-runtime client. It is safe for +// concurrent use; wrap with processor.NewCachedUserResolver to add caching +// and per-key single-flight. +type IAMUserResolver struct { + Client client.Client +} + +// NewIAMUserResolver returns a resolver that fetches iam Users via c. +func NewIAMUserResolver(c client.Client) *IAMUserResolver { + return &IAMUserResolver{Client: c} +} + +// LookupByEmail finds the first iam User whose spec.email matches the given +// address. Returns ok=false when no match is found or email is empty. +func (r *IAMUserResolver) LookupByEmail(ctx context.Context, email string) (processor.UserInfo, bool, error) { + if email == "" || r == nil || r.Client == nil { + return processor.UserInfo{}, false, nil + } + + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(schema.GroupVersionKind{ + Group: userGVK.Group, + Version: userGVK.Version, + Kind: userGVK.Kind + "List", + }) + + // Most clusters do not index spec.email server-side; list all and filter + // in process. The cached wrapper amortizes this across calls; if scale + // becomes a concern, register a field indexer for spec.email in the + // manager's cache. + if err := r.Client.List(ctx, list); err != nil { + return processor.UserInfo{}, false, fmt.Errorf("list iam users: %w", err) + } + + for i := range list.Items { + item := &list.Items[i] + got, _, _ := unstructured.NestedString(item.Object, "spec", "email") + if got == email { + return userInfoFromUnstructured(item), true, nil + } + } + + return processor.UserInfo{}, false, nil +} + +// LookupByName fetches an iam User by metadata.name and returns its display +// fields. Returns ok=false on NotFound. +func (r *IAMUserResolver) LookupByName(ctx context.Context, name string) (processor.UserInfo, bool, error) { + if name == "" || r == nil || r.Client == nil { + return processor.UserInfo{}, false, nil + } + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(userGVK) + + if err := r.Client.Get(ctx, client.ObjectKey{Name: name}, obj); err != nil { + if apierrors.IsNotFound(err) { + return processor.UserInfo{}, false, nil + } + return processor.UserInfo{}, false, fmt.Errorf("get iam user %q: %w", name, err) + } + + return userInfoFromUnstructured(obj), true, nil +} + +func userInfoFromUnstructured(obj *unstructured.Unstructured) processor.UserInfo { + given, _, _ := unstructured.NestedString(obj.Object, "spec", "givenName") + family, _, _ := unstructured.NestedString(obj.Object, "spec", "familyName") + email, _, _ := unstructured.NestedString(obj.Object, "spec", "email") + return processor.UserInfo{ + Name: obj.GetName(), + GivenName: given, + FamilyName: family, + Email: email, + UID: string(obj.GetUID()), + } +} diff --git a/internal/processor/activity.go b/internal/processor/activity.go index 84ded593..48b1073c 100644 --- a/internal/processor/activity.go +++ b/internal/processor/activity.go @@ -1,10 +1,12 @@ package processor import ( + "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" + "strings" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -15,6 +17,10 @@ import ( "go.miloapis.com/activity/pkg/apis/activity/v1alpha1" ) +// iamGroup is the API group for Milo IAM resources we enrich with user +// display names. +const iamGroup = "iam.miloapis.com" + // activityName generates a deterministic activity name from the origin event // identifier and the policy's resource target. The same input always produces // the same name, enabling NATS message deduplication on retries. @@ -35,6 +41,10 @@ type ActivityBuilder struct { // Resource information from the policy APIGroup string Kind string + + // UserResolver is consulted (when non-nil) to enrich the actor and any + // User-typed link targets with human-readable display names. + UserResolver UserResolver } // BuildFromAudit constructs an Activity from an audit event. @@ -64,8 +74,9 @@ func (b *ActivityBuilder) BuildFromAudit( resourceUID := extractResponseUID(audit.ResponseObject) // Classify change source and resolve actor + ctx := context.Background() changeSource := ClassifyChangeSource(audit.User) - actor := ResolveActor(audit.User) + actor := ResolveActorWithResolver(ctx, audit.User, b.UserResolver) tenant := ExtractTenant(audit.User) // Generate activity name @@ -77,6 +88,10 @@ func (b *ActivityBuilder) BuildFromAudit( return nil, fmt.Errorf("%w: %v", ErrActivityBuild, err) } + // Enrich: replace actor email with display name in summary, attach actor + // link, and hydrate any User-typed link targets with display names. + summary, activityLinks = enrichSummaryWithDisplayNames(ctx, summary, actor, activityLinks, b.UserResolver) + return &v1alpha1.Activity{ TypeMeta: metav1.TypeMeta{ APIVersion: v1alpha1.SchemeGroupVersion.String(), @@ -115,6 +130,101 @@ func (b *ActivityBuilder) BuildFromAudit( }, nil } +// enrichSummaryWithDisplayNames rewrites the summary to use human-readable +// display names for the actor and any User-typed link targets, and appends +// link metadata so the UI can render an email/UID tooltip. +// +// Behavior: +// - When the actor has a DisplayName, the first occurrence of the actor's +// Name (typically an email) in the summary is replaced with the +// DisplayName, and a synthetic actor link is appended carrying the +// DisplayName, Email, and UID. +// - For each existing link whose resource is an iam User, the resolver is +// queried by the link's resource name; on hit, the link's Marker is +// replaced in the summary with the user's DisplayName and the link's +// DisplayName/Email fields are populated. +// +// Returns the rewritten summary and links. When resolver is nil or no +// matches occur, the inputs are returned unchanged. +func enrichSummaryWithDisplayNames( + ctx context.Context, + summary string, + actor v1alpha1.ActivityActor, + links []v1alpha1.ActivityLink, + resolver UserResolver, +) (string, []v1alpha1.ActivityLink) { + // Actor: if we have a display name distinct from the name, swap it into + // the summary. If the policy template already wrapped the actor with + // link() (so a link entry exists with marker == actor.Name), upgrade + // that entry in place; otherwise append a synthetic actor link so the + // UI can render the hover tooltip. + if actor.DisplayName != "" && actor.DisplayName != actor.Name && actor.Name != "" { + summaryHadActor := strings.Contains(summary, actor.Name) + if summaryHadActor { + summary = strings.Replace(summary, actor.Name, actor.DisplayName, 1) + } + + upgraded := false + for i := range links { + if links[i].Marker == actor.Name { + links[i].Marker = actor.DisplayName + links[i].DisplayName = actor.DisplayName + if links[i].Email == "" { + links[i].Email = actor.Email + } + upgraded = true + break + } + } + if !upgraded && summaryHadActor { + links = append(links, v1alpha1.ActivityLink{ + Marker: actor.DisplayName, + Resource: v1alpha1.ActivityResource{ + APIGroup: iamGroup, + Kind: "User", + UID: actor.UID, + }, + DisplayName: actor.DisplayName, + Email: actor.Email, + }) + } + } + + // User-typed link targets: hydrate via resolver and rewrite the summary. + if resolver != nil { + for i := range links { + link := &links[i] + if !isUserLink(link.Resource) { + continue + } + if link.Resource.Name == "" || link.DisplayName != "" { + continue + } + info, ok, err := resolver.LookupByName(ctx, link.Resource.Name) + if err != nil || !ok { + continue + } + displayName := info.DisplayName() + if displayName == "" { + continue + } + if link.Marker != "" && link.Marker != displayName { + summary = strings.Replace(summary, link.Marker, displayName, 1) + link.Marker = displayName + } + link.DisplayName = displayName + link.Email = info.Email + } + } + + return summary, links +} + +// isUserLink reports whether the resource targets an iam User CR. +func isUserLink(r v1alpha1.ActivityResource) bool { + return r.APIGroup == iamGroup && r.Kind == "User" +} + // extractResponseUID extracts the UID from an audit response object's metadata. func extractResponseUID(responseObject *runtime.Unknown) string { if responseObject == nil || len(responseObject.Raw) == 0 { @@ -202,6 +312,10 @@ func (b *ActivityBuilder) BuildFromEvent( return nil, fmt.Errorf("%w: %v", ErrActivityBuild, err) } + // Hydrate User-typed links with display names (event actors are system + // components, so no actor enrichment is needed). + summary, activityLinks = enrichSummaryWithDisplayNames(context.Background(), summary, actor, activityLinks, b.UserResolver) + return &v1alpha1.Activity{ TypeMeta: metav1.TypeMeta{ APIVersion: v1alpha1.SchemeGroupVersion.String(), diff --git a/internal/processor/classifier.go b/internal/processor/classifier.go index 84e01ac1..14109d2b 100644 --- a/internal/processor/classifier.go +++ b/internal/processor/classifier.go @@ -1,6 +1,7 @@ package processor import ( + "context" "strings" authnv1 "k8s.io/api/authentication/v1" @@ -38,6 +39,14 @@ const ( // - user: Human users authenticated via OIDC or other providers // - system: Kubernetes controllers, service accounts, and other system components func ResolveActor(user authnv1.UserInfo) v1alpha1.ActivityActor { + return ResolveActorWithResolver(context.Background(), user, nil) +} + +// ResolveActorWithResolver behaves like ResolveActor but additionally +// populates ActivityActor.DisplayName for human users when resolver is non-nil +// and a matching User record is found. Resolver errors are silently ignored: +// the activity is still emitted with whatever data is available. +func ResolveActorWithResolver(ctx context.Context, user authnv1.UserInfo, resolver UserResolver) v1alpha1.ActivityActor { actor := v1alpha1.ActivityActor{ UID: user.UID, } @@ -62,6 +71,15 @@ func ResolveActor(user authnv1.UserInfo) v1alpha1.ActivityActor { actor.Name = "unknown" } + // Enrich with display name for human users when a resolver is available. + if resolver != nil && actor.Type == ActorTypeUser && actor.Email != "" { + if info, ok, err := resolver.LookupByEmail(ctx, actor.Email); err == nil && ok { + if dn := info.DisplayName(); dn != "" { + actor.DisplayName = dn + } + } + } + return actor } diff --git a/internal/processor/enrichment_test.go b/internal/processor/enrichment_test.go new file mode 100644 index 00000000..dd5422ee --- /dev/null +++ b/internal/processor/enrichment_test.go @@ -0,0 +1,232 @@ +package processor + +import ( + "context" + "testing" + + authnv1 "k8s.io/api/authentication/v1" + + "go.miloapis.com/activity/pkg/apis/activity/v1alpha1" +) + +// userInfoFixture builds a minimal authentication.UserInfo for tests. +func userInfoFixture(username, uid string) authnv1.UserInfo { + return authnv1.UserInfo{Username: username, UID: uid} +} + +func TestEnrichSummaryWithDisplayNames_ActorReplacement(t *testing.T) { + cases := []struct { + name string + summary string + actor v1alpha1.ActivityActor + links []v1alpha1.ActivityLink + wantSummary string + wantLinks int + wantMarker string + }{ + { + name: "no display name leaves summary untouched", + summary: "smith@datum.net created machine account ma-1", + actor: v1alpha1.ActivityActor{ + Type: ActorTypeUser, + Name: "smith@datum.net", + Email: "smith@datum.net", + UID: "uid-1", + }, + wantSummary: "smith@datum.net created machine account ma-1", + wantLinks: 0, + }, + { + name: "display name replaces email and synthetic link is appended", + summary: "smith@datum.net created machine account ma-1", + actor: v1alpha1.ActivityActor{ + Type: ActorTypeUser, + Name: "smith@datum.net", + Email: "smith@datum.net", + UID: "uid-1", + DisplayName: "Smith Nelson", + }, + wantSummary: "Smith Nelson created machine account ma-1", + wantLinks: 1, + wantMarker: "Smith Nelson", + }, + { + name: "existing actor link is upgraded in place", + summary: "smith@datum.net created machine account ma-1", + actor: v1alpha1.ActivityActor{ + Type: ActorTypeUser, + Name: "smith@datum.net", + Email: "smith@datum.net", + UID: "uid-1", + DisplayName: "Smith Nelson", + }, + links: []v1alpha1.ActivityLink{ + { + Marker: "smith@datum.net", + Resource: v1alpha1.ActivityResource{ + APIGroup: iamGroup, + Kind: "User", + Name: "smith", + }, + }, + }, + wantSummary: "Smith Nelson created machine account ma-1", + wantLinks: 1, + wantMarker: "Smith Nelson", + }, + { + name: "actor not in summary leaves summary alone and skips link", + summary: "system updated machine account ma-1", + actor: v1alpha1.ActivityActor{ + Type: ActorTypeUser, + Name: "smith@datum.net", + Email: "smith@datum.net", + UID: "uid-1", + DisplayName: "Smith Nelson", + }, + wantSummary: "system updated machine account ma-1", + wantLinks: 0, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + gotSummary, gotLinks := enrichSummaryWithDisplayNames( + context.Background(), tc.summary, tc.actor, tc.links, nil, + ) + if gotSummary != tc.wantSummary { + t.Fatalf("summary = %q, want %q", gotSummary, tc.wantSummary) + } + if len(gotLinks) != tc.wantLinks { + t.Fatalf("links = %d, want %d (links=%+v)", len(gotLinks), tc.wantLinks, gotLinks) + } + if tc.wantMarker != "" { + found := false + for _, l := range gotLinks { + if l.Marker == tc.wantMarker { + found = true + if l.DisplayName != tc.actor.DisplayName { + t.Errorf("link.DisplayName = %q, want %q", l.DisplayName, tc.actor.DisplayName) + } + if l.Email != tc.actor.Email { + t.Errorf("link.Email = %q, want %q", l.Email, tc.actor.Email) + } + } + } + if !found { + t.Errorf("no link with marker %q", tc.wantMarker) + } + } + }) + } +} + +func TestEnrichSummaryWithDisplayNames_UserLinkHydration(t *testing.T) { + resolver := &fakeResolver{ + names: map[string]UserInfo{ + "340583683847098197": { + Name: "340583683847098197", + GivenName: "Dean", + FamilyName: "Gaghan", + Email: "dgaghan@datum.net", + }, + }, + } + + summary := "Smith Nelson updated user 340583683847098197" + links := []v1alpha1.ActivityLink{ + { + Marker: "340583683847098197", + Resource: v1alpha1.ActivityResource{ + APIGroup: iamGroup, + Kind: "User", + Name: "340583683847098197", + }, + }, + } + actor := v1alpha1.ActivityActor{Type: ActorTypeUser, Name: "Smith Nelson", DisplayName: "Smith Nelson"} + + gotSummary, gotLinks := enrichSummaryWithDisplayNames(context.Background(), summary, actor, links, resolver) + + wantSummary := "Smith Nelson updated user Dean Gaghan" + if gotSummary != wantSummary { + t.Fatalf("summary = %q, want %q", gotSummary, wantSummary) + } + if len(gotLinks) != 1 { + t.Fatalf("links = %d, want 1", len(gotLinks)) + } + got := gotLinks[0] + if got.Marker != "Dean Gaghan" { + t.Errorf("Marker = %q, want %q", got.Marker, "Dean Gaghan") + } + if got.DisplayName != "Dean Gaghan" { + t.Errorf("DisplayName = %q, want %q", got.DisplayName, "Dean Gaghan") + } + if got.Email != "dgaghan@datum.net" { + t.Errorf("Email = %q", got.Email) + } +} + +func TestEnrichSummaryWithDisplayNames_NonUserLinkUntouched(t *testing.T) { + resolver := &fakeResolver{names: map[string]UserInfo{ + "some-resource": {GivenName: "Should not", FamilyName: "Be Used"}, + }} + + summary := "Smith Nelson created machine account ma-1" + links := []v1alpha1.ActivityLink{ + { + Marker: "ma-1", + Resource: v1alpha1.ActivityResource{ + APIGroup: iamGroup, + Kind: "MachineAccount", + Name: "ma-1", + }, + }, + } + actor := v1alpha1.ActivityActor{Type: ActorTypeUser, Name: "Smith Nelson"} + + gotSummary, gotLinks := enrichSummaryWithDisplayNames(context.Background(), summary, actor, links, resolver) + if gotSummary != summary { + t.Fatalf("summary changed: %q", gotSummary) + } + if gotLinks[0].DisplayName != "" || gotLinks[0].Email != "" { + t.Fatalf("MachineAccount link should not be hydrated: %+v", gotLinks[0]) + } + if resolver.nameCalls.Load() != 0 { + t.Fatalf("resolver should not be called for non-User links, got %d calls", resolver.nameCalls.Load()) + } +} + +func TestResolveActorWithResolver_PopulatesDisplayName(t *testing.T) { + resolver := &fakeResolver{ + emails: map[string]UserInfo{ + "smith@datum.net": {GivenName: "Smith", FamilyName: "Nelson", Email: "smith@datum.net"}, + }, + } + + actor := ResolveActorWithResolver(context.Background(), userInfoFixture("smith@datum.net", "uid-1"), resolver) + + if actor.DisplayName != "Smith Nelson" { + t.Fatalf("DisplayName = %q, want %q", actor.DisplayName, "Smith Nelson") + } + if actor.Email != "smith@datum.net" { + t.Errorf("Email = %q", actor.Email) + } +} + +func TestResolveActorWithResolver_NilResolverNoDisplayName(t *testing.T) { + actor := ResolveActorWithResolver(context.Background(), userInfoFixture("smith@datum.net", "uid-1"), nil) + if actor.DisplayName != "" { + t.Fatalf("DisplayName should be empty without resolver, got %q", actor.DisplayName) + } +} + +func TestResolveActorWithResolver_SystemActorSkipsLookup(t *testing.T) { + resolver := &fakeResolver{} + actor := ResolveActorWithResolver(context.Background(), userInfoFixture("system:admin", "uid-system"), resolver) + if actor.Type != ActorTypeSystem { + t.Fatalf("Type = %q, want %q", actor.Type, ActorTypeSystem) + } + if resolver.emailCalls.Load() != 0 { + t.Fatalf("resolver should not be called for system actors, got %d calls", resolver.emailCalls.Load()) + } +} diff --git a/internal/processor/evaluate.go b/internal/processor/evaluate.go index 28ae8f26..815faa94 100644 --- a/internal/processor/evaluate.go +++ b/internal/processor/evaluate.go @@ -28,10 +28,24 @@ type EvaluationResult struct { // EvaluateAuditRules evaluates audit rules against an audit log input. // Returns the generated Activity if a rule matches, or nil if no rule matched. // If resolveKind is provided, it will be used to resolve resource names to Kind in links. +// +// Use EvaluateAuditRulesWithResolver to also enrich activities with user +// display names; this convenience wrapper forwards a nil resolver. func EvaluateAuditRules( spec *v1alpha1.ActivityPolicySpec, audit *auditv1.Event, resolveKind KindResolver, +) (*EvaluationResult, error) { + return EvaluateAuditRulesWithResolver(spec, audit, resolveKind, nil) +} + +// EvaluateAuditRulesWithResolver is like EvaluateAuditRules but additionally +// enriches the resulting Activity with display names looked up via resolver. +func EvaluateAuditRulesWithResolver( + spec *v1alpha1.ActivityPolicySpec, + audit *auditv1.Event, + resolveKind KindResolver, + resolver UserResolver, ) (*EvaluationResult, error) { // Convert to map for CEL evaluation auditMap, err := toMap(audit) @@ -41,8 +55,9 @@ func EvaluateAuditRules( // Create activity builder builder := &ActivityBuilder{ - APIGroup: spec.Resource.APIGroup, - Kind: spec.Resource.Kind, + APIGroup: spec.Resource.APIGroup, + Kind: spec.Resource.Kind, + UserResolver: resolver, } // Try each audit rule in order @@ -83,10 +98,24 @@ func EvaluateAuditRules( // EvaluateEventRules evaluates event rules against a Kubernetes event input. // Returns the generated Activity if a rule matches, or nil if no rule matched. // If resolveKind is provided, it will be used to resolve resource names to Kind in links. +// +// Use EvaluateEventRulesWithResolver to also enrich activities with user +// display names; this convenience wrapper forwards a nil resolver. func EvaluateEventRules( spec *v1alpha1.ActivityPolicySpec, eventData interface{}, resolveKind KindResolver, +) (*EvaluationResult, error) { + return EvaluateEventRulesWithResolver(spec, eventData, resolveKind, nil) +} + +// EvaluateEventRulesWithResolver is like EvaluateEventRules but additionally +// enriches User-typed link targets with display names looked up via resolver. +func EvaluateEventRulesWithResolver( + spec *v1alpha1.ActivityPolicySpec, + eventData interface{}, + resolveKind KindResolver, + resolver UserResolver, ) (*EvaluationResult, error) { // Convert event data to map if needed eventMap, err := toMap(eventData) @@ -96,8 +125,9 @@ func EvaluateEventRules( // Create activity builder builder := &ActivityBuilder{ - APIGroup: spec.Resource.APIGroup, - Kind: spec.Resource.Kind, + APIGroup: spec.Resource.APIGroup, + Kind: spec.Resource.Kind, + UserResolver: resolver, } // Try each event rule in order diff --git a/internal/processor/userlookup.go b/internal/processor/userlookup.go new file mode 100644 index 00000000..a4f3242a --- /dev/null +++ b/internal/processor/userlookup.go @@ -0,0 +1,177 @@ +package processor + +import ( + "context" + "sync" + "time" +) + +// UserInfo is the subset of iam User fields needed to enrich activities with +// human-readable display names. Resolvers MUST populate at least one of +// GivenName/FamilyName/Email when returning ok=true; a fully-empty result +// should return ok=false so callers can treat it as a miss. +type UserInfo struct { + // Name is the User CR's metadata.name. Used to construct a + // resource reference in the activity Link. + Name string + GivenName string + FamilyName string + Email string + UID string +} + +// DisplayName returns a human-readable name from given/family, falling back +// to whichever component is populated, then to the email. Returns "" when +// nothing is available. +func (u UserInfo) DisplayName() string { + switch { + case u.GivenName != "" && u.FamilyName != "": + return u.GivenName + " " + u.FamilyName + case u.GivenName != "": + return u.GivenName + case u.FamilyName != "": + return u.FamilyName + default: + return u.Email + } +} + +// UserResolver looks up User records to enrich activities with display names. +// +// Implementations MUST be safe for concurrent use. When the resolver cannot +// find a matching user (or hits a transient error), it SHOULD return +// (UserInfo{}, false, nil). A non-nil error indicates a non-cacheable failure +// the caller should log; the activity is still emitted without enrichment. +type UserResolver interface { + // LookupByEmail resolves a user by their email address (typically the + // audit username for OIDC-authenticated requests). + LookupByEmail(ctx context.Context, email string) (UserInfo, bool, error) + + // LookupByName resolves a user by the User CR's metadata.name. Used to + // hydrate user-typed link targets, where audit.objectRef.name is the User + // resource name. + LookupByName(ctx context.Context, name string) (UserInfo, bool, error) +} + +// NoopUserResolver is a UserResolver that always returns a miss. Used as the +// default when no real resolver is wired (e.g., in unit tests). +type NoopUserResolver struct{} + +func (NoopUserResolver) LookupByEmail(context.Context, string) (UserInfo, bool, error) { + return UserInfo{}, false, nil +} + +func (NoopUserResolver) LookupByName(context.Context, string) (UserInfo, bool, error) { + return UserInfo{}, false, nil +} + +// CachedUserResolver wraps an underlying resolver with a TTL cache and +// per-key single-flight to collapse concurrent lookups for the same user. +// Negative results are cached briefly so a missing user doesn't translate to +// a request storm. +type CachedUserResolver struct { + inner UserResolver + posTTL time.Duration + negTTL time.Duration + now func() time.Time + mu sync.Mutex + emailCache map[string]cachedUser + nameCache map[string]cachedUser + emailFlight map[string]*flight + nameFlight map[string]*flight +} + +type cachedUser struct { + info UserInfo + ok bool + expires time.Time +} + +type flight struct { + done chan struct{} + info UserInfo + ok bool + err error +} + +// NewCachedUserResolver wraps inner with a TTL cache. posTTL applies to hits; +// negTTL to misses (kept short so newly-created users become resolvable +// quickly). When inner is nil, lookups always miss. +func NewCachedUserResolver(inner UserResolver, posTTL, negTTL time.Duration) *CachedUserResolver { + if inner == nil { + inner = NoopUserResolver{} + } + if posTTL <= 0 { + posTTL = 5 * time.Minute + } + if negTTL <= 0 { + negTTL = 30 * time.Second + } + return &CachedUserResolver{ + inner: inner, + posTTL: posTTL, + negTTL: negTTL, + now: time.Now, + emailCache: make(map[string]cachedUser), + nameCache: make(map[string]cachedUser), + emailFlight: make(map[string]*flight), + nameFlight: make(map[string]*flight), + } +} + +func (c *CachedUserResolver) LookupByEmail(ctx context.Context, email string) (UserInfo, bool, error) { + if email == "" { + return UserInfo{}, false, nil + } + return c.lookup(ctx, email, c.emailCache, c.emailFlight, c.inner.LookupByEmail) +} + +func (c *CachedUserResolver) LookupByName(ctx context.Context, name string) (UserInfo, bool, error) { + if name == "" { + return UserInfo{}, false, nil + } + return c.lookup(ctx, name, c.nameCache, c.nameFlight, c.inner.LookupByName) +} + +type lookupFn func(context.Context, string) (UserInfo, bool, error) + +func (c *CachedUserResolver) lookup( + ctx context.Context, + key string, + cache map[string]cachedUser, + flights map[string]*flight, + fetch lookupFn, +) (UserInfo, bool, error) { + c.mu.Lock() + if entry, found := cache[key]; found && c.now().Before(entry.expires) { + c.mu.Unlock() + return entry.info, entry.ok, nil + } + + if f, inflight := flights[key]; inflight { + c.mu.Unlock() + <-f.done + return f.info, f.ok, f.err + } + + f := &flight{done: make(chan struct{})} + flights[key] = f + c.mu.Unlock() + + info, ok, err := fetch(ctx, key) + + c.mu.Lock() + delete(flights, key) + if err == nil { + ttl := c.negTTL + if ok { + ttl = c.posTTL + } + cache[key] = cachedUser{info: info, ok: ok, expires: c.now().Add(ttl)} + } + c.mu.Unlock() + + f.info, f.ok, f.err = info, ok, err + close(f.done) + return info, ok, err +} diff --git a/internal/processor/userlookup_test.go b/internal/processor/userlookup_test.go new file mode 100644 index 00000000..d058e4dc --- /dev/null +++ b/internal/processor/userlookup_test.go @@ -0,0 +1,185 @@ +package processor + +import ( + "context" + "errors" + "sync" + "sync/atomic" + "testing" + "time" +) + +// fakeResolver is a UserResolver whose responses are configured per-test. +type fakeResolver struct { + emailCalls atomic.Int64 + nameCalls atomic.Int64 + emails map[string]UserInfo + names map[string]UserInfo + wait chan struct{} + emailErr error + nameErr error +} + +func (f *fakeResolver) LookupByEmail(ctx context.Context, email string) (UserInfo, bool, error) { + f.emailCalls.Add(1) + if f.wait != nil { + <-f.wait + } + if f.emailErr != nil { + return UserInfo{}, false, f.emailErr + } + info, ok := f.emails[email] + return info, ok, nil +} + +func (f *fakeResolver) LookupByName(ctx context.Context, name string) (UserInfo, bool, error) { + f.nameCalls.Add(1) + if f.wait != nil { + <-f.wait + } + if f.nameErr != nil { + return UserInfo{}, false, f.nameErr + } + info, ok := f.names[name] + return info, ok, nil +} + +func TestUserInfo_DisplayName(t *testing.T) { + cases := []struct { + name string + in UserInfo + want string + }{ + {"both", UserInfo{GivenName: "Smith", FamilyName: "Nelson"}, "Smith Nelson"}, + {"given only", UserInfo{GivenName: "Smith"}, "Smith"}, + {"family only", UserInfo{FamilyName: "Nelson"}, "Nelson"}, + {"email fallback", UserInfo{Email: "smith@datum.net"}, "smith@datum.net"}, + {"empty", UserInfo{}, ""}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.in.DisplayName(); got != tc.want { + t.Fatalf("DisplayName() = %q, want %q", got, tc.want) + } + }) + } +} + +func TestCachedUserResolver_PositiveHitCached(t *testing.T) { + inner := &fakeResolver{emails: map[string]UserInfo{ + "smith@datum.net": {GivenName: "Smith", FamilyName: "Nelson", Email: "smith@datum.net"}, + }} + c := NewCachedUserResolver(inner, time.Minute, time.Minute) + + for i := 0; i < 5; i++ { + info, ok, err := c.LookupByEmail(context.Background(), "smith@datum.net") + if err != nil || !ok || info.DisplayName() != "Smith Nelson" { + t.Fatalf("iteration %d: got info=%+v ok=%v err=%v", i, info, ok, err) + } + } + if got := inner.emailCalls.Load(); got != 1 { + t.Fatalf("inner LookupByEmail called %d times, want 1", got) + } +} + +func TestCachedUserResolver_NegativeHitCached(t *testing.T) { + inner := &fakeResolver{emails: map[string]UserInfo{}} + c := NewCachedUserResolver(inner, time.Minute, time.Minute) + + for i := 0; i < 3; i++ { + _, ok, err := c.LookupByEmail(context.Background(), "missing@datum.net") + if err != nil || ok { + t.Fatalf("iteration %d: ok=%v err=%v", i, ok, err) + } + } + if got := inner.emailCalls.Load(); got != 1 { + t.Fatalf("inner LookupByEmail called %d times, want 1 (negative cache miss)", got) + } +} + +func TestCachedUserResolver_TTLExpiry(t *testing.T) { + inner := &fakeResolver{emails: map[string]UserInfo{ + "smith@datum.net": {GivenName: "Smith", Email: "smith@datum.net"}, + }} + c := NewCachedUserResolver(inner, 50*time.Millisecond, 50*time.Millisecond) + + if _, ok, _ := c.LookupByEmail(context.Background(), "smith@datum.net"); !ok { + t.Fatal("first lookup must hit") + } + now := time.Now() + c.now = func() time.Time { return now.Add(time.Hour) } + if _, ok, _ := c.LookupByEmail(context.Background(), "smith@datum.net"); !ok { + t.Fatal("post-TTL lookup must still resolve via inner") + } + if got := inner.emailCalls.Load(); got != 2 { + t.Fatalf("inner LookupByEmail called %d times, want 2", got) + } +} + +func TestCachedUserResolver_SingleFlight(t *testing.T) { + inner := &fakeResolver{ + emails: map[string]UserInfo{"smith@datum.net": {GivenName: "Smith", Email: "smith@datum.net"}}, + wait: make(chan struct{}), + } + c := NewCachedUserResolver(inner, time.Minute, time.Minute) + + const concurrent = 20 + var wg sync.WaitGroup + for i := 0; i < concurrent; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _, _, _ = c.LookupByEmail(context.Background(), "smith@datum.net") + }() + } + + // Give all goroutines a moment to enqueue behind the in-flight lookup. + time.Sleep(20 * time.Millisecond) + close(inner.wait) + wg.Wait() + + if got := inner.emailCalls.Load(); got != 1 { + t.Fatalf("inner LookupByEmail called %d times, want 1 (single-flight)", got) + } +} + +func TestCachedUserResolver_ErrorNotCached(t *testing.T) { + sentinel := errors.New("boom") + inner := &fakeResolver{emailErr: sentinel} + c := NewCachedUserResolver(inner, time.Minute, time.Minute) + + for i := 0; i < 3; i++ { + _, _, err := c.LookupByEmail(context.Background(), "smith@datum.net") + if !errors.Is(err, sentinel) { + t.Fatalf("iteration %d: err=%v, want %v", i, err, sentinel) + } + } + if got := inner.emailCalls.Load(); got != 3 { + t.Fatalf("inner LookupByEmail called %d times, want 3 (errors are not cached)", got) + } +} + +func TestCachedUserResolver_EmptyKeysShortCircuit(t *testing.T) { + inner := &fakeResolver{} + c := NewCachedUserResolver(inner, time.Minute, time.Minute) + + if _, ok, err := c.LookupByEmail(context.Background(), ""); ok || err != nil { + t.Fatalf("empty email should miss without err; ok=%v err=%v", ok, err) + } + if _, ok, err := c.LookupByName(context.Background(), ""); ok || err != nil { + t.Fatalf("empty name should miss without err; ok=%v err=%v", ok, err) + } + if got := inner.emailCalls.Load() + inner.nameCalls.Load(); got != 0 { + t.Fatalf("inner called %d times, want 0", got) + } +} + +func TestNoopUserResolver(t *testing.T) { + r := NoopUserResolver{} + if _, ok, err := r.LookupByEmail(context.Background(), "x"); ok || err != nil { + t.Fatalf("noop email lookup ok=%v err=%v", ok, err) + } + if _, ok, err := r.LookupByName(context.Background(), "x"); ok || err != nil { + t.Fatalf("noop name lookup ok=%v err=%v", ok, err) + } +} diff --git a/pkg/apis/activity/v1alpha1/types_activity.go b/pkg/apis/activity/v1alpha1/types_activity.go index df15deed..cca25898 100644 --- a/pkg/apis/activity/v1alpha1/types_activity.go +++ b/pkg/apis/activity/v1alpha1/types_activity.go @@ -128,6 +128,17 @@ type ActivityActor struct { // // +optional Email string `json:"email,omitempty"` + + // DisplayName is the actor's human-readable name (e.g., "Smith Nelson"). + // For user actors, populated from the iam User's spec.givenName and + // spec.familyName when available. Empty if no User record is found or the + // actor is not a human user. + // + // UIs SHOULD prefer DisplayName for visible text and use Name/Email/UID + // only when DisplayName is empty. + // + // +optional + DisplayName string `json:"displayName,omitempty"` } // ActivityResource identifies the Kubernetes resource affected by an activity. @@ -179,6 +190,21 @@ type ActivityLink struct { // // +required Resource ActivityResource `json:"resource"` + + // DisplayName is the human-readable name for the linked entity, when one + // is available. Populated server-side for User-typed links from the iam + // User's givenName + familyName so the UI can render names instead of + // raw UIDs in the summary. + // + // +optional + DisplayName string `json:"displayName,omitempty"` + + // Email is the email address for the linked entity, when one is + // available. Populated server-side for User-typed links so the UI can + // surface the email on hover. + // + // +optional + Email string `json:"email,omitempty"` } // ActivityTenant identifies the scope for multi-tenant isolation. diff --git a/ui/Dockerfile b/ui/Dockerfile index 45cd3df9..1cae4e14 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -14,8 +14,6 @@ COPY package.json ./ # Copy library config files COPY tsconfig.json ./ COPY rollup.config.mjs ./ -COPY tailwind.config.js ./ -COPY postcss.config.js ./ # Copy source files for the library COPY src/ ./src/ diff --git a/ui/example/app/routes/resource-history.tsx b/ui/example/app/routes/resource-history.tsx index ed45c213..bfea3251 100644 --- a/ui/example/app/routes/resource-history.tsx +++ b/ui/example/app/routes/resource-history.tsx @@ -33,7 +33,9 @@ import { EventDetailModal } from "~/components/EventDetailModal"; export default function ResourceHistoryPage() { const [searchParams, setSearchParams] = useSearchParams(); const [client, setClient] = useState(null); - const [selectedActivity, setSelectedActivity] = useState(null); + const [selectedActivity, setSelectedActivity] = useState( + null, + ); // Read initial values from URL search params const initialApiGroup = searchParams.get("apiGroup") || ""; @@ -66,7 +68,9 @@ export default function ResourceHistoryPage() { }, [initialApiGroup, initialKind, initialNamespace, initialName, initialUid]); // Submitted filter - initialized from URL params - const [submittedFilter, setSubmittedFilter] = useState(filterFromParams); + const [submittedFilter, setSubmittedFilter] = useState( + filterFromParams, + ); // Sync submitted filter when URL params change (e.g., browser back/forward) useEffect(() => { @@ -77,7 +81,14 @@ export default function ResourceHistoryPage() { setNamespace(initialNamespace); setName(initialName); setUid(initialUid); - }, [filterFromParams, initialApiGroup, initialKind, initialNamespace, initialName, initialUid]); + }, [ + filterFromParams, + initialApiGroup, + initialKind, + initialNamespace, + initialName, + initialUid, + ]); useEffect(() => { // Check if in production environment @@ -95,7 +106,7 @@ export default function ResourceHistoryPage() { new ActivityApiClient({ baseUrl: apiUrl || "", token, - }) + }), ); } }, []); @@ -119,76 +130,82 @@ export default function ResourceHistoryPage() { } = useFacets( client!, { start: "now-30d" }, - currentFilters // Pass current selections to filter facet results + currentFilters, // Pass current selections to filter facet results ); // Convert facets to combobox options - const apiGroupOptions: ComboboxOption[] = useMemo(() => - apiGroups - .filter((f) => f.value) - .map((f) => ({ - value: f.value, - label: f.value, - count: f.count, - })), - [apiGroups] + const apiGroupOptions: ComboboxOption[] = useMemo( + () => + apiGroups + .filter((f) => f.value) + .map((f) => ({ + value: f.value, + label: f.value, + count: f.count, + })), + [apiGroups], ); - const kindOptions: ComboboxOption[] = useMemo(() => - resourceKinds - .filter((f) => f.value) - .map((f) => ({ - value: f.value, - label: f.value, - count: f.count, - })), - [resourceKinds] + const kindOptions: ComboboxOption[] = useMemo( + () => + resourceKinds + .filter((f) => f.value) + .map((f) => ({ + value: f.value, + label: f.value, + count: f.count, + })), + [resourceKinds], ); - const namespaceOptions: ComboboxOption[] = useMemo(() => - resourceNamespaces - .filter((f) => f.value) - .map((f) => ({ - value: f.value, - label: f.value, - count: f.count, - })), - [resourceNamespaces] + const namespaceOptions: ComboboxOption[] = useMemo( + () => + resourceNamespaces + .filter((f) => f.value) + .map((f) => ({ + value: f.value, + label: f.value, + count: f.count, + })), + [resourceNamespaces], ); - const handleSubmit = useCallback((e: React.FormEvent) => { - e.preventDefault(); - const filter: ResourceFilter = {}; - const params = new URLSearchParams(); + const handleSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + const filter: ResourceFilter = {}; + const params = new URLSearchParams(); - if (uid.trim()) { - filter.uid = uid.trim(); - params.set("uid", uid.trim()); - } else { - if (apiGroup) { - filter.apiGroup = apiGroup; - params.set("apiGroup", apiGroup); - } - if (kind) { - filter.kind = kind; - params.set("kind", kind); - } - if (namespace) { - filter.namespace = namespace; - params.set("namespace", namespace); + if (uid.trim()) { + filter.uid = uid.trim(); + params.set("uid", uid.trim()); + } else { + if (apiGroup) { + filter.apiGroup = apiGroup; + params.set("apiGroup", apiGroup); + } + if (kind) { + filter.kind = kind; + params.set("kind", kind); + } + if (namespace) { + filter.namespace = namespace; + params.set("namespace", namespace); + } + if (name.trim()) { + filter.name = name.trim(); + params.set("name", name.trim()); + } } - if (name.trim()) { - filter.name = name.trim(); - params.set("name", name.trim()); - } - } - // Only submit if we have at least one filter - if (Object.keys(filter).length > 0) { - setSubmittedFilter(filter); - setSearchParams(params, { replace: false }); - } - }, [uid, apiGroup, kind, namespace, name, setSearchParams]); + // Only submit if we have at least one filter + if (Object.keys(filter).length > 0) { + setSubmittedFilter(filter); + setSearchParams(params, { replace: false }); + } + }, + [uid, apiGroup, kind, namespace, name, setSearchParams], + ); const handleActivityClick = useCallback((activity: Activity) => { setSelectedActivity(activity); @@ -320,7 +337,8 @@ export default function ResourceHistoryPage() { disabled={isAttributeMode} />

- UID provides exact match. When specified, other filters are ignored. + UID provides exact match. When specified, other filters are + ignored.

@@ -336,18 +354,22 @@ export default function ResourceHistoryPage() {
  • - Dropdowns filter automatically based on other selections + Dropdowns filter automatically based on other + selections
  • - Name supports partial matching (e.g., "api" matches "api-gateway") + Name supports partial matching (e.g., "api" + matches "api-gateway")
  • - Combine filters to narrow down results (e.g., Kind + Namespace) + Combine filters to narrow down results (e.g., Kind + + Namespace)
  • Find a resource's UID with:{" "} - kubectl get <kind> <name> -o jsonpath='{"{.metadata.uid}"}' + kubectl get <kind> <name> -o jsonpath=' + {"{.metadata.uid}"}'
@@ -369,8 +391,10 @@ export default function ResourceHistoryPage() { {[ submittedFilter.kind, submittedFilter.name, - submittedFilter.namespace && `in ${submittedFilter.namespace}`, - submittedFilter.apiGroup && `(${submittedFilter.apiGroup})`, + submittedFilter.namespace && + `in ${submittedFilter.namespace}`, + submittedFilter.apiGroup && + `(${submittedFilter.apiGroup})`, ] .filter(Boolean) .join(" ")} diff --git a/ui/package.json b/ui/package.json index 3a7533d4..20e7e98f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -42,6 +42,7 @@ "directory": "ui" }, "peerDependencies": { + "@datum-cloud/datum-ui": "^0.8.0", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-checkbox": "^1.0.0", "@radix-ui/react-dialog": "^1.0.0", @@ -56,17 +57,16 @@ "react-dom": "^18.0.0 || ^19.0.0" }, "devDependencies": { + "@datum-cloud/datum-ui": "^0.8.0", "@monaco-editor/react": "^4.6.0", "@playwright/test": "^1.48.0", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.6", - "@tailwindcss/postcss": "^4.2.1", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@typescript-eslint/eslint-plugin": "^6.15.0", "@typescript-eslint/parser": "^6.15.0", - "autoprefixer": "^10.4.27", "eslint": "^8.56.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", @@ -75,18 +75,14 @@ "react-dom": "^19.0.0", "rollup": "^4.9.1", "rollup-plugin-peer-deps-external": "^2.2.4", - "rollup-plugin-postcss": "^4.0.2", - "tailwindcss": "^4.2.1", "tslib": "^2.6.2", "typescript": "^5.3.3" }, "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "date-fns": "^3.0.6", - "js-yaml": "^4.1.1", + "date-fns": "^4.1.0", "lucide-react": "^0.577.0", - "tailwind-merge": "^3.4.0", - "tailwindcss-animate": "^1.0.7" + "tailwind-merge": "^3.4.0" } } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index a93910e1..0909005a 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -39,21 +39,18 @@ importers: specifier: ^1.0.0 version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) date-fns: - specifier: ^3.0.6 - version: 3.6.0 - js-yaml: - specifier: ^4.1.1 - version: 4.1.1 + specifier: ^4.1.0 + version: 4.1.0 lucide-react: specifier: ^0.577.0 version: 0.577.0(react@19.2.4) tailwind-merge: specifier: ^3.4.0 version: 3.5.0 - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@4.2.2) devDependencies: + '@datum-cloud/datum-ui': + specifier: ^0.8.0 + version: 0.8.1(5a5221c0a41b09db639c978c9b3d3f5c) '@monaco-editor/react': specifier: ^4.6.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -69,9 +66,6 @@ importers: '@rollup/plugin-typescript': specifier: ^11.1.6 version: 11.1.6(rollup@4.60.0)(tslib@2.8.1)(typescript@5.9.3) - '@tailwindcss/postcss': - specifier: ^4.2.1 - version: 4.2.2 '@types/react': specifier: ^18.0.0 version: 18.3.28 @@ -84,9 +78,6 @@ importers: '@typescript-eslint/parser': specifier: ^6.15.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - autoprefixer: - specifier: ^10.4.27 - version: 10.4.27(postcss@8.5.8) eslint: specifier: ^8.56.0 version: 8.57.1 @@ -111,12 +102,6 @@ importers: rollup-plugin-peer-deps-external: specifier: ^2.2.4 version: 2.2.4(rollup@4.60.0) - rollup-plugin-postcss: - specifier: ^4.0.2 - version: 4.0.2(postcss@8.5.8) - tailwindcss: - specifier: ^4.2.1 - version: 4.2.2 tslib: specifier: ^2.6.2 version: 2.8.1 @@ -218,6 +203,21 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -357,6 +357,198 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@conform-to/dom@1.19.1': + resolution: {integrity: sha512-h4/T04zgASuiHMVEiXiyQA4jIW9zhpVFbp0HrWCQUDBxd8Zc1JbTarR7cuJyzkmqshC4tdm4VKIHguLp+7AYCw==} + + '@conform-to/react@1.19.1': + resolution: {integrity: sha512-RrSfpy3B7bn1RoXCztlmQYMs113AWcvtqAluDqAx4yVbbVB+EcHWc3Ze/EXJhbtFDmljT2nMtcx/VY8f2RPGFg==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + '@conform-to/zod@1.19.1': + resolution: {integrity: sha512-S/C+Byhk87RdutjCuxhKcDf9lrmTx0V1OrtZPt5zrVG3RqcXVZz3NvLLZ7pNqdSg5DHGvuzPuotGfmPVu936ow==} + peerDependencies: + zod: ^3.21.0 || ^4.0.0 + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@date-fns/tz@1.4.1': + resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + + '@datum-cloud/datum-ui@0.8.1': + resolution: {integrity: sha512-3wHvcTqFWNexixiEoaAUxQfDSmqGuw2eLfbA3xUugzywVW3gz8R4BNAm22NBAO8JhcX4xc3KlfghfFuQmQlVaA==} + peerDependencies: + '@conform-to/react': '>=1' + '@conform-to/zod': '>=1' + '@dnd-kit/core': '>=6' + '@dnd-kit/sortable': '>=8' + '@hookform/resolvers': '>=5.2.2' + '@monaco-editor/react': ^4.7.0 + '@stepperize/react': '>=4' + '@tanstack/react-table': '>=8' + '@tanstack/react-virtual': '>=3' + '@tiptap/extension-character-count': '>=3' + '@tiptap/extension-link': '>=3' + '@tiptap/extension-placeholder': '>=3' + '@tiptap/extension-underline': '>=3' + '@tiptap/react': '>=3' + '@tiptap/starter-kit': '>=3' + date-fns: '>=4.1.0' + date-fns-tz: '>=3' + js-yaml: ^4.1.0 + leaflet: '>=1.9' + leaflet-draw: '>=1' + leaflet.fullscreen: '>=5' + leaflet.markercluster: '>=1.5' + lucide-react: '>=0.400' + monaco-editor: '>=0.44.0' + motion: '>=11' + nprogress: '>=0.2' + nuqs: '>=2' + react: '>=19' + react-day-picker: '>=9' + react-dom: '>=19' + react-dropzone: '>=14' + react-hook-form: '>=7.55' + react-leaflet: '>=5' + react-leaflet-markercluster: '>=5.0.0-rc.0' + react-number-format: '>=5' + recharts: '>=2' + sonner: '>=2' + zod: '>=4' + peerDependenciesMeta: + '@conform-to/react': + optional: true + '@conform-to/zod': + optional: true + '@dnd-kit/core': + optional: true + '@dnd-kit/sortable': + optional: true + '@hookform/resolvers': + optional: true + '@monaco-editor/react': + optional: true + '@stepperize/react': + optional: true + '@tanstack/react-table': + optional: true + '@tanstack/react-virtual': + optional: true + '@tiptap/extension-character-count': + optional: true + '@tiptap/extension-link': + optional: true + '@tiptap/extension-placeholder': + optional: true + '@tiptap/extension-underline': + optional: true + '@tiptap/react': + optional: true + '@tiptap/starter-kit': + optional: true + date-fns: + optional: true + date-fns-tz: + optional: true + js-yaml: + optional: true + leaflet: + optional: true + leaflet-draw: + optional: true + leaflet.fullscreen: + optional: true + leaflet.markercluster: + optional: true + monaco-editor: + optional: true + motion: + optional: true + nprogress: + optional: true + nuqs: + optional: true + react-day-picker: + optional: true + react-dropzone: + optional: true + react-hook-form: + optional: true + react-leaflet: + optional: true + react-leaflet-markercluster: + optional: true + react-number-format: + optional: true + recharts: + optional: true + sonner: + optional: true + zod: + optional: true + + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} @@ -648,6 +840,15 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@floating-ui/core@1.7.4': resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} @@ -663,6 +864,11 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -768,6 +974,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-avatar@1.1.11': + resolution: {integrity: sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-checkbox@1.3.3': resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} peerDependencies: @@ -781,6 +1000,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: @@ -812,6 +1044,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.3': + resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.1.15': resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} peerDependencies: @@ -847,6 +1088,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.3': resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} peerDependencies: @@ -869,6 +1123,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-id@1.1.1': resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} peerDependencies: @@ -878,6 +1145,32 @@ packages: '@types/react': optional: true + '@radix-ui/react-label@2.1.8': + resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popover@1.1.15': resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} peerDependencies: @@ -956,6 +1249,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.11': resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} peerDependencies: @@ -1013,6 +1319,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tabs@1.1.13': resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} peerDependencies: @@ -1075,6 +1394,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.1.1': resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: @@ -1127,6 +1455,24 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-leaflet/core@3.0.0': + resolution: {integrity: sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==} + peerDependencies: + leaflet: ^1.9.0 + react: ^19.0.0 + react-dom: ^19.0.0 + + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@remix-run/dev@2.17.4': resolution: {integrity: sha512-El7r5W6ErX9KIy27+urbc4SIZnIlVDgTOUqzA7Zbv7caKYrsvgj/Z3i/LPy4VNfv0G1EdawPOrygJgIKT4r2FA==} engines: {node: '>=18.0.0'} @@ -1389,6 +1735,29 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@stepperize/core@2.1.0': + resolution: {integrity: sha512-edzIlkAUDrqan3G7XhY32PvciyQoJtyX3Fz8G1LBgSHIg2rMTBsZk8oz39NxT7XFxqibRXf2rvsH81iwg+Dd6w==} + peerDependencies: + typescript: '>=5.0.2' + + '@stepperize/react@6.1.0': + resolution: {integrity: sha512-ywTt0OQDoBKcdzeBK9V1hTsMAQ+zAoIF/au9fcATspdGWfYr/z+mDGZm2cA2rzMJF79J+fUbjZPb+7Js4PtKBA==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tabby_ai/hijri-converter@1.0.5': + resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==} + engines: {node: '>=16.0.0'} + '@tailwindcss/node@4.2.2': resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} @@ -1486,45 +1855,247 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@trysound/sax@0.2.0': - resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} - engines: {node: '>=10.13.0'} + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' - '@types/acorn@4.0.6': - resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + '@tanstack/react-virtual@3.13.24': + resolution: {integrity: sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@tanstack/virtual-core@3.14.0': + resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==} - '@types/estree-jsx@1.0.5': - resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@tiptap/core@3.22.5': + resolution: {integrity: sha512-L1lhWz6ujGny8LduTJ7MBWYhzigwOvfUJUrJ7IzOJSuy3+OAzisdGDD1GV7LEO/hU0Hr2Mkm1wajRIHExvS9HQ==} + peerDependencies: + '@tiptap/pm': 3.22.5 - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@tiptap/extension-blockquote@3.22.5': + resolution: {integrity: sha512-ajyP5W8fG5Hrru47T/eF3xMKOpNvWofgNJqBTeNuGl02sYxsy9a4EunyFxudsaZP9WW3VOD4SaIWr5+MqpbnOQ==} + peerDependencies: + '@tiptap/core': 3.22.5 - '@types/hast@2.3.10': - resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + '@tiptap/extension-bold@3.22.5': + resolution: {integrity: sha512-l/uDtpJISiFFyfctvnODNWBN/XPZI1jVZRacTRDDnSn8+x6KQ7G2qgFYueU7KvVJGDFVT39Iio56mcFRG/Pozg==} + peerDependencies: + '@tiptap/core': 3.22.5 - '@types/js-yaml@4.0.9': - resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@tiptap/extension-bubble-menu@3.22.5': + resolution: {integrity: sha512-yrNlFQQJY5MmhBpmD8tnmaSmyUQrEvgyPKa3bzVeWEhDSG1CW4A0ZSMx3hrA9yFO0HWfw3IJmvSCycEZQBalpQ==} + peerDependencies: + '@tiptap/core': 3.22.5 + '@tiptap/pm': 3.22.5 - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@tiptap/extension-bullet-list@3.22.5': + resolution: {integrity: sha512-cf54fG9AybU8NgPMv1TOcoqAkELeRc/VpnSCt/rIJZphWQx9nsFmrtkrlCatrIcCaGtNZYwlHlMnC5LVVMu0uA==} + peerDependencies: + '@tiptap/extension-list': 3.22.5 - '@types/mdast@3.0.15': - resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + '@tiptap/extension-character-count@3.22.5': + resolution: {integrity: sha512-+5sfRKmDr9/4+EtmM+kI0fuSZa5gb3rLyQjElGY0MPr9y2ieQSw6r1PCMN8dXXKzEg5jTXzw003L2hK1H6LoFw==} + peerDependencies: + '@tiptap/extensions': 3.22.5 - '@types/mdx@2.0.13': - resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + '@tiptap/extension-code-block@3.22.5': + resolution: {integrity: sha512-d123kCfLdJTi4fue1m0+TNFztDkmIRSZGZmGu6H9KqwG5Q7IzjT9o8lzRsz+pXxYqHvqgYmXoEpM6srbzXx/Ag==} + peerDependencies: + '@tiptap/core': 3.22.5 + '@tiptap/pm': 3.22.5 - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@tiptap/extension-code@3.22.5': + resolution: {integrity: sha512-mwDNOJC9rYbDu/JcqrN4dbUQRklJU8Fuk2raxD/IvFw9qUIcPCmxQ2XT9UTKmZz/Ju7Kdy72fss6XpgWv6gLAQ==} + peerDependencies: + '@tiptap/core': 3.22.5 - '@types/node@25.3.2': - resolution: {integrity: sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==} + '@tiptap/extension-document@3.22.5': + resolution: {integrity: sha512-8NJERd+pCtvSuEP4C4WMGYmRRCV12ePZL7bC+QUdFlbdXg+kNZS0zZ7hh879tYA0Kidbi8rWWD1Tx+H2ezkmMw==} + peerDependencies: + '@tiptap/core': 3.22.5 + + '@tiptap/extension-dropcursor@3.22.5': + resolution: {integrity: sha512-Mp40DaFrY3sEUVtFqmxrR0BmU4G3k8GCYYNGqNa9OqWv7BrcFDC03V2n3okESDKt4MKkzhQQmypq+ouLy8dLfA==} + peerDependencies: + '@tiptap/extensions': 3.22.5 + + '@tiptap/extension-floating-menu@3.22.5': + resolution: {integrity: sha512-dhem4sTPhyQgQ+pFp2Oud4k4FSQz9PVMgeQAC9288SmGwxBkJNveDAw6sKTMrumqDvwkJrtslXIupq9TZYQnzg==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.22.5 + '@tiptap/pm': 3.22.5 + + '@tiptap/extension-gapcursor@3.22.5': + resolution: {integrity: sha512-4WkMu7qqjbsm8hCQS+8X+la1wjriN0SKoRdvpfKH33qM50MB34tYJuGLAO+y7TTh4MMMco3AZCKPBL5JVMqNIg==} + peerDependencies: + '@tiptap/extensions': 3.22.5 + + '@tiptap/extension-hard-break@3.22.5': + resolution: {integrity: sha512-n0R2mUVYZU2AVbJhg/WcY9+zx690wVwvsItHJf0DrYbf1tCYHx+PRHUt/AoXk6u8BSmnkb8/FDziS8m3mjfpSg==} + peerDependencies: + '@tiptap/core': 3.22.5 + + '@tiptap/extension-heading@3.22.5': + resolution: {integrity: sha512-hjyEG4947PAhMBfP1G6B0QAh6+y9mp2C5BQmNjprA05/lQzDAT7KFZzNh8ZVp3ol6aICKq/N1gFOW9Dc/9FUOw==} + peerDependencies: + '@tiptap/core': 3.22.5 + + '@tiptap/extension-horizontal-rule@3.22.5': + resolution: {integrity: sha512-vUV0/ugIbXOc8SJib0h8UMhgcqZXWu/dkEhlswZN4VVven1o5enkfxEiDw+OyIJHi5rUkrdhsQ/KTxG/Xb7X8A==} + peerDependencies: + '@tiptap/core': 3.22.5 + '@tiptap/pm': 3.22.5 + + '@tiptap/extension-italic@3.22.5': + resolution: {integrity: sha512-4T8baSiLkeIymTgEwirxDFt5YgYofkP3m1+MGYdGy2HKcOK+1vpvlPhEO1X5qtZngtJW5S4+njKjinRg52A4PA==} + peerDependencies: + '@tiptap/core': 3.22.5 + + '@tiptap/extension-link@3.22.5': + resolution: {integrity: sha512-d671MvF3GPKoS2OVxjIlQ7hIE7MS3hREdR+d4cvnnoiLLD+ZJ6KgDnxmWqF0a1s4qxLWK2KxKRSOIfYGE31QWQ==} + peerDependencies: + '@tiptap/core': 3.22.5 + '@tiptap/pm': 3.22.5 + + '@tiptap/extension-list-item@3.22.5': + resolution: {integrity: sha512-W7uTmyKLhlsvuTPLv+8WwnsY+mlikBFIoLSvVcBaFt4MwpsZ+DeB6KQg02Y7tbtaAnG7rXu9Fvw2QORh2P728A==} + peerDependencies: + '@tiptap/extension-list': 3.22.5 + + '@tiptap/extension-list-keymap@3.22.5': + resolution: {integrity: sha512-cGUnxJ0y515e1bVHNjUmbx7oWHoEon59w6BA5N2KwV9iW2mZZchlTX4yxJSOX+ixeVRChsa7YwC3Z1jUZ6AMEg==} + peerDependencies: + '@tiptap/extension-list': 3.22.5 + + '@tiptap/extension-list@3.22.5': + resolution: {integrity: sha512-cVO3ZHCgxAWZ4zrFSs81FO2nyCk1wb2EHkpLpW98FzbJLkN9rDkazhW99P3HRWy/CvUldOT+8ecI1YrQtBojMg==} + peerDependencies: + '@tiptap/core': 3.22.5 + '@tiptap/pm': 3.22.5 + + '@tiptap/extension-ordered-list@3.22.5': + resolution: {integrity: sha512-OXdh4k4CNrukwiSdWdEQ49uvgnqvR0Z9aNSP4HI5/kZQ/Te1NtRtYCpUrzWyO/7CtjcCisXHti0o9C/TV8YMbQ==} + peerDependencies: + '@tiptap/extension-list': 3.22.5 + + '@tiptap/extension-paragraph@3.22.5': + resolution: {integrity: sha512-52KCto4+XKpnBWpIufspWLyq4UWxAWC72ANPdGuIhbi72NRTabiTbTVN40uwGSPkyakeESG0/vKdWJCVvB4f0g==} + peerDependencies: + '@tiptap/core': 3.22.5 + + '@tiptap/extension-placeholder@3.22.5': + resolution: {integrity: sha512-MZAohQ3FCS763BkhGXgaWRya6WruZjwRwEAkXP8vkxbERzl2OJRjniS4uXCWzAlRb3ttE103SnY7LMdM8FvsXw==} + peerDependencies: + '@tiptap/extensions': 3.22.5 + + '@tiptap/extension-strike@3.22.5': + resolution: {integrity: sha512-42WrrFK5gOom/0znH85x12Mw5IQ/6O6DWdyUWoRIrNA/qJpuHtU8oVU+bIgU2tuomMGHruRjIzgBQv5sBjEtww==} + peerDependencies: + '@tiptap/core': 3.22.5 + + '@tiptap/extension-text@3.22.5': + resolution: {integrity: sha512-bzpDOdAEo1JeoVZDIyV0oY0jGXkEG+AzF70SzHoRSjOvFDtKWunyXf9eO1OnOr2/fmMcckT2qwUBNBMQplWBzw==} + peerDependencies: + '@tiptap/core': 3.22.5 + + '@tiptap/extension-underline@3.22.5': + resolution: {integrity: sha512-9ut09rJD0iEbS6sk7yd2j6IwuFDLTNmDEGTDLodvqAfi+bq7ddsTDv0YviXoZaA9sdHAdTEVr2ITy2m6WK5jpA==} + peerDependencies: + '@tiptap/core': 3.22.5 + + '@tiptap/extensions@3.22.5': + resolution: {integrity: sha512-Ifg4MzKCj3uRqe3ieTwYnomu2y4p7EXr2avVSKZYfh12i2dyWe2Gkn1KuZDREANVE+gHqFlQjJRYzhJFwzSCrg==} + peerDependencies: + '@tiptap/core': 3.22.5 + '@tiptap/pm': 3.22.5 + + '@tiptap/pm@3.22.5': + resolution: {integrity: sha512-Cr9Mv4igxvI2tKMiahw48sZxva3PfDzypErH8IB82N+9qa9n9ygVMt0BOaDg53hLKxEEVeYr2S/wCcJIVFgBTw==} + + '@tiptap/react@3.22.5': + resolution: {integrity: sha512-36WHEs+vPmB//V1ff7Ujcnpz7Ey5g8lhpI/0+hoanSbdiPMTQ7qZVWwMovIkMKDlqWVp2fxBgeYM1861jyFzTw==} + peerDependencies: + '@tiptap/core': 3.22.5 + '@tiptap/pm': 3.22.5 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@3.22.5': + resolution: {integrity: sha512-LZ/LYbwH6rnDi5DnRyagkuNsYAVyhM+yJvvz+ZuYA0JkPiTXJV86J5PWSKew8M0gVfMHcNVtKjfQCvViFCeIgw==} + + '@types/acorn@4.0.6': + resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@2.3.10': + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mdast@3.0.15': + resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@25.3.2': + resolution: {integrity: sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==} '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -1549,6 +2120,9 @@ packages: '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@typescript-eslint/eslint-plugin@6.21.0': resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1726,6 +2300,10 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + autoprefixer@10.4.27: resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} engines: {node: ^10 || ^12 || >=14} @@ -1755,6 +2333,9 @@ packages: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1766,9 +2347,6 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1821,9 +2399,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-api@3.0.0: - resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001774: resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} @@ -1893,16 +2468,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - colord@2.9.3: - resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} - comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -1917,9 +2485,6 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concat-with-sourcemaps@1.1.0: - resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==} - confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -1955,18 +2520,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css-declaration-sorter@6.4.1: - resolution: {integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==} - engines: {node: ^10 || ^12 || >=14} - peerDependencies: - postcss: ^8.0.9 - - css-select@4.3.0: - resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} - - css-tree@1.1.3: - resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} - engines: {node: '>=8.0.0'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} @@ -1977,35 +2533,61 @@ packages: engines: {node: '>=4'} hasBin: true - cssnano-preset-default@5.2.14: - resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - cssnano-utils@3.1.0: - resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} - cssnano@5.1.15: - resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} - csso@4.2.0: - resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} - engines: {node: '>=8.0.0'} + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} data-uri-to-buffer@3.0.1: resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} engines: {node: '>= 6'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2018,8 +2600,16 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - date-fns@3.6.0: - resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} + peerDependencies: + date-fns: ^3.0.0 || ^4.0.0 + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} @@ -2038,6 +2628,12 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -2105,21 +2701,11 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} - dom-serializer@1.4.1: - resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@4.3.1: - resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} - engines: {node: '>= 4'} - dompurify@3.2.7: resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} - domutils@2.8.0: - resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dompurify@3.4.2: + resolution: {integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==} dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} @@ -2158,8 +2744,9 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} - entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} err-code@2.0.3: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} @@ -2199,6 +2786,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.46.1: + resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + esbuild-plugins-node-modules-polyfill@1.8.1: resolution: {integrity: sha512-7vxzmyTFDhYUNhjlciMPmp32eUafNIHiXvo8ZD22PzccnxMoGpPnsYn17gSBoFZgpRYNdCJcAWsQ60YVKgKg3A==} engines: {node: '>=14.0.0'} @@ -2290,9 +2880,6 @@ packages: estree-util-visit@1.2.1: resolution: {integrity: sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==} - estree-walker@0.6.1: - resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} - estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -2315,8 +2902,8 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} @@ -2339,6 +2926,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -2368,6 +2959,10 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + file-selector@2.1.2: + resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} + engines: {node: '>= 12'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2406,6 +3001,20 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + framer-motion@12.38.0: + resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -2563,6 +3172,10 @@ packages: resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -2575,9 +3188,6 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - icss-replace-symbols@1.1.0: - resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} - icss-utils@5.1.0: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -2591,18 +3201,16 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - import-cwd@3.0.0: - resolution: {integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==} - engines: {node: '>=8'} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-from@3.0.0: - resolution: {integrity: sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==} - engines: {node: '>=8'} - imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -2625,6 +3233,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2747,6 +3359,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -2810,6 +3425,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic-dompurify@3.11.0: + resolution: {integrity: sha512-il0sNhLnfawc6vKoMqnZX1JXzEzw0pP3DZWg7mM7VJNJ8cq6DCxeAjEcfjTazyBF8dkUn+rBsBPS5NQs4ZaI3g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -2831,6 +3450,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -2868,6 +3496,22 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + leaflet-draw@1.0.4: + resolution: {integrity: sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==} + + leaflet.fullscreen@5.3.1: + resolution: {integrity: sha512-2IO5WJ5xpQWyn2ZLICwwpyiWJ2KdZMuxAUCsrK7b4dj770GZ/zwPp4uVQNKOxqnz9xRWEN1d2VNpLaX4ptSdnA==} + peerDependencies: + leaflet: ^1.7.0 || >=2.0.0-alpha.1 + + leaflet.markercluster@1.5.3: + resolution: {integrity: sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==} + peerDependencies: + leaflet: ^1.3.1 + + leaflet@1.9.4: + resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -2946,14 +3590,13 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + loader-utils@3.3.1: resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} engines: {node: '>= 12.13.0'} @@ -2972,15 +3615,9 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.uniq@4.5.0: - resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} @@ -2998,6 +3635,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3059,8 +3700,8 @@ packages: mdast-util-to-string@3.2.0: resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} - mdn-data@2.0.14: - resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} media-query-parser@2.0.2: resolution: {integrity: sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==} @@ -3258,6 +3899,26 @@ packages: resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} engines: {node: '>= 0.8.0'} + motion-dom@12.38.0: + resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} + + motion-utils@12.36.0: + resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} + + motion@12.38.0: + resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -3303,10 +3964,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-url@6.1.0: - resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} - engines: {node: '>=10'} - npm-install-checks@6.3.0: resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -3327,8 +3984,26 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nuqs@2.8.9: + resolution: {integrity: sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ==} + peerDependencies: + '@remix-run/react': '>=2' + '@tanstack/react-router': ^1 + next: '>=14.2.0' + react: '>=18.2.0 || ^19.0.0-0' + react-router: ^5 || ^6 || ^7 + react-router-dom: ^5 || ^6 || ^7 + peerDependenciesMeta: + '@remix-run/react': + optional: true + '@tanstack/react-router': + optional: true + next: + optional: true + react-router: + optional: true + react-router-dom: + optional: true object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -3385,6 +4060,9 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + outdent@0.8.0: resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} @@ -3392,10 +4070,6 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} - p-finally@1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} - engines: {node: '>=4'} - p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -3408,14 +4082,6 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} - p-queue@6.6.2: - resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} - engines: {node: '>=8'} - - p-timeout@3.2.0: - resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} - engines: {node: '>=8'} - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -3433,6 +4099,9 @@ packages: resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} engines: {node: '>=6'} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -3491,10 +4160,6 @@ packages: engines: {node: '>=0.10'} hasBin: true - pify@5.0.0: - resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} - engines: {node: '>=10'} - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -3515,59 +4180,12 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-calc@8.2.4: - resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} - peerDependencies: - postcss: ^8.2.2 - - postcss-colormin@5.3.1: - resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-convert-values@5.1.3: - resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-comments@5.1.2: - resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - postcss-discard-duplicates@5.1.0: resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 - postcss-discard-empty@5.1.1: - resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-overridden@5.1.0: - resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-load-config@3.1.4: - resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} - engines: {node: '>= 10'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - postcss-load-config@4.0.2: resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} engines: {node: '>= 14'} @@ -3580,57 +4198,21 @@ packages: ts-node: optional: true - postcss-merge-longhand@5.1.7: - resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} - engines: {node: ^10 || ^12 || >=14.0} + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.1.0 - postcss-merge-rules@5.1.4: - resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} - engines: {node: ^10 || ^12 || >=14.0} + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.1.0 - postcss-minify-font-values@5.1.0: - resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-gradients@5.1.1: - resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-params@5.1.4: - resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-selectors@5.2.1: - resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-modules-extract-imports@3.1.0: - resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-local-by-default@4.2.0: - resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-scope@3.2.1: - resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} - engines: {node: ^10 || ^12 || >= 14} + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 @@ -3640,108 +4222,15 @@ packages: peerDependencies: postcss: ^8.1.0 - postcss-modules@4.3.1: - resolution: {integrity: sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==} - peerDependencies: - postcss: ^8.0.0 - postcss-modules@6.0.1: resolution: {integrity: sha512-zyo2sAkVvuZFFy0gc2+4O+xar5dYlaVy/ebO24KT0ftk/iJevSNyPyQellsBLlnccwh7f6V6Y4GvuKRYToNgpQ==} peerDependencies: postcss: ^8.0.0 - postcss-normalize-charset@5.1.0: - resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-display-values@5.1.0: - resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-positions@5.1.1: - resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-repeat-style@5.1.1: - resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-string@5.1.0: - resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-timing-functions@5.1.0: - resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-unicode@5.1.1: - resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-url@5.1.0: - resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-whitespace@5.1.1: - resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-ordered-values@5.1.3: - resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-reduce-initial@5.1.2: - resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-reduce-transforms@5.1.0: - resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} - engines: {node: '>=4'} - postcss-selector-parser@7.1.1: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} - postcss-svgo@5.1.0: - resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-unique-selectors@5.1.1: - resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -3781,16 +4270,48 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} - promise.series@0.2.0: - resolution: {integrity: sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==} - engines: {node: '>=0.12'} - prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + prosemirror-changeset@2.4.1: + resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.41.8: + resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3826,6 +4347,12 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} + react-day-picker@9.14.0: + resolution: {integrity: sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==} + engines: {node: '>=18'} + peerDependencies: + react: '>=16.8.0' + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -3836,9 +4363,54 @@ packages: peerDependencies: react: ^19.2.4 + react-dropzone@15.0.0: + resolution: {integrity: sha512-lGjYV/EoqEjEWPnmiSvH4v5IoIAwQM2W4Z1C0Q/Pw2xD0eVzKPS359BQTUMum+1fa0kH2nrKjuavmTPOGhpLPg==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + + react-hook-form@7.74.0: + resolution: {integrity: sha512-yR6wHr99p9wFv686jhRWVSFhUvDvNbdUf2dKlbno8/VKOCuoNobDGC6S+M2dua9A9Yo8vpcrp8assIYbsZCQ9g==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-leaflet-markercluster@5.0.0-rc.0: + resolution: {integrity: sha512-jWa4bPD5LfLV3Lid1RWgl+yKUuQtnqeYtJzzLb/fiRjvX+rtwzY8pMoUFuygqyxNrWxMTQlWKBHxkpI7Sxvu4Q==} + peerDependencies: + leaflet: ^1.9.4 + leaflet.markercluster: ^1.5.3 + react: ^19.0.0 + react-leaflet: ^5.0.0 + + react-leaflet@5.0.0: + resolution: {integrity: sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==} + peerDependencies: + leaflet: ^1.9.0 + react: ^19.0.0 + react-dom: ^19.0.0 + + react-number-format@5.4.5: + resolution: {integrity: sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw==} + peerDependencies: + react: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -3905,6 +4477,22 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + recharts@3.8.1: + resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -3929,17 +4517,20 @@ packages: remark-rehype@10.1.0: resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + require-like@0.1.2: resolution: {integrity: sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve.exports@2.0.3: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} @@ -3976,20 +4567,14 @@ packages: peerDependencies: rollup: '*' - rollup-plugin-postcss@4.0.2: - resolution: {integrity: sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==} - engines: {node: '>=10'} - peerDependencies: - postcss: 8.x - - rollup-pluginutils@2.8.2: - resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} - rollup@4.60.0: resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4007,9 +4592,6 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-identifier@0.4.2: - resolution: {integrity: sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==} - safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -4021,6 +4603,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -4097,6 +4683,12 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4131,10 +4723,6 @@ packages: resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - stable@0.1.8: - resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} - deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' - state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} @@ -4211,18 +4799,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - style-inject@0.3.0: - resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==} - style-to-object@0.4.4: resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} - stylehacks@5.1.1: - resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -4231,10 +4810,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svgo@2.8.0: - resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} - engines: {node: '>=10.13.0'} - hasBin: true + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} @@ -4269,6 +4846,16 @@ packages: through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tldts-core@7.0.29: + resolution: {integrity: sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==} + + tldts@7.0.29: + resolution: {integrity: sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4280,6 +4867,14 @@ packages: toml@3.0.0: resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -4302,6 +4897,9 @@ packages: turbo-stream@2.4.1: resolution: {integrity: sha512-v8kOJXpG3WoTN/+at8vK7erSzo6nW6CIaeOvNOkHQVDajfz1ZVeSxCbc6tOH4hrGZW7VUCV0TOXd8CPzYnYkrw==} + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4349,6 +4947,10 @@ packages: resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} engines: {node: '>=18.17'} + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + unified@10.1.2: resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} @@ -4421,6 +5023,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4461,6 +5068,9 @@ packages: vfile@5.3.7: resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite-node@1.6.1: resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -4502,6 +5112,13 @@ packages: terser: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -4512,6 +5129,18 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -4565,6 +5194,13 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -4575,10 +5211,6 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} @@ -4588,6 +5220,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@4.4.1: + resolution: {integrity: sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -4595,6 +5230,26 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -4788,6 +5443,150 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@conform-to/dom@1.19.1': + optional: true + + '@conform-to/react@1.19.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@conform-to/dom': 1.19.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optional: true + + '@conform-to/zod@1.19.1(zod@4.4.1)': + dependencies: + '@conform-to/dom': 1.19.1 + zod: 4.4.1 + optional: true + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@date-fns/tz@1.4.1': + optional: true + + '@datum-cloud/datum-ui@0.8.1(5a5221c0a41b09db639c978c9b3d3f5c)': + dependencies: + '@dnd-kit/utilities': 3.2.2(react@19.2.4) + '@radix-ui/react-avatar': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-label': 2.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.4(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + class-variance-authority: 0.7.1 + clsx: 2.1.1 + cmdk: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + isomorphic-dompurify: 3.11.0 + lucide-react: 0.577.0(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tailwind-merge: 3.5.0 + tw-animate-css: 1.4.0 + optionalDependencies: + '@conform-to/react': 1.19.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@conform-to/zod': 1.19.1(zod@4.4.1) + '@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@dnd-kit/sortable': 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + '@hookform/resolvers': 5.2.2(react-hook-form@7.74.0(react@19.2.4)) + '@monaco-editor/react': 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@stepperize/react': 6.1.0(react@19.2.4)(typescript@5.9.3) + '@tanstack/react-table': 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/react-virtual': 3.13.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tiptap/extension-character-count': 3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + '@tiptap/extension-link': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-placeholder': 3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + '@tiptap/extension-underline': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + '@tiptap/react': 3.22.5(@floating-ui/dom@1.7.5)(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tiptap/starter-kit': 3.22.5 + date-fns: 4.1.0 + date-fns-tz: 3.2.0(date-fns@4.1.0) + js-yaml: 4.1.1 + leaflet: 1.9.4 + leaflet-draw: 1.0.4 + leaflet.fullscreen: 5.3.1(leaflet@1.9.4) + leaflet.markercluster: 1.5.3(leaflet@1.9.4) + monaco-editor: 0.55.1 + motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + nuqs: 2.8.9(@remix-run/react@2.17.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-router-dom@6.30.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@6.30.3(react@19.2.4))(react@19.2.4) + react-day-picker: 9.14.0(react@19.2.4) + react-dropzone: 15.0.0(react@19.2.4) + react-hook-form: 7.74.0(react@19.2.4) + react-leaflet: 5.0.0(leaflet@1.9.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-leaflet-markercluster: 5.0.0-rc.0(leaflet.markercluster@1.5.3(leaflet@1.9.4))(leaflet@1.9.4)(react-dom@19.2.4(react@19.2.4))(react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + react-number-format: 5.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + recharts: 3.8.1(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4)(redux@5.0.1) + sonner: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + zod: 4.4.1 + transitivePeerDependencies: + - '@noble/hashes' + - '@types/react' + - '@types/react-dom' + - canvas + + '@dnd-kit/accessibility@3.1.1(react@19.2.4)': + dependencies: + react: 19.2.4 + tslib: 2.8.1 + optional: true + + '@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.2.4) + '@dnd-kit/utilities': 3.2.2(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tslib: 2.8.1 + optional: true + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@dnd-kit/utilities': 3.2.2(react@19.2.4) + react: 19.2.4 + tslib: 2.8.1 + optional: true + + '@dnd-kit/utilities@3.2.2(react@19.2.4)': + dependencies: + react: 19.2.4 + tslib: 2.8.1 + '@emotion/hash@0.9.2': {} '@esbuild/aix-ppc64@0.21.5': @@ -4948,6 +5747,8 @@ snapshots: '@eslint/js@8.57.1': {} + '@exodus/bytes@1.15.0': {} + '@floating-ui/core@1.7.4': dependencies: '@floating-ui/utils': 0.2.10 @@ -4965,6 +5766,12 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@hookform/resolvers@5.2.2(react-hook-form@7.74.0(react@19.2.4))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.74.0(react@19.2.4) + optional: true + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -5112,16 +5919,45 @@ snapshots: '@types/react': 18.3.28 '@types/react-dom': 18.3.7(@types/react@18.3.28) - '@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-avatar@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@19.2.4) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@19.2.4) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-context': 1.1.3(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: @@ -5152,6 +5988,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.28 + '@radix-ui/react-context@1.1.3(@types/react@18.3.28)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 18.3.28 + '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -5193,6 +6035,21 @@ snapshots: '@types/react': 18.3.28 '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.28)(react@19.2.4)': dependencies: react: 19.2.4 @@ -5210,6 +6067,23 @@ snapshots: '@types/react': 18.3.28 '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@radix-ui/react-id@1.1.1(@types/react@18.3.28)(react@19.2.4)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@19.2.4) @@ -5217,6 +6091,41 @@ snapshots: optionalDependencies: '@types/react': 18.3.28 + '@radix-ui/react-label@2.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@19.2.4) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -5296,6 +6205,24 @@ snapshots: '@types/react': 18.3.28 '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -5365,6 +6292,21 @@ snapshots: optionalDependencies: '@types/react': 18.3.28 + '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -5429,6 +6371,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.28 + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@18.3.28)(react@19.2.4)': + dependencies: + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 18.3.28 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.28)(react@19.2.4)': dependencies: react: 19.2.4 @@ -5466,6 +6415,26 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + leaflet: 1.9.4 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optional: true + + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@18.3.28)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.4 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.4 + react-redux: 9.2.0(@types/react@18.3.28)(react@19.2.4)(redux@5.0.1) + optional: true + '@remix-run/dev@2.17.4(@remix-run/react@2.17.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.4(typescript@5.9.3))(@types/node@25.3.2)(lightningcss@1.32.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.3.2)(lightningcss@1.32.0))': dependencies: '@babel/core': 7.29.0 @@ -5576,6 +6545,19 @@ snapshots: optionalDependencies: typescript: 5.9.3 + '@remix-run/react@2.17.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)': + dependencies: + '@remix-run/router': 1.23.2 + '@remix-run/server-runtime': 2.17.4(typescript@5.9.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-router: 6.30.3(react@19.2.4) + react-router-dom: 6.30.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + turbo-stream: 2.4.1 + optionalDependencies: + typescript: 5.9.3 + optional: true + '@remix-run/router@1.23.2': {} '@remix-run/serve@2.17.4(typescript@5.9.3)': @@ -5746,6 +6728,31 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.0': optional: true + '@standard-schema/spec@1.0.0': + optional: true + + '@standard-schema/spec@1.1.0': + optional: true + + '@standard-schema/utils@0.3.0': + optional: true + + '@stepperize/core@2.1.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + optional: true + + '@stepperize/react@6.1.0(react@19.2.4)(typescript@5.9.3)': + dependencies: + '@stepperize/core': 2.1.0(typescript@5.9.3) + react: 19.2.4 + transitivePeerDependencies: + - typescript + optional: true + + '@tabby_ai/hijri-converter@1.0.5': + optional: true + '@tailwindcss/node@4.2.2': dependencies: '@jridgewell/remapping': 2.3.5 @@ -5776,58 +6783,316 @@ snapshots: '@tailwindcss/oxide-linux-arm64-musl@4.2.2': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide@4.2.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-x64': 4.2.2 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + + '@tailwindcss/postcss@4.2.2': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + postcss: 8.5.8 + tailwindcss: 4.2.2 + + '@tailwindcss/vite@4.2.2(vite@5.4.21(@types/node@25.3.2)(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + tailwindcss: 4.2.2 + vite: 5.4.21(@types/node@25.3.2)(lightningcss@1.32.0) + + '@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optional: true + + '@tanstack/react-virtual@3.13.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/virtual-core': 3.14.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optional: true + + '@tanstack/table-core@8.21.3': + optional: true + + '@tanstack/virtual-core@3.14.0': + optional: true + + '@tiptap/core@3.22.5(@tiptap/pm@3.22.5)': + dependencies: + '@tiptap/pm': 3.22.5 + optional: true + + '@tiptap/extension-blockquote@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-bold@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-bubble-menu@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + dependencies: + '@floating-ui/dom': 1.7.5 + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/pm': 3.22.5 + optional: true + + '@tiptap/extension-bullet-list@3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-character-count@3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/extensions': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-code-block@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/pm': 3.22.5 + optional: true + + '@tiptap/extension-code@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-document@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-dropcursor@3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/extensions': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-floating-menu@3.22.5(@floating-ui/dom@1.7.5)(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + dependencies: + '@floating-ui/dom': 1.7.5 + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/pm': 3.22.5 + optional: true + + '@tiptap/extension-gapcursor@3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/extensions': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-hard-break@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-heading@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-horizontal-rule@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/pm': 3.22.5 + optional: true + + '@tiptap/extension-italic@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-link@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/pm': 3.22.5 + linkifyjs: 4.3.2 + optional: true + + '@tiptap/extension-list-item@3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-list-keymap@3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/pm': 3.22.5 + optional: true + + '@tiptap/extension-ordered-list@3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-paragraph@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-placeholder@3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/extensions': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-strike@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-text@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extension-underline@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + optional: true + + '@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/pm': 3.22.5 + optional: true + + '@tiptap/pm@3.22.5': + dependencies: + prosemirror-changeset: 2.4.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + optional: true + + '@tiptap/react@3.22.5(@floating-ui/dom@1.7.5)(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/pm': 3.22.5 + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@types/use-sync-external-store': 0.0.6 + fast-equals: 5.4.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-floating-menu': 3.22.5(@floating-ui/dom@1.7.5)(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + transitivePeerDependencies: + - '@floating-ui/dom' + optional: true + + '@tiptap/starter-kit@3.22.5': + dependencies: + '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/extension-blockquote': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + '@tiptap/extension-bold': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + '@tiptap/extension-bullet-list': 3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + '@tiptap/extension-code': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + '@tiptap/extension-code-block': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-document': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + '@tiptap/extension-dropcursor': 3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + '@tiptap/extension-gapcursor': 3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + '@tiptap/extension-hard-break': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + '@tiptap/extension-heading': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + '@tiptap/extension-horizontal-rule': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-italic': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + '@tiptap/extension-link': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-list-item': 3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + '@tiptap/extension-list-keymap': 3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + '@tiptap/extension-ordered-list': 3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + '@tiptap/extension-paragraph': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + '@tiptap/extension-strike': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + '@tiptap/extension-text': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + '@tiptap/extension-underline': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + '@tiptap/extensions': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/pm': 3.22.5 optional: true - '@tailwindcss/oxide-linux-x64-musl@4.2.2': + '@types/acorn@4.0.6': + dependencies: + '@types/estree': 1.0.8 + + '@types/cookie@0.6.0': {} + + '@types/d3-array@3.2.2': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.2.2': + '@types/d3-color@3.1.3': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + '@types/d3-ease@3.0.2': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 optional: true - '@tailwindcss/oxide@4.2.2': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-x64': 4.2.2 - '@tailwindcss/oxide-freebsd-x64': 4.2.2 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 - '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 - '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-x64-musl': 4.2.2 - '@tailwindcss/oxide-wasm32-wasi': 4.2.2 - '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 - '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + '@types/d3-path@3.1.1': + optional: true - '@tailwindcss/postcss@4.2.2': + '@types/d3-scale@4.0.9': dependencies: - '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.2.2 - '@tailwindcss/oxide': 4.2.2 - postcss: 8.5.8 - tailwindcss: 4.2.2 + '@types/d3-time': 3.0.4 + optional: true - '@tailwindcss/vite@4.2.2(vite@5.4.21(@types/node@25.3.2)(lightningcss@1.32.0))': + '@types/d3-shape@3.1.8': dependencies: - '@tailwindcss/node': 4.2.2 - '@tailwindcss/oxide': 4.2.2 - tailwindcss: 4.2.2 - vite: 5.4.21(@types/node@25.3.2)(lightningcss@1.32.0) - - '@trysound/sax@0.2.0': {} + '@types/d3-path': 3.1.1 + optional: true - '@types/acorn@4.0.6': - dependencies: - '@types/estree': 1.0.8 + '@types/d3-time@3.0.4': + optional: true - '@types/cookie@0.6.0': {} + '@types/d3-timer@3.0.2': + optional: true '@types/debug@4.1.12': dependencies: @@ -5879,6 +7144,9 @@ snapshots: '@types/unist@2.0.11': {} + '@types/use-sync-external-store@0.0.6': + optional: true + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -6139,6 +7407,9 @@ snapshots: async-function@1.0.0: {} + attr-accept@2.2.5: + optional: true + autoprefixer@10.4.27(postcss@8.5.8): dependencies: browserslist: 4.28.1 @@ -6164,6 +7435,10 @@ snapshots: dependencies: safe-buffer: 5.1.2 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} bl@4.1.0: @@ -6189,8 +7464,6 @@ snapshots: transitivePeerDependencies: - supports-color - boolbase@1.0.0: {} - brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -6261,13 +7534,6 @@ snapshots: callsites@3.1.0: {} - caniuse-api@3.0.0: - dependencies: - browserslist: 4.28.1 - caniuse-lite: 1.0.30001774 - lodash.memoize: 4.1.2 - lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001774: {} ccount@2.0.1: {} @@ -6335,12 +7601,8 @@ snapshots: color-name@1.1.4: {} - colord@2.9.3: {} - comma-separated-tokens@2.0.3: {} - commander@7.2.0: {} - commondir@1.0.1: {} compressible@2.0.18: @@ -6361,10 +7623,6 @@ snapshots: concat-map@0.0.1: {} - concat-with-sourcemaps@1.1.0: - dependencies: - source-map: 0.6.1 - confbox@0.1.8: {} confbox@0.2.4: {} @@ -6391,79 +7649,75 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-declaration-sorter@6.4.1(postcss@8.5.8): + css-tree@3.2.1: dependencies: - postcss: 8.5.8 + mdn-data: 2.27.1 + source-map-js: 1.2.1 - css-select@4.3.0: - dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 4.3.1 - domutils: 2.8.0 - nth-check: 2.1.1 + css-what@6.2.2: {} + + cssesc@3.0.0: {} + + csstype@3.2.3: {} - css-tree@1.1.3: + d3-array@3.2.4: dependencies: - mdn-data: 2.0.14 - source-map: 0.6.1 + internmap: 2.0.3 + optional: true - css-what@6.2.2: {} + d3-color@3.1.0: + optional: true - cssesc@3.0.0: {} + d3-ease@3.0.1: + optional: true + + d3-format@3.1.2: + optional: true - cssnano-preset-default@5.2.14(postcss@8.5.8): + d3-interpolate@3.0.1: dependencies: - css-declaration-sorter: 6.4.1(postcss@8.5.8) - cssnano-utils: 3.1.0(postcss@8.5.8) - postcss: 8.5.8 - postcss-calc: 8.2.4(postcss@8.5.8) - postcss-colormin: 5.3.1(postcss@8.5.8) - postcss-convert-values: 5.1.3(postcss@8.5.8) - postcss-discard-comments: 5.1.2(postcss@8.5.8) - postcss-discard-duplicates: 5.1.0(postcss@8.5.8) - postcss-discard-empty: 5.1.1(postcss@8.5.8) - postcss-discard-overridden: 5.1.0(postcss@8.5.8) - postcss-merge-longhand: 5.1.7(postcss@8.5.8) - postcss-merge-rules: 5.1.4(postcss@8.5.8) - postcss-minify-font-values: 5.1.0(postcss@8.5.8) - postcss-minify-gradients: 5.1.1(postcss@8.5.8) - postcss-minify-params: 5.1.4(postcss@8.5.8) - postcss-minify-selectors: 5.2.1(postcss@8.5.8) - postcss-normalize-charset: 5.1.0(postcss@8.5.8) - postcss-normalize-display-values: 5.1.0(postcss@8.5.8) - postcss-normalize-positions: 5.1.1(postcss@8.5.8) - postcss-normalize-repeat-style: 5.1.1(postcss@8.5.8) - postcss-normalize-string: 5.1.0(postcss@8.5.8) - postcss-normalize-timing-functions: 5.1.0(postcss@8.5.8) - postcss-normalize-unicode: 5.1.1(postcss@8.5.8) - postcss-normalize-url: 5.1.0(postcss@8.5.8) - postcss-normalize-whitespace: 5.1.1(postcss@8.5.8) - postcss-ordered-values: 5.1.3(postcss@8.5.8) - postcss-reduce-initial: 5.1.2(postcss@8.5.8) - postcss-reduce-transforms: 5.1.0(postcss@8.5.8) - postcss-svgo: 5.1.0(postcss@8.5.8) - postcss-unique-selectors: 5.1.1(postcss@8.5.8) - - cssnano-utils@3.1.0(postcss@8.5.8): + d3-color: 3.1.0 + optional: true + + d3-path@3.1.0: + optional: true + + d3-scale@4.0.2: dependencies: - postcss: 8.5.8 + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + optional: true - cssnano@5.1.15(postcss@8.5.8): + d3-shape@3.2.0: dependencies: - cssnano-preset-default: 5.2.14(postcss@8.5.8) - lilconfig: 2.1.0 - postcss: 8.5.8 - yaml: 1.10.2 + d3-path: 3.1.0 + optional: true - csso@4.2.0: + d3-time-format@4.1.0: dependencies: - css-tree: 1.1.3 + d3-time: 3.1.0 + optional: true - csstype@3.2.3: {} + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + optional: true + + d3-timer@3.0.1: + optional: true data-uri-to-buffer@3.0.1: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -6482,7 +7736,15 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - date-fns@3.6.0: {} + date-fns-jalali@4.1.0-0: + optional: true + + date-fns-tz@3.2.0(date-fns@4.1.0): + dependencies: + date-fns: 4.1.0 + optional: true + + date-fns@4.1.0: {} debug@2.6.9: dependencies: @@ -6492,6 +7754,11 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: + optional: true + + decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -6544,27 +7811,13 @@ snapshots: dependencies: esutils: 2.0.3 - dom-serializer@1.4.1: - dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - entities: 2.2.0 - - domelementtype@2.3.0: {} - - domhandler@4.3.1: - dependencies: - domelementtype: 2.3.0 - dompurify@3.2.7: optionalDependencies: '@types/trusted-types': 2.0.7 - domutils@2.8.0: - dependencies: - dom-serializer: 1.4.1 - domelementtype: 2.3.0 - domhandler: 4.3.1 + dompurify@3.4.2: + optionalDependencies: + '@types/trusted-types': 2.0.7 dotenv@16.6.1: {} @@ -6602,7 +7855,7 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.2 - entities@2.2.0: {} + entities@8.0.0: {} err-code@2.0.3: {} @@ -6709,6 +7962,9 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.46.1: + optional: true + esbuild-plugins-node-modules-polyfill@1.8.1(esbuild@0.17.6): dependencies: '@jspm/core': 2.1.0 @@ -6894,8 +8150,6 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/unist': 2.0.11 - estree-walker@0.6.1: {} - estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -6913,7 +8167,8 @@ snapshots: event-target-shim@5.0.1: {} - eventemitter3@4.0.7: {} + eventemitter3@5.0.4: + optional: true execa@5.1.1: dependencies: @@ -6971,6 +8226,9 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.4.0: + optional: true + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6999,6 +8257,11 @@ snapshots: dependencies: flat-cache: 3.2.0 + file-selector@2.1.2: + dependencies: + tslib: 2.8.1 + optional: true + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -7043,6 +8306,16 @@ snapshots: fraction.js@5.3.4: {} + framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + motion-dom: 12.38.0 + motion-utils: 12.36.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optional: true + fresh@0.5.2: {} fs-constants@1.0.0: {} @@ -7227,6 +8500,12 @@ snapshots: dependencies: lru-cache: 7.18.3 + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -7241,8 +8520,6 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-replace-symbols@1.1.0: {} - icss-utils@5.1.0(postcss@8.5.8): dependencies: postcss: 8.5.8 @@ -7251,19 +8528,17 @@ snapshots: ignore@5.3.2: {} - import-cwd@3.0.0: - dependencies: - import-from: 3.0.0 + immer@10.2.0: + optional: true + + immer@11.1.4: + optional: true import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - import-from@3.0.0: - dependencies: - resolve-from: 5.0.0 - imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -7283,6 +8558,9 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + internmap@2.0.3: + optional: true + ipaddr.js@1.9.1: {} is-alphabetical@2.0.1: {} @@ -7392,6 +8670,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -7451,6 +8731,14 @@ snapshots: isexe@2.0.0: {} + isomorphic-dompurify@3.11.0: + dependencies: + dompurify: 3.4.2 + jsdom: 29.1.1 + transitivePeerDependencies: + - '@noble/hashes' + - canvas + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -7476,6 +8764,32 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@exodus/bytes': 1.15.0 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.5 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.0.2: {} json-buffer@3.0.1: {} @@ -7507,6 +8821,22 @@ snapshots: kleur@4.1.5: {} + leaflet-draw@1.0.4: + optional: true + + leaflet.fullscreen@5.3.1(leaflet@1.9.4): + dependencies: + leaflet: 1.9.4 + optional: true + + leaflet.markercluster@1.5.3(leaflet@1.9.4): + dependencies: + leaflet: 1.9.4 + optional: true + + leaflet@1.9.4: + optional: true + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -7561,10 +8891,11 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 - lilconfig@2.1.0: {} - lilconfig@3.1.3: {} + linkifyjs@4.3.2: + optional: true + loader-utils@3.3.1: {} local-pkg@1.1.2: @@ -7581,12 +8912,8 @@ snapshots: lodash.debounce@4.0.8: {} - lodash.memoize@4.1.2: {} - lodash.merge@4.6.2: {} - lodash.uniq@4.5.0: {} - lodash@4.17.23: {} log-symbols@4.1.0: @@ -7602,6 +8929,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.3.5: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -7733,7 +9062,7 @@ snapshots: dependencies: '@types/mdast': 3.0.15 - mdn-data@2.0.14: {} + mdn-data@2.27.1: {} media-query-parser@2.0.2: dependencies: @@ -8046,6 +9375,23 @@ snapshots: transitivePeerDependencies: - supports-color + motion-dom@12.38.0: + dependencies: + motion-utils: 12.36.0 + optional: true + + motion-utils@12.36.0: + optional: true + + motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + framer-motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optional: true + mri@1.2.0: {} mrmime@1.0.1: {} @@ -8080,8 +9426,6 @@ snapshots: normalize-path@3.0.0: {} - normalize-url@6.1.0: {} - npm-install-checks@6.3.0: dependencies: semver: 7.7.4 @@ -8106,9 +9450,15 @@ snapshots: dependencies: path-key: 3.1.1 - nth-check@2.1.1: + nuqs@2.8.9(@remix-run/react@2.17.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-router-dom@6.30.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@6.30.3(react@19.2.4))(react@19.2.4): dependencies: - boolbase: 1.0.0 + '@standard-schema/spec': 1.0.0 + react: 19.2.4 + optionalDependencies: + '@remix-run/react': 2.17.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + react-router: 6.30.3(react@19.2.4) + react-router-dom: 6.30.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + optional: true object-assign@4.1.1: {} @@ -8185,6 +9535,9 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + orderedmap@2.1.1: + optional: true + outdent@0.8.0: {} own-keys@1.0.1: @@ -8193,8 +9546,6 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - p-finally@1.0.0: {} - p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -8207,15 +9558,6 @@ snapshots: dependencies: aggregate-error: 3.1.0 - p-queue@6.6.2: - dependencies: - eventemitter3: 4.0.7 - p-timeout: 3.2.0 - - p-timeout@3.2.0: - dependencies: - p-finally: 1.0.0 - package-json-from-dist@1.0.1: {} pako@0.2.9: {} @@ -8236,6 +9578,10 @@ snapshots: parse-ms@2.1.0: {} + parse5@8.0.1: + dependencies: + entities: 8.0.0 + parseurl@1.3.3: {} path-exists@4.0.0: {} @@ -8279,117 +9625,38 @@ snapshots: pidtree@0.6.0: {} - pify@5.0.0: {} - - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.0 - pathe: 2.0.3 - - pkg-types@2.3.0: - dependencies: - confbox: 0.2.4 - exsolve: 1.0.8 - pathe: 2.0.3 - - playwright-core@1.58.2: {} - - playwright@1.58.2: - dependencies: - playwright-core: 1.58.2 - optionalDependencies: - fsevents: 2.3.2 - - possible-typed-array-names@1.1.0: {} - - postcss-calc@8.2.4(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-selector-parser: 6.1.2 - postcss-value-parser: 4.2.0 - - postcss-colormin@5.3.1(postcss@8.5.8): - dependencies: - browserslist: 4.28.1 - caniuse-api: 3.0.0 - colord: 2.9.3 - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - - postcss-convert-values@5.1.3(postcss@8.5.8): - dependencies: - browserslist: 4.28.1 - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - - postcss-discard-comments@5.1.2(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - postcss-discard-duplicates@5.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - postcss-discard-empty@5.1.1(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - postcss-discard-overridden@5.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - postcss-load-config@3.1.4(postcss@8.5.8): - dependencies: - lilconfig: 2.1.0 - yaml: 1.10.2 - optionalDependencies: - postcss: 8.5.8 - - postcss-load-config@4.0.2(postcss@8.5.8): - dependencies: - lilconfig: 3.1.3 - yaml: 2.8.2 - optionalDependencies: - postcss: 8.5.8 - - postcss-merge-longhand@5.1.7(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - stylehacks: 5.1.1(postcss@8.5.8) - - postcss-merge-rules@5.1.4(postcss@8.5.8): + pkg-types@1.3.1: dependencies: - browserslist: 4.28.1 - caniuse-api: 3.0.0 - cssnano-utils: 3.1.0(postcss@8.5.8) - postcss: 8.5.8 - postcss-selector-parser: 6.1.2 + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 - postcss-minify-font-values@5.1.0(postcss@8.5.8): + pkg-types@2.3.0: dependencies: - postcss: 8.5.8 - postcss-value-parser: 4.2.0 + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 - postcss-minify-gradients@5.1.1(postcss@8.5.8): + playwright-core@1.58.2: {} + + playwright@1.58.2: dependencies: - colord: 2.9.3 - cssnano-utils: 3.1.0(postcss@8.5.8) - postcss: 8.5.8 - postcss-value-parser: 4.2.0 + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 - postcss-minify-params@5.1.4(postcss@8.5.8): + possible-typed-array-names@1.1.0: {} + + postcss-discard-duplicates@5.1.0(postcss@8.5.8): dependencies: - browserslist: 4.28.1 - cssnano-utils: 3.1.0(postcss@8.5.8) postcss: 8.5.8 - postcss-value-parser: 4.2.0 - postcss-minify-selectors@5.2.1(postcss@8.5.8): + postcss-load-config@4.0.2(postcss@8.5.8): dependencies: + lilconfig: 3.1.3 + yaml: 2.8.2 + optionalDependencies: postcss: 8.5.8 - postcss-selector-parser: 6.1.2 postcss-modules-extract-imports@3.1.0(postcss@8.5.8): dependencies: @@ -8412,18 +9679,6 @@ snapshots: icss-utils: 5.1.0(postcss@8.5.8) postcss: 8.5.8 - postcss-modules@4.3.1(postcss@8.5.8): - dependencies: - generic-names: 4.0.0 - icss-replace-symbols: 1.1.0 - lodash.camelcase: 4.3.0 - postcss: 8.5.8 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.8) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.8) - postcss-modules-scope: 3.2.1(postcss@8.5.8) - postcss-modules-values: 4.0.0(postcss@8.5.8) - string-hash: 1.1.3 - postcss-modules@6.0.1(postcss@8.5.8): dependencies: generic-names: 4.0.0 @@ -8436,90 +9691,11 @@ snapshots: postcss-modules-values: 4.0.0(postcss@8.5.8) string-hash: 1.1.3 - postcss-normalize-charset@5.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - postcss-normalize-display-values@5.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - - postcss-normalize-positions@5.1.1(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - - postcss-normalize-repeat-style@5.1.1(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - - postcss-normalize-string@5.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - - postcss-normalize-timing-functions@5.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - - postcss-normalize-unicode@5.1.1(postcss@8.5.8): - dependencies: - browserslist: 4.28.1 - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - - postcss-normalize-url@5.1.0(postcss@8.5.8): - dependencies: - normalize-url: 6.1.0 - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - - postcss-normalize-whitespace@5.1.1(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - - postcss-ordered-values@5.1.3(postcss@8.5.8): - dependencies: - cssnano-utils: 3.1.0(postcss@8.5.8) - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - - postcss-reduce-initial@5.1.2(postcss@8.5.8): - dependencies: - browserslist: 4.28.1 - caniuse-api: 3.0.0 - postcss: 8.5.8 - - postcss-reduce-transforms@5.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - - postcss-selector-parser@6.1.2: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@5.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - svgo: 2.8.0 - - postcss-unique-selectors@5.1.1(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-selector-parser: 6.1.2 - postcss-value-parser@4.2.0: {} postcss@8.5.8: @@ -8547,8 +9723,6 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 - promise.series@0.2.0: {} - prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -8557,6 +9731,87 @@ snapshots: property-information@6.5.0: {} + prosemirror-changeset@2.4.1: + dependencies: + prosemirror-transform: 1.12.0 + optional: true + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + optional: true + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + optional: true + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + optional: true + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + rope-sequence: 1.3.4 + optional: true + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + optional: true + + prosemirror-model@1.25.4: + dependencies: + orderedmap: 2.1.1 + optional: true + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + optional: true + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + optional: true + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + optional: true + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.4 + optional: true + + prosemirror-view@1.41.8: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + optional: true + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -8597,6 +9852,15 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + react-day-picker@9.14.0(react@19.2.4): + dependencies: + '@date-fns/tz': 1.4.1 + '@tabby_ai/hijri-converter': 1.0.5 + date-fns: 4.1.0 + date-fns-jalali: 4.1.0-0 + react: 19.2.4 + optional: true + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -8608,8 +9872,56 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 + react-dropzone@15.0.0(react@19.2.4): + dependencies: + attr-accept: 2.2.5 + file-selector: 2.1.2 + prop-types: 15.8.1 + react: 19.2.4 + optional: true + + react-hook-form@7.74.0(react@19.2.4): + dependencies: + react: 19.2.4 + optional: true + react-is@16.13.1: {} + react-leaflet-markercluster@5.0.0-rc.0(leaflet.markercluster@1.5.3(leaflet@1.9.4))(leaflet@1.9.4)(react-dom@19.2.4(react@19.2.4))(react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): + dependencies: + '@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + leaflet: 1.9.4 + leaflet.markercluster: 1.5.3(leaflet@1.9.4) + react: 19.2.4 + react-leaflet: 5.0.0(leaflet@1.9.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + transitivePeerDependencies: + - react-dom + optional: true + + react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + leaflet: 1.9.4 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optional: true + + react-number-format@5.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optional: true + + react-redux@9.2.0(@types/react@18.3.28)(react@19.2.4)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 18.3.28 + redux: 5.0.1 + optional: true + react-refresh@0.14.2: {} react-remove-scroll-bar@2.3.8(@types/react@18.3.28)(react@19.2.4): @@ -8638,11 +9950,25 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-router: 6.30.3(react@18.3.1) + react-router-dom@6.30.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@remix-run/router': 1.23.2 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-router: 6.30.3(react@19.2.4) + optional: true + react-router@6.30.3(react@18.3.1): dependencies: '@remix-run/router': 1.23.2 react: 18.3.1 + react-router@6.30.3(react@19.2.4): + dependencies: + '@remix-run/router': 1.23.2 + react: 19.2.4 + optional: true + react-style-singleton@2.2.3(@types/react@18.3.28)(react@19.2.4): dependencies: get-nonce: 1.0.1 @@ -8677,6 +10003,35 @@ snapshots: dependencies: picomatch: 2.3.1 + recharts@3.8.1(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@18.3.28)(react@19.2.4)(redux@5.0.1))(react@19.2.4) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.46.1 + eventemitter3: 5.0.4 + immer: 10.2.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-is: 16.13.1 + react-redux: 9.2.0(@types/react@18.3.28)(react@19.2.4)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.4) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + optional: true + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + optional: true + + redux@5.0.1: + optional: true + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -8733,11 +10088,14 @@ snapshots: mdast-util-to-hast: 12.3.0 unified: 10.1.2 + require-from-string@2.0.2: {} + require-like@0.1.2: {} - resolve-from@4.0.0: {} + reselect@5.1.1: + optional: true - resolve-from@5.0.0: {} + resolve-from@4.0.0: {} resolve.exports@2.0.3: {} @@ -8773,29 +10131,6 @@ snapshots: dependencies: rollup: 4.60.0 - rollup-plugin-postcss@4.0.2(postcss@8.5.8): - dependencies: - chalk: 4.1.2 - concat-with-sourcemaps: 1.1.0 - cssnano: 5.1.15(postcss@8.5.8) - import-cwd: 3.0.0 - p-queue: 6.6.2 - pify: 5.0.0 - postcss: 8.5.8 - postcss-load-config: 3.1.4(postcss@8.5.8) - postcss-modules: 4.3.1(postcss@8.5.8) - promise.series: 0.2.0 - resolve: 1.22.11 - rollup-pluginutils: 2.8.2 - safe-identifier: 0.4.2 - style-inject: 0.3.0 - transitivePeerDependencies: - - ts-node - - rollup-pluginutils@2.8.2: - dependencies: - estree-walker: 0.6.1 - rollup@4.60.0: dependencies: '@types/estree': 1.0.8 @@ -8827,6 +10162,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.0 fsevents: 2.3.3 + rope-sequence@1.3.4: + optional: true + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -8847,8 +10185,6 @@ snapshots: safe-buffer@5.2.1: {} - safe-identifier@0.4.2: {} - safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -8862,6 +10198,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -8965,6 +10305,12 @@ snapshots: slash@3.0.0: {} + sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optional: true + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -8996,8 +10342,6 @@ snapshots: dependencies: minipass: 7.1.3 - stable@0.1.8: {} - state-local@1.0.7: {} statuses@2.0.2: {} @@ -9096,33 +10440,17 @@ snapshots: strip-json-comments@3.1.1: {} - style-inject@0.3.0: {} - style-to-object@0.4.4: dependencies: inline-style-parser: 0.1.1 - stylehacks@5.1.1(postcss@8.5.8): - dependencies: - browserslist: 4.28.1 - postcss: 8.5.8 - postcss-selector-parser: 6.1.2 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 supports-preserve-symlinks-flag@1.0.0: {} - svgo@2.8.0: - dependencies: - '@trysound/sax': 0.2.0 - commander: 7.2.0 - css-select: 4.3.0 - css-tree: 1.1.3 - csso: 4.2.0 - picocolors: 1.1.1 - stable: 0.1.8 + symbol-tree@3.2.4: {} tailwind-merge@3.5.0: {} @@ -9165,6 +10493,15 @@ snapshots: readable-stream: 2.3.8 xtend: 4.0.2 + tiny-invariant@1.3.3: + optional: true + + tldts-core@7.0.29: {} + + tldts@7.0.29: + dependencies: + tldts-core: 7.0.29 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -9173,6 +10510,14 @@ snapshots: toml@3.0.0: {} + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.29 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -9191,6 +10536,8 @@ snapshots: turbo-stream@2.4.1: {} + tw-animate-css@1.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -9250,6 +10597,8 @@ snapshots: undici@6.23.0: {} + undici@7.25.0: {} + unified@10.1.2: dependencies: '@types/unist': 2.0.11 @@ -9331,6 +10680,10 @@ snapshots: optionalDependencies: '@types/react': 18.3.28 + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + util-deprecate@1.0.2: {} util@0.12.5: @@ -9375,6 +10728,24 @@ snapshots: unist-util-stringify-position: 3.0.3 vfile-message: 3.1.4 + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + optional: true + vite-node@1.6.1(@types/node@25.3.2)(lightningcss@1.32.0): dependencies: cac: 6.7.14 @@ -9421,6 +10792,13 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.32.0 + w3c-keyname@2.2.8: + optional: true + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -9433,6 +10811,18 @@ snapshots: web-streams-polyfill@3.3.3: {} + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -9500,16 +10890,21 @@ snapshots: ws@7.5.10: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xtend@4.0.2: {} yallist@3.1.1: {} yallist@4.0.0: {} - yaml@1.10.2: {} - yaml@2.8.2: {} yocto-queue@0.1.0: {} + zod@4.4.1: + optional: true + zwitch@2.0.4: {} diff --git a/ui/postcss.config.js b/ui/postcss.config.js deleted file mode 100644 index 51a6e4e6..00000000 --- a/ui/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - '@tailwindcss/postcss': {}, - autoprefixer: {}, - }, -}; diff --git a/ui/rollup.config.mjs b/ui/rollup.config.mjs index 071bb9b7..f116569a 100644 --- a/ui/rollup.config.mjs +++ b/ui/rollup.config.mjs @@ -2,8 +2,10 @@ import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import typescript from '@rollup/plugin-typescript'; import peerDepsExternal from 'rollup-plugin-peer-deps-external'; -import postcss from 'rollup-plugin-postcss'; -import autoprefixer from 'autoprefixer'; + +// We don't ship CSS — the host app's Tailwind compiles utility classes +// directly from this package's dist via @source. No PostCSS pipeline +// needed. export default { input: 'src/index.ts', @@ -36,15 +38,6 @@ export default { declarationDir: 'dist', noEmitOnError: false, }), - postcss({ - // Don't extract CSS - host app provides Tailwind - // This avoids CSS layer conflicts with host applications - inject: false, - minimize: true, - plugins: [ - autoprefixer(), - ], - }), ], external: [ 'react', @@ -53,5 +46,9 @@ export default { /^react\//, /^react-dom\//, /^@radix-ui\//, + // Externalize @datum-cloud/datum-ui (and its subpath imports) so the + // consumer brings its own pinned copy. Avoids duplicating primitives + // and prevents CSS conflicts with the host's datum-ui styles. + /^@datum-cloud\/datum-ui($|\/)/, ], }; diff --git a/ui/src/components/ActionMultiSelect.tsx b/ui/src/components/ActionMultiSelect.tsx index fb9a41a2..94fad7b8 100644 --- a/ui/src/components/ActionMultiSelect.tsx +++ b/ui/src/components/ActionMultiSelect.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as Popover from '@radix-ui/react-popover'; import { ChevronDown } from 'lucide-react'; -import { Checkbox } from './ui/checkbox'; +import { Checkbox } from '@datum-cloud/datum-ui/checkbox'; import { cn } from '../lib/utils'; export interface ActionMultiSelectOption { diff --git a/ui/src/components/ActionToggle.tsx b/ui/src/components/ActionToggle.tsx index cc1eae1f..679421ba 100644 --- a/ui/src/components/ActionToggle.tsx +++ b/ui/src/components/ActionToggle.tsx @@ -1,4 +1,4 @@ -import { Button } from './ui/button'; +import { Button } from '@datum-cloud/datum-ui/button'; import { cn } from '../lib/utils'; export type ActionOption = 'all' | 'create' | 'update' | 'delete' | 'get' | 'list' | 'watch'; @@ -70,26 +70,29 @@ export function ActionToggle({ role="group" aria-label="Filter by action" > - {OPTIONS.map((option, index) => ( - - ))} + {OPTIONS.map((option, index) => { + const active = value === option.value; + return ( + + ); + })} ); } diff --git a/ui/src/components/ActivityExpandedDetails.tsx b/ui/src/components/ActivityExpandedDetails.tsx index 14dc44ea..43d93f07 100644 --- a/ui/src/components/ActivityExpandedDetails.tsx +++ b/ui/src/components/ActivityExpandedDetails.tsx @@ -1,13 +1,7 @@ -import { useState } from 'react'; -import { Copy, Check } from 'lucide-react'; import type { Activity, TenantLinkResolver } from '../types/activity'; -import { TenantBadge } from './TenantBadge'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from './ui/tooltip'; +import { TooltipProvider } from './ui/tooltip'; +import { Timestamp } from './Timestamp'; +import { DetailGrid, DetailPanelShell, Field, Section } from './details'; export interface ActivityExpandedDetailsProps { /** The activity to display details for */ @@ -19,206 +13,141 @@ export interface ActivityExpandedDetailsProps { } /** - * Format timestamp for display (in UTC) + * ActivityExpandedDetails renders the expanded details for an activity row. + * Layout: an optional "Changes" block on top, then four grouped sections — + * When / Actor / Resource / Origin — laid out as a responsive grid. */ -function formatTimestampFull(timestamp?: string): string { - if (!timestamp) return 'Unknown time'; - try { - const date = new Date(timestamp); - return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')} ${String(date.getUTCHours()).padStart(2, '0')}:${String(date.getUTCMinutes()).padStart(2, '0')}:${String(date.getUTCSeconds()).padStart(2, '0')} UTC`; - } catch { - return timestamp; - } -} - -/** - * CopyButton component for copying field values to clipboard - */ -function CopyButton({ value, label }: { value: string; label: string }) { - const [isCopied, setIsCopied] = useState(false); - - const handleCopy = async (e: React.MouseEvent) => { - e.stopPropagation(); - try { - await navigator.clipboard.writeText(value); - setIsCopied(true); - setTimeout(() => setIsCopied(false), 2000); - } catch (err) { - console.error('Failed to copy:', err); - } - }; - - return ( - - - - - -

{isCopied ? 'Copied!' : `Copy ${label}`}

-
-
- ); -} - -/** - * ActivityExpandedDetails renders the expanded details section for an activity. - * Used by both feed and timeline variants of ActivityFeedItem for consistent UX. - * - * Section order (most to least relevant for investigation): - * 1. Changes - what changed (most actionable) - * 2. Timestamp - when it happened - * 3. Tenant - scope of the activity - * 4. Actor - who made the change - * 5. Resource - what resource was affected - * 6. Origin - correlation to audit logs - */ -export function ActivityExpandedDetails({ activity, tenantLinkResolver, compact = false }: ActivityExpandedDetailsProps) { +export function ActivityExpandedDetails({ + activity, + compact = false, +}: ActivityExpandedDetailsProps) { const { spec, metadata } = activity; - const { actor, resource, origin, changes, tenant } = spec; + const { actor, resource, origin, changes } = spec; const timestamp = metadata?.creationTimestamp; - return ( - -
- {/* Field Changes - Most actionable, shown first */} - {changes && changes.length > 0 && ( -
-

- Changes -

-
- {changes.map((change, index) => ( -
- - {change.field} + const actorDisplay = actor.displayName || actor.name; + const actorIsUser = actor.type === 'user'; + + const body = ( + <> + {changes && changes.length > 0 ? ( +
+

+ Changes +

+
+ {changes.map((change, index) => ( +
+ + {change.field} + + {change.old ? ( + + + {change.old} - {change.old && ( - - - {change.old} - - )} - {change.new && ( - - + - {change.new} - - )} -
- ))} -
-
- )} - - {/* CSS Grid layout with reduced min-width for more columns */} -
- {/* 1. Timestamp */} -
-
Timestamp:
-
- {formatTimestampFull(timestamp)} - -
-
- - {/* 2. Actor Type */} -
-
Actor Type:
-
- {actor.type} - -
-
- - {/* 3. Actor */} -
-
Actor:
-
- {actor.name} - -
-
- - {/* 4. API Group */} - {resource.apiGroup && ( -
-
API Group:
-
- {resource.apiGroup} - -
-
- )} - - {/* 5. Resource */} -
-
Resource:
-
- {resource.kind} - -
-
- - {/* 6. Resource Name */} -
-
Resource Name:
-
- {resource.name} - -
-
- - {/* 7. Namespace */} - {resource.namespace && ( -
-
Namespace:
-
- {resource.namespace} - -
-
- )} - - {/* 8. Resource UID */} - {resource.uid && ( -
-
Resource UID:
-
- {resource.uid} - -
+ ) : null} + {change.new ? ( + + + + {change.new} + + ) : null} +
+ ))}
- )} - - {/* 9. Origin */} -
-
Origin:
-
- {origin.type} - -
+ ) : null} + + +
+ } + copyValue={timestamp ?? ''} + copyLabel="timestamp" + disableTooltip + /> +
+ +
+ + {actorDisplay} + {actor.type ? ( + ({actor.type}) + ) : null} + + } + copyValue={actorDisplay} + copyLabel="actor name" + /> + {actorIsUser && actor.email ? ( + + ) : null} + {actor.uid ? ( + + ) : null} +
+ +
+ + {resource.kind} + {resource.apiGroup ? ( + · {resource.apiGroup} + ) : null} + + } + copyValue={resource.kind} + copyLabel="resource kind" + /> + {resource.name ? ( + + ) : null} + {resource.namespace ? ( + + ) : null} + {resource.uid ? ( + + ) : null} +
+ +
+ + {origin.id ? ( + + ) : null} +
+
+ + ); - {/* 10. Origin ID */} -
-
Origin ID:
-
- {origin.id} - -
-
- -
+ return ( + + {compact ? ( + {body} + ) : ( +
{body}
+ )}
); } diff --git a/ui/src/components/ActivityFeed.tsx b/ui/src/components/ActivityFeed.tsx index 61b3aee2..68c37ce1 100644 --- a/ui/src/components/ActivityFeed.tsx +++ b/ui/src/components/ActivityFeed.tsx @@ -1,19 +1,44 @@ -import { useEffect, useRef, useCallback, useState } from 'react'; -import type { Activity, ResourceRef, ResourceLinkResolver, TenantLinkResolver, TenantRenderer, EffectiveTimeRangeCallback, ErrorFormatter } from '../types/activity'; +import { useEffect, useRef, useCallback, useState } from "react"; +import type { + Activity, + ResourceRef, + ResourceLinkResolver, + TenantLinkResolver, + TenantRenderer, + EffectiveTimeRangeCallback, + ErrorFormatter, +} from "../types/activity"; import type { ActivityFeedFilters as FilterState, TimeRange, -} from '../hooks/useActivityFeed'; -import { useActivityFeed } from '../hooks/useActivityFeed'; -import { ActivityFeedItem } from './ActivityFeedItem'; -import { ActivityFeedItemSkeleton } from './ActivityFeedItemSkeleton'; -import { ActivityFeedFilters } from './ActivityFeedFilters'; -import { ActivityApiClient } from '../api/client'; -import { Button } from './ui/button'; -import { Card } from './ui/card'; -import { Badge } from './ui/badge'; -import { ApiErrorAlert } from './ApiErrorAlert'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip'; +} from "../hooks/useActivityFeed"; +import { useActivityFeed } from "../hooks/useActivityFeed"; +import { + ActivityFeedItem, + ACTIVITY_FEED_COLUMN_COUNT, +} from "./ActivityFeedItem"; +import { ActivityFeedItemSkeleton } from "./ActivityFeedItemSkeleton"; +import { Skeleton } from "@datum-cloud/datum-ui/skeleton"; +import { ActivityFeedFilters } from "./ActivityFeedFilters"; +import { ActivityApiClient } from "../api/client"; +import { Button } from "@datum-cloud/datum-ui/button"; +import { Card } from "@datum-cloud/datum-ui/card"; +import { Badge } from "./ui/badge"; +import { ApiErrorAlert } from "./ApiErrorAlert"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "./ui/tooltip"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@datum-cloud/datum-ui/table"; export interface ActivityFeedProps { /** API client instance */ @@ -37,13 +62,20 @@ export interface ActivityFeedProps { /** Whether to show in compact mode (for resource detail tabs) */ compact?: boolean; /** Layout variant for activity items: 'feed' (default) or 'timeline' */ - variant?: 'feed' | 'timeline'; + variant?: "feed" | "timeline"; /** Filter to a specific resource UID */ resourceUid?: string; /** Whether to show filters */ showFilters?: boolean; /** Filters that should be locked and hidden from the UI (programmatically set by parent) */ - hiddenFilters?: Array<'resourceKinds' | 'actorNames' | 'apiGroups' | 'resourceNamespaces' | 'resourceName' | 'changeSource'>; + hiddenFilters?: Array< + | "resourceKinds" + | "actorNames" + | "apiGroups" + | "resourceNamespaces" + | "resourceName" + | "changeSource" + >; /** Additional CSS class */ className?: string; /** Enable infinite scroll (default: true) */ @@ -75,8 +107,8 @@ export interface ActivityFeedProps { */ export function ActivityFeed({ client, - initialFilters = { changeSource: 'human' }, - initialTimeRange = { start: 'now-7d' }, + initialFilters = { changeSource: "human" }, + initialTimeRange = { start: "now-7d" }, pageSize = 30, onResourceClick, resourceLinkResolver, @@ -84,11 +116,11 @@ export function ActivityFeed({ tenantRenderer, onActivityClick, compact = false, - variant = 'feed', + variant = "feed", resourceUid, showFilters = true, hiddenFilters = [], - className = '', + className = "", infiniteScroll = true, loadMoreThreshold = 200, onCreatePolicy, @@ -165,14 +197,31 @@ export function ActivityFeed({ loadMoreRef.current = loadMore; }, [loadMore]); + // Mirror isLoading into a ref so the IntersectionObserver callback can + // read the latest value without listing it as an effect dep. Listing + // isLoading caused the observer to tear down and rebuild on every + // isLoading toggle; the rebuilt observer fired immediately when the + // trigger element was in the viewport, which in turn called loadMore() + // and toggled isLoading again — a cycle that disabled the toolbar + // repeatedly until items filled the viewport. + // + // hasMore *is* a dep, because the trigger element is conditionally + // rendered (`{infiniteScroll && hasMore &&
}`). When + // hasMore flips false→true after the initial fetch we need the effect + // to re-run so the observer attaches to the now-mounted trigger. + const isLoadingRef = useRef(isLoading); + useEffect(() => { + isLoadingRef.current = isLoading; + }, [isLoading]); + // Infinite scroll using Intersection Observer useEffect(() => { - if (!infiniteScroll || !loadMoreTriggerRef.current) return; + if (!infiniteScroll || !hasMore || !loadMoreTriggerRef.current) return; const observer = new IntersectionObserver( (entries) => { const entry = entries[0]; - if (entry.isIntersecting && hasMore && !isLoading) { + if (entry.isIntersecting && !isLoadingRef.current) { // Call through the ref to always use the latest function loadMoreRef.current(); } @@ -181,7 +230,7 @@ export function ActivityFeed({ root: scrollContainerRef.current, rootMargin: `${loadMoreThreshold}px`, threshold: 0, - } + }, ); observer.observe(loadMoreTriggerRef.current); @@ -189,7 +238,7 @@ export function ActivityFeed({ return () => { observer.disconnect(); }; - }, [infiniteScroll, hasMore, isLoading, loadMoreThreshold]); + }, [infiniteScroll, loadMoreThreshold, hasMore]); // Handle filter changes - refresh is automatic via the hook const handleFiltersChange = useCallback( @@ -197,7 +246,7 @@ export function ActivityFeed({ setFilters(newFilters); onFiltersChangeProp?.(newFilters, timeRange); }, - [setFilters, onFiltersChangeProp, timeRange] + [setFilters, onFiltersChangeProp, timeRange], ); // Handle time range changes - refresh is automatic via the hook @@ -206,7 +255,7 @@ export function ActivityFeed({ setTimeRange(newTimeRange); onFiltersChangeProp?.(filters, newTimeRange); }, - [setTimeRange, onFiltersChangeProp, filters] + [setTimeRange, onFiltersChangeProp, filters], ); // Handle manual load more click @@ -224,207 +273,343 @@ export function ActivityFeed({ }, [isStreaming, startStreaming, stopStreaming]); // Handle actor click - filter by actor name - const handleActorClick = useCallback((actorName: string) => { - setFilters({ - ...filters, - actorNames: [actorName], - }); - }, [filters, setFilters]); + const handleActorClick = useCallback( + (actorName: string) => { + setFilters({ + ...filters, + actorNames: [actorName], + }); + }, + [filters, setFilters], + ); // Build container classes - use flex layout to properly fill available space // flex-1 min-h-0 allows the Card to fill parent flex container and enable child scrolling const containerClasses = compact - ? `flex-1 min-h-0 flex flex-col p-0 shadow-none border-none ${className}` - : `flex-1 min-h-0 flex flex-col p-3 ${className}`; + ? `flex-1 min-h-0 flex flex-col p-3 shadow-none border-none gap-0 ${className}` + : `flex-1 min-h-0 flex flex-col p-3 gap-0 ${className}`; // Build list classes - use flex-1 min-h-0 for flex-based scrolling // Parent containers must have proper height constraints (h-screen/h-full + overflow-hidden) - const effectiveMaxHeight = maxHeight === 'none' ? undefined : maxHeight; - const listClasses = 'flex-1 min-h-0 overflow-y-auto flex flex-col'; + const effectiveMaxHeight = maxHeight === "none" ? undefined : maxHeight; + const listClasses = "flex-1 min-h-0 overflow-y-auto flex flex-col"; return ( - - {/* Header with streaming status */} - {enableStreaming && ( -
-
- {isStreaming && !watchError && ( - - - -
- - - - - Streaming activity... -
-
- -

New activities will appear automatically

-
-
-
- )} - {watchError && ( - - - -
- - - - Connection error -
-
- -

Stream connection lost

-
-
-
- )} - {newActivitiesCount > 0 && !watchError && ( - - +{newActivitiesCount} new - - )} + + + {/* Header with streaming status */} + {enableStreaming && ( +
+
+ {isStreaming && !watchError && ( + + + +
+ + + + + + Streaming activity... + +
+
+ +

New activities will appear automatically

+
+
+
+ )} + {watchError && ( + + + +
+ + + + + Connection error + +
+
+ +

Stream connection lost

+
+
+
+ )} + {newActivitiesCount > 0 && !watchError && ( + + +{newActivitiesCount} new + + )} +
+
- -
- )} - - {/* Filters */} - {showFilters && ( - - )} - - {/* Query Error Display */} - + )} - {/* Watch Stream Error Display */} - + {/* Filters */} + {showFilters && ( + + )} - {/* No Policies Empty State */} - {!policiesLoading && hasPolicies === false && ( -
-
- - - - - - -
-

Get started with activity logging

-

- Activity policies define which resources to track and how to summarize changes. - Create your first policy to start seeing activity logs here. -

- {onCreatePolicy && ( - - )} -
- )} + {/* Query Error Display */} + - {/* Activity List */} -
- {/* Skeleton Loading State - show when loading and no items yet */} - {isLoading && activities.length === 0 && ( - <> - {Array.from({ length: 8 }).map((_, index) => ( - - ))} - - )} + {/* Watch Stream Error Display */} + - {/* Empty State - only show when not loading */} - {!isLoading && activities.length === 0 && hasPolicies !== false && ( -
-

No activities found

-

- Try adjusting your filters or time range + {/* No Policies Empty State */} + {!policiesLoading && hasPolicies === false && ( +

+
+ + + + + + +
+

+ Get started with activity logging +

+

+ Activity policies define which resources to track and how to + summarize changes. Create your first policy to start seeing + activity logs here.

+ {onCreatePolicy && ( + + )}
)} - {activities.map((activity, index) => ( - - ))} + {/* Activity List */} +
+ {/* Empty State - only show when not loading and we're using the + feed variant. Empty state for the table case is rendered as an + in-table message row below. */} + {!isLoading && + activities.length === 0 && + hasPolicies !== false && + variant === "timeline" && ( +
+

No activities found

+

+ Try adjusting your filters or time range +

+
+ )} - {/* Load More Trigger for Infinite Scroll */} - {infiniteScroll && hasMore && ( -
- )} + {variant === "timeline" ? ( + <> + {isLoading && activities.length === 0 && ( + <> + {Array.from({ length: 8 }).map((_, index) => ( + + ))} + + )} + {activities.map((activity, index) => ( + + ))} + + ) : ( + + + + + Summary + Tenant + When + + + + + {isLoading && activities.length === 0 && ( + <> + {Array.from({ length: 8 }).map((_, index) => ( + + {/* Standard per-cell skeleton row, matching the + shape of the real columns: avatar, summary, + tenant, when, expand. */} + + + + + + + + + + + + + + + + + ))} + + )} + {!isLoading && + activities.length === 0 && + hasPolicies !== false && ( + + +
No activities found
+
+ Try adjusting your filters or time range +
+
+
+ )} + {activities.map((activity, index) => ( + + ))} +
+
+ )} - {/* Load More Button (when infinite scroll is disabled) */} - {!infiniteScroll && hasMore && !isLoading && ( -
- -
- )} + {/* Load More Trigger for Infinite Scroll */} + {infiniteScroll && hasMore && ( +
+ )} - {/* End of Results */} - {!hasMore && activities.length > 0 && !isLoading && ( -
- No more activities to load -
- )} -
- + {/* Manual pagination footer: shown for both table and timeline + variants when infinite scroll is disabled. Mirrors the look + of other staff-portal data tables — count on the left, + action on the right. */} + {!infiniteScroll && activities.length > 0 ? ( +
+ + {activities.length}{" "} + {activities.length === 1 ? "activity" : "activities"} + {hasMore ? " so far" : ""} + + {hasMore ? ( + + ) : ( + End of results + )} +
+ ) : null} + +
+ + ); } diff --git a/ui/src/components/ActivityFeedFilters.tsx b/ui/src/components/ActivityFeedFilters.tsx index 2f8d1045..5d841a4e 100644 --- a/ui/src/components/ActivityFeedFilters.tsx +++ b/ui/src/components/ActivityFeedFilters.tsx @@ -1,16 +1,16 @@ -import { useState, useCallback, useEffect, useRef, useMemo } from 'react'; -import { formatISO, subDays } from 'date-fns'; -import { Search, X } from 'lucide-react'; - -import type { ActivityFeedFilters as FilterState } from '../hooks/useActivityFeed'; -import type { TimeRange } from '../hooks/useActivityFeed'; -import type { ActivityApiClient } from '../api/client'; -import { useFacets } from '../hooks/useFacets'; -import { ChangeSourceToggle, ChangeSourceOption } from './ChangeSourceToggle'; -import { TimeRangeDropdown } from './ui/time-range-dropdown'; -import { FilterChip } from './ui/filter-chip'; -import { AddFilterDropdown, type FilterOption } from './ui/add-filter-dropdown'; -import { Input } from './ui/input'; +import { useState, useCallback, useEffect, useRef, useMemo } from "react"; +import { formatISO, subDays } from "date-fns"; +import { Search, X } from "lucide-react"; + +import type { ActivityFeedFilters as FilterState } from "../hooks/useActivityFeed"; +import type { TimeRange } from "../hooks/useActivityFeed"; +import type { ActivityApiClient } from "../api/client"; +import { useFacets } from "../hooks/useFacets"; +import { ChangeSourceToggle, ChangeSourceOption } from "./ChangeSourceToggle"; +import { TimeRangeDropdown } from "./ui/time-range-dropdown"; +import { FilterChip } from "./ui/filter-chip"; +import { AddFilterDropdown, type FilterOption } from "./ui/add-filter-dropdown"; +import { Input } from "@datum-cloud/datum-ui/input"; export interface ActivityFeedFiltersProps { /** API client instance for fetching facets */ @@ -26,7 +26,15 @@ export interface ActivityFeedFiltersProps { /** Whether the filters are disabled (e.g., during loading) */ disabled?: boolean; /** Filters that should be locked and hidden from the UI (programmatically set by parent) */ - hiddenFilters?: Array<'resourceKinds' | 'actorNames' | 'apiGroups' | 'resourceNamespaces' | 'resourceName' | 'actions' | 'changeSource'>; + hiddenFilters?: Array< + | "resourceKinds" + | "actorNames" + | "apiGroups" + | "resourceNamespaces" + | "resourceName" + | "actions" + | "changeSource" + >; /** Additional CSS class */ className?: string; } @@ -35,61 +43,67 @@ export interface ActivityFeedFiltersProps { * Preset time ranges */ const TIME_PRESETS = [ - { key: 'now-1h', label: 'Last hour' }, - { key: 'now-24h', label: 'Last 24 hours' }, - { key: 'now-7d', label: 'Last 7 days' }, - { key: 'now-30d', label: 'Last 30 days' }, + { key: "now-1h", label: "Last hour" }, + { key: "now-24h", label: "Last 24 hours" }, + { key: "now-7d", label: "Last 7 days" }, + { key: "now-30d", label: "Last 30 days" }, ]; /** * Filter configuration registry */ -type FilterId = 'resourceKinds' | 'actorNames' | 'apiGroups' | 'resourceNamespaces' | 'resourceName' | 'actions'; +type FilterId = + | "resourceKinds" + | "actorNames" + | "apiGroups" + | "resourceNamespaces" + | "resourceName" + | "actions"; interface FilterConfig { id: FilterId; label: string; - inputMode: 'typeahead' | 'text'; + inputMode: "typeahead" | "text"; placeholder?: string; searchPlaceholder?: string; } const FILTER_CONFIGS: Record = { resourceKinds: { - id: 'resourceKinds', - label: 'Kind', - inputMode: 'typeahead', - searchPlaceholder: 'Search kinds...', + id: "resourceKinds", + label: "Kind", + inputMode: "typeahead", + searchPlaceholder: "Search kinds...", }, actorNames: { - id: 'actorNames', - label: 'Actor', - inputMode: 'typeahead', - searchPlaceholder: 'Search actors...', + id: "actorNames", + label: "Actor", + inputMode: "typeahead", + searchPlaceholder: "Search actors...", }, apiGroups: { - id: 'apiGroups', - label: 'API Group', - inputMode: 'typeahead', - searchPlaceholder: 'Search API groups...', + id: "apiGroups", + label: "API Group", + inputMode: "typeahead", + searchPlaceholder: "Search API groups...", }, resourceNamespaces: { - id: 'resourceNamespaces', - label: 'Namespace', - inputMode: 'typeahead', - searchPlaceholder: 'Search namespaces...', + id: "resourceNamespaces", + label: "Namespace", + inputMode: "typeahead", + searchPlaceholder: "Search namespaces...", }, resourceName: { - id: 'resourceName', - label: 'Resource Name', - inputMode: 'text', - placeholder: 'Enter resource name...', + id: "resourceName", + label: "Resource Name", + inputMode: "text", + placeholder: "Enter resource name...", }, actions: { - id: 'actions', - label: 'Action', - inputMode: 'typeahead', - searchPlaceholder: 'Search actions...', + id: "actions", + label: "Action", + inputMode: "typeahead", + searchPlaceholder: "Search actions...", }, }; @@ -99,10 +113,10 @@ const FILTER_CONFIGS: Record = { const formatDatetimeLocal = (isoString: string): string => { const date = new Date(isoString); const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); return `${year}-${month}-${day}T${hours}:${minutes}`; }; @@ -111,7 +125,7 @@ const formatDatetimeLocal = (isoString: string): string => { */ const getSelectedPreset = (timeRange: TimeRange): string => { const preset = TIME_PRESETS.find((p) => timeRange.start === p.key); - return preset ? preset.key : 'custom'; + return preset ? preset.key : "custom"; }; /** @@ -125,13 +139,19 @@ export function ActivityFeedFilters({ onTimeRangeChange, disabled = false, hiddenFilters = [], - className = '', + className = "", }: ActivityFeedFiltersProps) { - const { resourceKinds, actorNames, apiGroups, resourceNamespaces, error: facetsError } = useFacets(client, timeRange, filters); + const { + resourceKinds, + actorNames, + apiGroups, + resourceNamespaces, + error: facetsError, + } = useFacets(client, timeRange, filters); // Log facets error for debugging if (facetsError) { - console.error('Failed to load facets:', facetsError); + console.error("Failed to load facets:", facetsError); } // Track which filter was just added to auto-open it @@ -140,13 +160,13 @@ export function ActivityFeedFilters({ // Custom time range state const selectedPreset = getSelectedPreset(timeRange); const [customStart, setCustomStart] = useState(() => { - if (selectedPreset === 'custom') { + if (selectedPreset === "custom") { return formatDatetimeLocal(timeRange.start); } return formatDatetimeLocal(formatISO(subDays(new Date(), 1))); }); const [customEnd, setCustomEnd] = useState(() => { - if (selectedPreset === 'custom' && timeRange.end) { + if (selectedPreset === "custom" && timeRange.end) { return formatDatetimeLocal(timeRange.end); } return formatDatetimeLocal(formatISO(new Date())); @@ -160,7 +180,7 @@ export function ActivityFeedFilters({ changeSource: value, }); }, - [filters, onFiltersChange] + [filters, onFiltersChange], ); // Handle time range preset selection @@ -171,7 +191,7 @@ export function ActivityFeedFilters({ end: undefined, }); }, - [onTimeRangeChange] + [onTimeRangeChange], ); // Handle custom time range apply @@ -184,37 +204,59 @@ export function ActivityFeedFilters({ end: new Date(end).toISOString(), }); }, - [onTimeRangeChange] + [onTimeRangeChange], ); // Get display label for time range const getTimeRangeLabel = () => { const preset = TIME_PRESETS.find((p) => p.key === selectedPreset); if (preset) return preset.label; - if (selectedPreset === 'custom' && timeRange.start && timeRange.end) { + if (selectedPreset === "custom" && timeRange.start && timeRange.end) { const start = new Date(timeRange.start); const end = new Date(timeRange.end); return `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`; } - return 'Select time range'; + return "Select time range"; }; // Determine which filters are currently active (have values) and not hidden const filtersWithValues = useMemo(() => { const result: FilterId[] = []; - if (filters.resourceKinds && filters.resourceKinds.length > 0 && !hiddenFilters.includes('resourceKinds')) result.push('resourceKinds'); - if (filters.actorNames && filters.actorNames.length > 0 && !hiddenFilters.includes('actorNames')) result.push('actorNames'); - if (filters.apiGroups && filters.apiGroups.length > 0 && !hiddenFilters.includes('apiGroups')) result.push('apiGroups'); - if (filters.resourceNamespaces && filters.resourceNamespaces.length > 0 && !hiddenFilters.includes('resourceNamespaces')) result.push('resourceNamespaces'); - if (filters.resourceName && !hiddenFilters.includes('resourceName')) result.push('resourceName'); - if (filters.actions && filters.actions.length > 0) result.push('actions'); + if ( + filters.resourceKinds && + filters.resourceKinds.length > 0 && + !hiddenFilters.includes("resourceKinds") + ) + result.push("resourceKinds"); + if ( + filters.actorNames && + filters.actorNames.length > 0 && + !hiddenFilters.includes("actorNames") + ) + result.push("actorNames"); + if ( + filters.apiGroups && + filters.apiGroups.length > 0 && + !hiddenFilters.includes("apiGroups") + ) + result.push("apiGroups"); + if ( + filters.resourceNamespaces && + filters.resourceNamespaces.length > 0 && + !hiddenFilters.includes("resourceNamespaces") + ) + result.push("resourceNamespaces"); + if (filters.resourceName && !hiddenFilters.includes("resourceName")) + result.push("resourceName"); + if (filters.actions && filters.actions.length > 0) result.push("actions"); return result; }, [filters, hiddenFilters]); // Include pendingFilter (newly added filter awaiting value selection) in the displayed filters - const activeFilterIds: FilterId[] = pendingFilter && !filtersWithValues.includes(pendingFilter) - ? [...filtersWithValues, pendingFilter] - : filtersWithValues; + const activeFilterIds: FilterId[] = + pendingFilter && !filtersWithValues.includes(pendingFilter) + ? [...filtersWithValues, pendingFilter] + : filtersWithValues; // Clear pending filter when filter values change (user selected something) useEffect(() => { @@ -226,11 +268,11 @@ export function ActivityFeedFilters({ // Build available filters list (exclude hidden filters) const availableFilters: FilterOption[] = [ - { id: 'resourceKinds', label: 'Kind' }, - { id: 'actorNames', label: 'Actor' }, - { id: 'apiGroups', label: 'API Group' }, - { id: 'resourceNamespaces', label: 'Namespace' }, - { id: 'resourceName', label: 'Resource Name' }, + { id: "resourceKinds", label: "Kind" }, + { id: "actorNames", label: "Actor" }, + { id: "apiGroups", label: "API Group" }, + { id: "resourceNamespaces", label: "Namespace" }, + { id: "resourceName", label: "Resource Name" }, // 'actions' hidden until backend facet support is available ].filter((filter) => !hiddenFilters.includes(filter.id as FilterId)); @@ -245,7 +287,7 @@ export function ActivityFeedFilters({ if (pendingFilter === filterId) { const hasValues = (() => { const value = filters[filterId]; - if (filterId === 'resourceName') return !!value; + if (filterId === "resourceName") return !!value; return Array.isArray(value) && value.length > 0; })(); if (!hasValues) { @@ -253,7 +295,7 @@ export function ActivityFeedFilters({ } } }, - [pendingFilter, filters] + [pendingFilter, filters], ); // Handle filter value changes @@ -264,7 +306,7 @@ export function ActivityFeedFilters({ [filterId]: values.length > 0 ? values : undefined, }); }, - [filters, onFiltersChange] + [filters, onFiltersChange], ); // Handle filter clear @@ -275,13 +317,13 @@ export function ActivityFeedFilters({ [filterId]: undefined, }); }, - [filters, onFiltersChange] + [filters, onFiltersChange], ); // Get options for a specific filter const getFilterOptions = (filterId: FilterId) => { switch (filterId) { - case 'resourceKinds': + case "resourceKinds": return resourceKinds .filter((facet) => facet.value) .map((facet) => ({ @@ -289,7 +331,7 @@ export function ActivityFeedFilters({ label: facet.value, count: facet.count, })); - case 'actorNames': + case "actorNames": return actorNames .filter((facet) => facet.value) .map((facet) => ({ @@ -297,7 +339,7 @@ export function ActivityFeedFilters({ label: facet.value, count: facet.count, })); - case 'apiGroups': + case "apiGroups": return apiGroups .filter((facet) => facet.value) .map((facet) => ({ @@ -305,7 +347,7 @@ export function ActivityFeedFilters({ label: facet.value, count: facet.count, })); - case 'resourceNamespaces': + case "resourceNamespaces": return resourceNamespaces .filter((facet) => facet.value) .map((facet) => ({ @@ -313,7 +355,7 @@ export function ActivityFeedFilters({ label: facet.value, count: facet.count, })); - case 'actions': + case "actions": // TODO: Return action facets when backend supports it return []; default: @@ -324,17 +366,19 @@ export function ActivityFeedFilters({ // Get values for a specific filter const getFilterValues = (filterId: FilterId): string[] => { const value = filters[filterId]; - if (filterId === 'resourceName') { + if (filterId === "resourceName") { return value ? [value as string] : []; } - if (filterId === 'actions') { + if (filterId === "actions") { return (value as string[] | undefined) || []; } return (value as string[] | undefined) || []; }; // Local search value for debouncing — keeps input responsive while query runs - const [searchInputValue, setSearchInputValue] = useState(filters.search || ''); + const [searchInputValue, setSearchInputValue] = useState( + filters.search || "", + ); const searchDebounceRef = useRef | null>(null); // Use refs so the debounced callback never closes over stale values const filtersRef = useRef(filters); @@ -349,28 +393,34 @@ export function ActivityFeedFilters({ }; }, []); - const handleSearchChange = useCallback((event: React.ChangeEvent) => { - const value = event.target.value; - setSearchInputValue(value); - if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); - searchDebounceRef.current = setTimeout(() => { - onFiltersChangeRef.current({ ...filtersRef.current, search: value || undefined }); - }, 400); - }, []); + const handleSearchChange = useCallback( + (event: React.ChangeEvent) => { + const value = event.target.value; + setSearchInputValue(value); + if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); + searchDebounceRef.current = setTimeout(() => { + onFiltersChangeRef.current({ + ...filtersRef.current, + search: value || undefined, + }); + }, 400); + }, + [], + ); const handleSearchClear = useCallback(() => { - setSearchInputValue(''); + setSearchInputValue(""); if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); onFiltersChangeRef.current({ ...filtersRef.current, search: undefined }); }, []); return ( -
+
{/* Change Source Toggle */} - {!hiddenFilters.includes('changeSource') && ( + {!hiddenFilters.includes("changeSource") && ( @@ -405,7 +455,11 @@ export function ActivityFeedFilters({ key={filterId} label={config.label} values={getFilterValues(filterId)} - options={config.inputMode === 'typeahead' ? getFilterOptions(filterId) : undefined} + options={ + config.inputMode === "typeahead" + ? getFilterOptions(filterId) + : undefined + } onValuesChange={(values) => handleFilterChange(filterId, values)} onClear={() => handleFilterClear(filterId)} onPopoverClose={() => handlePopoverClose(filterId)} diff --git a/ui/src/components/ActivityFeedItem.tsx b/ui/src/components/ActivityFeedItem.tsx index 397979f3..3c22d248 100644 --- a/ui/src/components/ActivityFeedItem.tsx +++ b/ui/src/components/ActivityFeedItem.tsx @@ -1,13 +1,26 @@ -import { useState } from 'react'; -import { formatDistanceToNow } from 'date-fns'; -import type { Activity, ResourceLinkResolver, TenantLinkResolver, TenantRenderer } from '../types/activity'; -import { ActivityFeedSummary, ResourceLinkClickHandler } from './ActivityFeedSummary'; -import { ActivityExpandedDetails } from './ActivityExpandedDetails'; -import { TenantBadge } from './TenantBadge'; -import { cn } from '../lib/utils'; -import { Button } from './ui/button'; -import { Card } from './ui/card'; -import { Plus, Pencil, Trash2, Activity as ActivityIcon } from 'lucide-react'; +import { useState } from "react"; +import type { + Activity, + ResourceLinkResolver, + TenantLinkResolver, + TenantRenderer, +} from "../types/activity"; +import { + ActivityFeedSummary, + ResourceLinkClickHandler, +} from "./ActivityFeedSummary"; +import { ActivityExpandedDetails } from "./ActivityExpandedDetails"; +import { TenantBadge } from "./TenantBadge"; +import { cn } from "../lib/utils"; +import { Button } from "@datum-cloud/datum-ui/button"; +import { Plus, Pencil, Trash2, Activity as ActivityIcon } from "lucide-react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; +import { TableCell, TableRow } from "@datum-cloud/datum-ui/table"; +import { Timestamp } from "./Timestamp"; + +// Number of columns rendered for the feed variant. Used as the colSpan on +// the expanded-detail row so it stretches across the full width. +export const ACTIVITY_FEED_COLUMN_COUNT = 5; export interface ActivityFeedItemProps { /** The activity to render */ @@ -33,45 +46,19 @@ export interface ActivityFeedItemProps { /** Whether this is a newly streamed activity */ isNew?: boolean; /** Layout variant: 'feed' (default) or 'timeline' */ - variant?: 'feed' | 'timeline'; + variant?: "feed" | "timeline"; /** Whether this is the last item in the list (hides bottom border, only used in timeline variant) */ isLast?: boolean; /** Whether the item starts expanded */ defaultExpanded?: boolean; } -/** - * Format timestamp for display - */ -function formatTimestamp(timestamp?: string): string { - if (!timestamp) return 'Unknown time'; - try { - const date = new Date(timestamp); - return formatDistanceToNow(date, { addSuffix: true }); - } catch { - return timestamp; - } -} - -/** - * Format timestamp for tooltip (in UTC) - */ -function formatTimestampFull(timestamp?: string): string { - if (!timestamp) return 'Unknown time'; - try { - const date = new Date(timestamp); - return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')} ${String(date.getUTCHours()).padStart(2, '0')}:${String(date.getUTCMinutes()).padStart(2, '0')}:${String(date.getUTCSeconds()).padStart(2, '0')} UTC`; - } catch { - return timestamp; - } -} - /** * Get avatar initials from actor name */ function getActorInitials(name: string): string { const parts = name.split(/[@\s.]+/).filter(Boolean); - if (parts.length === 0) return '?'; + if (parts.length === 0) return "?"; if (parts.length === 1) return parts[0].charAt(0).toUpperCase(); return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase(); } @@ -81,18 +68,18 @@ function getActorInitials(name: string): string { */ function getActorAvatarClasses(actorType: string, compact: boolean): string { const baseClasses = cn( - 'rounded-full flex items-center justify-center shrink-0 font-semibold', - compact ? 'w-5 h-5 text-xs' : 'w-6 h-6 text-xs' + "rounded-full flex items-center justify-center shrink-0 font-semibold", + compact ? "w-5 h-5 text-1xs" : "w-6 h-6 text-1xs", ); switch (actorType) { - case 'user': - return cn(baseClasses, 'bg-lime-200 text-slate-900 dark:bg-lime-800 dark:text-lime-100'); - case 'controller': - return cn(baseClasses, 'bg-rose-300 text-slate-900 dark:bg-rose-800 dark:text-rose-100'); - case 'machine account': - return cn(baseClasses, 'bg-muted text-muted-foreground'); + case "user": + return cn(baseClasses, "bg-primary text-primary-foreground"); + case "controller": + return cn(baseClasses, "bg-secondary text-secondary-foreground"); + case "machine account": + return cn(baseClasses, "bg-muted text-muted-foreground"); default: - return cn(baseClasses, 'bg-muted text-muted-foreground'); + return cn(baseClasses, "bg-muted text-muted-foreground"); } } @@ -104,34 +91,58 @@ function extractVerb(summary: string): string { if (words.length >= 2) { return words[1].toLowerCase(); } - return 'unknown'; + return "unknown"; } /** * Normalize verb to a canonical form for coloring */ -function normalizeVerb(verb: string): 'create' | 'update' | 'delete' | 'other' { +function normalizeVerb(verb: string): "create" | "update" | "delete" | "other" { const normalized = verb.toLowerCase(); - if (normalized.includes('create') || normalized.includes('add')) return 'create'; - if (normalized.includes('delete') || normalized.includes('remove')) return 'delete'; - if (normalized.includes('update') || normalized.includes('patch') || normalized.includes('modify') || normalized.includes('change') || normalized.includes('edit')) return 'update'; - return 'other'; + if (normalized.includes("create") || normalized.includes("add")) + return "create"; + if (normalized.includes("delete") || normalized.includes("remove")) + return "delete"; + if ( + normalized.includes("update") || + normalized.includes("patch") || + normalized.includes("modify") || + normalized.includes("change") || + normalized.includes("edit") + ) + return "update"; + return "other"; } /** * Get icon container + icon color classes based on verb */ -function getActionIconClasses(verb: string): { container: string; icon: string } { +function getActionIconClasses(verb: string): { + container: string; + icon: string; +} { const normalizedVerb = normalizeVerb(verb); switch (normalizedVerb) { - case 'create': - return { container: 'bg-blue-50 dark:bg-blue-950', icon: 'text-blue-500 dark:text-blue-400' }; - case 'update': - return { container: 'bg-green-50 dark:bg-green-950', icon: 'text-green-600 dark:text-green-400' }; - case 'delete': - return { container: 'bg-red-50 dark:bg-red-950', icon: 'text-red-500 dark:text-red-400' }; + case "create": + return { + container: "bg-blue-50 dark:bg-blue-950", + icon: "text-blue-500 dark:text-blue-400", + }; + case "update": + return { + container: "bg-green-50 dark:bg-green-950", + icon: "text-green-600 dark:text-green-400", + }; + case "delete": + return { + container: "bg-red-50 dark:bg-red-950", + icon: "text-red-500 dark:text-red-400", + }; default: - return { container: 'bg-slate-100 dark:bg-slate-800', icon: 'text-slate-500 dark:text-slate-400' }; + return { + container: "bg-slate-100 dark:bg-slate-800", + icon: "text-slate-500 dark:text-slate-400", + }; } } @@ -141,11 +152,11 @@ function getActionIconClasses(verb: string): { container: string; icon: string } function getTimelineIcon(verb: string): React.ElementType { const normalizedVerb = normalizeVerb(verb); switch (normalizedVerb) { - case 'create': + case "create": return Plus; - case 'update': + case "update": return Pencil; - case 'delete': + case "delete": return Trash2; default: return ActivityIcon; @@ -164,10 +175,10 @@ export function ActivityFeedItem({ onActorClick, onActivityClick, isSelected = false, - className = '', + className = "", compact = false, isNew = false, - variant = 'feed', + variant = "feed", isLast = false, defaultExpanded = false, }: ActivityFeedItemProps) { @@ -176,10 +187,6 @@ export function ActivityFeedItem({ const { spec, metadata } = activity; const { actor, summary, links, tenant } = spec; - const handleClick = () => { - onActivityClick?.(activity); - }; - const handleActorClick = (e: React.MouseEvent) => { e.stopPropagation(); if (onActorClick) { @@ -194,26 +201,32 @@ export function ActivityFeedItem({ const timestamp = metadata?.creationTimestamp; const verb = extractVerb(summary); - const isTimeline = variant === 'timeline'; + const isTimeline = variant === "timeline"; // Timeline variant — flat list row with bottom border if (isTimeline) { const { container: iconBg, icon: iconColor } = getActionIconClasses(verb); const Icon = getTimelineIcon(verb); return ( -
+
{/* Action icon square */}
@@ -233,112 +246,188 @@ export function ActivityFeedItem({ {/* Tenant badge */} {tenant && (
- {tenantRenderer ? tenantRenderer(tenant) : } + {tenantRenderer ? ( + tenantRenderer(tenant) + ) : ( + + )}
)} {/* Timestamp */} - - {formatTimestamp(timestamp)} + + {/* Expand toggle */}
{/* Expanded Details */} {isExpanded && ( - + )}
); } - // Feed variant (single-row layout) - return ( - - {/* Single row layout */} -
- {/* Actor Avatar */} -
- {actor.type === 'controller' ? ( - - ) : actor.type === 'machine account' ? ( - 🤖 - ) : ( - {getActorInitials(actor.name)} - )} -
- - {/* Summary - takes remaining space */} -
- -
+ {actor.type === "controller" ? ( + + ) : actor.type === "machine account" ? ( + 🤖 + ) : ( + + {getActorInitials(actorInitialsSource)} + + )} +
+ ); - {/* Tenant badge */} - {tenant && ( -
- {tenantRenderer ? tenantRenderer(tenant) : } -
- )} + // Actor column = avatar only. The display name / email / UID are + // surfaced via a hover tooltip on the avatar so the column stays narrow. + const actorTooltipBody = ( +
+ {actorVisible} + {actor.email && actor.email !== actorVisible ? ( + {actor.email} + ) : null} + {actor.uid ? ( + {actor.uid} + ) : null} +
+ ); - {/* Timestamp */} - - {formatTimestamp(timestamp)} - + const actorCell = ( + + {avatar} + {actorTooltipBody} + + ); - {/* Expand button */} - -
- - {/* Expanded Details */} - {isExpanded && } - + + +
+ +
+
+ + {summary} + +
+ + + {tenant ? ( + tenantRenderer ? ( + tenantRenderer(tenant) + ) : ( + + ) + ) : null} + + + + + + + + + {isExpanded ? ( + + + + + + ) : null} + ); } diff --git a/ui/src/components/ActivityFeedItemSkeleton.tsx b/ui/src/components/ActivityFeedItemSkeleton.tsx index dae319a7..3f191a8e 100644 --- a/ui/src/components/ActivityFeedItemSkeleton.tsx +++ b/ui/src/components/ActivityFeedItemSkeleton.tsx @@ -1,48 +1,40 @@ -import { Card } from './ui/card'; -import { Skeleton } from './ui/skeleton'; +import { Skeleton } from '@datum-cloud/datum-ui/skeleton'; import { cn } from '../lib/utils'; export interface ActivityFeedItemSkeletonProps { /** Whether to show as compact (for resource detail tabs) */ compact?: boolean; + /** Whether this is the last item in the list (hides bottom border) */ + isLast?: boolean; /** Additional CSS class */ className?: string; } /** - * ActivityFeedItemSkeleton renders a loading placeholder that matches ActivityFeedItem layout + * Loading placeholder that mirrors the rendered shape of an + * ActivityFeedItem in `variant="timeline"` mode: an action-icon square + + * summary + tenant badge + timestamp + expand toggle, separated by the + * same bottom border the live row uses. */ export function ActivityFeedItemSkeleton({ compact = false, + isLast = false, className = '', }: ActivityFeedItemSkeletonProps) { return ( - - {/* Single row layout */} -
- {/* Actor Avatar skeleton */} - - - {/* Summary skeleton - takes remaining space */} - - - {/* Tenant badge skeleton */} - - - {/* Timestamp skeleton */} - - - {/* Expand button skeleton */} +
+
+ {/* Action icon square */} + + {/* Summary text */} + + {/* Tenant badge */} + + {/* Timestamp */} + + {/* Expand toggle */}
- +
); } diff --git a/ui/src/components/ActivityFeedSummary.tsx b/ui/src/components/ActivityFeedSummary.tsx index e9ec930c..bd3c822e 100644 --- a/ui/src/components/ActivityFeedSummary.tsx +++ b/ui/src/components/ActivityFeedSummary.tsx @@ -1,4 +1,36 @@ import type { ActivityLink, ResourceRef, ResourceLinkResolver, ResourceLinkContext } from '../types/activity'; +import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; + +/** + * Returns the visible text for a link: prefer the server-provided + * displayName (e.g. "Smith Nelson") and fall back to the original marker + * (e.g. an email or UID baked into the summary by the policy template). + */ +function linkVisibleText(link: ActivityLink): string { + return link.displayName && link.displayName.length > 0 ? link.displayName : link.marker; +} + +/** + * Returns true when the link carries hover-worthy detail beyond the + * visible text (e.g. an email or UID that we want to surface but not + * show inline). + */ +function linkHasHoverDetail(link: ActivityLink): boolean { + if (link.email && link.email !== linkVisibleText(link)) return true; + const uid = link.resource?.uid; + if (uid && uid !== linkVisibleText(link)) return true; + return false; +} + +/** Renders the tooltip body shown when a link is hovered. */ +function LinkHoverBody({ link }: { link: ActivityLink }) { + return ( +
+ {link.email ? {link.email} : null} + {link.resource?.uid ? {link.resource.uid} : null} +
+ ); +} export interface ResourceLinkClickHandler { (resource: ResourceRef): void; @@ -33,8 +65,12 @@ function parseSummaryWithLinks( return [summary]; } - // Sort links by marker length (longest first) to avoid partial matches - const sortedLinks = [...links].sort((a, b) => b.marker.length - a.marker.length); + // Sort links by marker length (longest first) to avoid partial matches. + // Skip empty markers — indexOf('') clamps to summary.length and would + // produce an infinite loop below. + const sortedLinks = [...links] + .filter((l) => l.marker && l.marker.length > 0) + .sort((a, b) => b.marker.length - a.marker.length); // Track positions that have been replaced interface ReplacedRange { @@ -82,24 +118,55 @@ function parseSummaryWithLinks( result.push(summary.substring(lastEnd, range.start)); } + const visibleText = linkVisibleText(range.link); + const showHover = linkHasHoverDetail(range.link); + // If resourceLinkResolver is provided, render as tag if (resourceLinkResolver) { const url = resourceLinkResolver(range.link.resource, resourceLinkContext); if (url) { - result.push( + const anchor = ( e.stopPropagation()} > - {range.link.marker} + {visibleText} ); + + if (showHover) { + result.push( + + {anchor} + + + + + ); + } else { + result.push(anchor); + } + } else if (showHover) { + // Resolver opted out of linking but we still want to surface the + // hover detail (email/UID) for user-typed references. + result.push( + + + + {visibleText} + + + + + + + ); } else { - // Resolver returned undefined, render as plain text - result.push(range.link.marker); + // Resolver returned undefined and there's no hover detail, render plain text + result.push(visibleText); } } else { // Fallback to button with onResourceClick handler for backward compatibility @@ -111,17 +178,30 @@ function parseSummaryWithLinks( } : undefined; - result.push( + const button = ( ); + + if (showHover) { + result.push( + + {button} + + + + + ); + } else { + result.push(button); + } } lastEnd = range.end; diff --git a/ui/src/components/ActivityLayout.tsx b/ui/src/components/ActivityLayout.tsx index 9e11989a..7a508e6b 100644 --- a/ui/src/components/ActivityLayout.tsx +++ b/ui/src/components/ActivityLayout.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Tabs, TabsList, TabsTrigger } from './ui/tabs'; +import { Tabs, TabsList, TabsTrigger } from '@datum-cloud/datum-ui/tabs'; import { cn } from '../lib/utils'; export interface ActivityTab { diff --git a/ui/src/components/ApiErrorAlert.tsx b/ui/src/components/ApiErrorAlert.tsx index 1aae8ed9..d278b889 100644 --- a/ui/src/components/ApiErrorAlert.tsx +++ b/ui/src/components/ApiErrorAlert.tsx @@ -1,6 +1,6 @@ import { AlertCircle, AlertTriangle, RotateCw } from 'lucide-react'; -import { Alert, AlertDescription } from './ui/alert'; -import { Button } from './ui/button'; +import { Alert, AlertDescription } from '@datum-cloud/datum-ui/alert'; +import { Button } from '@datum-cloud/datum-ui/button'; import { defaultErrorFormatter } from '../lib/errors'; import type { ErrorFormatter } from '../types/activity'; @@ -46,8 +46,8 @@ export function ApiErrorAlert({ error, onRetry, className, errorFormatter }: Api {message} {onRetry && (
@@ -163,26 +189,52 @@ export function AuditEventViewer({ {isExpanded && (
-

Event Information

+

+ Event Information +

-
Audit ID:
-
{event.auditID || 'N/A'}
-
Stage:
-
{event.stage || 'N/A'}
-
Level:
-
{event.level || 'N/A'}
-
Request URI:
-
{event.requestURI || 'N/A'}
+
+ Audit ID: +
+
+ {event.auditID || "N/A"} +
+
+ Stage: +
+
+ {event.stage || "N/A"} +
+
+ Level: +
+
+ {event.level || "N/A"} +
+
+ Request URI: +
+
+ {event.requestURI || "N/A"} +
{event.userAgent && ( <> -
User Agent:
-
{event.userAgent}
+
+ User Agent: +
+
+ {event.userAgent} +
)} {event.sourceIPs && event.sourceIPs.length > 0 && ( <> -
Source IPs:
-
{event.sourceIPs.join(', ')}
+
+ Source IPs: +
+
+ {event.sourceIPs.join(", ")} +
)}
@@ -190,23 +242,42 @@ export function AuditEventViewer({ {tenant && (
-

Tenant

- +

+ Tenant +

+
)} {event.user && (
-

User Information

+

+ User Information +

-
Username:
-
{event.user.username || 'N/A'}
-
UID:
-
{event.user.uid || 'N/A'}
+
+ Username: +
+
+ {event.user.username || "N/A"} +
+
+ UID: +
+
+ {event.user.uid || "N/A"} +
{event.user.groups && event.user.groups.length > 0 && ( <> -
Groups:
-
{event.user.groups.join(', ')}
+
+ Groups: +
+
+ {event.user.groups.join(", ")} +
)}
@@ -215,49 +286,80 @@ export function AuditEventViewer({ {event.responseStatus && (
-

Response Status

+

+ Response Status +

-
Code:
-
{event.responseStatus.code || 'N/A'}
-
Status:
-
{event.responseStatus.status || 'N/A'}
+
+ Code: +
+
+ {event.responseStatus.code || "N/A"} +
+
+ Status: +
+
+ {event.responseStatus.status || "N/A"} +
{event.responseStatus.message && ( <> -
Message:
-
{event.responseStatus.message}
+
+ Message: +
+
+ {event.responseStatus.message} +
)}
)} - {event.annotations && Object.keys(event.annotations).length > 0 && ( -
-

Annotations

-
- {Object.entries(event.annotations).map(([key, value]) => ( -
-
{key}:
-
{value}
-
- ))} -
-
- )} + {event.annotations && + Object.keys(event.annotations).length > 0 && ( +
+

+ Annotations +

+
+ {Object.entries(event.annotations).map( + ([key, value]) => ( +
+
+ {key}: +
+
{value}
+
+ ), + )} +
+
+ )} - {(event.requestObject || event.responseObject) ? ( + {event.requestObject || event.responseObject ? (
-

Request/Response Data

+

+ Request/Response Data +

{event.requestObject ? (
- Request Object -
{JSON.stringify(event.requestObject, null, 2)}
+ + Request Object + +
+                            {JSON.stringify(event.requestObject, null, 2)}
+                          
) : null} {event.responseObject ? (
- Response Object -
{JSON.stringify(event.responseObject, null, 2)}
+ + Response Object + +
+                            {JSON.stringify(event.responseObject, null, 2)}
+                          
) : null}
diff --git a/ui/src/components/AuditLogExpandedDetails.tsx b/ui/src/components/AuditLogExpandedDetails.tsx index b2a2c944..81bf75b4 100644 --- a/ui/src/components/AuditLogExpandedDetails.tsx +++ b/ui/src/components/AuditLogExpandedDetails.tsx @@ -1,274 +1,203 @@ -import { format } from 'date-fns'; import type { Event } from '../types'; +import { TooltipProvider } from './ui/tooltip'; +import { Timestamp } from './Timestamp'; +import { DetailGrid, DetailPanelShell, Field, Section } from './details'; export interface AuditLogExpandedDetailsProps { /** The audit event to display details for */ event: Event; + /** When true, applies the in-table padded shell (default true) */ + compact?: boolean; } /** - * Format timestamp for display (with timezone) + * AuditLogExpandedDetails renders the expanded details for an audit event. + * Sections: Request / Response / When / User / Resource / Source. Advanced + * fields and raw request/response objects are tucked under collapsed + *
sections at the end. */ -function formatTimestampFull(timestamp?: string): string { - if (!timestamp) return 'Unknown time'; - try { - return format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss \'UTC\''); - } catch { - return timestamp; - } -} - -/** - * AuditLogExpandedDetails renders the expanded details section for an audit log event. - * - * Section order (most to least relevant for investigation): - * 1. Request Summary (verb, URI) - * 2. Response Summary (status code with icon, message) - * 3. Timestamp (full) - * 4. User (username, UID, groups) - * 5. Resource (kind, name, namespace, API group) - * 6. Request Details (user agent, source IPs) - * 7. Advanced (collapsed) - audit ID, stage, level, annotations - * 8. Raw Objects (collapsed) - request/response JSON - */ -export function AuditLogExpandedDetails({ event }: AuditLogExpandedDetailsProps) { +export function AuditLogExpandedDetails({ event, compact = true }: AuditLogExpandedDetailsProps) { const timestamp = event.stageTimestamp || event.requestReceivedTimestamp; + const status = event.responseStatus; + const isOk = status?.code != null && status.code >= 200 && status.code < 300; - return ( -
- {/* Request Summary */} -
-

- Request Summary -

-
-
Verb:
-
{event.verb || 'Unknown'}
- {event.requestURI && ( - <> -
URI:
-
{event.requestURI}
- - )} -
-
+ const body = ( + <> + +
+ + {event.requestURI ? ( + + ) : null} +
- {/* Response Summary */} - {event.responseStatus && ( -
-

- Response Summary -

-
- {event.responseStatus.code !== undefined && ( - <> -
Status Code:
-
+ {status ? ( +
+ {status.code != null ? ( + = 200 && event.responseStatus.code < 300 + isOk ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400' } > - {event.responseStatus.code >= 200 && event.responseStatus.code < 300 ? '✓ ' : '✗ '} - {event.responseStatus.code} + {isOk ? '✓ ' : '✗ '} + {status.code} -
- - )} - {event.responseStatus.status && ( - <> -
Status:
-
{event.responseStatus.status}
- - )} - {event.responseStatus.message && ( - <> -
Message:
-
{event.responseStatus.message}
- - )} - {event.responseStatus.reason && ( - <> -
Reason:
-
{event.responseStatus.reason}
- - )} -
-
- )} + } + copyValue={String(status.code)} + copyLabel="status code" + /> + ) : null} + {status.status ? : null} + {status.reason ? : null} + {status.message ? ( + + ) : null} + + ) : null} - {/* Timestamp */} -
-

- Timestamp -

-

- {formatTimestampFull(timestamp)} -

-
+
+ } + copyValue={timestamp || ''} + copyLabel="timestamp" + disableTooltip + /> +
- {/* User Information */} - {event.user ? ( -
-

- User -

-
- {event.user.username && ( - <> -
Username:
-
{event.user.username}
- - )} - {event.user.uid && ( - <> -
UID:
-
{event.user.uid}
- - )} - {event.user.groups && event.user.groups.length > 0 && ( - <> -
Groups:
-
- {event.user.groups.join(', ')} -
- - )} -
-
- ) : null} + {event.user ? ( +
+ {event.user.username ? ( + + ) : null} + {event.user.uid ? ( + + ) : null} + {event.user.groups && event.user.groups.length > 0 ? ( + + ) : null} +
+ ) : null} - {/* Resource Information */} - {event.objectRef && ( -
-

- Resource -

-
- {event.objectRef.resource && ( - <> -
Kind:
-
{event.objectRef.resource}
- - )} - {event.objectRef.name && ( - <> -
Name:
-
{event.objectRef.name}
- - )} - {event.objectRef.namespace && ( - <> -
Namespace:
-
{event.objectRef.namespace}
- - )} - {event.objectRef.apiGroup && ( - <> -
API Group:
-
{event.objectRef.apiGroup}
- - )} - {event.objectRef.apiVersion && ( - <> -
API Version:
-
{event.objectRef.apiVersion}
- - )} - {event.objectRef.uid && ( - <> -
UID:
-
{event.objectRef.uid}
- - )} - {event.objectRef.subresource && ( - <> -
Subresource:
-
{event.objectRef.subresource}
- - )} -
-
- )} + {event.objectRef ? ( +
+ + {event.objectRef.resource || 'Unknown'} + {event.objectRef.apiGroup ? ( + · {event.objectRef.apiGroup} + ) : null} + + } + copyValue={event.objectRef.resource} + copyLabel="resource kind" + /> + {event.objectRef.name ? ( + + ) : null} + {event.objectRef.namespace ? ( + + ) : null} + {event.objectRef.apiVersion ? ( + + ) : null} + {event.objectRef.subresource ? ( + + ) : null} + {event.objectRef.uid ? ( + + ) : null} +
+ ) : null} - {/* Request Details */} - {(event.userAgent || (event.sourceIPs && event.sourceIPs.length > 0)) && ( -
-

- Request Details -

-
- {event.userAgent && ( - <> -
User Agent:
-
{event.userAgent}
- - )} - {event.sourceIPs && event.sourceIPs.length > 0 && ( - <> -
Source IPs:
-
{event.sourceIPs.join(', ')}
- - )} -
-
- )} + {event.userAgent || (event.sourceIPs && event.sourceIPs.length > 0) ? ( +
+ {event.sourceIPs && event.sourceIPs.length > 0 ? ( + + ) : null} + {event.userAgent ? ( + + ) : null} +
+ ) : null} +
- {/* Advanced Details (collapsed) */} - {(event.auditID || event.stage || event.level || (event.annotations && Object.keys(event.annotations).length > 0)) && ( -
+ {/* Advanced (collapsed) */} + {event.auditID || event.stage || event.level || + (event.annotations && Object.keys(event.annotations).length > 0) ? ( +
-

+

Advanced

-
-
- {event.auditID && ( - <> -
Audit ID:
-
{event.auditID}
- - )} - {event.stage && ( - <> -
Stage:
-
{event.stage}
- - )} - {event.level && ( - <> -
Level:
-
{event.level}
- - )} - {event.annotations && Object.entries(event.annotations).map(([key, value]) => ( -
-
{key}:
-
{value}
-
- ))} -
+
+ +
+ {event.auditID ? ( + + ) : null} + {event.stage ? : null} + {event.level ? : null} +
+ {event.annotations && Object.keys(event.annotations).length > 0 ? ( +
+ {Object.entries(event.annotations).map(([key, value]) => ( + + ))} +
+ ) : null} +
- )} + ) : null} - {/* Raw Objects (collapsed) */} - {(event.requestObject || event.responseObject) ? ( -
+ {/* Raw request/response (collapsed) */} + {event.requestObject || event.responseObject ? ( +
-

+

- Raw Objects + Raw objects

-
+
{event.requestObject ? (
-
Request Object
+
+ Request +
                   {JSON.stringify(event.requestObject, null, 2)}
                 
@@ -276,7 +205,9 @@ export function AuditLogExpandedDetails({ event }: AuditLogExpandedDetailsProps) ) : null} {event.responseObject ? (
-
Response Object
+
+ Response +
                   {JSON.stringify(event.responseObject, null, 2)}
                 
@@ -285,6 +216,16 @@ export function AuditLogExpandedDetails({ event }: AuditLogExpandedDetailsProps)
) : null} -
+ + ); + + return ( + + {compact ? ( + {body} + ) : ( +
{body}
+ )} +
); } diff --git a/ui/src/components/AuditLogFeedItem.tsx b/ui/src/components/AuditLogFeedItem.tsx index 5e1baec1..a2ba89a4 100644 --- a/ui/src/components/AuditLogFeedItem.tsx +++ b/ui/src/components/AuditLogFeedItem.tsx @@ -1,11 +1,17 @@ import { useState } from 'react'; -import { format, formatDistanceToNow } from 'date-fns'; +import { Activity as ActivityIcon, Pencil, Plus, Trash2 } from 'lucide-react'; import type { Event } from '../types'; import { AuditLogExpandedDetails } from './AuditLogExpandedDetails'; import { cn } from '../lib/utils'; -import { Button } from './ui/button'; -import { Card } from './ui/card'; +import { Button } from '@datum-cloud/datum-ui/button'; import { Badge } from './ui/badge'; +import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; +import { TableCell, TableRow } from '@datum-cloud/datum-ui/table'; +import { Timestamp } from './Timestamp'; + +// Number of columns rendered for the audit log table. Used by the colSpan +// on the expanded-detail row so it spans the full width. +export const AUDIT_LOG_COLUMN_COUNT = 5; export interface AuditLogFeedItemProps { /** The audit event to render */ @@ -22,31 +28,10 @@ export interface AuditLogFeedItemProps { isNew?: boolean; /** Whether the item starts expanded */ defaultExpanded?: boolean; -} - -/** - * Format timestamp for display - */ -function formatTimestamp(timestamp?: string): string { - if (!timestamp) return 'Unknown time'; - try { - const date = new Date(timestamp); - return formatDistanceToNow(date, { addSuffix: true }); - } catch { - return timestamp; - } -} - -/** - * Format timestamp for tooltip (with timezone) - */ -function formatTimestampFull(timestamp?: string): string { - if (!timestamp) return 'Unknown time'; - try { - return format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss \'UTC\''); - } catch { - return timestamp; - } + /** Layout variant: 'feed' (table row, default) or 'timeline' (flat list row) */ + variant?: 'feed' | 'timeline'; + /** Whether this is the last item (only used in timeline variant) */ + isLast?: boolean; } /** @@ -116,6 +101,8 @@ export function AuditLogFeedItem({ compact = false, isNew = false, defaultExpanded = false, + variant = 'feed', + isLast = false, }: AuditLogFeedItemProps) { const [isExpanded, setIsExpanded] = useState(defaultExpanded); @@ -132,65 +119,172 @@ export function AuditLogFeedItem({ const summary = buildAuditSummary(event); const statusIndicator = getResponseStatusIndicator(event.responseStatus?.code); - return ( - -
- {/* Main Content */} -
- {/* Single row layout: Summary + Metadata + Timestamp + Expand */} -
- {/* Summary - takes remaining space */} -
- {summary} -
- - {/* Verb badge */} - - {event.verb?.toUpperCase() || 'UNKNOWN'} - - - {/* Response status */} - - {statusIndicator.icon} - {event.responseStatus?.code && ( - {event.responseStatus.code} - )} - - - {/* Timestamp */} - - {formatTimestamp(timestamp)} - - - {/* Expand button */} - + if (variant === 'timeline') { + const verb = event.verb?.toLowerCase() || ''; + const Icon = + verb === 'create' ? Plus : + verb === 'delete' ? Trash2 : + verb === 'update' || verb === 'patch' ? Pencil : + ActivityIcon; + const iconBg = + verb === 'create' ? 'bg-green-50 dark:bg-green-950' : + verb === 'delete' ? 'bg-red-50 dark:bg-red-950' : + verb === 'update' || verb === 'patch' ? 'bg-amber-50 dark:bg-amber-950' : + 'bg-slate-100 dark:bg-slate-800'; + const iconColor = + verb === 'create' ? 'text-green-600 dark:text-green-400' : + verb === 'delete' ? 'text-red-500 dark:text-red-400' : + verb === 'update' || verb === 'patch' ? 'text-amber-600 dark:text-amber-400' : + 'text-slate-500 dark:text-slate-400'; + + return ( +
+
+
+
+
+ + +
+ {summary} +
+
+ + {summary} + +
+
+ + {statusIndicator.icon} + {event.responseStatus?.code ? {event.responseStatus.code} : null} + + + + +
+ {isExpanded ? : null}
+ ); + } - {/* Expanded Details */} - {isExpanded && } - + return ( + <> + { + toggleExpand(e); + handleClick(); + }} + aria-expanded={isExpanded} + > + + + {event.verb?.toUpperCase() || 'UNKNOWN'} + + + + + +
+ {summary} +
+
+ + {summary} + +
+
+ + + {statusIndicator.icon} + {event.responseStatus?.code ? {event.responseStatus.code} : null} + + + + + + + + +
+ {isExpanded ? ( + + + + + + ) : null} + ); } diff --git a/ui/src/components/AuditLogFilters.tsx b/ui/src/components/AuditLogFilters.tsx index bebe9115..d164439f 100644 --- a/ui/src/components/AuditLogFilters.tsx +++ b/ui/src/components/AuditLogFilters.tsx @@ -1,13 +1,16 @@ -import { useState, useCallback, useEffect } from 'react'; -import { formatISO, subDays } from 'date-fns'; - -import type { ActivityApiClient } from '../api/client'; -import { useAuditLogFacets, type AuditLogTimeRange } from '../hooks/useAuditLogFacets'; -import { TimeRangeDropdown } from './ui/time-range-dropdown'; -import { FilterChip } from './ui/filter-chip'; -import { AddFilterDropdown, type FilterOption } from './ui/add-filter-dropdown'; -import { ActionMultiSelect } from './ActionMultiSelect'; -import { UserSelect } from './UserSelect'; +import { useState, useCallback, useEffect } from "react"; +import { formatISO, subDays } from "date-fns"; + +import type { ActivityApiClient } from "../api/client"; +import { + useAuditLogFacets, + type AuditLogTimeRange, +} from "../hooks/useAuditLogFacets"; +import { TimeRangeDropdown } from "./ui/time-range-dropdown"; +import { FilterChip } from "./ui/filter-chip"; +import { AddFilterDropdown, type FilterOption } from "./ui/add-filter-dropdown"; +import { ActionMultiSelect } from "./ActionMultiSelect"; +import { UserSelect } from "./UserSelect"; /** * Filter state for audit logs @@ -56,12 +59,12 @@ export interface AuditLogFiltersProps { * Preset time ranges */ const TIME_PRESETS = [ - { key: 'last15min', label: 'Last 15 min' }, - { key: 'last1hour', label: 'Last hour' }, - { key: 'last6hours', label: 'Last 6 hours' }, - { key: 'last24hours', label: 'Last 24 hours' }, - { key: 'last7days', label: 'Last 7 days' }, - { key: 'last30days', label: 'Last 30 days' }, + { key: "last15min", label: "Last 15 min" }, + { key: "last1hour", label: "Last hour" }, + { key: "last6hours", label: "Last 6 hours" }, + { key: "last24hours", label: "Last 24 hours" }, + { key: "last7days", label: "Last 7 days" }, + { key: "last30days", label: "Last 30 days" }, ]; /** @@ -72,22 +75,22 @@ function presetToTimeRange(presetKey: string): AuditLogTimeRange { let start: Date; switch (presetKey) { - case 'last15min': + case "last15min": start = new Date(now.getTime() - 15 * 60 * 1000); break; - case 'last1hour': + case "last1hour": start = new Date(now.getTime() - 60 * 60 * 1000); break; - case 'last6hours': + case "last6hours": start = new Date(now.getTime() - 6 * 60 * 60 * 1000); break; - case 'last24hours': + case "last24hours": start = new Date(now.getTime() - 24 * 60 * 60 * 1000); break; - case 'last7days': + case "last7days": start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); break; - case 'last30days': + case "last30days": start = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); break; default: @@ -103,46 +106,51 @@ function presetToTimeRange(presetKey: string): AuditLogTimeRange { /** * Filter configuration registry */ -type FilterId = 'verbs' | 'resourceTypes' | 'namespaces' | 'usernames' | 'resourceName'; +type FilterId = + | "verbs" + | "resourceTypes" + | "namespaces" + | "usernames" + | "resourceName"; interface FilterConfig { id: FilterId; label: string; - inputMode: 'typeahead' | 'text'; + inputMode: "typeahead" | "text"; placeholder?: string; searchPlaceholder?: string; } const FILTER_CONFIGS: Record = { verbs: { - id: 'verbs', - label: 'Action', - inputMode: 'typeahead', - searchPlaceholder: 'Search actions...', + id: "verbs", + label: "Action", + inputMode: "typeahead", + searchPlaceholder: "Search actions...", }, resourceTypes: { - id: 'resourceTypes', - label: 'Resource', - inputMode: 'typeahead', - searchPlaceholder: 'Search resources...', + id: "resourceTypes", + label: "Resource", + inputMode: "typeahead", + searchPlaceholder: "Search resources...", }, namespaces: { - id: 'namespaces', - label: 'Namespace', - inputMode: 'typeahead', - searchPlaceholder: 'Search namespaces...', + id: "namespaces", + label: "Namespace", + inputMode: "typeahead", + searchPlaceholder: "Search namespaces...", }, usernames: { - id: 'usernames', - label: 'User', - inputMode: 'typeahead', - searchPlaceholder: 'Search users...', + id: "usernames", + label: "User", + inputMode: "typeahead", + searchPlaceholder: "Search users...", }, resourceName: { - id: 'resourceName', - label: 'Name', - inputMode: 'text', - placeholder: 'Enter resource name...', + id: "resourceName", + label: "Name", + inputMode: "text", + placeholder: "Enter resource name...", }, }; @@ -152,14 +160,13 @@ const FILTER_CONFIGS: Record = { const formatDatetimeLocal = (isoString: string): string => { const date = new Date(isoString); const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); return `${year}-${month}-${day}T${hours}:${minutes}`; }; - /** * Build CEL filter expression from filter state */ @@ -172,7 +179,7 @@ export function buildAuditLogCEL(filters: AuditLogFilterState): string { conditions.push(`verb == "${filters.verbs[0]}"`); } else { const verbConditions = filters.verbs.map((v) => `verb == "${v}"`); - conditions.push(`(${verbConditions.join(' || ')})`); + conditions.push(`(${verbConditions.join(" || ")})`); } } @@ -181,8 +188,10 @@ export function buildAuditLogCEL(filters: AuditLogFilterState): string { if (filters.resourceTypes.length === 1) { conditions.push(`objectRef.resource == "${filters.resourceTypes[0]}"`); } else { - const resConditions = filters.resourceTypes.map((r) => `objectRef.resource == "${r}"`); - conditions.push(`(${resConditions.join(' || ')})`); + const resConditions = filters.resourceTypes.map( + (r) => `objectRef.resource == "${r}"`, + ); + conditions.push(`(${resConditions.join(" || ")})`); } } @@ -191,8 +200,10 @@ export function buildAuditLogCEL(filters: AuditLogFilterState): string { if (filters.namespaces.length === 1) { conditions.push(`objectRef.namespace == "${filters.namespaces[0]}"`); } else { - const nsConditions = filters.namespaces.map((ns) => `objectRef.namespace == "${ns}"`); - conditions.push(`(${nsConditions.join(' || ')})`); + const nsConditions = filters.namespaces.map( + (ns) => `objectRef.namespace == "${ns}"`, + ); + conditions.push(`(${nsConditions.join(" || ")})`); } } @@ -201,8 +212,10 @@ export function buildAuditLogCEL(filters: AuditLogFilterState): string { if (filters.usernames.length === 1) { conditions.push(`user.username == "${filters.usernames[0]}"`); } else { - const userConditions = filters.usernames.map((u) => `user.username == "${u}"`); - conditions.push(`(${userConditions.join(' || ')})`); + const userConditions = filters.usernames.map( + (u) => `user.username == "${u}"`, + ); + conditions.push(`(${userConditions.join(" || ")})`); } } @@ -216,7 +229,7 @@ export function buildAuditLogCEL(filters: AuditLogFilterState): string { conditions.push(filters.customFilter); } - return conditions.join(' && '); + return conditions.join(" && "); } /** @@ -229,34 +242,38 @@ export function AuditLogFilters({ onFiltersChange, onTimeRangeChange, disabled = false, - className = '', + className = "", }: AuditLogFiltersProps) { // Convert timeRange to format expected by useAuditLogFacets - const [facetTimeRange, setFacetTimeRange] = useState(() => - presetToTimeRange('last24hours') - ); + const [facetTimeRange, setFacetTimeRange] = + useState(() => presetToTimeRange("last24hours")); - const { verbs, resources, namespaces, usernames, error: facetsError } = useAuditLogFacets( - client, - facetTimeRange - ); + const { + verbs, + resources, + namespaces, + usernames, + error: facetsError, + } = useAuditLogFacets(client, facetTimeRange); // Log facets error for debugging if (facetsError) { - console.error('Failed to load audit log facets:', facetsError); + console.error("Failed to load audit log facets:", facetsError); } // Track which filter was just added to auto-open it const [pendingFilter, setPendingFilter] = useState(null); // Track selected preset - const [selectedPreset, setSelectedPreset] = useState('last24hours'); + const [selectedPreset, setSelectedPreset] = useState("last24hours"); // Custom time range state const [customStart, setCustomStart] = useState(() => - formatDatetimeLocal(formatISO(subDays(new Date(), 1))) + formatDatetimeLocal(formatISO(subDays(new Date(), 1))), + ); + const [customEnd, setCustomEnd] = useState(() => + formatDatetimeLocal(formatISO(new Date())), ); - const [customEnd, setCustomEnd] = useState(() => formatDatetimeLocal(formatISO(new Date()))); // Handle time range preset selection const handleTimePresetSelect = useCallback( @@ -269,13 +286,13 @@ export function AuditLogFilters({ end: range.end, }); }, - [onTimeRangeChange] + [onTimeRangeChange], ); // Handle custom time range apply const handleCustomRangeApply = useCallback( (start: string, end: string) => { - setSelectedPreset('custom'); + setSelectedPreset("custom"); setCustomStart(start); setCustomEnd(end); const startIso = new Date(start).toISOString(); @@ -286,29 +303,31 @@ export function AuditLogFilters({ end: endIso, }); }, - [onTimeRangeChange] + [onTimeRangeChange], ); // Get display label for time range const getTimeRangeLabel = () => { const preset = TIME_PRESETS.find((p) => p.key === selectedPreset); if (preset) return preset.label; - if (selectedPreset === 'custom' && timeRange.start && timeRange.end) { + if (selectedPreset === "custom" && timeRange.start && timeRange.end) { const start = new Date(timeRange.start); const end = new Date(timeRange.end); return `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`; } - return 'Select time range'; + return "Select time range"; }; // Determine which filters are currently active (have values) // Note: We exclude verbs and usernames from filter chips since they're handled by quick filters const filtersWithValues: FilterId[] = []; // if (filters.verbs && filters.verbs.length > 0) filtersWithValues.push('verbs'); // Handled by ActionToggle - if (filters.resourceTypes && filters.resourceTypes.length > 0) filtersWithValues.push('resourceTypes'); - if (filters.namespaces && filters.namespaces.length > 0) filtersWithValues.push('namespaces'); + if (filters.resourceTypes && filters.resourceTypes.length > 0) + filtersWithValues.push("resourceTypes"); + if (filters.namespaces && filters.namespaces.length > 0) + filtersWithValues.push("namespaces"); // if (filters.usernames && filters.usernames.length > 0) filtersWithValues.push('usernames'); // Handled by UserSelect - if (filters.resourceName) filtersWithValues.push('resourceName'); + if (filters.resourceName) filtersWithValues.push("resourceName"); // Include pendingFilter (newly added filter awaiting value selection) in the displayed filters const activeFilterIds: FilterId[] = @@ -327,9 +346,9 @@ export function AuditLogFilters({ // Build available filters list // Note: Action and User are now quick filters, so they're excluded from the dropdown const availableFilters: FilterOption[] = [ - { id: 'resourceTypes', label: 'Resource' }, - { id: 'namespaces', label: 'Namespace' }, - { id: 'resourceName', label: 'Name' }, + { id: "resourceTypes", label: "Resource" }, + { id: "namespaces", label: "Namespace" }, + { id: "resourceName", label: "Name" }, ]; // Handle adding a filter @@ -343,7 +362,7 @@ export function AuditLogFilters({ if (pendingFilter === filterId) { const hasValues = (() => { const value = filters[filterId]; - if (filterId === 'resourceName') return !!value; + if (filterId === "resourceName") return !!value; return Array.isArray(value) && value.length > 0; })(); if (!hasValues) { @@ -351,7 +370,7 @@ export function AuditLogFilters({ } } }, - [pendingFilter, filters] + [pendingFilter, filters], ); // Handle filter value changes @@ -362,7 +381,7 @@ export function AuditLogFilters({ [filterId]: values.length > 0 ? values : undefined, }); }, - [filters, onFiltersChange] + [filters, onFiltersChange], ); // Handle filter clear @@ -373,13 +392,13 @@ export function AuditLogFilters({ [filterId]: undefined, }); }, - [filters, onFiltersChange] + [filters, onFiltersChange], ); // Get options for a specific filter const getFilterOptions = (filterId: FilterId) => { switch (filterId) { - case 'verbs': + case "verbs": return verbs .filter((facet) => facet.value) .map((facet) => ({ @@ -387,7 +406,7 @@ export function AuditLogFilters({ label: facet.value, count: facet.count, })); - case 'resourceTypes': + case "resourceTypes": return resources .filter((facet) => facet.value) .map((facet) => ({ @@ -395,7 +414,7 @@ export function AuditLogFilters({ label: facet.value, count: facet.count, })); - case 'namespaces': + case "namespaces": return namespaces .filter((facet) => facet.value) .map((facet) => ({ @@ -403,7 +422,7 @@ export function AuditLogFilters({ label: facet.value, count: facet.count, })); - case 'usernames': + case "usernames": return usernames .filter((facet) => facet.value) .map((facet) => ({ @@ -419,7 +438,7 @@ export function AuditLogFilters({ // Get values for a specific filter const getFilterValues = (filterId: FilterId): string[] => { const value = filters[filterId]; - if (filterId === 'resourceName') { + if (filterId === "resourceName") { return value ? [value as string] : []; } return (value as string[] | undefined) || []; @@ -433,7 +452,7 @@ export function AuditLogFilters({ verbs: selectedVerbs.length > 0 ? selectedVerbs : undefined, }); }, - [filters, onFiltersChange] + [filters, onFiltersChange], ); // Get current action values for multi-select @@ -458,7 +477,7 @@ export function AuditLogFilters({ usernames: username ? [username] : undefined, }); }, - [filters, onFiltersChange] + [filters, onFiltersChange], ); // Get current user value for select (single selection for quick filter) @@ -478,7 +497,7 @@ export function AuditLogFilters({ })); return ( -
+
{/* Action Multi-Select */} handleFilterChange(filterId, values)} onClear={() => handleFilterClear(filterId)} onPopoverClose={() => handlePopoverClose(filterId)} diff --git a/ui/src/components/AuditLogQueryComponent.tsx b/ui/src/components/AuditLogQueryComponent.tsx index 82574b7e..9a886ac4 100644 --- a/ui/src/components/AuditLogQueryComponent.tsx +++ b/ui/src/components/AuditLogQueryComponent.tsx @@ -1,13 +1,30 @@ -import { useState, useEffect, useRef, useCallback } from 'react'; -import { formatISO, subDays } from 'date-fns'; -import { AuditLogFilters, buildAuditLogCEL, type AuditLogFilterState, type TimeRange } from './AuditLogFilters'; -import { AuditLogFeedItem } from './AuditLogFeedItem'; -import { useAuditLogQuery } from '../hooks/useAuditLogQuery'; -import type { AuditLogQuerySpec, Event } from '../types'; -import type { ActivityApiClient } from '../api/client'; -import type { ErrorFormatter } from '../types/activity'; -import { Card } from './ui/card'; -import { ApiErrorAlert } from './ApiErrorAlert'; +import { useState, useEffect, useRef, useCallback } from "react"; +import { formatISO, subDays } from "date-fns"; +import { + AuditLogFilters, + buildAuditLogCEL, + type AuditLogFilterState, + type TimeRange, +} from "./AuditLogFilters"; +import { AuditLogFeedItem, AUDIT_LOG_COLUMN_COUNT } from "./AuditLogFeedItem"; +import { useAuditLogQuery } from "../hooks/useAuditLogQuery"; +import type { AuditLogQuerySpec, Event } from "../types"; +import type { ActivityApiClient } from "../api/client"; +import type { ErrorFormatter } from "../types/activity"; +import { Card } from "@datum-cloud/datum-ui/card"; +import { Button } from "@datum-cloud/datum-ui/button"; +import { Skeleton } from "@datum-cloud/datum-ui/skeleton"; +import { ApiErrorAlert } from "./ApiErrorAlert"; +import { cn } from "../lib/utils"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@datum-cloud/datum-ui/table"; +import { TooltipProvider } from "./ui/tooltip"; // Debounce delay for filter changes (ms) const FILTER_DEBOUNCE_MS = 300; @@ -23,6 +40,8 @@ export interface AuditLogQueryComponentProps { initialTimeRange?: TimeRange; /** Custom error formatter for customizing error messages */ errorFormatter?: ErrorFormatter; + /** Layout variant: 'feed' (table, default) or 'timeline' (icon-list rows) */ + variant?: 'feed' | 'timeline'; } /** @@ -30,7 +49,7 @@ export interface AuditLogQueryComponentProps { */ export function AuditLogQueryComponent({ client, - className = '', + className = "", onEventSelect, initialFilters = {}, initialTimeRange = { @@ -38,6 +57,7 @@ export function AuditLogQueryComponent({ end: formatISO(new Date()), }, errorFormatter, + variant = 'feed', }: AuditLogQueryComponentProps) { const [filters, setFilters] = useState(initialFilters); const [timeRange, setTimeRange] = useState(initialTimeRange); @@ -45,7 +65,6 @@ export function AuditLogQueryComponent({ const { events, isLoading, error, hasMore, executeQuery, loadMore } = useAuditLogQuery({ client }); - const loadMoreTriggerRef = useRef(null); const scrollContainerRef = useRef(null); // Store the latest loadMore function in a ref to avoid observer re-subscription const loadMoreRef = useRef(loadMore); @@ -55,7 +74,7 @@ export function AuditLogQueryComponent({ // Build query spec from current filter state const buildQuerySpec = useCallback((): AuditLogQuerySpec => { const spec: AuditLogQuerySpec = { - filter: buildAuditLogCEL(filters) || '', + filter: buildAuditLogCEL(filters) || "", startTime: timeRange.start, endTime: timeRange.end, limit: DEFAULT_PAGE_SIZE, @@ -71,40 +90,34 @@ export function AuditLogQueryComponent({ }, [buildQuerySpec, executeQuery]); // Handle filter changes with debounced auto-refresh - const handleFiltersChange = useCallback( - (newFilters: AuditLogFilterState) => { - setFilters(newFilters); + const handleFiltersChange = useCallback((newFilters: AuditLogFilterState) => { + setFilters(newFilters); - // Cancel any pending debounced refresh - if (filterDebounceRef.current) { - clearTimeout(filterDebounceRef.current); - } + // Cancel any pending debounced refresh + if (filterDebounceRef.current) { + clearTimeout(filterDebounceRef.current); + } - // Debounce the refresh to avoid excessive API calls - filterDebounceRef.current = setTimeout(() => { - filterDebounceRef.current = null; - }, FILTER_DEBOUNCE_MS); - }, - [] - ); + // Debounce the refresh to avoid excessive API calls + filterDebounceRef.current = setTimeout(() => { + filterDebounceRef.current = null; + }, FILTER_DEBOUNCE_MS); + }, []); // Handle time range changes with debounced auto-refresh - const handleTimeRangeChange = useCallback( - (newTimeRange: TimeRange) => { - setTimeRange(newTimeRange); + const handleTimeRangeChange = useCallback((newTimeRange: TimeRange) => { + setTimeRange(newTimeRange); - // Cancel any pending debounced refresh - if (filterDebounceRef.current) { - clearTimeout(filterDebounceRef.current); - } + // Cancel any pending debounced refresh + if (filterDebounceRef.current) { + clearTimeout(filterDebounceRef.current); + } - // Debounce the refresh - filterDebounceRef.current = setTimeout(() => { - filterDebounceRef.current = null; - }, FILTER_DEBOUNCE_MS); - }, - [] - ); + // Debounce the refresh + filterDebounceRef.current = setTimeout(() => { + filterDebounceRef.current = null; + }, FILTER_DEBOUNCE_MS); + }, []); // Auto-refresh when filters or time range change (debounced) useEffect(() => { @@ -142,32 +155,9 @@ export function AuditLogQueryComponent({ loadMoreRef.current = loadMore; }, [loadMore]); - // Infinite scroll using Intersection Observer - useEffect(() => { - if (!loadMoreTriggerRef.current) return; - - const observer = new IntersectionObserver( - (entries) => { - const entry = entries[0]; - if (entry.isIntersecting && hasMore && !isLoading) { - console.log('[AuditLogQueryComponent] Intersection triggered, loading more...'); - // Call through the ref to always use the latest function - loadMoreRef.current(); - } - }, - { - root: scrollContainerRef.current, - rootMargin: '200px', - threshold: 0, - } - ); - - observer.observe(loadMoreTriggerRef.current); - - return () => { - observer.disconnect(); - }; - }, [hasMore, isLoading]); + // Audit log results use manual "Load more" pagination for consistency + // with the activity feed; the previous IntersectionObserver-driven + // infinite scroll caused observer rebuild loops on isLoading toggles. // Cleanup on unmount useEffect(() => { @@ -179,7 +169,7 @@ export function AuditLogQueryComponent({ }, []); return ( - + {/* Filters */} {/* Error Display */} - + {/* Event List with Infinite Scroll */} -
- {/* Loading State (initial load) */} - {isLoading && events.length === 0 && ( -
-
- Searching audit logs... -
- )} - - {/* Empty State */} - {!isLoading && events.length === 0 && !error && ( -
-

No audit events found

-

- Try adjusting your filters or time range -

-
- )} - - {/* Event List */} - {events.map((event, index) => ( - - ))} - - {/* Load More Trigger for Infinite Scroll */} - {hasMore &&
} - - {/* Loading Indicator (pagination) */} - {isLoading && events.length > 0 && ( -
-
- Loading more events... -
- )} - - {/* End of Results */} - {!hasMore && events.length > 0 && !isLoading && ( -
- End of results +
+ + {variant === 'timeline' ? ( +
+ {isLoading && events.length === 0 + ? Array.from({ length: 8 }).map((_, index) => ( +
+
+ + + + + +
+
+ )) + : null} + {!isLoading && events.length === 0 && !error ? ( +
+
No audit events found
+
+ Try adjusting your filters or time range +
+
+ ) : null} + {events.map((event, index) => ( + + ))} +
+ ) : ( + + + + Verb + Summary + Status + When + + + + + {isLoading && events.length === 0 + ? Array.from({ length: 8 }).map((_, index) => ( + + + + + + + + + + + + + + + + + + )) + : null} + {!isLoading && events.length === 0 && !error ? ( + + +
No audit events found
+
+ Try adjusting your filters or time range +
+
+
+ ) : null} + {events.map((event, index) => ( + + ))} +
+
+ )} +
+ + {/* Manual pagination footer */} + {events.length > 0 ? ( +
+ + {events.length} {events.length === 1 ? "event" : "events"} + {hasMore ? " so far" : ""} + + {hasMore ? ( + + ) : ( + End of results + )}
- )} + ) : null}
); diff --git a/ui/src/components/ChangeSourceToggle.tsx b/ui/src/components/ChangeSourceToggle.tsx index 909f136c..82643e6d 100644 --- a/ui/src/components/ChangeSourceToggle.tsx +++ b/ui/src/components/ChangeSourceToggle.tsx @@ -1,8 +1,8 @@ -import type { ChangeSource } from '../types/activity'; -import { Button } from './ui/button'; -import { cn } from '../lib/utils'; +import type { ChangeSource } from "../types/activity"; +import { Button } from "@datum-cloud/datum-ui/button"; +import { cn } from "../lib/utils"; -export type ChangeSourceOption = ChangeSource | 'all'; +export type ChangeSourceOption = ChangeSource | "all"; export interface ChangeSourceToggleProps { /** Current selected value */ @@ -18,21 +18,25 @@ export interface ChangeSourceToggleProps { /** * Options for the change source toggle */ -const OPTIONS: { value: ChangeSourceOption; label: string; description: string }[] = [ +const OPTIONS: { + value: ChangeSourceOption; + label: string; + description: string; +}[] = [ { - value: 'all', - label: 'All', - description: 'Show all activities', + value: "all", + label: "All", + description: "Show all activities", }, { - value: 'human', - label: 'Human', - description: 'Show only human-initiated activities', + value: "human", + label: "Human", + description: "Show only human-initiated activities", }, { - value: 'system', - label: 'System', - description: 'Show only system-initiated activities', + value: "system", + label: "System", + description: "Show only system-initiated activities", }, ]; @@ -42,35 +46,46 @@ const OPTIONS: { value: ChangeSourceOption; label: string; description: string } export function ChangeSourceToggle({ value, onChange, - className = '', + className = "", disabled = false, }: ChangeSourceToggleProps) { return (
- {OPTIONS.map((option, index) => ( - - ))} + {OPTIONS.map((option, index) => { + const active = value === option.value; + return ( + + ); + })}
); } diff --git a/ui/src/components/DateTimeRangePicker.tsx b/ui/src/components/DateTimeRangePicker.tsx index 10392f96..ba48bacf 100644 --- a/ui/src/components/DateTimeRangePicker.tsx +++ b/ui/src/components/DateTimeRangePicker.tsx @@ -8,10 +8,10 @@ import { endOfDay, formatISO } from 'date-fns'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { Label } from './ui/label'; -import { Card, CardContent } from './ui/card'; +import { Button } from '@datum-cloud/datum-ui/button'; +import { Input } from '@datum-cloud/datum-ui/input'; +import { Label } from '@datum-cloud/datum-ui/label'; +import { Card, CardContent } from '@datum-cloud/datum-ui/card'; export interface DateTimeRange { start: string; // ISO 8601 timestamp @@ -180,8 +180,9 @@ export function DateTimeRangePicker({ {(Object.keys(PRESETS) as PresetKey[]).map((key) => (
+ ); } diff --git a/ui/src/components/EventFeedItem.tsx b/ui/src/components/EventFeedItem.tsx index c90af15e..dd68b695 100644 --- a/ui/src/components/EventFeedItem.tsx +++ b/ui/src/components/EventFeedItem.tsx @@ -1,17 +1,27 @@ import { useState } from 'react'; -import { format, formatDistanceToNow } from 'date-fns'; -import { Copy, Check, ChevronDown, ChevronRight } from 'lucide-react'; +import { + AlertTriangle, + Bell, + Check, + ChevronDown, + ChevronRight, + Copy, +} from 'lucide-react'; import type { K8sEvent } from '../types/k8s-event'; import { EventExpandedDetails } from './EventExpandedDetails'; import { cn } from '../lib/utils'; -import { Button } from './ui/button'; -import { Card } from './ui/card'; +import { Button } from '@datum-cloud/datum-ui/button'; import { Tooltip, TooltipContent, - TooltipProvider, TooltipTrigger, } from './ui/tooltip'; +import { TableCell, TableRow } from '@datum-cloud/datum-ui/table'; +import { Timestamp } from './Timestamp'; + +// Number of columns rendered for the events table. Used by the colSpan +// on the expanded-detail row so it spans the full width. +export const EVENT_COLUMN_COUNT = 6; export interface EventFeedItemProps { /** The event to render */ @@ -35,6 +45,10 @@ export interface EventFeedItemProps { isNew?: boolean; /** Whether the item starts expanded */ defaultExpanded?: boolean; + /** Layout variant: 'feed' (table row, default) or 'timeline' (flat list row) */ + variant?: 'feed' | 'timeline'; + /** Whether this is the last item in the list (only used in timeline variant) */ + isLast?: boolean; } /** @@ -87,34 +101,7 @@ function getTimestamp(event: K8sEvent): string | undefined { } /** - * Format timestamp for display - */ -function formatTimestamp(timestamp?: string): string { - if (!timestamp) return 'Unknown time'; - try { - const date = new Date(timestamp); - return formatDistanceToNow(date, { addSuffix: true }); - } catch { - return timestamp; - } -} - -/** - * Format timestamp for tooltip (human-friendly UTC format with timezone) - */ -function formatTimestampFull(timestamp?: string): string { - if (!timestamp) return 'Unknown time'; - try { - const date = new Date(timestamp); - return format(date, "MMMM d, yyyy 'at' h:mm:ss a 'UTC'"); - } catch { - return timestamp; - } -} - - -/** - * EventFeedItem renders a single Kubernetes event in the feed + * EventFeedItem renders a single Kubernetes event as a table row. */ export function EventFeedItem({ event, @@ -125,6 +112,8 @@ export function EventFeedItem({ compact = false, isNew = false, defaultExpanded = false, + variant = 'feed', + isLast = false, }: EventFeedItemProps) { const [isExpanded, setIsExpanded] = useState(defaultExpanded); const [isCopied, setIsCopied] = useState(false); @@ -172,140 +161,220 @@ export function EventFeedItem({ }; const isWarning = type === 'Warning'; + const noteWithCount = note + ? `${note}${count && count > 1 ? ` (x${count})` : ''}` + : ''; - return ( - - -
- {/* Main Content */} -
- {/* Header row: Type badge + Reason + Kind + Timestamp */} -
- {/* Type badge */} - - {type || 'Unknown'} - - - {/* Reason */} - {reason && ( - - {reason} - - )} - - {/* Involved Kind */} - {regarding.kind && ( - - {regarding.kind} - - )} + // Timeline variant — flat list row mirroring ActivityFeedItem timeline: + // an icon square keyed off event type + reason/object summary + + // timestamp + expand toggle. + if (variant === 'timeline') { + const TypeIcon = isWarning ? AlertTriangle : Bell; + const iconBg = isWarning + ? 'bg-red-50 dark:bg-red-950' + : 'bg-blue-50 dark:bg-blue-950'; + const iconColor = isWarning + ? 'text-red-500 dark:text-red-400' + : 'text-blue-500 dark:text-blue-400'; + const objectLabel = regarding.namespace + ? `${regarding.kind || 'Unknown'} · ${regarding.namespace}/${regarding.name || ''}` + : `${regarding.kind || 'Unknown'} · ${regarding.name || ''}`; + const summary = noteWithCount || `${reason || 'Event'} on ${regarding.name || 'unknown'}`; - {/* Spacer to push timestamp to the right */} - + return ( +
+
+ {/* Type icon square */} +
+ +
- {/* Timestamp with tooltip */} - - - - {formatTimestamp(timestamp)} - - - -

{formatTimestampFull(timestamp)}

-
-
+ {/* Reason + summary text */} +
+
+ {reason || 'Event'}
+ + +
+ {summary} +
+
+ + {summary} + +
+
- {/* Content row: Message + Object + Timestamp + Expand */} -
- {/* Note with count - takes remaining space */} - {note && ( -

- {note}{count && count > 1 && (x{count})} -

- )} + {/* Object label */} + + {regarding.name} + - {/* Regarding Object with Tooltip and Copy Button */} -
- - - - {regarding.name || 'Unknown'} - - - -

- {regarding.namespace - ? `${regarding.kind || 'Unknown'} in namespace ${regarding.namespace}` - : regarding.kind || 'Unknown'} -

-
-
- - - - - -

Click to copy

-
-
-
+ {/* Timestamp */} + + + - {/* Expand button - larger and positioned at the end */} - -
-
+ {/* Expand toggle */} +
+ {isExpanded ? : null} +
+ ); + } - {/* Expanded Details */} - {isExpanded && } - - + return ( + <> + { + toggleExpand(e); + handleClick(); + }} + aria-expanded={isExpanded} + > + + + {type || 'Unknown'} + + + + {reason || ''} + + + {note ? ( + + +
+ {noteWithCount} +
+
+ + {noteWithCount} + +
+ ) : null} +
+ +
+ + + + {regarding.name || 'Unknown'} + + + + {regarding.namespace + ? `${regarding.kind || 'Unknown'} · ${regarding.namespace}/${regarding.name}` + : `${regarding.kind || 'Unknown'} · ${regarding.name}`} + + + +
+
+ + + + + + +
+ {isExpanded ? ( + + + + + + ) : null} + ); } diff --git a/ui/src/components/EventFeedItemSkeleton.tsx b/ui/src/components/EventFeedItemSkeleton.tsx deleted file mode 100644 index 667e88d0..00000000 --- a/ui/src/components/EventFeedItemSkeleton.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Card } from './ui/card'; -import { Skeleton } from './ui/skeleton'; -import { cn } from '../lib/utils'; - -export interface EventFeedItemSkeletonProps { - /** Whether to show as compact (for resource detail tabs) */ - compact?: boolean; - /** Additional CSS class */ - className?: string; -} - -/** - * EventFeedItemSkeleton renders a loading placeholder that matches EventFeedItem layout - */ -export function EventFeedItemSkeleton({ - compact = false, - className = '', -}: EventFeedItemSkeletonProps) { - return ( - -
- {/* Main Content */} -
- {/* Single row layout: Message + Object + Timestamp + Expand */} -
- {/* Note skeleton - takes remaining space */} - - - {/* Regarding Object skeleton */} - - - {/* Timestamp skeleton */} - - - {/* Expand button skeleton */} - -
-
-
-
- ); -} diff --git a/ui/src/components/EventTypeToggle.tsx b/ui/src/components/EventTypeToggle.tsx index 0da790d5..adfa2071 100644 --- a/ui/src/components/EventTypeToggle.tsx +++ b/ui/src/components/EventTypeToggle.tsx @@ -1,4 +1,4 @@ -import { Button } from './ui/button'; +import { Button } from '@datum-cloud/datum-ui/button'; import { cn } from '../lib/utils'; import type { K8sEventType } from '../types/k8s-event'; @@ -51,26 +51,29 @@ export function EventTypeToggle({ role="group" aria-label="Filter by event type" > - {OPTIONS.map((option, index) => ( - - ))} + {OPTIONS.map((option, index) => { + const active = value === option.value; + return ( + + ); + })}
); } diff --git a/ui/src/components/EventsFeed.tsx b/ui/src/components/EventsFeed.tsx index 32f5249c..ddadc6c6 100644 --- a/ui/src/components/EventsFeed.tsx +++ b/ui/src/components/EventsFeed.tsx @@ -1,19 +1,37 @@ -import { useEffect, useRef, useCallback } from 'react'; -import type { K8sEvent } from '../types/k8s-event'; -import type { EffectiveTimeRangeCallback, ErrorFormatter } from '../types/activity'; +import { useEffect, useRef, useCallback } from "react"; +import type { K8sEvent } from "../types/k8s-event"; +import type { + EffectiveTimeRangeCallback, + ErrorFormatter, +} from "../types/activity"; import type { EventsFeedFilters as FilterState, TimeRange, -} from '../hooks/useEventsFeed'; -import { useEventsFeed } from '../hooks/useEventsFeed'; -import { EventFeedItem } from './EventFeedItem'; -import { EventFeedItemSkeleton } from './EventFeedItemSkeleton'; -import { EventsFeedFilters } from './EventsFeedFilters'; -import { ActivityApiClient } from '../api/client'; -import { Button } from './ui/button'; -import { Card } from './ui/card'; -import { Badge } from './ui/badge'; -import { ApiErrorAlert } from './ApiErrorAlert'; +} from "../hooks/useEventsFeed"; +import { useEventsFeed } from "../hooks/useEventsFeed"; +import { EventFeedItem, EVENT_COLUMN_COUNT } from "./EventFeedItem"; +import { EventsFeedFilters } from "./EventsFeedFilters"; +import { ActivityApiClient } from "../api/client"; +import { Button } from "@datum-cloud/datum-ui/button"; +import { Card } from "@datum-cloud/datum-ui/card"; +import { Badge } from "./ui/badge"; +import { Skeleton } from "@datum-cloud/datum-ui/skeleton"; +import { ApiErrorAlert } from "./ApiErrorAlert"; +import { cn } from "../lib/utils"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@datum-cloud/datum-ui/table"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "./ui/tooltip"; export interface EventsFeedProps { /** API client instance */ @@ -40,7 +58,14 @@ export interface EventsFeedProps { /** Whether to show filters */ showFilters?: boolean; /** Filters that should be locked and hidden from the UI (programmatically set by parent) */ - hiddenFilters?: Array<'involvedKinds' | 'reasons' | 'namespaces' | 'sourceComponents' | 'involvedName' | 'eventType'>; + hiddenFilters?: Array< + | "involvedKinds" + | "reasons" + | "namespaces" + | "sourceComponents" + | "involvedName" + | "eventType" + >; /** Additional CSS class */ className?: string; /** Enable infinite scroll (default: true) */ @@ -55,6 +80,8 @@ export interface EventsFeedProps { errorFormatter?: ErrorFormatter; /** Callback invoked when filters or time range change (useful for URL state management) */ onFiltersChange?: (filters: FilterState, timeRange: TimeRange) => void; + /** Layout variant: 'feed' (table, default) or 'timeline' (icon-list rows) */ + variant?: 'feed' | 'timeline'; } /** @@ -64,7 +91,7 @@ export interface EventsFeedProps { export function EventsFeed({ client, initialFilters = {}, - initialTimeRange = { start: 'now-24h' }, + initialTimeRange = { start: "now-24h" }, pageSize = 50, onEventClick, onResourceClick, @@ -72,13 +99,11 @@ export function EventsFeed({ namespace, showFilters = true, hiddenFilters = [], - className = '', - infiniteScroll = true, - loadMoreThreshold = 200, + className = "", enableStreaming = false, - onEffectiveTimeRangeChange, errorFormatter, onFiltersChange: onFiltersChangeProp, + variant = 'feed', }: EventsFeedProps) { // Merge namespace into initial filters if provided const mergedInitialFilters: FilterState = { @@ -112,7 +137,7 @@ export function EventsFeed({ }); const scrollContainerRef = useRef(null); - const loadMoreTriggerRef = useRef(null); + // Store the latest loadMore function in a ref to avoid observer re-subscription const loadMoreRef = useRef(loadMore); @@ -126,31 +151,9 @@ export function EventsFeed({ loadMoreRef.current = loadMore; }, [loadMore]); - // Infinite scroll using Intersection Observer - useEffect(() => { - if (!infiniteScroll || !loadMoreTriggerRef.current) return; - - const observer = new IntersectionObserver( - (entries) => { - const entry = entries[0]; - if (entry.isIntersecting && hasMore && !isLoading) { - // Call through the ref to always use the latest function - loadMoreRef.current(); - } - }, - { - root: scrollContainerRef.current, - rootMargin: `${loadMoreThreshold}px`, - threshold: 0, - } - ); - - observer.observe(loadMoreTriggerRef.current); - - return () => { - observer.disconnect(); - }; - }, [infiniteScroll, hasMore, isLoading, loadMoreThreshold]); + // EventsFeed uses manual "Load more" pagination for consistency with the + // activity feed; the previous IntersectionObserver-driven infinite + // scroll caused observer rebuild loops on isLoading toggles. // Handle filter changes - refresh is automatic via the hook const handleFiltersChange = useCallback( @@ -158,7 +161,7 @@ export function EventsFeed({ setFilters(newFilters); onFiltersChangeProp?.(newFilters, timeRange); }, - [setFilters, onFiltersChangeProp, timeRange] + [setFilters, onFiltersChangeProp, timeRange], ); // Handle time range changes - refresh is automatic via the hook @@ -167,7 +170,7 @@ export function EventsFeed({ setTimeRange(newTimeRange); onFiltersChangeProp?.(filters, newTimeRange); }, - [setTimeRange, onFiltersChangeProp, filters] + [setTimeRange, onFiltersChangeProp, filters], ); // Handle manual load more click @@ -187,42 +190,54 @@ export function EventsFeed({ // Build container classes - use flex layout to properly fill available space // flex-1 min-h-0 allows the Card to fill parent flex container and enable child scrolling const containerClasses = compact - ? `flex-1 min-h-0 flex flex-col p-2 shadow-none border-border ${className}` - : `flex-1 min-h-0 flex flex-col p-3 ${className}`; + ? `flex-1 min-h-0 flex flex-col p-2 shadow-none border-border gap-0 ${className}` + : `flex-1 min-h-0 flex flex-col p-3 gap-0 ${className}`; // Build list classes - use flex-1 min-h-0 for flex-based scrolling - const listClasses = 'flex-1 min-h-0 overflow-y-auto pr-2'; + const listClasses = "flex-1 min-h-0 overflow-y-auto pr-2"; return ( - {/* Header with streaming status */} + {/* Header with streaming status — matches ActivityFeed: no border, + tooltipped indicator, outlined Pause/Resume button. */} {enableStreaming && ( -
-
- {isStreaming && ( -
- - - - - Streaming events... -
- )} - {newEventsCount > 0 && ( - +
+
+ {isStreaming ? ( + + + +
+ + + + + + Streaming events... + +
+
+ +

New events will appear automatically

+
+
+
+ ) : null} + {newEventsCount > 0 ? ( + +{newEventsCount} new - )} + ) : null}
- -
- )} - - {/* End of Results */} - {!hasMore && events.length > 0 && !isLoading && ( -
- No more events to load + {/* Manual pagination footer */} + {events.length > 0 ? ( +
+ + {events.length} {events.length === 1 ? "event" : "events"} + {hasMore ? " so far" : ""} + + {hasMore ? ( + + ) : ( + End of results + )}
- )} + ) : null}
); diff --git a/ui/src/components/EventsFeedFilters.tsx b/ui/src/components/EventsFeedFilters.tsx index 724e0fd5..3facfc6b 100644 --- a/ui/src/components/EventsFeedFilters.tsx +++ b/ui/src/components/EventsFeedFilters.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useEffect } from 'react'; import { formatISO, subDays } from 'date-fns'; -import { Search } from 'lucide-react'; +import { Search, X } from 'lucide-react'; import type { EventsFeedFilters as FilterState } from '../hooks/useEventsFeed'; import type { TimeRange } from '../hooks/useEventsFeed'; @@ -10,7 +10,7 @@ import { EventTypeToggle, EventTypeOption } from './EventTypeToggle'; import { TimeRangeDropdown } from './ui/time-range-dropdown'; import { FilterChip } from './ui/filter-chip'; import { AddFilterDropdown, type FilterOption } from './ui/add-filter-dropdown'; -import { Input } from './ui/input'; +import { Input } from '@datum-cloud/datum-ui/input'; export interface EventsFeedFiltersProps { /** API client instance for fetching facets */ @@ -336,7 +336,7 @@ export function EventsFeedFilters({ ); return ( -
+
{/* Event Type Toggle */} {!hiddenFilters.includes('eventType') && ( @@ -347,7 +347,7 @@ export function EventsFeedFilters({ /> )} - {/* Search Input */} + {/* Search Input — matches ActivityFeed search styling. */}
+ {filters.search ? ( + + ) : null}
{/* Active Filter Chips */} diff --git a/ui/src/components/FilterBuilder.tsx b/ui/src/components/FilterBuilder.tsx index 8fb7136e..8820a16a 100644 --- a/ui/src/components/FilterBuilder.tsx +++ b/ui/src/components/FilterBuilder.tsx @@ -1,12 +1,12 @@ import { useState } from 'react'; import type { AuditLogQuerySpec } from '../types'; import { FILTER_FIELDS } from '../types'; -import { Button } from './ui/button'; -import { Card, CardContent, CardHeader } from './ui/card'; -import { Input } from './ui/input'; -import { Label } from './ui/label'; -import { Separator } from './ui/separator'; -import { Textarea } from './ui/textarea'; +import { Button } from '@datum-cloud/datum-ui/button'; +import { Card, CardContent, CardHeader } from '@datum-cloud/datum-ui/card'; +import { Input } from '@datum-cloud/datum-ui/input'; +import { Label } from '@datum-cloud/datum-ui/label'; +import { Separator } from '@datum-cloud/datum-ui/separator'; +import { Textarea } from '@datum-cloud/datum-ui/textarea'; export interface FilterBuilderProps { onFilterChange: (spec: AuditLogQuerySpec) => void; @@ -51,8 +51,8 @@ export function FilterBuilder({

Build Your Query

@@ -83,10 +83,10 @@ export function FilterBuilder({ @@ -150,29 +150,29 @@ export function FilterBuilder({
diff --git a/ui/src/components/FilterBuilderWithAutocomplete.tsx b/ui/src/components/FilterBuilderWithAutocomplete.tsx index b059c0cf..9c3cebeb 100644 --- a/ui/src/components/FilterBuilderWithAutocomplete.tsx +++ b/ui/src/components/FilterBuilderWithAutocomplete.tsx @@ -1,11 +1,11 @@ import { useState, useRef, useEffect } from 'react'; import type { AuditLogQuerySpec } from '../types'; import { FILTER_FIELDS } from '../types'; -import { Input } from './ui/input'; -import { Textarea } from './ui/textarea'; -import { Button } from './ui/button'; -import { Label } from './ui/label'; -import { Card, CardHeader, CardContent } from './ui/card'; +import { Input } from '@datum-cloud/datum-ui/input'; +import { Textarea } from '@datum-cloud/datum-ui/textarea'; +import { Button } from '@datum-cloud/datum-ui/button'; +import { Label } from '@datum-cloud/datum-ui/label'; +import { Card, CardHeader, CardContent } from '@datum-cloud/datum-ui/card'; import { Badge } from './ui/badge'; export interface FilterBuilderWithAutocompleteProps { @@ -278,18 +278,18 @@ export function FilterBuilderWithAutocomplete({
)} - {editor.isDirty && ( - - Unsaved changes - - )}
{onCancel && (
)} - {editor.isDirty && ( - - Unsaved changes - - )}
{onCancel && ( {onCreatePolicy && ( )}
@@ -202,7 +231,12 @@ export function PolicyList({ {/* Error Display */} - + {/* Loading State - Skeleton */} {policyList.isLoading && policyList.policies.length === 0 && ( @@ -220,14 +254,14 @@ export function PolicyList({
📋

No policies found

- Activity policies define how audit events and Kubernetes events are - translated into human-readable activity summaries. + Activity policies define how audit events and Kubernetes events + are translated into human-readable activity summaries.

{onCreatePolicy && ( @@ -239,7 +273,10 @@ export function PolicyList({ {policyList.groups.length > 0 && (
{policyList.groups.map((group) => ( -
+