diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index ab5c9ed9..17b50690 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -21,7 +21,7 @@ jobs: env: TINYGO_VERSION: '0.39.0' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-go@v6 with: go-version: '1.25' diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d93f171f..a9c5ac5b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set Version Output id: get_version @@ -37,7 +37,7 @@ jobs: needs: publish runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Go uses: actions/setup-go@v6 with: @@ -77,7 +77,7 @@ jobs: env: TINYGO_VERSION: '0.39.0' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Go uses: actions/setup-go@v6 diff --git a/.github/workflows/release_dev.yaml b/.github/workflows/release_dev.yaml index 72453049..1fcbcdf0 100644 --- a/.github/workflows/release_dev.yaml +++ b/.github/workflows/release_dev.yaml @@ -12,7 +12,7 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/.github/workflows/release_request.yaml b/.github/workflows/release_request.yaml index 4e0c89f0..5715a217 100644 --- a/.github/workflows/release_request.yaml +++ b/.github/workflows/release_request.yaml @@ -15,7 +15,7 @@ jobs: pull-requests: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/web.yaml b/.github/workflows/web.yaml index 1ce1fd46..8e271c48 100644 --- a/.github/workflows/web.yaml +++ b/.github/workflows/web.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: diff --git a/db/federation/create.go b/db/federation/create.go index 0cf686e9..5bb97d7f 100644 --- a/db/federation/create.go +++ b/db/federation/create.go @@ -682,17 +682,21 @@ func processCreateOrUpdateSummitLogActivity(activity pub.Activity, app core.App, } } - if len(photoURLs) > 0 { - photos := make([]*filesystem.File, len(photoURLs)) - for i, purl := range photoURLs { + if len(photoURLs) == 0 { + record.Set("photos", []*filesystem.File{}) + } else { + photos := []*filesystem.File{} + for _, purl := range photoURLs { photo, err := filesystem.NewFileFromURL(context.Background(), purl) if err != nil { continue } - photos[i] = photo + photos = append(photos, photo) } - record.Set("photos", photos) + if len(photos) > 0 { + record.Set("photos", photos) + } } if gpxURL != "" { diff --git a/db/go.mod b/db/go.mod index 35113081..0ac162eb 100644 --- a/db/go.mod +++ b/db/go.mod @@ -3,7 +3,7 @@ module pocketbase go 1.25.0 require ( - github.com/doyensec/safeurl v0.2.3 + github.com/doyensec/safeurl v0.2.5 github.com/extism/go-sdk v1.7.1 github.com/go-ap/jsonld v0.0.0-20250905102310-8480b0fe24d9 github.com/meilisearch/meilisearch-go v0.36.2 @@ -53,13 +53,13 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 // indirect github.com/twpayne/go-polyline v1.1.1 - golang.org/x/crypto v0.51.0 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/image v0.39.0 // indirect - golang.org/x/net v0.55.0 + golang.org/x/net v0.56.0 golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 - golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/sync v0.21.0 + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 modernc.org/libc v1.72.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/db/go.sum b/db/go.sum index e23a8ad8..3e0bd3ec 100644 --- a/db/go.sum +++ b/db/go.sum @@ -17,8 +17,8 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1 github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8= github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= -github.com/doyensec/safeurl v0.2.3 h1:KJZHxTUMI17yUSy5umKmDLtzYBUxN6MkdSIyRI81DvY= -github.com/doyensec/safeurl v0.2.3/go.mod h1:3H0cgRpPYPSpgxRRn5yGD35Ns/LgGX/BVWSBbzUqXtY= +github.com/doyensec/safeurl v0.2.5 h1:kKu0JNQy0tJ8jkDyB5h6Aml9vWWniq+mpoa12EGLcOQ= +github.com/doyensec/safeurl v0.2.5/go.mod h1:3H0cgRpPYPSpgxRRn5yGD35Ns/LgGX/BVWSBbzUqXtY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= @@ -124,39 +124,39 @@ go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/db/hooks/categories.go b/db/hooks/categories.go new file mode 100644 index 00000000..f3d061d2 --- /dev/null +++ b/db/hooks/categories.go @@ -0,0 +1,125 @@ +package hooks + +import ( + "pocketbase/util" + + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" +) + +func ValidateCategoryHandler() func(e *core.RecordRequestEvent) error { + return func(e *core.RecordRequestEvent) error { + if err := util.ValidateCategoryRecord(e.App, e.Record); err != nil { + return apis.NewBadRequestError(err.Error(), err) + } + + return e.Next() + } +} + +func ValidateSubcategoryHandler() func(e *core.RecordRequestEvent) error { + return func(e *core.RecordRequestEvent) error { + if err := util.ValidateSubcategoryRecord(e.App, e.Record); err != nil { + return apis.NewBadRequestError(err.Error(), err) + } + + return e.Next() + } +} + +func BackfillRemoteTrailCategoryHandler() func(e *core.RecordEvent) error { + return func(e *core.RecordEvent) error { + if e.Record.Original().Id != "" && e.Record.GetString("name") == e.Record.Original().GetString("name") { + return e.Next() + } + + if err := util.BackfillRemoteTrailCategory(e.App, e.Record); err != nil { + e.App.Logger().Warn("failed to backfill remote trail categories after category save", "category", e.Record.Id, "error", err) + } + + return e.Next() + } +} + +func BackfillRemoteTrailSubcategoryHandler() func(e *core.RecordEvent) error { + return func(e *core.RecordEvent) error { + original := e.Record.Original() + if original.Id != "" && + e.Record.GetString("name") == original.GetString("name") && + e.Record.GetString("category") == original.GetString("category") { + return e.Next() + } + + if err := util.BackfillRemoteTrailSubcategory(e.App, e.Record); err != nil { + e.App.Logger().Warn("failed to backfill remote trail subcategories after subcategory save", "subcategory", e.Record.Id, "error", err) + } + + return e.Next() + } +} + +func ValidateUserCategoryPreferenceHandler() func(e *core.RecordRequestEvent) error { + return func(e *core.RecordRequestEvent) error { + requestInfo, err := e.RequestInfo() + if err != nil { + return err + } + + if err := util.ValidateUserCategoryPreferenceRequest(requestBodyHasField(requestInfo.Body, "priority")); err != nil { + return apis.NewBadRequestError(err.Error(), err) + } + + return e.Next() + } +} + +func ValidateUserSubcategoryPreferenceHandler() func(e *core.RecordRequestEvent) error { + return func(e *core.RecordRequestEvent) error { + requestInfo, err := e.RequestInfo() + if err != nil { + return err + } + + if err := util.ValidateUserSubcategoryPreferenceRequest(requestBodyHasField(requestInfo.Body, "priority")); err != nil { + return apis.NewBadRequestError(err.Error(), err) + } + + return e.Next() + } +} + +func ValidateTrailSubcategoryHandler() func(e *core.RecordRequestEvent) error { + return func(e *core.RecordRequestEvent) error { + requestInfo, err := e.RequestInfo() + if err != nil { + return err + } + + subcategoryExplicit := requestBodyHasField(requestInfo.Body, "subcategory") + if err := util.ValidateTrailSubcategoryRecord(e.App, e.Record, subcategoryExplicit); err != nil { + return apis.NewBadRequestError(err.Error(), err) + } + + return e.Next() + } +} + +func requestBodyHasField(body map[string]any, field string) bool { + _, ok := body[field] + if ok { + return true + } + + _, ok = body[field+"+"] + if ok { + return true + } + + _, ok = body["+"+field] + if ok { + return true + } + + _, ok = body[field+"-"] + return ok +} diff --git a/db/hooks/users.go b/db/hooks/users.go index 638167b1..43e0585c 100644 --- a/db/hooks/users.go +++ b/db/hooks/users.go @@ -17,6 +17,10 @@ func CreateUserHandler(client meilisearch.ServiceManager) func(e *core.RecordEve return err } + if err := util.EnsureUserCategoryPriority(e.App, e.Record.Id, ""); err != nil { + return err + } + _, err = util.ActorFromUser(e.App, e.Record) if err != nil { return err diff --git a/db/main.go b/db/main.go index 08cc9e92..fad75242 100644 --- a/db/main.go +++ b/db/main.go @@ -5,14 +5,12 @@ import ( "fmt" "log" "os" - "strings" "github.com/meilisearch/meilisearch-go" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/plugins/migratecmd" - "github.com/pocketbase/pocketbase/tools/filesystem" "pocketbase/commands" "pocketbase/hooks" @@ -96,6 +94,22 @@ func setupEventHandlers(app *pocketbase.PocketBase, client meilisearch.ServiceMa app.OnRecordAfterUpdateSuccess("activitypub_actors").BindFunc(hooks.UpdateActorHandler(client)) app.OnRecordAfterDeleteSuccess("activitypub_actors").BindFunc(hooks.DeleteActorHandler(client)) + app.OnRecordCreateRequest("categories").BindFunc(hooks.ValidateCategoryHandler()) + app.OnRecordUpdateRequest("categories").BindFunc(hooks.ValidateCategoryHandler()) + app.OnRecordAfterCreateSuccess("categories").BindFunc(hooks.BackfillRemoteTrailCategoryHandler()) + app.OnRecordAfterUpdateSuccess("categories").BindFunc(hooks.BackfillRemoteTrailCategoryHandler()) + app.OnRecordCreateRequest("subcategories").BindFunc(hooks.ValidateSubcategoryHandler()) + app.OnRecordUpdateRequest("subcategories").BindFunc(hooks.ValidateSubcategoryHandler()) + app.OnRecordAfterCreateSuccess("subcategories").BindFunc(hooks.BackfillRemoteTrailSubcategoryHandler()) + app.OnRecordAfterUpdateSuccess("subcategories").BindFunc(hooks.BackfillRemoteTrailSubcategoryHandler()) + + app.OnRecordCreateRequest("user_category_preferences").BindFunc(hooks.ValidateUserCategoryPreferenceHandler()) + app.OnRecordUpdateRequest("user_category_preferences").BindFunc(hooks.ValidateUserCategoryPreferenceHandler()) + app.OnRecordCreateRequest("user_subcategory_preferences").BindFunc(hooks.ValidateUserSubcategoryPreferenceHandler()) + app.OnRecordUpdateRequest("user_subcategory_preferences").BindFunc(hooks.ValidateUserSubcategoryPreferenceHandler()) + + app.OnRecordCreateRequest("trails").BindFunc(hooks.ValidateTrailSubcategoryHandler()) + app.OnRecordUpdateRequest("trails").BindFunc(hooks.ValidateTrailSubcategoryHandler()) app.OnRecordAfterCreateSuccess("trails").BindFunc(hooks.CreateTrailHandler(client)) app.OnRecordAfterUpdateSuccess("trails").BindFunc(hooks.UpdateTrailHandler(client)) app.OnRecordAfterDeleteSuccess("trails").BindFunc(hooks.DeleteTrailHandler(client)) @@ -166,6 +180,8 @@ func registerRoutes(se *core.ServeEvent, client meilisearch.ServiceManager) { se.Router.POST("/auth/token", routes.AuthToken) se.Router.POST("/user/email", routes.UserEmailChange) se.Router.POST("/waypoint/cluster", routes.WaypointCluster) + se.Router.POST("/category-preferences/reorder", routes.CategoryPreferencesReorder) + se.Router.POST("/subcategory-preferences/reorder", routes.SubcategoryPreferencesReorder) se.Router.POST("/trail-merge/suggest", routes.TrailMergeSuggest) se.Router.POST("/trail-merge", routes.TrailMerge(client)) @@ -213,6 +229,9 @@ func registerCronJobs(app core.App, client meilisearch.ServiceManager) { func initData(app core.App, client meilisearch.ServiceManager) error { initCategories(app) + if err := util.SeedDefaultSubcategories(app); err != nil { + return err + } initPlugins(app) initMeilisearchConfig(client) go func() { @@ -280,31 +299,29 @@ func initCategories(app core.App) error { if err := query.All(&records); err != nil { return err } - if len(records) != 0 { - return nil - } - collection, err := app.FindCollectionByNameOrId("categories") if err != nil { return err } - categories := []string{"Hiking", "Walking", "Climbing", "Skiing", "Canoeing", "Biking", "Other"} - for _, element := range categories { - record := core.NewRecord(collection) - record.Set("name", element) - record.Set("settings", map[string]any{ - "wp_merge_enabled": true, - "wp_merge_radius": 50, - }) - if f, err := filesystem.NewFileFromPath("migrations/initial_data/" + strings.ToLower(element) + ".jpg"); err == nil { - record.Set("img", f) - } - if err := app.Save(record); err != nil { - return err + if len(records) == 0 { + for _, element := range util.DefaultCategoryNames() { + record := core.NewRecord(collection) + record.Set("name", element) + record.Set("settings", map[string]any{ + "wp_merge_enabled": true, + "wp_merge_radius": 50, + }) + err := app.Save(record) + if err != nil { + return err + } } } - return nil + if err := util.PrepopulateDefaultCategoryTranslations(app); err != nil { + return err + } + return util.PrepopulateDefaultCategoryIcons(app) } func initMeilisearchConfig(client meilisearch.ServiceManager) { @@ -312,13 +329,15 @@ func initMeilisearchConfig(client meilisearch.ServiceManager) { "trails": { SearchableAttributes: []string{"author_name", "name", "description", "location", "tags"}, FilterableAttributes: []string{ - "id", "_geo", "author", "category", "completed", "date", "difficulty", - "distance", "elevation_gain", "elevation_loss", "likes", "public", - "shares", "tags", "min_lat", "max_lat", "min_lon", "max_lon", "bounding_box_diagonal", + "id", "_geo", "author", "category_id", "subcategory_id", + "is_federated", "completed", "date", "difficulty", "distance", + "elevation_gain", "elevation_loss", "likes", "public", "shares", + "tags", "min_lat", "max_lat", "min_lon", "max_lon", "bounding_box_diagonal", }, SortableAttributes: []string{ "author", "created", "date", "difficulty", "distance", "duration", "elevation_gain", "elevation_loss", "like_count", "name", + "min_lat", "max_lat", "min_lon", "max_lon", }, RankingRules: []string{"words", "typo", "proximity", "attribute", "sort", "exactness"}, }, diff --git a/db/migrations/1781000000_categories_redesign.go b/db/migrations/1781000000_categories_redesign.go new file mode 100644 index 00000000..c2b947b7 --- /dev/null +++ b/db/migrations/1781000000_categories_redesign.go @@ -0,0 +1,803 @@ +package migrations + +import ( + "encoding/json" + "errors" + "fmt" + "pocketbase/util" + "sort" + "strings" + + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/tools/filesystem" +) + +func init() { + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("categories") + if err != nil { + return err + } + + if err := resolveCategoryNameCollisions(app); err != nil { + return fmt.Errorf("failed to resolve category name collisions: %w", err) + } + + if err := deleteCategoryImageFiles(app); err != nil { + return fmt.Errorf("failed to delete category image files: %w", err) + } + + collection.Fields.RemoveById("64dsnxtb") + + if err := collection.Fields.AddMarshaledJSONAt(len(collection.Fields), []byte(`{ + "hidden": false, + "id": "texti4ksx4gm", + "max": 0, + "min": 0, + "name": "short_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(len(collection.Fields), []byte(`{ + "hidden": false, + "id": "text0r6k2h4gi", + "max": 0, + "min": 0, + "name": "icon", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(len(collection.Fields), []byte(`{ + "hidden": false, + "id": "jsonvkf7o88i", + "maxSize": 0, + "name": "translations", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }`)); err != nil { + return err + } + + if err := app.Save(collection); err != nil { + return err + } + + if err := ensureRunningCategory(app); err != nil { + return fmt.Errorf("failed to ensure running category: %w", err) + } + + if err := createSubcategoriesCollection(app); err != nil { + return err + } + + trailsCollection, err := app.FindCollectionByNameOrId("trails") + if err != nil { + return err + } + if err := trailsCollection.Fields.AddMarshaledJSONAt(len(trailsCollection.Fields), []byte(`{ + "cascadeDelete": false, + "collectionId": "pbc_1781100000", + "hidden": false, + "id": "relphase2subct", + "maxSelect": 1, + "minSelect": 0, + "name": "subcategory", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + if err := trailsCollection.Fields.AddMarshaledJSONAt(len(trailsCollection.Fields), []byte(`{ + "hidden": false, + "id": "textremotecat1", + "max": 0, + "min": 0, + "name": "federated_category_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }`)); err != nil { + return err + } + if err := trailsCollection.Fields.AddMarshaledJSONAt(len(trailsCollection.Fields), []byte(`{ + "hidden": false, + "id": "textremotesub1", + "max": 0, + "min": 0, + "name": "federated_subcategory_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }`)); err != nil { + return err + } + if err := app.Save(trailsCollection); err != nil { + return err + } + + if err := util.PrepopulateDefaultCategoryTranslations(app); err != nil { + return fmt.Errorf("failed to prepopulate default category translations: %w", err) + } + + if err := util.PrepopulateDefaultCategoryIcons(app); err != nil { + return fmt.Errorf("failed to prepopulate default category icons: %w", err) + } + + if err := util.SeedDefaultSubcategories(app); err != nil { + return fmt.Errorf("failed to seed default subcategories: %w", err) + } + + if err := createUserCategoryPreferencesCollection(app); err != nil { + return err + } + + if err := createUserSubcategoryPreferencesCollection(app); err != nil { + return err + } + + if err := migrateFavouriteSportToPriority(app); err != nil { + return fmt.Errorf("failed to migrate favourite sport to category priority: %w", err) + } + + if err := removeSettingsCategoryField(app); err != nil { + return fmt.Errorf("failed to remove settings.category field: %w", err) + } + + if err := util.ValidateCategoryCollectionState(app); err != nil { + return fmt.Errorf("categories redesign migration failed validation: %w", err) + } + + return nil + }, func(app core.App) error { + if collection, err := app.FindCollectionByNameOrId("pbc_1781250000"); err == nil { + if err := app.Delete(collection); err != nil { + return err + } + } + + if collection, err := app.FindCollectionByNameOrId("pbc_1781200000"); err == nil { + if err := app.Delete(collection); err != nil { + return err + } + } + + if trailsCollection, err := app.FindCollectionByNameOrId("trails"); err == nil { + trailsCollection.Fields.RemoveById("relphase2subct") + trailsCollection.Fields.RemoveById("textremotecat1") + trailsCollection.Fields.RemoveById("textremotesub1") + if err := app.Save(trailsCollection); err != nil { + return err + } + } + + if collection, err := app.FindCollectionByNameOrId("pbc_1781100000"); err == nil { + if err := app.Delete(collection); err != nil { + return err + } + } + + if settingsCollection, err := app.FindCollectionByNameOrId("settings"); err == nil { + if err := settingsCollection.Fields.AddMarshaledJSONAt(len(settingsCollection.Fields), []byte(`{ + "cascadeDelete": false, + "collectionId": "kjxvi8asj2igqwf", + "hidden": false, + "id": "owlyzl1x", + "maxSelect": 1, + "minSelect": 0, + "name": "category", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + if err := app.Save(settingsCollection); err != nil { + return err + } + } + + collection, err := app.FindCollectionByNameOrId("categories") + if err != nil { + return err + } + + collection.Fields.RemoveById("texti4ksx4gm") + collection.Fields.RemoveById("text0r6k2h4gi") + collection.Fields.RemoveById("jsonvkf7o88i") + + if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ + "hidden": false, + "id": "64dsnxtb", + "maxSelect": 1, + "maxSize": 5242880, + "mimeTypes": null, + "name": "img", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": null, + "type": "file" + }`)); err != nil { + return err + } + + return app.Save(collection) + }) +} + +type categoryCollisionCandidate struct { + id string + name string + created string +} + +func resolveCategoryNameCollisions(app core.App) error { + allCategories, err := app.FindAllRecords("categories") + if err != nil { + return err + } + + candidates := make([]categoryCollisionCandidate, 0, len(allCategories)) + for _, category := range allCategories { + candidates = append(candidates, categoryCollisionCandidate{ + id: category.Id, + name: category.GetString("name"), + created: category.GetString("created"), + }) + } + resolvedNames := resolveCategoryNameCollisionCandidates(candidates) + + for _, category := range allCategories { + resolvedName, ok := resolvedNames[category.Id] + if !ok { + continue + } + + originalName := category.GetString("name") + category.Set("name", resolvedName) + if err := app.Save(category); err != nil { + return fmt.Errorf("failed to resolve category name collision for %q: %w", originalName, err) + } + } + + return nil +} + +func collisionResolvedCategoryName(name string, id string, seen map[string]struct{}) string { + baseName := strings.TrimSpace(name) + if baseName == "" { + baseName = "Category" + } + + for i := 0; ; i++ { + suffix := id + if i > 0 { + suffix = fmt.Sprintf("%s-%d", id, i+1) + } + + candidate := fmt.Sprintf("%s (%s)", baseName, suffix) + if _, ok := seen[util.NormalizeCategoryName(candidate)]; !ok { + return candidate + } + } +} + +func resolveCategoryNameCollisionCandidates(candidates []categoryCollisionCandidate) map[string]string { + sorted := append([]categoryCollisionCandidate(nil), candidates...) + sort.SliceStable(sorted, func(i, j int) bool { + left := sorted[i] + right := sorted[j] + + leftName := util.NormalizeCategoryName(left.name) + rightName := util.NormalizeCategoryName(right.name) + if leftName != rightName { + return leftName < rightName + } + + if left.created != right.created { + return left.created < right.created + } + + return left.id < right.id + }) + + seen := map[string]struct{}{} + resolved := map[string]string{} + for _, category := range sorted { + normalizedName := util.NormalizeCategoryName(category.name) + if _, ok := seen[normalizedName]; !ok { + seen[normalizedName] = struct{}{} + continue + } + + resolvedName := collisionResolvedCategoryName(category.name, category.id, seen) + resolved[category.id] = resolvedName + seen[util.NormalizeCategoryName(resolvedName)] = struct{}{} + } + + return resolved +} + +func ensureRunningCategory(app core.App) error { + allCategories, err := app.FindAllRecords("categories") + if err != nil { + return err + } + + if len(allCategories) == 0 { + return util.SeedDefaultCategories(app) + } + + for _, category := range allCategories { + if util.NormalizeCategoryName(category.GetString("name")) == util.NormalizeCategoryName("Running") { + return nil + } + } + + collection, err := app.FindCollectionByNameOrId("categories") + if err != nil { + return err + } + + record := core.NewRecord(collection) + record.Set("name", "Running") + record.Set("settings", map[string]any{ + "wp_merge_enabled": true, + "wp_merge_radius": 50, + }) + + return app.Save(record) +} + +func deleteCategoryImageFiles(app core.App) error { + allCategories, err := app.FindAllRecords("categories") + if err != nil { + return err + } + + fsys, err := app.NewFilesystem() + if err != nil { + return err + } + defer fsys.Close() + + var failures []error + for _, category := range allCategories { + for _, filename := range categoryImageFilenames(category) { + if filename == "" || strings.ContainsAny(filename, `/\`) { + continue + } + + path := category.BaseFilesPath() + "/" + filename + if err := fsys.Delete(path); err != nil && !errors.Is(err, filesystem.ErrNotFound) { + failures = append(failures, fmt.Errorf("failed to delete category image %q: %w", path, err)) + } + + if errs := fsys.DeletePrefix(category.BaseFilesPath() + "/thumbs_" + filename + "/"); len(errs) > 0 { + failures = append(failures, fmt.Errorf("failed to delete category image thumbs for %q: %w", path, errors.Join(errs...))) + } + } + } + + if len(failures) > 0 { + return errors.Join(failures...) + } + + return nil +} + +func categoryImageFilenames(record *core.Record) []string { + filenames := record.GetStringSlice("img") + if len(filenames) > 0 { + return filenames + } + + filename := record.GetString("img") + if filename == "" { + return nil + } + + return []string{filename} +} + +func createSubcategoriesCollection(app core.App) error { + jsonData := `{ + "createRule": null, + "deleteRule": null, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": true, + "collectionId": "kjxvi8asj2igqwf", + "hidden": false, + "id": "relphase2cat01", + "maxSelect": 1, + "minSelect": 0, + "name": "category", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "textphase2name", + "max": 0, + "min": 1, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "textphase2shrt", + "max": 0, + "min": 0, + "name": "short_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "textphase2icon", + "max": 0, + "min": 0, + "name": "icon", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "textphase2badge", + "max": 0, + "min": 0, + "name": "badge_icon", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "jsonphase2trns", + "maxSize": 0, + "name": "translations", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "id": "pbc_1781100000", + "indexes": [], + "listRule": "", + "name": "subcategories", + "system": false, + "type": "base", + "updateRule": null, + "viewRule": "" + }` + + return saveCollectionFromJSON(app, jsonData) +} + +func createUserCategoryPreferencesCollection(app core.App) error { + jsonData := `{ + "createRule": "@request.auth.id != \"\" && user = @request.auth.id", + "deleteRule": "@request.auth.id != \"\" && user = @request.auth.id", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": true, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relphase3user", + "maxSelect": 1, + "minSelect": 0, + "name": "user", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": true, + "collectionId": "kjxvi8asj2igqwf", + "hidden": false, + "id": "relphase3cat", + "maxSelect": 1, + "minSelect": 0, + "name": "category", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "boolphase3visible", + "name": "visible", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "numphase3prio", + "max": null, + "min": 1, + "name": "priority", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "id": "pbc_1781200000", + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_user_category_preferences_user_category` + "`" + ` ON ` + "`" + `user_category_preferences` + "`" + ` (` + "`" + `user` + "`" + `, ` + "`" + `category` + "`" + `)" + ], + "listRule": "@request.auth.id != \"\" && user = @request.auth.id", + "name": "user_category_preferences", + "system": false, + "type": "base", + "updateRule": "@request.auth.id != \"\" && user = @request.auth.id", + "viewRule": "@request.auth.id != \"\" && user = @request.auth.id" + }` + + return saveCollectionFromJSON(app, jsonData) +} + +func createUserSubcategoryPreferencesCollection(app core.App) error { + jsonData := `{ + "createRule": "@request.auth.id != \"\" && user = @request.auth.id", + "deleteRule": "@request.auth.id != \"\" && user = @request.auth.id", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text178125id", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": true, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "rel178125user", + "maxSelect": 1, + "minSelect": 0, + "name": "user", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": true, + "collectionId": "pbc_1781100000", + "hidden": false, + "id": "rel178125subcat", + "maxSelect": 1, + "minSelect": 0, + "name": "subcategory", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "bool178125visible", + "name": "visible", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "num178125prio", + "max": null, + "min": 1, + "name": "priority", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "autodate178125created", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate178125updated", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "id": "pbc_1781250000", + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_user_subcategory_preferences_user_subcategory` + "`" + ` ON ` + "`" + `user_subcategory_preferences` + "`" + ` (` + "`" + `user` + "`" + `, ` + "`" + `subcategory` + "`" + `)" + ], + "listRule": "@request.auth.id != \"\" && user = @request.auth.id", + "name": "user_subcategory_preferences", + "system": false, + "type": "base", + "updateRule": "@request.auth.id != \"\" && user = @request.auth.id", + "viewRule": "@request.auth.id != \"\" && user = @request.auth.id" + }` + + return saveCollectionFromJSON(app, jsonData) +} + +// migrateFavouriteSportToPriority carries the former per-user "favourite sport" +// (settings.category) over to the new category priority model: the favourite becomes +// the user's priority-1 category. Users without a favourite get a common default +// instead of falling back to category sort order. Users who already organized +// categories by priority are left untouched. +func migrateFavouriteSportToPriority(app core.App) error { + settingsRecords, err := app.FindAllRecords("settings") + if err != nil { + return err + } + + for _, settings := range settingsRecords { + if err := util.EnsureUserCategoryPriority(app, settings.GetString("user"), settings.GetString("category")); err != nil { + return err + } + } + + return nil +} + +func removeSettingsCategoryField(app core.App) error { + settings, err := app.FindCollectionByNameOrId("settings") + if err != nil { + return err + } + + settings.Fields.RemoveById("owlyzl1x") + return app.Save(settings) +} + +func saveCollectionFromJSON(app core.App, jsonData string) error { + collection := &core.Collection{} + if err := json.Unmarshal([]byte(jsonData), collection); err != nil { + return err + } + + return app.Save(collection) +} diff --git a/db/migrations/initial_data/biking.jpg b/db/migrations/initial_data/biking.jpg deleted file mode 100644 index 18a00b40..00000000 Binary files a/db/migrations/initial_data/biking.jpg and /dev/null differ diff --git a/db/migrations/initial_data/canoeing.jpg b/db/migrations/initial_data/canoeing.jpg deleted file mode 100644 index 3809cdfc..00000000 Binary files a/db/migrations/initial_data/canoeing.jpg and /dev/null differ diff --git a/db/migrations/initial_data/climbing.jpg b/db/migrations/initial_data/climbing.jpg deleted file mode 100644 index 2ebef9b9..00000000 Binary files a/db/migrations/initial_data/climbing.jpg and /dev/null differ diff --git a/db/migrations/initial_data/hiking.jpg b/db/migrations/initial_data/hiking.jpg deleted file mode 100644 index 9700419b..00000000 Binary files a/db/migrations/initial_data/hiking.jpg and /dev/null differ diff --git a/db/migrations/initial_data/skiing.jpg b/db/migrations/initial_data/skiing.jpg deleted file mode 100644 index dede5023..00000000 Binary files a/db/migrations/initial_data/skiing.jpg and /dev/null differ diff --git a/db/migrations/initial_data/walking.jpg b/db/migrations/initial_data/walking.jpg deleted file mode 100644 index 167a70d5..00000000 Binary files a/db/migrations/initial_data/walking.jpg and /dev/null differ diff --git a/db/plugins/importer/importer.go b/db/plugins/importer/importer.go index a427c9b4..7d1dd8c1 100644 --- a/db/plugins/importer/importer.go +++ b/db/plugins/importer/importer.go @@ -29,7 +29,7 @@ type Options struct { ActorID string DefaultPublic bool CreateSummitLogForCompleted bool - CategoryMapping map[string]string + CategoryMapping map[string]CategoryMappingValue Manifest pluginsystem.Manifest Policy pluginsystem.RequestPolicyContext Auth map[string]any @@ -43,6 +43,16 @@ type Result struct { Skipped bool } +type CategoryMappingTarget struct { + CategoryID string + SubcategoryID string +} + +type CategoryMappingValue struct { + Category string + Subcategory string +} + // ImportTrail is the boundary between plugin output and wanderer records. It // validates the provider identity, deduplicates by trail_external_reference, // stores the GPX/photos, maps GPX metrics onto the trail record, and creates the @@ -78,7 +88,7 @@ func ImportTrail(ctx context.Context, app core.App, item pluginsystem.TrailImpor applyProviderStart(&metrics, trackIndex, item.Metadata) applyProviderMetrics(&metrics, item.Metadata) public := publicFromPrivacy(item.Privacy, opts.DefaultPublic) - categoryID := categoryIDForImport(app, item, opts.CategoryMapping) + categoryTarget := categoryTargetForImport(app, item, opts.CategoryMapping) date := dateFromImport(item, metrics) mediaBudget := &pluginMediaBudget{} photos := photoFiles(ctx, app, item.Photos, opts, mediaBudget) @@ -96,7 +106,8 @@ func ImportTrail(ctx context.Context, app core.App, item pluginsystem.TrailImpor "lat": metrics.StartLat, "lon": metrics.StartLon, "difficulty": "easy", - "category": categoryID, + "category": categoryTarget.CategoryID, + "subcategory": categoryTarget.SubcategoryID, "author": opts.ActorID, }) record.Set("gpx", gpxFile) @@ -772,11 +783,15 @@ func createSummitLog(app core.App, trailID string, actorID string, date time.Tim return app.Save(record) } -func categoryIDForImport(app core.App, item pluginsystem.TrailImport, mapping map[string]string) string { - if category, matched := CategoryFromProviderMapping(app, ProviderCategoryFromImport(item), mapping); matched { - return category +func categoryTargetForImport(app core.App, item pluginsystem.TrailImport, mapping map[string]CategoryMappingValue) CategoryMappingTarget { + if categoryTarget, matched := CategoryTargetFromProviderMapping(app, ProviderCategoryFromImport(item), mapping); matched { + return categoryTarget } - return categoryIDForActivityType(app, item.ActivityType) + return categoryTargetForActivityType(app, item.ActivityType) +} + +func categoryIDForImport(app core.App, item pluginsystem.TrailImport, mapping map[string]CategoryMappingValue) string { + return categoryTargetForImport(app, item, mapping).CategoryID } func ProviderCategoryFromImport(item pluginsystem.TrailImport) string { @@ -788,58 +803,109 @@ func ProviderCategoryFromImport(item pluginsystem.TrailImport) string { return strings.TrimSpace(value) } -func CategoryFromProviderMapping(app core.App, providerCategory string, mapping map[string]string) (string, bool) { +func CategoryFromProviderMapping(app core.App, providerCategory string, mapping map[string]CategoryMappingValue) (string, bool) { + target, matched := CategoryTargetFromProviderMapping(app, providerCategory, mapping) + return target.CategoryID, matched +} + +func CategoryTargetFromProviderMapping(app core.App, providerCategory string, mapping map[string]CategoryMappingValue) (CategoryMappingTarget, bool) { providerCategory = strings.TrimSpace(providerCategory) if providerCategory == "" || len(mapping) == 0 { - return "", false + return CategoryMappingTarget{}, false } - rawTarget, matched := mapping[providerCategory] + mappingTarget, matched := mapping[providerCategory] if !matched { - return "", false + return CategoryMappingTarget{}, false } - target := strings.TrimSpace(rawTarget) - if target == "" { - return "", true + if mappingTarget.Category == "" && mappingTarget.Subcategory == "" { + return CategoryMappingTarget{}, true } - if category, err := app.FindRecordById("categories", target); err == nil && category != nil { - return category.Id, true - } - category, _ := app.FindFirstRecordByData("categories", "name", target) - if category == nil { - return "", false - } - return category.Id, true + + return resolveCategoryMappingTarget(app, mappingTarget) } // categoryIDForActivityType maps common provider activity labels to wanderer's // built-in categories. Unknown labels intentionally leave the category empty. func categoryIDForActivityType(app core.App, activityType string) string { - categoryMap := map[string]string{ - "hiking": "Hiking", - "hike": "Hiking", - "walking": "Walking", - "walk": "Walking", - "running": "Walking", - "run": "Walking", - "biking": "Biking", - "cycling": "Biking", - "ride": "Biking", - "mtb": "Biking", - "skiing": "Skiing", - "canoeing": "Canoeing", - "climbing": "Climbing", - } + return categoryTargetForActivityType(app, activityType).CategoryID +} - name := categoryMap[strings.ToLower(activityType)] +func categoryTargetForActivityType(app core.App, activityType string) CategoryMappingTarget { + name := categoryNameForActivityType(activityType) if name == "" { - return "" + return CategoryMappingTarget{} } - category, _ := app.FindFirstRecordByData("categories", "name", name) - if category == nil { - return "" + target, matched := resolveCategoryMappingTarget(app, CategoryMappingValue{Category: name}) + if !matched { + return CategoryMappingTarget{} } - return category.Id + return target +} + +func categoryNameForActivityType(activityType string) string { + categoryMap := map[string]string{ + "hiking": "Hiking", + "hike": "Hiking", + "walking": "Walking", + "walk": "Walking", + "running": "Running", + "run": "Running", + "virtualrun": "Running", + "trailrun": "Running", + "jogging": "Running", + "biking": "Biking", + "cycling": "Biking", + "ride": "Biking", + "mtb": "Biking", + "skiing": "Skiing", + "canoeing": "Canoeing", + "climbing": "Climbing", + } + + return categoryMap[strings.ToLower(strings.TrimSpace(activityType))] +} + +func resolveCategoryMappingTarget(app core.App, target CategoryMappingValue) (CategoryMappingTarget, bool) { + categoryNameOrID := strings.TrimSpace(target.Category) + subcategoryNameOrID := strings.TrimSpace(target.Subcategory) + if categoryNameOrID == "" && subcategoryNameOrID == "" { + return CategoryMappingTarget{}, false + } + + if subcategoryNameOrID != "" { + if subcategory, err := app.FindRecordById("subcategories", subcategoryNameOrID); err == nil && subcategory != nil { + categoryID := subcategory.GetString("category") + if categoryID == "" { + return CategoryMappingTarget{}, false + } + return CategoryMappingTarget{CategoryID: categoryID, SubcategoryID: subcategory.Id}, true + } + category, subcategory, err := util.ResolveCategoryAndSubcategoryByNormalizedNames(app, categoryNameOrID, subcategoryNameOrID) + if err == nil && category != nil && subcategory != nil { + return CategoryMappingTarget{CategoryID: category.Id, SubcategoryID: subcategory.Id}, true + } + return CategoryMappingTarget{}, false + } + + if category, err := app.FindRecordById("categories", categoryNameOrID); err == nil && category != nil { + return CategoryMappingTarget{CategoryID: category.Id}, true + } + + if subcategory, err := app.FindRecordById("subcategories", categoryNameOrID); err == nil && subcategory != nil { + categoryID := subcategory.GetString("category") + if categoryID == "" { + return CategoryMappingTarget{}, false + } + return CategoryMappingTarget{CategoryID: categoryID, SubcategoryID: subcategory.Id}, true + } + + category, _ := util.FindCategoryByNormalizedName(app, categoryNameOrID) + if category != nil { + return CategoryMappingTarget{CategoryID: category.Id}, true + } + + return CategoryMappingTarget{}, false } func fallbackName(name string) string { diff --git a/db/plugins/importer/importer_test.go b/db/plugins/importer/importer_test.go index ce08dc67..8d85a887 100644 --- a/db/plugins/importer/importer_test.go +++ b/db/plugins/importer/importer_test.go @@ -7,6 +7,9 @@ import ( "testing" "time" + "github.com/pocketbase/pocketbase/core" + pbtests "github.com/pocketbase/pocketbase/tests" + pluginsystem "pocketbase/pluginsystem" "pocketbase/util" ) @@ -264,7 +267,7 @@ func TestCategoryIDForImportDoesNotFallbackWhenProviderMappingIsBlank(t *testing }, } - if got := categoryIDForImport(nil, item, map[string]string{"Ride": ""}); got != "" { + if got := categoryIDForImport(nil, item, map[string]CategoryMappingValue{"Ride": {}}); got != "" { t.Fatalf("expected blank provider mapping to suppress activity fallback, got %q", got) } } @@ -282,6 +285,121 @@ func TestProviderCategoryFromImport(t *testing.T) { } } +func TestCategoryNameForActivityType(t *testing.T) { + cases := map[string]string{ + "run": "Running", + "running": "Running", + "VirtualRun": "Running", + "trailrun": "Running", + "jogging": "Running", + "walk": "Walking", + "hike": "Hiking", + "unknown": "", + } + + for activityType, want := range cases { + if got := categoryNameForActivityType(activityType); got != want { + t.Fatalf("categoryNameForActivityType(%q) = %q, want %q", activityType, got, want) + } + } +} + +func TestCategoryFromProviderMappingUsesNormalizedCategoryName(t *testing.T) { + app := setupImporterCategoryTestApp(t) + + category := core.NewRecord(mustFindImporterTestCollection(t, app, "categories")) + category.Set("name", "Trail Running") + if err := app.Save(category); err != nil { + t.Fatal(err) + } + + got, matched := CategoryFromProviderMapping(app, "Run", map[string]CategoryMappingValue{"Run": {Category: "trail-running"}}) + if !matched { + t.Fatal("expected provider mapping to match") + } + if got != category.Id { + t.Fatalf("CategoryFromProviderMapping() = %q, want %q", got, category.Id) + } +} + +func TestCategoryTargetFromProviderMappingSupportsSubcategoryPath(t *testing.T) { + app := setupImporterCategoryTestApp(t) + + category := core.NewRecord(mustFindImporterTestCollection(t, app, "categories")) + category.Set("name", "Running") + if err := app.Save(category); err != nil { + t.Fatal(err) + } + + subcategory := core.NewRecord(mustFindImporterTestCollection(t, app, "subcategories")) + subcategory.Set("category", category.Id) + subcategory.Set("name", "Trail") + if err := app.Save(subcategory); err != nil { + t.Fatal(err) + } + + target, matched := CategoryTargetFromProviderMapping(app, "TrailRun", map[string]CategoryMappingValue{"TrailRun": {Category: "Running", Subcategory: "Trail"}}) + if !matched { + t.Fatal("expected provider mapping to match") + } + if target.CategoryID != category.Id || target.SubcategoryID != subcategory.Id { + t.Fatalf("CategoryTargetFromProviderMapping() = %#v, want category=%q subcategory=%q", target, category.Id, subcategory.Id) + } +} + +func TestCategoryTargetFromProviderMappingPrefersLiteralCategoryWithSlash(t *testing.T) { + app := setupImporterCategoryTestApp(t) + + slashCategory := core.NewRecord(mustFindImporterTestCollection(t, app, "categories")) + slashCategory.Set("name", "Foo/Bar") + if err := app.Save(slashCategory); err != nil { + t.Fatal(err) + } + + parentCategory := core.NewRecord(mustFindImporterTestCollection(t, app, "categories")) + parentCategory.Set("name", "Foo") + if err := app.Save(parentCategory); err != nil { + t.Fatal(err) + } + + subcategory := core.NewRecord(mustFindImporterTestCollection(t, app, "subcategories")) + subcategory.Set("category", parentCategory.Id) + subcategory.Set("name", "Bar") + if err := app.Save(subcategory); err != nil { + t.Fatal(err) + } + + target, matched := CategoryTargetFromProviderMapping(app, "Provider", map[string]CategoryMappingValue{"Provider": {Category: "Foo/Bar"}}) + if !matched { + t.Fatal("expected provider mapping to match") + } + if target.CategoryID != slashCategory.Id || target.SubcategoryID != "" { + t.Fatalf("CategoryTargetFromProviderMapping() = %#v, want literal category %q", target, slashCategory.Id) + } +} + +func TestCategoryTargetFromProviderMappingDoesNotSplitSlashCategoryName(t *testing.T) { + app := setupImporterCategoryTestApp(t) + + category := core.NewRecord(mustFindImporterTestCollection(t, app, "categories")) + category.Set("name", "Foo") + if err := app.Save(category); err != nil { + t.Fatal(err) + } + + subcategory := core.NewRecord(mustFindImporterTestCollection(t, app, "subcategories")) + subcategory.Set("category", category.Id) + subcategory.Set("name", "Bar") + if err := app.Save(subcategory); err != nil { + t.Fatal(err) + } + + target, matched := CategoryTargetFromProviderMapping(app, "Provider", map[string]CategoryMappingValue{"Provider": {Category: "Foo/Bar"}}) + if matched { + t.Fatalf("CategoryTargetFromProviderMapping() = %#v, expected slash category name not to be split", target) + } +} + func TestDateFromImport(t *testing.T) { started := time.Date(2025, 6, 1, 8, 0, 0, 0, time.UTC) @@ -430,3 +548,42 @@ func TestRemoveRawQueryParamOrdered(t *testing.T) { t.Fatalf("unexpected query: %q", got) } } + +func setupImporterCategoryTestApp(t *testing.T) *pbtests.TestApp { + t.Helper() + + app, err := pbtests.NewTestApp(t.TempDir()) + if err != nil { + t.Fatal(err) + } + + categories := core.NewBaseCollection("categories") + categories.Fields.Add(&core.TextField{Name: "name", Required: true}) + if err := app.Save(categories); err != nil { + app.Cleanup() + t.Fatal(err) + } + + subcategories := core.NewBaseCollection("subcategories") + subcategories.Fields.Add( + &core.RelationField{Name: "category", CollectionId: categories.Id, MaxSelect: 1, Required: true}, + &core.TextField{Name: "name", Required: true}, + ) + if err := app.Save(subcategories); err != nil { + app.Cleanup() + t.Fatal(err) + } + + return app +} + +func mustFindImporterTestCollection(t *testing.T, app core.App, name string) *core.Collection { + t.Helper() + + collection, err := app.FindCollectionByNameOrId(name) + if err != nil { + t.Fatal(err) + } + + return collection +} diff --git a/db/routes/category_preferences.go b/db/routes/category_preferences.go new file mode 100644 index 00000000..8e2a7060 --- /dev/null +++ b/db/routes/category_preferences.go @@ -0,0 +1,52 @@ +package routes + +import ( + "net/http" + "pocketbase/util" + + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" +) + +type categoryPreferenceReorderRequest struct { + Categories []string `json:"categories"` +} + +type subcategoryPreferenceReorderRequest struct { + Category string `json:"category"` + Subcategories []string `json:"subcategories"` +} + +func CategoryPreferencesReorder(e *core.RequestEvent) error { + if e.Auth == nil { + return apis.NewUnauthorizedError("authentication required", nil) + } + + var request categoryPreferenceReorderRequest + if err := e.BindBody(&request); err != nil { + return apis.NewBadRequestError("failed to read request data", err) + } + + if err := util.ReorderUserCategoryPreferences(e.App, e.Auth.Id, request.Categories); err != nil { + return apis.NewBadRequestError(err.Error(), err) + } + + return e.JSON(http.StatusOK, map[string]any{"acknowledged": true}) +} + +func SubcategoryPreferencesReorder(e *core.RequestEvent) error { + if e.Auth == nil { + return apis.NewUnauthorizedError("authentication required", nil) + } + + var request subcategoryPreferenceReorderRequest + if err := e.BindBody(&request); err != nil { + return apis.NewBadRequestError("failed to read request data", err) + } + + if err := util.ReorderUserSubcategoryPreferences(e.App, e.Auth.Id, request.Category, request.Subcategories); err != nil { + return apis.NewBadRequestError(err.Error(), err) + } + + return e.JSON(http.StatusOK, map[string]any{"acknowledged": true}) +} diff --git a/db/routes/plugin_system_category_remap.go b/db/routes/plugin_system_category_remap.go index 70794183..88447e20 100644 --- a/db/routes/plugin_system_category_remap.go +++ b/db/routes/plugin_system_category_remap.go @@ -24,8 +24,9 @@ type pluginCategoryRemapResponse struct { } type pluginCategoryRemapCandidate struct { - Trail *core.Record - CategoryID string + Trail *core.Record + CategoryID string + SubcategoryID string } type pluginCategoryTrailReference struct { @@ -72,6 +73,7 @@ func PluginSystemCategoryRemapApply(e *core.RequestEvent) error { return err } trail.Set("category", candidate.CategoryID) + trail.Set("subcategory", candidate.SubcategoryID) if err := txApp.Save(trail); err != nil { return err } @@ -84,7 +86,7 @@ func PluginSystemCategoryRemapApply(e *core.RequestEvent) error { return e.JSON(http.StatusOK, pluginCategoryRemapResponse{Count: len(candidates), Remapped: remapped}) } -func pluginCategoryRemapInput(e *core.RequestEvent) (*core.Record, map[string]string, error) { +func pluginCategoryRemapInput(e *core.RequestEvent) (*core.Record, map[string]importer.CategoryMappingValue, error) { if e.Auth == nil { return nil, nil, apis.NewUnauthorizedError("authentication required", nil) } @@ -109,7 +111,7 @@ func pluginCategoryRemapInput(e *core.RequestEvent) (*core.Record, map[string]st return instance, categoryMapping(pluginHostConfig(config)), nil } -func pluginCategoryRemapCandidates(app core.App, userID string, pluginID string, mapping map[string]string) ([]pluginCategoryRemapCandidate, error) { +func pluginCategoryRemapCandidates(app core.App, userID string, pluginID string, mapping map[string]importer.CategoryMappingValue) ([]pluginCategoryRemapCandidate, error) { if userID == "" || pluginID == "" || len(mapping) == 0 { return nil, nil } @@ -122,7 +124,7 @@ func pluginCategoryRemapCandidates(app core.App, userID string, pluginID string, return pluginCategoryRemapCandidatesFromRefs(app, refs, mapping), nil } -func pluginCategoryRemapCandidatesFromRefs(app core.App, refs []pluginCategoryTrailReference, mapping map[string]string) []pluginCategoryRemapCandidate { +func pluginCategoryRemapCandidatesFromRefs(app core.App, refs []pluginCategoryTrailReference, mapping map[string]importer.CategoryMappingValue) []pluginCategoryRemapCandidate { if len(refs) == 0 || len(mapping) == 0 { return nil } @@ -130,19 +132,23 @@ func pluginCategoryRemapCandidatesFromRefs(app core.App, refs []pluginCategoryTr candidates := make([]pluginCategoryRemapCandidate, 0, len(refs)) for _, ref := range refs { providerCategory := strings.TrimSpace(ref.Ref.GetString("provider_category")) - categoryID, matched := importer.CategoryFromProviderMapping(app, providerCategory, mapping) - if !matched || categoryID == "" || ref.Trail.GetString("category") == categoryID { + target, matched := importer.CategoryTargetFromProviderMapping(app, providerCategory, mapping) + if !matched || target.CategoryID == "" { + continue + } + if ref.Trail.GetString("category") == target.CategoryID && ref.Trail.GetString("subcategory") == target.SubcategoryID { continue } candidates = append(candidates, pluginCategoryRemapCandidate{ - Trail: ref.Trail, - CategoryID: categoryID, + Trail: ref.Trail, + CategoryID: target.CategoryID, + SubcategoryID: target.SubcategoryID, }) } return candidates } -func pluginCategoryBackfilledSinceMappingCountFromRefs(app core.App, instance *core.Record, refs []pluginCategoryTrailReference, mapping map[string]string) int { +func pluginCategoryBackfilledSinceMappingCountFromRefs(app core.App, instance *core.Record, refs []pluginCategoryTrailReference, mapping map[string]importer.CategoryMappingValue) int { mappingUpdatedAt := categoryMappingUpdatedAt(app, instance) if mappingUpdatedAt.IsZero() || len(refs) == 0 || len(mapping) == 0 { return 0 @@ -155,8 +161,8 @@ func pluginCategoryBackfilledSinceMappingCountFromRefs(app core.App, instance *c continue } providerCategory := strings.TrimSpace(ref.Ref.GetString("provider_category")) - categoryID, matched := importer.CategoryFromProviderMapping(app, providerCategory, mapping) - if matched && categoryID != "" && ref.Trail.GetString("category") != categoryID { + target, matched := importer.CategoryTargetFromProviderMapping(app, providerCategory, mapping) + if matched && target.CategoryID != "" && (ref.Trail.GetString("category") != target.CategoryID || ref.Trail.GetString("subcategory") != target.SubcategoryID) { count++ } } diff --git a/db/routes/plugin_system_sync.go b/db/routes/plugin_system_sync.go index 7c5cb471..93443807 100644 --- a/db/routes/plugin_system_sync.go +++ b/db/routes/plugin_system_sync.go @@ -578,24 +578,31 @@ func boolOption(config map[string]any, key string, fallback bool) bool { return value } -func categoryMapping(config map[string]any) map[string]string { +func categoryMapping(config map[string]any) map[string]importer.CategoryMappingValue { raw, ok := config["categoryMapping"].(map[string]any) if !ok { return nil } - result := make(map[string]string, len(raw)) + result := make(map[string]importer.CategoryMappingValue, len(raw)) for key, value := range raw { - category, ok := value.(string) - if ok { - result[key] = category + switch typed := value.(type) { + case string: + result[key] = importer.CategoryMappingValue{Category: strings.TrimSpace(typed)} + case map[string]any: + category, _ := typed["category"].(string) + subcategory, _ := typed["subcategory"].(string) + result[key] = importer.CategoryMappingValue{ + Category: strings.TrimSpace(category), + Subcategory: strings.TrimSpace(subcategory), + } } } return result } -func hasUsableCategoryMapping(mapping map[string]string) bool { - for _, category := range mapping { - if strings.TrimSpace(category) != "" { +func hasUsableCategoryMapping(mapping map[string]importer.CategoryMappingValue) bool { + for _, target := range mapping { + if strings.TrimSpace(target.Category) != "" || strings.TrimSpace(target.Subcategory) != "" { return true } } diff --git a/db/routes/plugin_system_sync_test.go b/db/routes/plugin_system_sync_test.go index 62f87aaf..1d21b9b6 100644 --- a/db/routes/plugin_system_sync_test.go +++ b/db/routes/plugin_system_sync_test.go @@ -1,6 +1,10 @@ package routes -import "testing" +import ( + "testing" + + "pocketbase/plugins/importer" +) func TestCategoryMappingPreservesExplicitEmptyMap(t *testing.T) { mapping := categoryMapping(map[string]any{ @@ -29,7 +33,22 @@ func TestCategoryMappingPreservesBlankProviderMapping(t *testing.T) { if mapping == nil { t.Fatal("expected category mapping") } - if value, ok := mapping["Ride"]; !ok || value != "" { + if value, ok := mapping["Ride"]; !ok || value != (importer.CategoryMappingValue{}) { t.Fatalf("expected blank provider mapping to be preserved, got %#v", mapping) } } + +func TestCategoryMappingParsesStructuredTarget(t *testing.T) { + mapping := categoryMapping(map[string]any{ + "categoryMapping": map[string]any{ + "TrailRun": map[string]any{ + "category": "Running", + "subcategory": "Trail", + }, + }, + }) + want := importer.CategoryMappingValue{Category: "Running", Subcategory: "Trail"} + if value, ok := mapping["TrailRun"]; !ok || value != want { + t.Fatalf("structured provider mapping = %#v, want %#v", mapping, want) + } +} diff --git a/db/routes/remote_trail.go b/db/routes/remote_trail.go index 3f9eeb84..a32fd8bf 100644 --- a/db/routes/remote_trail.go +++ b/db/routes/remote_trail.go @@ -237,15 +237,39 @@ func performFullSync(app core.App, ctx context.Context, reqURL *url.URL, localTr // --- Sub-Sync Helpers --- func syncTrailMetadata(app core.App, record *core.Record, data map[string]any) { - // Resolve Category if present in expand + var federatedCategoryName, federatedSubcategoryName string + if expand, ok := data["expand"].(map[string]any); ok { if cat, ok := expand["category"].(map[string]any); ok { if name, ok := cat["name"].(string); ok { - if c, _ := app.FindFirstRecordByData("categories", "name", name); c != nil { - record.Set("category", c.Id) - } + federatedCategoryName = name } } + if subcat, ok := expand["subcategory"].(map[string]any); ok { + if name, ok := subcat["name"].(string); ok { + federatedSubcategoryName = name + } + } + } + + if federatedCategoryName != "" { + record.Set("federated_category_name", federatedCategoryName) + } + if federatedSubcategoryName != "" { + record.Set("federated_subcategory_name", federatedSubcategoryName) + } + + category, subcategory, err := util.ResolveCategoryAndSubcategoryByNormalizedNames(app, federatedCategoryName, federatedSubcategoryName) + if err == nil && category != nil { + record.Set("category", category.Id) + if subcategory != nil { + record.Set("subcategory", subcategory.Id) + } else { + record.Set("subcategory", "") + } + } else if err == nil && federatedCategoryName != "" { + record.Set("category", "") + record.Set("subcategory", "") } // Resolve Tags @@ -260,8 +284,11 @@ func syncTrailMetadata(app core.App, record *core.Record, data map[string]any) { delete(data, "gpx") delete(data, "author") delete(data, "category") + delete(data, "subcategory") delete(data, "tags") delete(data, "iri") + delete(data, "federated_category_name") + delete(data, "federated_subcategory_name") record.Load(data) } diff --git a/db/util/activitypub.go b/db/util/activitypub.go index 74fde892..452614ec 100644 --- a/db/util/activitypub.go +++ b/db/util/activitypub.go @@ -173,7 +173,15 @@ func TrailFromActivity(activity pub.Activity, app core.App, actor *core.Record) } } else { // this trail exists already - // ensure that it is fully synced to catch waypoint/summit log updates + // keep searchable category metadata fresh from the update activity while + // still requiring a full sync to catch waypoint/summit log updates. + categoryMetadata, err := trailCategoryMetadataFromActivityObject(t) + if err != nil { + return nil, err + } + if err := applyTrailActivityCategoryMetadata(app, record, categoryMetadata); err != nil { + return nil, err + } record.Set("needs_full_sync", true) err = app.Save(record) @@ -185,22 +193,22 @@ func TrailFromActivity(activity pub.Activity, app core.App, actor *core.Record) } var distance, duration, elevation_gain, elevation_loss float64 - var diffculty, category string + var diffculty string trailTags := []string{} tags, err := pub.ToItemCollection(t.Tag) if err != nil { return nil, err } + categoryMetadata := trailCategoryMetadataFromTags(tags) for _, tag := range tags.Collection() { tagObj, err := pub.ToObject(tag) if err != nil { continue } + content := tagObj.Content.First().Value.String() switch tagObj.Name.First().Value.String() { - case "category": - category = content case "difficulty": diffculty = content case "elevation_gain": @@ -254,9 +262,8 @@ func TrailFromActivity(activity pub.Activity, app core.App, actor *core.Record) record.Set("author", actor.Id) record.Set("needs_full_sync", true) - categoryRecord, err := app.FindFirstRecordByData("categories", "name", category) - if err == nil { - record.Set("category", categoryRecord.Id) + if err := applyTrailActivityCategoryMetadata(app, record, categoryMetadata); err != nil { + return nil, err } if t.Attachment != nil { @@ -282,12 +289,12 @@ func TrailFromActivity(activity pub.Activity, app core.App, actor *core.Record) if len(photoURLs) > 0 { photos := []*filesystem.File{} - for i, purl := range photoURLs { + for _, purl := range photoURLs { photo, err := filesystem.NewFileFromURL(context.Background(), purl) if err != nil { continue } - photos[i] = photo + photos = append(photos, photo) } record.Set("photos", photos) @@ -306,6 +313,80 @@ func TrailFromActivity(activity pub.Activity, app core.App, actor *core.Record) return record, app.Save(record) } +type trailActivityCategoryMetadata struct { + category string + subcategory string + categorySet bool + subcategorySet bool +} + +func trailCategoryMetadataFromActivityObject(object *pub.Object) (trailActivityCategoryMetadata, error) { + if len(object.Tag) == 0 { + return trailActivityCategoryMetadata{}, nil + } + + tags, err := pub.ToItemCollection(object.Tag) + if err != nil { + return trailActivityCategoryMetadata{}, err + } + + return trailCategoryMetadataFromTags(tags), nil +} + +func trailCategoryMetadataFromTags(tags *pub.ItemCollection) trailActivityCategoryMetadata { + metadata := trailActivityCategoryMetadata{} + for _, tag := range tags.Collection() { + tagObj, err := pub.ToObject(tag) + if err != nil { + continue + } + + switch tagObj.Name.First().Value.String() { + case "category": + metadata.category = tagObj.Content.First().Value.String() + metadata.categorySet = true + case "subcategory": + metadata.subcategory = tagObj.Content.First().Value.String() + metadata.subcategorySet = true + } + } + + return metadata +} + +func applyTrailActivityCategoryMetadata(app core.App, record *core.Record, metadata trailActivityCategoryMetadata) error { + if metadata.categorySet { + record.Set("federated_category_name", metadata.category) + } + if metadata.subcategorySet { + record.Set("federated_subcategory_name", metadata.subcategory) + } else if metadata.categorySet { + record.Set("federated_subcategory_name", "") + } + + if !metadata.categorySet { + return nil + } + + categoryRecord, subcategoryRecord, err := ResolveCategoryAndSubcategoryByNormalizedNames(app, metadata.category, metadata.subcategory) + if err != nil { + return err + } + if categoryRecord != nil { + record.Set("category", categoryRecord.Id) + if subcategoryRecord != nil { + record.Set("subcategory", subcategoryRecord.Id) + } else { + record.Set("subcategory", "") + } + } else { + record.Set("category", "") + record.Set("subcategory", "") + } + + return nil +} + func ObjectFromTrail(app core.App, trail *core.Record, mentions *pub.ItemCollection) (*pub.Object, error) { origin := os.Getenv("ORIGIN") if origin == "" { @@ -320,9 +401,9 @@ func ObjectFromTrail(app core.App, trail *core.Record, mentions *pub.ItemCollect if len(errs) > 0 { return nil, fmt.Errorf("failed to expand tags: %v", errs) } - errs = app.ExpandRecord(trail, []string{"category"}, nil) + errs = app.ExpandRecord(trail, []string{"category", "subcategory"}, nil) if len(errs) > 0 { - return nil, fmt.Errorf("failed to expand category: %v", errs) + return nil, fmt.Errorf("failed to expand category/subcategory: %v", errs) } category := "" @@ -330,6 +411,11 @@ func ObjectFromTrail(app core.App, trail *core.Record, mentions *pub.ItemCollect if categoryRecord != nil { category = categoryRecord.GetString("name") } + subcategory := "" + subcategoryRecord := trail.ExpandedOne("subcategory") + if subcategoryRecord != nil { + subcategory = subcategoryRecord.GetString("name") + } tagRecords := trail.ExpandedAll("tags") @@ -372,6 +458,14 @@ func ObjectFromTrail(app core.App, trail *core.Record, mentions *pub.ItemCollect } } + if subcategory != "" { + tags.Append(pub.Object{ + Type: pub.NoteType, + Name: pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, "subcategory")), + Content: pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, subcategory)), + }) + } + for _, v := range tagRecords { hashtag := pub.ObjectNew(pub.NoteType) hashtag.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, "tag")) diff --git a/db/util/category.go b/db/util/category.go new file mode 100644 index 00000000..7270d0ac --- /dev/null +++ b/db/util/category.go @@ -0,0 +1,216 @@ +package util + +import ( + "encoding/json" + "fmt" + "strings" + "unicode" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/types" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "golang.org/x/text/unicode/norm" +) + +type CategoryTranslation struct { + Name string `json:"name"` + ShortName string `json:"short_name"` +} + +func NormalizeCategoryName(name string) string { + decomposed := norm.NFD.String(name) + + var b strings.Builder + b.Grow(len(decomposed)) + for _, r := range decomposed { + if unicode.Is(unicode.Mn, r) { + continue + } + b.WriteRune(r) + } + + folded := cases.Fold().String(b.String()) + + b.Reset() + b.Grow(len(folded)) + lastWasSeparator := false + for _, r := range folded { + if unicode.IsSpace(r) || r == '-' || r == '_' { + if !lastWasSeparator { + b.WriteByte(' ') + lastWasSeparator = true + } + continue + } + + b.WriteRune(r) + lastWasSeparator = false + } + + return strings.TrimSpace(b.String()) +} + +func ParseCategoryTranslations(raw any) (map[string]CategoryTranslation, error) { + if raw == nil { + return nil, nil + } + + switch value := raw.(type) { + case map[string]CategoryTranslation: + return value, nil + case map[string]any: + return normalizeCategoryTranslations(value) + case types.JSONRaw: + if len(value) == 0 { + return nil, nil + } + + var decoded map[string]any + if err := json.Unmarshal(value, &decoded); err != nil { + return nil, fmt.Errorf("translations must be valid JSON: %w", err) + } + + return normalizeCategoryTranslations(decoded) + case []byte: + if len(value) == 0 { + return nil, nil + } + + var decoded map[string]any + if err := json.Unmarshal(value, &decoded); err != nil { + return nil, fmt.Errorf("translations must be valid JSON: %w", err) + } + + return normalizeCategoryTranslations(decoded) + case string: + if strings.TrimSpace(value) == "" { + return nil, nil + } + + var decoded map[string]any + if err := json.Unmarshal([]byte(value), &decoded); err != nil { + return nil, fmt.Errorf("translations must be valid JSON: %w", err) + } + + return normalizeCategoryTranslations(decoded) + default: + return nil, fmt.Errorf("translations must be a JSON object") + } +} + +func ValidateCategoryRecord(app core.App, record *core.Record) error { + name := record.GetString("name") + normalizedName := NormalizeCategoryName(name) + + allCategories, err := app.FindAllRecords("categories") + if err != nil { + return err + } + + for _, existing := range allCategories { + if existing.Id == record.Id { + continue + } + + if NormalizeCategoryName(existing.GetString("name")) == normalizedName { + return fmt.Errorf("category name %q collides with existing category %q after normalization", name, existing.GetString("name")) + } + } + + if _, err := ParseCategoryTranslations(record.Get("translations")); err != nil { + return err + } + + return nil +} + +func FindCategoryByNormalizedName(app core.App, name string) (*core.Record, error) { + normalizedName := NormalizeCategoryName(name) + if normalizedName == "" { + return nil, nil + } + + allCategories, err := app.FindAllRecords("categories") + if err != nil { + return nil, err + } + + for _, category := range allCategories { + if NormalizeCategoryName(category.GetString("name")) == normalizedName { + return category, nil + } + } + + return nil, nil +} + +func ValidateCategoryCollectionState(app core.App) error { + allCategories, err := app.FindAllRecords("categories") + if err != nil { + return err + } + + seen := map[string]string{} + for _, category := range allCategories { + name := category.GetString("name") + normalizedName := NormalizeCategoryName(name) + if other, ok := seen[normalizedName]; ok { + return fmt.Errorf("category normalization collision: %q conflicts with %q", name, other) + } + seen[normalizedName] = name + + if _, err := ParseCategoryTranslations(category.Get("translations")); err != nil { + return fmt.Errorf("invalid translations for category %q: %w", name, err) + } + } + + return nil +} + +func normalizeCategoryTranslations(raw map[string]any) (map[string]CategoryTranslation, error) { + if len(raw) == 0 { + return nil, nil + } + + translations := make(map[string]CategoryTranslation, len(raw)) + for locale, entry := range raw { + tag, err := language.Parse(locale) + if err != nil { + return nil, fmt.Errorf("translations locale %q is invalid", locale) + } + base, _ := tag.Base() + if locale != base.String() { + return nil, fmt.Errorf("translations locale %q must use the base locale %q", locale, base.String()) + } + if _, ok := supportedCategoryLocales[base.String()]; !ok { + return nil, fmt.Errorf("translations locale %q is not supported", locale) + } + + entryMap, ok := entry.(map[string]any) + if !ok { + return nil, fmt.Errorf("translations[%s] must be an object", locale) + } + + translation := CategoryTranslation{} + if name, ok := entryMap["name"]; ok { + nameString, ok := name.(string) + if !ok { + return nil, fmt.Errorf("translations[%s].name must be a string", locale) + } + translation.Name = nameString + } + + if shortName, ok := entryMap["short_name"]; ok { + shortNameString, ok := shortName.(string) + if !ok { + return nil, fmt.Errorf("translations[%s].short_name must be a string", locale) + } + translation.ShortName = shortNameString + } + + translations[locale] = translation + } + + return translations, nil +} diff --git a/db/util/category_defaults.go b/db/util/category_defaults.go new file mode 100644 index 00000000..b81b9750 --- /dev/null +++ b/db/util/category_defaults.go @@ -0,0 +1,289 @@ +package util + +import ( + "fmt" + + "github.com/pocketbase/pocketbase/core" +) + +var supportedCategoryLocales = map[string]struct{}{ + "cs": {}, + "de": {}, + "en": {}, + "es": {}, + "eu": {}, + "fr": {}, + "hu": {}, + "it": {}, + "nl": {}, + "no": {}, + "pl": {}, + "pt": {}, + "ru": {}, + "zh": {}, +} + +var defaultCategoryNames = []string{"Hiking", "Walking", "Running", "Climbing", "Skiing", "Canoeing", "Biking", "Other"} + +func DefaultCategoryNames() []string { + return append([]string(nil), defaultCategoryNames...) +} + +func SeedDefaultCategories(app core.App) error { + collection, err := app.FindCollectionByNameOrId("categories") + if err != nil { + return err + } + + allCategories, err := app.FindAllRecords("categories") + if err != nil { + return err + } + + existing := make(map[string]struct{}, len(allCategories)) + for _, category := range allCategories { + existing[NormalizeCategoryName(category.GetString("name"))] = struct{}{} + } + + for _, name := range defaultCategoryNames { + if _, ok := existing[NormalizeCategoryName(name)]; ok { + continue + } + + record := core.NewRecord(collection) + record.Set("name", name) + if collection.Fields.GetByName("settings") != nil { + record.Set("settings", defaultCategorySettings()) + } + if err := app.Save(record); err != nil { + return fmt.Errorf("failed to seed default category %q: %w", name, err) + } + } + + return nil +} + +func defaultCategorySettings() map[string]any { + return map[string]any{ + "wp_merge_enabled": true, + "wp_merge_radius": 50, + } +} + +var defaultCategoryTranslations = map[string]map[string]string{ + "Biking": { + "cs": "Cyklistika", + "de": "Radfahren", + "en": "Biking", + "es": "Ciclismo", + "eu": "Bizikleta", + "fr": "Vélo", + "hu": "Biking", + "it": "Ciclismo", + "nl": "Fietsen", + "no": "Sykling", + "pl": "Rower", + "pt": "Ciclismo", + "ru": "Велоспорт", + "zh": "骑行", + }, + "Canoeing": { + "cs": "Kanoistika", + "de": "Kanufahren", + "en": "Canoeing", + "es": "Remo", + "eu": "Kanoa", + "fr": "Canoë", + "hu": "Canoeing", + "it": "Canoa", + "nl": "Kanoën", + "no": "Padling", + "pl": "Kajak", + "pt": "Canoagem", + "ru": "Каякинг", + "zh": "划艇", + }, + "Climbing": { + "cs": "Horolezectví", + "de": "Klettern", + "en": "Climbing", + "es": "Escalada", + "eu": "Eskalada", + "fr": "Escalade", + "hu": "Climbing", + "it": "Arrampicata", + "nl": "Klimmen", + "no": "Klatring", + "pl": "Wspinaczka", + "pt": "Escalada", + "ru": "Скалолазание", + "zh": "攀岩", + }, + "Hiking": { + "cs": "Turistika", + "de": "Wandern", + "en": "Hiking", + "es": "Senderismo", + "eu": "Mendi-ibilaldia", + "fr": "Randonnée", + "hu": "Hiking", + "it": "Escursionismo", + "nl": "Hiken", + "no": "Vandring", + "pl": "Wędrówka", + "pt": "Montanhismo", + "ru": "Пеший туризм", + "zh": "徒步", + }, + "Other": { + "cs": "Ostatní", + "de": "Sonstiges", + "en": "Other", + "es": "Otros", + "eu": "Bestelakoak", + "fr": "Autre", + "hu": "Egyéb", + "it": "Altro", + "nl": "Overig", + "no": "Annet", + "pl": "Inne", + "pt": "Outros", + "ru": "Другое", + "zh": "其他", + }, + "Running": { + "cs": "Běh", + "de": "Laufen", + "en": "Running", + "es": "Carrera", + "eu": "Korrika", + "fr": "Course à pied", + "hu": "Futás", + "it": "Corsa", + "nl": "Hardlopen", + "no": "Løping", + "pl": "Bieganie", + "pt": "Corrida", + "ru": "Бег", + "zh": "跑步", + }, + "Skiing": { + "de": "Skifahren", + "no": "Skisport", + }, + "Walking": { + "cs": "Chůze", + "de": "Spazieren", + "en": "Walking", + "es": "Paseo", + "eu": "Oinez", + "fr": "Marche", + "hu": "Walking", + "it": "Camminare", + "nl": "Wandelen", + "no": "Gåtur", + "pl": "Spacer", + "pt": "Caminhada", + "ru": "Прогулка", + "zh": "步行", + }, +} + +var defaultCategoryIcons = map[string]string{ + "Biking": "person-biking", + "Canoeing": "sailboat", + "Climbing": "mountain", + "Hiking": "person-hiking", + "Other": "shapes", + "Running": "person-running", + "Skiing": "person-skiing-nordic", + "Walking": "person-walking", +} + +func PrepopulateDefaultCategoryTranslations(app core.App) error { + allCategories, err := app.FindAllRecords("categories") + if err != nil { + return err + } + + for _, category := range allCategories { + staticTranslations, ok := defaultCategoryTranslations[category.GetString("name")] + if !ok { + continue + } + + currentTranslations, err := ParseCategoryTranslations(category.Get("translations")) + if err != nil { + return fmt.Errorf("invalid existing translations for category %q: %w", category.GetString("name"), err) + } + mergedTranslations, changed := mergeDefaultCategoryTranslations(staticTranslations, currentTranslations) + if !changed { + continue + } + + category.Set("translations", mergedTranslations) + if err := app.Save(category); err != nil { + return fmt.Errorf("failed to prepopulate translations for category %q: %w", category.GetString("name"), err) + } + } + + return nil +} + +func PrepopulateDefaultCategoryIcons(app core.App) error { + collection, err := app.FindCollectionByNameOrId("categories") + if err != nil { + return err + } + if collection.Fields.GetByName("icon") == nil { + return nil + } + + allCategories, err := app.FindAllRecords("categories") + if err != nil { + return err + } + + for _, category := range allCategories { + categoryName := category.GetString("name") + defaultIcon, ok := defaultCategoryIcons[categoryName] + if !ok { + continue + } + + if category.GetString("icon") != "" { + continue + } + + category.Set("icon", defaultIcon) + if err := app.Save(category); err != nil { + return fmt.Errorf("failed to prepopulate icon for category %q: %w", category.GetString("name"), err) + } + } + + return nil +} + +func mergeDefaultCategoryTranslations(staticTranslations map[string]string, currentTranslations map[string]CategoryTranslation) (map[string]CategoryTranslation, bool) { + if currentTranslations == nil { + currentTranslations = map[string]CategoryTranslation{} + } + + changed := false + for locale, name := range staticTranslations { + if name == "" { + continue + } + + translation := currentTranslations[locale] + if translation.Name != "" { + continue + } + + translation.Name = name + currentTranslations[locale] = translation + changed = true + } + + return currentTranslations, changed +} diff --git a/db/util/category_preference.go b/db/util/category_preference.go new file mode 100644 index 00000000..fdea759f --- /dev/null +++ b/db/util/category_preference.go @@ -0,0 +1,241 @@ +package util + +import ( + "fmt" + + "github.com/pocketbase/pocketbase/core" +) + +const DefaultPriorityCategoryName = "Hiking" + +func ValidateUserCategoryPreferenceRequest(priorityExplicit bool) error { + if priorityExplicit { + return fmt.Errorf("category preference priority can only be changed through the reorder endpoint") + } + + return nil +} + +func ValidateUserSubcategoryPreferenceRequest(priorityExplicit bool) error { + if priorityExplicit { + return fmt.Errorf("subcategory preference priority can only be changed through the reorder endpoint") + } + + return nil +} + +func EnsureUserCategoryPriority(app core.App, userID, categoryID string) error { + if userID == "" { + return nil + } + + prioritized, err := app.FindRecordsByFilter( + "user_category_preferences", + "user = {:user} && priority > 0", + "", + 1, + 0, + map[string]any{"user": userID}, + ) + if err != nil { + return err + } + if len(prioritized) > 0 { + return nil + } + + if categoryID == "" { + category, err := FindCategoryByNormalizedName(app, DefaultPriorityCategoryName) + if err != nil { + return err + } + if category == nil { + return nil + } + categoryID = category.Id + } + + collection, err := app.FindCollectionByNameOrId("user_category_preferences") + if err != nil { + return err + } + + existing, err := app.FindRecordsByFilter( + "user_category_preferences", + "user = {:user} && category = {:category}", + "", + 1, + 0, + map[string]any{"user": userID, "category": categoryID}, + ) + if err != nil { + return err + } + + var record *core.Record + if len(existing) > 0 { + record = existing[0] + } else { + record = core.NewRecord(collection) + record.Set("user", userID) + record.Set("category", categoryID) + record.Set("visible", true) + } + + record.Set("priority", 1) + return app.SaveNoValidate(record) +} + +func ReorderUserCategoryPreferences(app core.App, userID string, categoryIDs []string) error { + if userID == "" { + return fmt.Errorf("authentication required") + } + + categories, err := app.FindAllRecords("categories") + if err != nil { + return err + } + + if len(categoryIDs) != len(categories) { + return fmt.Errorf("reorder request must include all categories") + } + + validCategories := make(map[string]struct{}, len(categories)) + for _, category := range categories { + validCategories[category.Id] = struct{}{} + } + + seen := make(map[string]struct{}, len(categoryIDs)) + for _, categoryID := range categoryIDs { + if _, ok := validCategories[categoryID]; !ok { + return fmt.Errorf("unknown category %q", categoryID) + } + if _, ok := seen[categoryID]; ok { + return fmt.Errorf("duplicate category %q", categoryID) + } + seen[categoryID] = struct{}{} + } + + return app.RunInTransaction(func(txApp core.App) error { + collection, err := txApp.FindCollectionByNameOrId("user_category_preferences") + if err != nil { + return err + } + + existing, err := txApp.FindRecordsByFilter( + "user_category_preferences", + "user = {:user}", + "", + 0, + 0, + map[string]any{"user": userID}, + ) + if err != nil { + return err + } + + byCategory := make(map[string]*core.Record, len(existing)) + for _, record := range existing { + byCategory[record.GetString("category")] = record + } + + for index, categoryID := range categoryIDs { + record := byCategory[categoryID] + if record == nil { + record = core.NewRecord(collection) + record.Set("user", userID) + record.Set("category", categoryID) + record.Set("visible", true) + } + + record.Set("priority", index+1) + if err := txApp.SaveNoValidate(record); err != nil { + return err + } + } + + return nil + }) +} + +func ReorderUserSubcategoryPreferences(app core.App, userID, categoryID string, subcategoryIDs []string) error { + if userID == "" { + return fmt.Errorf("authentication required") + } + if categoryID == "" { + return fmt.Errorf("category is required") + } + + subcategories, err := app.FindRecordsByFilter( + "subcategories", + "category = {:category}", + "", + 0, + 0, + map[string]any{"category": categoryID}, + ) + if err != nil { + return err + } + + if len(subcategoryIDs) != len(subcategories) { + return fmt.Errorf("reorder request must include all subcategories for the category") + } + + validSubcategories := make(map[string]struct{}, len(subcategories)) + for _, subcategory := range subcategories { + validSubcategories[subcategory.Id] = struct{}{} + } + + seen := make(map[string]struct{}, len(subcategoryIDs)) + for _, subcategoryID := range subcategoryIDs { + if _, ok := validSubcategories[subcategoryID]; !ok { + return fmt.Errorf("unknown subcategory %q", subcategoryID) + } + if _, ok := seen[subcategoryID]; ok { + return fmt.Errorf("duplicate subcategory %q", subcategoryID) + } + seen[subcategoryID] = struct{}{} + } + + return app.RunInTransaction(func(txApp core.App) error { + collection, err := txApp.FindCollectionByNameOrId("user_subcategory_preferences") + if err != nil { + return err + } + + existing, err := txApp.FindRecordsByFilter( + "user_subcategory_preferences", + "user = {:user}", + "", + 0, + 0, + map[string]any{"user": userID}, + ) + if err != nil { + return err + } + + bySubcategory := make(map[string]*core.Record, len(existing)) + for _, record := range existing { + bySubcategory[record.GetString("subcategory")] = record + } + + for index, subcategoryID := range subcategoryIDs { + record := bySubcategory[subcategoryID] + if record == nil { + record = core.NewRecord(collection) + record.Set("user", userID) + record.Set("subcategory", subcategoryID) + record.Set("visible", true) + } + + record.Set("priority", index+1) + if err := txApp.SaveNoValidate(record); err != nil { + return err + } + } + + return nil + }) +} diff --git a/db/util/category_test.go b/db/util/category_test.go new file mode 100644 index 00000000..a2245f8d --- /dev/null +++ b/db/util/category_test.go @@ -0,0 +1,1301 @@ +package util + +import ( + "reflect" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + pbtests "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestNormalizeCategoryName(t *testing.T) { + t.Run("WhitespaceAndSeparators", func(t *testing.T) { + for _, value := range []string{"E-Bike", "E Bike", "e-bike"} { + if got := NormalizeCategoryName(value); got != "e bike" { + t.Fatalf("NormalizeCategoryName(%q) = %q, want %q", value, got, "e bike") + } + } + + if got := NormalizeCategoryName(" Mountain Biking "); got != "mountain biking" { + t.Fatalf("NormalizeCategoryName(%q) = %q, want %q", " Mountain Biking ", got, "mountain biking") + } + }) + + t.Run("AccentFolding", func(t *testing.T) { + for _, value := range []string{"Canoë", "Canoe"} { + if got := NormalizeCategoryName(value); got != "canoe" { + t.Fatalf("NormalizeCategoryName(%q) = %q, want %q", value, got, "canoe") + } + } + }) + + t.Run("SeparatorsNotRemoved", func(t *testing.T) { + if got := NormalizeCategoryName("EBike"); got != "ebike" { + t.Fatalf("NormalizeCategoryName(%q) = %q, want %q", "EBike", got, "ebike") + } + + if NormalizeCategoryName("EBike") == NormalizeCategoryName("E Bike") { + t.Fatalf("expected %q and %q to normalize differently", "EBike", "E Bike") + } + }) +} + +func TestParseCategoryTranslations(t *testing.T) { + tests := []struct { + name string + input types.JSONRaw + wantErr bool + want map[string]CategoryTranslation + }{ + { + name: "valid base locale", + input: types.JSONRaw(`{"de": {"name": "Wandern", "short_name": "WAND"}}`), + want: map[string]CategoryTranslation{"de": {Name: "Wandern", ShortName: "WAND"}}, + }, + { + name: "null is nil without error", + input: types.JSONRaw(`null`), + want: nil, + }, + { + name: "region locale rejected", + input: types.JSONRaw(`{"pt-BR": {"name": "Caminhada"}}`), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseCategoryTranslations(tt.input) + if tt.wantErr { + if err == nil { + t.Fatal("ParseCategoryTranslations() error = nil, want error") + } + return + } + if err != nil { + t.Fatalf("ParseCategoryTranslations() error = %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("ParseCategoryTranslations() = %#v, want %#v", got, tt.want) + } + }) + } +} + +func TestDefaultCategoryTranslationsAreSupportedAndNonEmpty(t *testing.T) { + for _, category := range DefaultCategoryNames() { + translations, ok := defaultCategoryTranslations[category] + if !ok { + t.Fatalf("defaultCategoryTranslations missing category %q", category) + } + + for locale, name := range translations { + if _, ok := supportedCategoryLocales[locale]; !ok { + t.Fatalf("defaultCategoryTranslations[%q] contains unsupported locale %q", category, locale) + } + + if name == "" { + t.Fatalf("defaultCategoryTranslations[%q][%q] is empty", category, locale) + } + } + } +} + +func TestDefaultCategoryIconsAreNonEmpty(t *testing.T) { + for _, category := range DefaultCategoryNames() { + icon, ok := defaultCategoryIcons[category] + if !ok { + t.Fatalf("defaultCategoryIcons missing category %q", category) + } + + if strings.TrimSpace(icon) == "" { + t.Fatalf("defaultCategoryIcons[%q] is empty", category) + } + } +} + +func TestSeedDefaultCategoriesAddsMissingDefaults(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + running := createTestCategory(t, app, "Running") + + if err := SeedDefaultCategories(app); err != nil { + t.Fatalf("SeedDefaultCategories() error = %v", err) + } + + categories, err := app.FindAllRecords("categories") + if err != nil { + t.Fatal(err) + } + + byName := map[string]*core.Record{} + for _, category := range categories { + byName[category.GetString("name")] = category + } + + for _, name := range DefaultCategoryNames() { + if byName[name] == nil { + t.Fatalf("default category %q was not seeded", name) + } + } + if byName["Running"].Id != running.Id { + t.Fatalf("existing Running category was replaced: got %q, want %q", byName["Running"].Id, running.Id) + } + if len(categories) != len(DefaultCategoryNames()) { + t.Fatalf("category count = %d, want %d", len(categories), len(DefaultCategoryNames())) + } +} + +func TestMergeDefaultCategoryTranslations(t *testing.T) { + merged, changed := mergeDefaultCategoryTranslations( + map[string]string{ + "de": "Wandern", + "en": "Hiking", + "fr": "", + }, + map[string]CategoryTranslation{ + "de": { + Name: "Custom Wandern", + ShortName: "WAND", + }, + }, + ) + + if !changed { + t.Fatal("mergeDefaultCategoryTranslations changed = false, want true") + } + + want := map[string]CategoryTranslation{ + "de": { + Name: "Custom Wandern", + ShortName: "WAND", + }, + "en": { + Name: "Hiking", + }, + } + if !reflect.DeepEqual(merged, want) { + t.Fatalf("mergeDefaultCategoryTranslations() = %#v, want %#v", merged, want) + } +} + +func TestMergeDefaultSubcategoryTranslations(t *testing.T) { + merged, changed := mergeDefaultSubcategoryTranslations( + map[string]CategoryTranslation{ + "de": { + Name: "Rennrad", + ShortName: "ROAD", + }, + "en": { + Name: "Road", + ShortName: "ROAD", + }, + }, + map[string]CategoryTranslation{ + "de": { + Name: "Custom Road", + ShortName: "ROADX", + }, + "en": { + Name: "Road", + }, + }, + ) + + if !changed { + t.Fatal("mergeDefaultSubcategoryTranslations changed = false, want true") + } + + want := map[string]CategoryTranslation{ + "de": { + Name: "Custom Road", + ShortName: "ROADX", + }, + "en": { + Name: "Road", + ShortName: "ROAD", + }, + } + if !reflect.DeepEqual(merged, want) { + t.Fatalf("mergeDefaultSubcategoryTranslations() = %#v, want %#v", merged, want) + } +} + +func TestPrepopulateDefaultCategoryIcons(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + hiking := createTestCategory(t, app, "Hiking") + walking := createTestCategory(t, app, "Walking") + canoeing := createTestCategory(t, app, "Canoeing") + walking.Set("icon", "custom-icon") + if err := app.Save(walking); err != nil { + t.Fatal(err) + } + canoeing.Set("icon", "ship") + if err := app.Save(canoeing); err != nil { + t.Fatal(err) + } + + if err := PrepopulateDefaultCategoryIcons(app); err != nil { + t.Fatalf("PrepopulateDefaultCategoryIcons() error = %v", err) + } + + hiking, err := app.FindRecordById("categories", hiking.Id) + if err != nil { + t.Fatal(err) + } + walking, err = app.FindRecordById("categories", walking.Id) + if err != nil { + t.Fatal(err) + } + canoeing, err = app.FindRecordById("categories", canoeing.Id) + if err != nil { + t.Fatal(err) + } + + if got := hiking.GetString("icon"); got != defaultCategoryIcons["Hiking"] { + t.Fatalf("hiking icon = %q, want %q", got, defaultCategoryIcons["Hiking"]) + } + if got := walking.GetString("icon"); got != "custom-icon" { + t.Fatalf("walking icon = %q, want custom icon", got) + } + if got := canoeing.GetString("icon"); got != "ship" { + t.Fatalf("canoeing icon = %q, want existing icon", got) + } +} + +func TestDefaultSubcategories(t *testing.T) { + seen := map[string]struct{}{} + wantDefaults := map[string]struct{}{ + "Hiking/Winter": {}, + "Hiking/Alpine": {}, + "Hiking/Long-distance": {}, + "Hiking/Snowshoeing": {}, + "Hiking/Family": {}, + "Hiking/Pilgrimage": {}, + "Biking/Touring": {}, + "Running/Trail": {}, + "Running/Road": {}, + "Skiing/Cross-country": {}, + "Skiing/Skating": {}, + "Skiing/Backcountry": {}, + } + wantBadgeIcons := map[string]string{ + "Biking/MTB": "mountain", + "Biking/Road": "grip-lines-vertical", + "Biking/E-Bike": "bolt", + "Hiking/Winter": "snowflake", + "Hiking/Alpine": "mountain", + "Hiking/Snowshoeing": "snowflake", + "Hiking/Family": "child", + "Hiking/Pilgrimage": "cross", + "Running/Road": "grip-lines-vertical", + } + for _, subcategory := range defaultSubcategories { + if subcategory.parentCategory == "" { + t.Fatalf("default subcategory %q parent is empty", subcategory.name) + } + if subcategory.name == "" { + t.Fatal("default subcategory name is empty") + } + if subcategory.shortName == "" { + t.Fatalf("default subcategory %q shortName is empty", subcategory.name) + } + + normalizedName := NormalizeCategoryName(subcategory.name) + key := subcategory.parentCategory + "/" + normalizedName + if _, ok := seen[key]; ok { + t.Fatalf("default subcategory %q duplicates normalized key %q", subcategory.name, key) + } + seen[key] = struct{}{} + + if _, ok := wantDefaults[subcategory.parentCategory+"/"+subcategory.name]; ok { + delete(wantDefaults, subcategory.parentCategory+"/"+subcategory.name) + } + if wantBadgeIcon, ok := wantBadgeIcons[subcategory.parentCategory+"/"+subcategory.name]; ok { + if subcategory.badgeIcon != wantBadgeIcon { + t.Fatalf("%s/%s badgeIcon = %q, want %q", subcategory.parentCategory, subcategory.name, subcategory.badgeIcon, wantBadgeIcon) + } + } + if subcategory.parentCategory == "Hiking" && subcategory.name == "Winter" { + if subcategory.translations["de"].Name != "Winterwandern" { + t.Fatalf("Winter de translation = %q, want Winterwandern", subcategory.translations["de"].Name) + } + } + if subcategory.parentCategory == "Hiking" && subcategory.name == "Pilgrimage" { + if subcategory.translations["de"].Name != "Pilgern" { + t.Fatalf("Pilgrimage de translation = %q, want Pilgern", subcategory.translations["de"].Name) + } + } + if subcategory.parentCategory == "Biking" && subcategory.name == "Touring" { + if subcategory.translations["de"].Name != "Tourenrad" { + t.Fatalf("Touring de translation = %q, want Tourenrad", subcategory.translations["de"].Name) + } + } + } + + if len(wantDefaults) > 0 { + t.Fatalf("default subcategories missing entries: %#v", wantDefaults) + } +} + +func TestValidateSubcategoryRecord(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + biking := createTestCategory(t, app, "Biking") + hiking := createTestCategory(t, app, "Hiking") + createTestSubcategory(t, app, biking.Id, "MTB", "MTB") + + subcategoriesCollection := mustFindTestCollection(t, app, "subcategories") + + tests := []struct { + name string + category string + record string + wantError bool + }{ + { + name: "requires parent category", + category: "", + record: "Road", + wantError: true, + }, + { + name: "rejects normalized collision in same parent", + category: biking.Id, + record: "mtb", + wantError: true, + }, + { + name: "allows same normalized name in different parent", + category: hiking.Id, + record: "mtb", + }, + { + name: "allows unique name in same parent", + category: biking.Id, + record: "Road", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + record := core.NewRecord(subcategoriesCollection) + record.Set("category", tt.category) + record.Set("name", tt.record) + + err := ValidateSubcategoryRecord(app, record) + if (err != nil) != tt.wantError { + t.Fatalf("ValidateSubcategoryRecord() error = %v, wantError %v", err, tt.wantError) + } + }) + } +} + +func TestValidateTrailSubcategoryRecord(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + biking := createTestCategory(t, app, "Biking") + hiking := createTestCategory(t, app, "Hiking") + mtb := createTestSubcategory(t, app, biking.Id, "MTB", "MTB") + + trailsCollection := mustFindTestCollection(t, app, "trails") + missingSubcategoryID := "missing1234567" + + tests := []struct { + name string + category string + subcategory string + subcategoryExplicit bool + wantSubcategory string + wantError bool + }{ + { + name: "allows empty subcategory", + }, + { + name: "requires category when subcategory is explicit", + subcategory: mtb.Id, + subcategoryExplicit: true, + wantSubcategory: mtb.Id, + wantError: true, + }, + { + name: "clears subcategory without category when subcategory is implicit", + subcategory: mtb.Id, + wantSubcategory: "", + }, + { + name: "allows matching parent category", + category: biking.Id, + subcategory: mtb.Id, + subcategoryExplicit: true, + wantSubcategory: mtb.Id, + }, + { + name: "rejects mismatched explicit subcategory", + category: hiking.Id, + subcategory: mtb.Id, + subcategoryExplicit: true, + wantSubcategory: mtb.Id, + wantError: true, + }, + { + name: "clears mismatched implicit subcategory", + category: hiking.Id, + subcategory: mtb.Id, + wantSubcategory: "", + }, + { + name: "rejects missing explicit subcategory", + category: biking.Id, + subcategory: missingSubcategoryID, + subcategoryExplicit: true, + wantSubcategory: missingSubcategoryID, + wantError: true, + }, + { + name: "clears missing implicit subcategory", + category: biking.Id, + subcategory: missingSubcategoryID, + wantSubcategory: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + record := core.NewRecord(trailsCollection) + record.Set("category", tt.category) + record.Set("subcategory", tt.subcategory) + + err := ValidateTrailSubcategoryRecord(app, record, tt.subcategoryExplicit) + if (err != nil) != tt.wantError { + t.Fatalf("ValidateTrailSubcategoryRecord() error = %v, wantError %v", err, tt.wantError) + } + + if got := record.GetString("subcategory"); got != tt.wantSubcategory { + t.Fatalf("record subcategory = %q, want %q", got, tt.wantSubcategory) + } + }) + } +} + +func TestSeedDefaultSubcategoriesSkipsNormalizedExisting(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + biking := createTestCategory(t, app, "Biking") + mtb := createTestSubcategory(t, app, biking.Id, "mtb", "CUSTOM") + + if err := SeedDefaultSubcategories(app); err != nil { + t.Fatalf("SeedDefaultSubcategories() error = %v", err) + } + + records, err := app.FindRecordsByFilter( + "subcategories", + "category = {:category}", + "", + 0, + 0, + map[string]any{"category": biking.Id}, + ) + if err != nil { + t.Fatal(err) + } + + var normalizedMTBCount int + for _, record := range records { + if NormalizeCategoryName(record.GetString("name")) == NormalizeCategoryName("MTB") { + normalizedMTBCount++ + } + } + + if normalizedMTBCount != 1 { + t.Fatalf("normalized MTB subcategory count = %d, want 1", normalizedMTBCount) + } + + updatedMTB, err := app.FindRecordById("subcategories", mtb.Id) + if err != nil { + t.Fatal(err) + } + if got := updatedMTB.GetString("name"); got != "mtb" { + t.Fatalf("existing MTB subcategory name = %q, want mtb", got) + } + if got := updatedMTB.GetString("short_name"); got != "CUSTOM" { + t.Fatalf("existing MTB short_name = %q, want CUSTOM", got) + } + + defaultBikingSubcategories := 0 + for _, subcategory := range defaultSubcategories { + if subcategory.parentCategory == "Biking" { + defaultBikingSubcategories++ + } + } + + if len(records) != defaultBikingSubcategories { + t.Fatalf("seeded Biking subcategory count = %d, want %d", len(records), defaultBikingSubcategories) + } +} + +func TestResolveCategoryAndSubcategoryByNormalizedNames(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + biking := createTestCategory(t, app, "Biking") + mtb := createTestSubcategory(t, app, biking.Id, "E-Bike", "EBIKE") + hiking := createTestCategory(t, app, "Hiking") + createTestSubcategory(t, app, hiking.Id, "E-Bike", "EBIKE") + + category, subcategory, err := ResolveCategoryAndSubcategoryByNormalizedNames(app, " biking ", "e bike") + if err != nil { + t.Fatalf("ResolveCategoryAndSubcategoryByNormalizedNames() error = %v", err) + } + if category == nil || category.Id != biking.Id { + t.Fatalf("category = %#v, want Biking", category) + } + if subcategory == nil || subcategory.Id != mtb.Id { + t.Fatalf("subcategory = %#v, want Biking/E-Bike", subcategory) + } + + category, subcategory, err = ResolveCategoryAndSubcategoryByNormalizedNames(app, "Biking", "Bikepacking") + if err != nil { + t.Fatalf("ResolveCategoryAndSubcategoryByNormalizedNames() unknown subcategory error = %v", err) + } + if category == nil || category.Id != biking.Id { + t.Fatalf("unknown subcategory category = %#v, want Biking", category) + } + if subcategory != nil { + t.Fatalf("unknown subcategory = %#v, want nil", subcategory) + } + + category, subcategory, err = ResolveCategoryAndSubcategoryByNormalizedNames(app, "Skydiving", "Wingsuit") + if err != nil { + t.Fatalf("ResolveCategoryAndSubcategoryByNormalizedNames() unknown category error = %v", err) + } + if category != nil || subcategory != nil { + t.Fatalf("unknown category resolved category=%#v subcategory=%#v, want nil/nil", category, subcategory) + } +} + +func TestBackfillRemoteTrailCategoryBackfillsTargetCategoryOnly(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + biking := createTestCategory(t, app, "Biking") + gravel := createTestSubcategory(t, app, biking.Id, "Gravel", "GRVL") + matchingTrail := createTestTrail(t, app, "", "", "biking", "gravel") + otherTrail := createTestTrail(t, app, "", "", "Hiking", "gravel") + + if err := BackfillRemoteTrailCategory(app, biking); err != nil { + t.Fatalf("BackfillRemoteTrailCategory() error = %v", err) + } + + updatedMatchingTrail, err := app.FindRecordById("trails", matchingTrail.Id) + if err != nil { + t.Fatal(err) + } + if got := updatedMatchingTrail.GetString("category"); got != biking.Id { + t.Fatalf("matching trail category = %q, want %q", got, biking.Id) + } + if got := updatedMatchingTrail.GetString("subcategory"); got != gravel.Id { + t.Fatalf("matching trail subcategory = %q, want %q", got, gravel.Id) + } + + updatedOtherTrail, err := app.FindRecordById("trails", otherTrail.Id) + if err != nil { + t.Fatal(err) + } + if got := updatedOtherTrail.GetString("category"); got != "" { + t.Fatalf("other trail category = %q, want empty", got) + } + if got := updatedOtherTrail.GetString("subcategory"); got != "" { + t.Fatalf("other trail subcategory = %q, want empty", got) + } +} + +func TestBackfillRemoteTrailSubcategoryBackfillsWithinParentScope(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + biking := createTestCategory(t, app, "Biking") + hiking := createTestCategory(t, app, "Hiking") + bikingGravel := createTestSubcategory(t, app, biking.Id, "Gravel", "GRVL") + createTestSubcategory(t, app, hiking.Id, "Gravel", "GRVL") + + existingParentTrail := createTestTrail(t, app, biking.Id, "", "Biking", "gravel") + emptyParentTrail := createTestTrail(t, app, "", "", "biking", "gravel") + wrongParentTrail := createTestTrail(t, app, hiking.Id, "", "Hiking", "gravel") + + if err := BackfillRemoteTrailSubcategory(app, bikingGravel); err != nil { + t.Fatalf("BackfillRemoteTrailSubcategory() error = %v", err) + } + + updatedExistingParentTrail, err := app.FindRecordById("trails", existingParentTrail.Id) + if err != nil { + t.Fatal(err) + } + if got := updatedExistingParentTrail.GetString("category"); got != biking.Id { + t.Fatalf("existing parent trail category = %q, want %q", got, biking.Id) + } + if got := updatedExistingParentTrail.GetString("subcategory"); got != bikingGravel.Id { + t.Fatalf("existing parent trail subcategory = %q, want %q", got, bikingGravel.Id) + } + + updatedEmptyParentTrail, err := app.FindRecordById("trails", emptyParentTrail.Id) + if err != nil { + t.Fatal(err) + } + if got := updatedEmptyParentTrail.GetString("category"); got != biking.Id { + t.Fatalf("empty parent trail category = %q, want %q", got, biking.Id) + } + if got := updatedEmptyParentTrail.GetString("subcategory"); got != bikingGravel.Id { + t.Fatalf("empty parent trail subcategory = %q, want %q", got, bikingGravel.Id) + } + + updatedWrongParentTrail, err := app.FindRecordById("trails", wrongParentTrail.Id) + if err != nil { + t.Fatal(err) + } + if got := updatedWrongParentTrail.GetString("category"); got != hiking.Id { + t.Fatalf("wrong parent trail category = %q, want %q", got, hiking.Id) + } + if got := updatedWrongParentTrail.GetString("subcategory"); got != "" { + t.Fatalf("wrong parent trail subcategory = %q, want empty", got) + } +} + +func TestSeedDefaultSubcategoriesRenamesLegacyDefault(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + hiking := createTestCategory(t, app, "Hiking") + winter := createTestSubcategory(t, app, hiking.Id, "Winter Hiking", "WINT") + + if err := SeedDefaultSubcategories(app); err != nil { + t.Fatalf("SeedDefaultSubcategories() error = %v", err) + } + if err := SeedDefaultSubcategories(app); err != nil { + t.Fatalf("second SeedDefaultSubcategories() error = %v", err) + } + + records, err := app.FindRecordsByFilter( + "subcategories", + "category = {:category} && short_name = 'WINT'", + "", + 0, + 0, + map[string]any{"category": hiking.Id}, + ) + if err != nil { + t.Fatal(err) + } + + if len(records) != 1 { + t.Fatalf("Winter subcategory count = %d, want 1", len(records)) + } + if got := records[0].GetString("name"); got != "Winter" { + t.Fatalf("Winter subcategory name = %q, want Winter", got) + } + if records[0].Id != winter.Id { + t.Fatalf("Winter subcategory id = %q, want %q", records[0].Id, winter.Id) + } + if got := records[0].GetString("badge_icon"); got != "snowflake" { + t.Fatalf("Winter badge_icon = %q, want snowflake", got) + } +} + +func TestSeedDefaultSubcategoriesDoesNotOverwriteBadgeIcon(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + hiking := createTestCategory(t, app, "Hiking") + winter := createTestSubcategory(t, app, hiking.Id, "Winter", "WINT") + winter.Set("badge_icon", "custom") + if err := app.Save(winter); err != nil { + t.Fatal(err) + } + + if err := SeedDefaultSubcategories(app); err != nil { + t.Fatalf("SeedDefaultSubcategories() error = %v", err) + } + + updatedWinter, err := app.FindRecordById("subcategories", winter.Id) + if err != nil { + t.Fatal(err) + } + if got := updatedWinter.GetString("badge_icon"); got != "custom" { + t.Fatalf("Winter badge_icon = %q, want custom", got) + } +} + +func TestSeedDefaultSubcategoriesDoesNotRenameAliasWhenCanonicalExists(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + hiking := createTestCategory(t, app, "Hiking") + winter := createTestSubcategory(t, app, hiking.Id, "Winter", "WINT") + legacy := createTestSubcategory(t, app, hiking.Id, "Winter Hiking", "WHIKE") + + if err := SeedDefaultSubcategories(app); err != nil { + t.Fatalf("SeedDefaultSubcategories() error = %v", err) + } + + updatedWinter, err := app.FindRecordById("subcategories", winter.Id) + if err != nil { + t.Fatal(err) + } + if got := updatedWinter.GetString("name"); got != "Winter" { + t.Fatalf("canonical subcategory name = %q, want Winter", got) + } + + updatedLegacy, err := app.FindRecordById("subcategories", legacy.Id) + if err != nil { + t.Fatal(err) + } + if got := updatedLegacy.GetString("name"); got != "Winter Hiking" { + t.Fatalf("legacy subcategory name = %q, want Winter Hiking", got) + } + + records, err := app.FindRecordsByFilter( + "subcategories", + "category = {:category} && name = 'Winter'", + "", + 0, + 0, + map[string]any{"category": hiking.Id}, + ) + if err != nil { + t.Fatal(err) + } + if len(records) != 1 { + t.Fatalf("canonical Winter subcategory count = %d, want 1", len(records)) + } +} + +func TestValidateUserCategoryPreferenceRequest(t *testing.T) { + tests := []struct { + name string + priorityExplicit bool + wantError bool + }{ + { + name: "allows requests without priority", + }, + { + name: "rejects explicit priority", + priorityExplicit: true, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateUserCategoryPreferenceRequest(tt.priorityExplicit) + if (err != nil) != tt.wantError { + t.Fatalf("ValidateUserCategoryPreferenceRequest() error = %v, wantError %v", err, tt.wantError) + } + }) + } +} + +func TestValidateUserSubcategoryPreferenceRequest(t *testing.T) { + tests := []struct { + name string + priorityExplicit bool + wantError bool + }{ + { + name: "allows requests without priority", + }, + { + name: "rejects explicit priority", + priorityExplicit: true, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateUserSubcategoryPreferenceRequest(tt.priorityExplicit) + if (err != nil) != tt.wantError { + t.Fatalf("ValidateUserSubcategoryPreferenceRequest() error = %v, wantError %v", err, tt.wantError) + } + }) + } +} + +func TestReorderUserCategoryPreferencesValidation(t *testing.T) { + tests := []struct { + name string + userID string + categoryIDs func(categories []*core.Record) []string + wantError bool + }{ + { + name: "requires authenticated user", + userID: "", + categoryIDs: func(categories []*core.Record) []string { + return []string{categories[0].Id, categories[1].Id} + }, + wantError: true, + }, + { + name: "rejects incomplete list", + userID: "user12345678901", + categoryIDs: func(categories []*core.Record) []string { + return []string{categories[0].Id} + }, + wantError: true, + }, + { + name: "rejects duplicate category", + userID: "user12345678901", + categoryIDs: func(categories []*core.Record) []string { + return []string{categories[0].Id, categories[0].Id} + }, + wantError: true, + }, + { + name: "rejects unknown category", + userID: "user12345678901", + categoryIDs: func(categories []*core.Record) []string { + return []string{categories[0].Id, "unknown12345678"} + }, + wantError: true, + }, + { + name: "allows complete category list", + userID: "user12345678901", + categoryIDs: func(categories []*core.Record) []string { + return []string{categories[1].Id, categories[0].Id} + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + categories := []*core.Record{ + createTestCategory(t, app, "Biking"), + createTestCategory(t, app, "Hiking"), + } + + err := ReorderUserCategoryPreferences(app, tt.userID, tt.categoryIDs(categories)) + if (err != nil) != tt.wantError { + t.Fatalf("ReorderUserCategoryPreferences() error = %v, wantError %v", err, tt.wantError) + } + }) + } +} + +func TestReorderUserSubcategoryPreferencesValidation(t *testing.T) { + tests := []struct { + name string + userID string + categoryID func(categories []*core.Record) string + subcategoryIDs func(subcategories []*core.Record) []string + wantError bool + }{ + { + name: "requires authenticated user", + categoryID: func(categories []*core.Record) string { + return categories[0].Id + }, + subcategoryIDs: func(subcategories []*core.Record) []string { + return []string{subcategories[0].Id, subcategories[1].Id} + }, + wantError: true, + }, + { + name: "requires category", + userID: "user12345678901", + categoryID: func(categories []*core.Record) string { + return "" + }, + subcategoryIDs: func(subcategories []*core.Record) []string { + return []string{subcategories[0].Id, subcategories[1].Id} + }, + wantError: true, + }, + { + name: "rejects incomplete list", + userID: "user12345678901", + categoryID: func(categories []*core.Record) string { + return categories[0].Id + }, + subcategoryIDs: func(subcategories []*core.Record) []string { + return []string{subcategories[0].Id} + }, + wantError: true, + }, + { + name: "rejects duplicate subcategory", + userID: "user12345678901", + categoryID: func(categories []*core.Record) string { + return categories[0].Id + }, + subcategoryIDs: func(subcategories []*core.Record) []string { + return []string{subcategories[0].Id, subcategories[0].Id} + }, + wantError: true, + }, + { + name: "rejects subcategory from another category", + userID: "user12345678901", + categoryID: func(categories []*core.Record) string { + return categories[0].Id + }, + subcategoryIDs: func(subcategories []*core.Record) []string { + return []string{subcategories[0].Id, subcategories[2].Id} + }, + wantError: true, + }, + { + name: "allows complete subcategory list", + userID: "user12345678901", + categoryID: func(categories []*core.Record) string { + return categories[0].Id + }, + subcategoryIDs: func(subcategories []*core.Record) []string { + return []string{subcategories[1].Id, subcategories[0].Id} + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + categories := []*core.Record{ + createTestCategory(t, app, "Hiking"), + createTestCategory(t, app, "Biking"), + } + subcategories := []*core.Record{ + createTestSubcategory(t, app, categories[0].Id, "Mountain Hiking", "", ""), + createTestSubcategory(t, app, categories[0].Id, "Family Hiking", "", ""), + createTestSubcategory(t, app, categories[1].Id, "E-Bike", "", ""), + } + + err := ReorderUserSubcategoryPreferences(app, tt.userID, tt.categoryID(categories), tt.subcategoryIDs(subcategories)) + if (err != nil) != tt.wantError { + t.Fatalf("ReorderUserSubcategoryPreferences() error = %v, wantError %v", err, tt.wantError) + } + }) + } +} + +func TestEnsureUserCategoryPriorityDefaultsToHiking(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + createTestCategory(t, app, "Canoeing") + hiking := createTestCategory(t, app, "Hiking") + userID := "user12345678901" + + if err := EnsureUserCategoryPriority(app, userID, ""); err != nil { + t.Fatalf("EnsureUserCategoryPriority() error = %v", err) + } + + records, err := app.FindRecordsByFilter( + "user_category_preferences", + "user = {:user}", + "", + 0, + 0, + map[string]any{"user": userID}, + ) + if err != nil { + t.Fatal(err) + } + if len(records) != 1 { + t.Fatalf("preference count = %d, want 1", len(records)) + } + + record := records[0] + if got := record.GetString("category"); got != hiking.Id { + t.Fatalf("category = %q, want Hiking id %q", got, hiking.Id) + } + if got := record.GetInt("priority"); got != 1 { + t.Fatalf("priority = %d, want 1", got) + } + if !record.GetBool("visible") { + t.Fatal("visible = false, want true") + } +} + +func TestReorderUserCategoryPreferencesUpsertsAndRenumbers(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + biking := createTestCategory(t, app, "Biking") + hiking := createTestCategory(t, app, "Hiking") + walking := createTestCategory(t, app, "Walking") + userID := "user12345678901" + + existing := core.NewRecord(mustFindTestCollection(t, app, "user_category_preferences")) + existing.Set("user", userID) + existing.Set("category", biking.Id) + existing.Set("visible", false) + existing.Set("priority", 99) + if err := app.SaveNoValidate(existing); err != nil { + t.Fatal(err) + } + + if err := ReorderUserCategoryPreferences(app, userID, []string{walking.Id, biking.Id, hiking.Id}); err != nil { + t.Fatalf("ReorderUserCategoryPreferences() error = %v", err) + } + + records, err := app.FindRecordsByFilter( + "user_category_preferences", + "user = {:user}", + "priority", + 0, + 0, + map[string]any{"user": userID}, + ) + if err != nil { + t.Fatal(err) + } + + if len(records) != 3 { + t.Fatalf("preference count = %d, want 3", len(records)) + } + + wantPriorityByCategory := map[string]int{ + walking.Id: 1, + biking.Id: 2, + hiking.Id: 3, + } + for _, record := range records { + categoryID := record.GetString("category") + if got := record.GetInt("priority"); got != wantPriorityByCategory[categoryID] { + t.Fatalf("priority for category %q = %d, want %d", categoryID, got, wantPriorityByCategory[categoryID]) + } + if categoryID == biking.Id { + if record.Id != existing.Id { + t.Fatalf("biking preference id = %q, want existing id %q", record.Id, existing.Id) + } + if record.GetBool("visible") { + t.Fatal("existing preference visible = true, want false") + } + } else { + if !record.GetBool("visible") { + t.Fatalf("new preference for category %q visible = false, want true", categoryID) + } + } + } +} + +func TestReorderUserSubcategoryPreferencesUpsertsAndRenumbers(t *testing.T) { + app := setupCategoryValidationTestApp(t) + defer app.Cleanup() + + hiking := createTestCategory(t, app, "Hiking") + biking := createTestCategory(t, app, "Biking") + mountain := createTestSubcategory(t, app, hiking.Id, "Mountain Hiking", "", "") + family := createTestSubcategory(t, app, hiking.Id, "Family Hiking", "", "") + ebike := createTestSubcategory(t, app, biking.Id, "E-Bike", "", "") + userID := "user12345678901" + + existing := core.NewRecord(mustFindTestCollection(t, app, "user_subcategory_preferences")) + existing.Set("user", userID) + existing.Set("subcategory", family.Id) + existing.Set("visible", false) + existing.Set("priority", 99) + if err := app.SaveNoValidate(existing); err != nil { + t.Fatal(err) + } + + otherCategoryPreference := core.NewRecord(mustFindTestCollection(t, app, "user_subcategory_preferences")) + otherCategoryPreference.Set("user", userID) + otherCategoryPreference.Set("subcategory", ebike.Id) + otherCategoryPreference.Set("visible", true) + otherCategoryPreference.Set("priority", 12) + if err := app.SaveNoValidate(otherCategoryPreference); err != nil { + t.Fatal(err) + } + + if err := ReorderUserSubcategoryPreferences(app, userID, hiking.Id, []string{family.Id, mountain.Id}); err != nil { + t.Fatalf("ReorderUserSubcategoryPreferences() error = %v", err) + } + + records, err := app.FindRecordsByFilter( + "user_subcategory_preferences", + "user = {:user}", + "priority", + 0, + 0, + map[string]any{"user": userID}, + ) + if err != nil { + t.Fatal(err) + } + + if len(records) != 3 { + t.Fatalf("preference count = %d, want 3", len(records)) + } + + wantPriorityBySubcategory := map[string]int{ + family.Id: 1, + mountain.Id: 2, + ebike.Id: 12, + } + for _, record := range records { + subcategoryID := record.GetString("subcategory") + if got := record.GetInt("priority"); got != wantPriorityBySubcategory[subcategoryID] { + t.Fatalf("priority for subcategory %q = %d, want %d", subcategoryID, got, wantPriorityBySubcategory[subcategoryID]) + } + if subcategoryID == family.Id { + if record.Id != existing.Id { + t.Fatalf("family preference id = %q, want existing id %q", record.Id, existing.Id) + } + if record.GetBool("visible") { + t.Fatal("existing preference visible = true, want false") + } + } + if subcategoryID == mountain.Id && !record.GetBool("visible") { + t.Fatal("new preference visible = false, want true") + } + if subcategoryID == ebike.Id && record.Id != otherCategoryPreference.Id { + t.Fatalf("other category preference id = %q, want existing id %q", record.Id, otherCategoryPreference.Id) + } + } +} + +func setupCategoryValidationTestApp(t *testing.T) *pbtests.TestApp { + t.Helper() + + app, err := pbtests.NewTestApp(t.TempDir()) + if err != nil { + t.Fatal(err) + } + + categories := core.NewBaseCollection("categories") + categories.Fields.Add( + &core.TextField{Name: "name", Required: true}, + &core.TextField{Name: "short_name"}, + &core.TextField{Name: "icon"}, + &core.JSONField{Name: "translations"}, + ) + if err := app.Save(categories); err != nil { + app.Cleanup() + t.Fatal(err) + } + + subcategories := core.NewBaseCollection("subcategories") + subcategories.Fields.Add( + &core.RelationField{Name: "category", CollectionId: categories.Id, MaxSelect: 1, Required: true}, + &core.TextField{Name: "name", Required: true}, + &core.TextField{Name: "short_name"}, + &core.TextField{Name: "icon"}, + &core.TextField{Name: "badge_icon"}, + &core.JSONField{Name: "translations"}, + ) + if err := app.Save(subcategories); err != nil { + app.Cleanup() + t.Fatal(err) + } + + trails := core.NewBaseCollection("trails") + trails.Fields.Add( + &core.RelationField{Name: "category", CollectionId: categories.Id, MaxSelect: 1}, + &core.RelationField{Name: "subcategory", CollectionId: subcategories.Id, MaxSelect: 1}, + &core.TextField{Name: "federated_category_name"}, + &core.TextField{Name: "federated_subcategory_name"}, + ) + if err := app.Save(trails); err != nil { + app.Cleanup() + t.Fatal(err) + } + + preferences := core.NewBaseCollection("user_category_preferences") + preferences.Fields.Add( + &core.RelationField{Name: "user", CollectionId: "_pb_users_auth_", MaxSelect: 1, Required: true}, + &core.RelationField{Name: "category", CollectionId: categories.Id, MaxSelect: 1, Required: true}, + &core.BoolField{Name: "visible"}, + &core.NumberField{Name: "priority", OnlyInt: true}, + ) + if err := app.Save(preferences); err != nil { + app.Cleanup() + t.Fatal(err) + } + + subcategoryPreferences := core.NewBaseCollection("user_subcategory_preferences") + subcategoryPreferences.Fields.Add( + &core.RelationField{Name: "user", CollectionId: "_pb_users_auth_", MaxSelect: 1, Required: true}, + &core.RelationField{Name: "subcategory", CollectionId: subcategories.Id, MaxSelect: 1, Required: true}, + &core.BoolField{Name: "visible"}, + &core.NumberField{Name: "priority", OnlyInt: true}, + ) + if err := app.Save(subcategoryPreferences); err != nil { + app.Cleanup() + t.Fatal(err) + } + + return app +} + +func mustFindTestCollection(t *testing.T, app core.App, name string) *core.Collection { + t.Helper() + + collection, err := app.FindCollectionByNameOrId(name) + if err != nil { + t.Fatal(err) + } + + return collection +} + +func createTestCategory(t *testing.T, app core.App, name string) *core.Record { + t.Helper() + + record := core.NewRecord(mustFindTestCollection(t, app, "categories")) + record.Set("name", name) + if err := app.Save(record); err != nil { + t.Fatal(err) + } + + return record +} + +func createTestSubcategory(t *testing.T, app core.App, categoryID string, name string, shortName string, badgeIcon ...string) *core.Record { + t.Helper() + + record := core.NewRecord(mustFindTestCollection(t, app, "subcategories")) + record.Set("category", categoryID) + record.Set("name", name) + record.Set("short_name", shortName) + if len(badgeIcon) > 0 { + record.Set("badge_icon", badgeIcon[0]) + } + if err := app.Save(record); err != nil { + t.Fatal(err) + } + + return record +} + +func createTestTrail(t *testing.T, app core.App, categoryID string, subcategoryID string, federatedCategoryName string, federatedSubcategoryName string) *core.Record { + t.Helper() + + record := core.NewRecord(mustFindTestCollection(t, app, "trails")) + record.Set("category", categoryID) + record.Set("subcategory", subcategoryID) + record.Set("federated_category_name", federatedCategoryName) + record.Set("federated_subcategory_name", federatedSubcategoryName) + if err := app.Save(record); err != nil { + t.Fatal(err) + } + + return record +} diff --git a/db/util/meilisearch.go b/db/util/meilisearch.go index 395c5cd8..bae8546b 100644 --- a/db/util/meilisearch.go +++ b/db/util/meilisearch.go @@ -33,10 +33,24 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares tags[i] = v.GetString("name") } + categoryID := r.GetString("category") + var categoryIDValue any + if categoryID != "" { + categoryIDValue = categoryID + } + + subcategoryID := r.GetString("subcategory") + var subcategoryIDValue any + if subcategoryID != "" { + subcategoryIDValue = subcategoryID + } + category := "" + categoryIcon := "" trailCategory := r.ExpandedOne("category") if trailCategory != nil { category = trailCategory.GetString("name") + categoryIcon = trailCategory.GetString("icon") } bounds := getStoredBounds(r) @@ -52,34 +66,40 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares } document := map[string]any{ - "id": r.Id, - "author": author.Id, - "author_name": author.GetString("preferred_username"), - "author_avatar": author.GetString("icon"), - "name": r.GetString("name"), - "description": r.GetString("description"), - "location": r.GetString("location"), - "distance": r.GetFloat("distance"), - "elevation_gain": r.GetFloat("elevation_gain"), - "elevation_loss": r.GetFloat("elevation_loss"), - "duration": r.GetFloat("duration"), - "difficulty": difficultyToNumber(r.GetString("difficulty")), - "category": category, - "completed": r.GetBool("completed"), - "date": r.GetDateTime("date").Time().Unix(), - "created": r.GetDateTime("created").Time().Unix(), - "public": r.GetBool("public"), - "thumbnail": thumbnail, - "gpx": r.GetString("gpx"), - "tags": tags, - "polyline": r.GetString("polyline"), - "domain": domain, - "iri": r.GetString("iri"), - "min_lat": bounds[0], - "max_lat": bounds[1], - "min_lon": bounds[2], - "max_lon": bounds[3], - "bounding_box_diagonal": diagonal, + "id": r.Id, + "author": author.Id, + "author_name": author.GetString("preferred_username"), + "author_avatar": author.GetString("icon"), + "name": r.GetString("name"), + "description": r.GetString("description"), + "location": r.GetString("location"), + "distance": r.GetFloat("distance"), + "elevation_gain": r.GetFloat("elevation_gain"), + "elevation_loss": r.GetFloat("elevation_loss"), + "duration": r.GetFloat("duration"), + "difficulty": difficultyToNumber(r.GetString("difficulty")), + "category": category, + "category_id": categoryIDValue, + "category_icon": categoryIcon, + "subcategory_id": subcategoryIDValue, + "is_federated": !author.GetBool("is_local"), + "federated_category_name": r.GetString("federated_category_name"), + "federated_subcategory_name": r.GetString("federated_subcategory_name"), + "completed": r.GetBool("completed"), + "date": r.GetDateTime("date").Time().Unix(), + "created": r.GetDateTime("created").Time().Unix(), + "public": r.GetBool("public"), + "thumbnail": thumbnail, + "gpx": r.GetString("gpx"), + "tags": tags, + "polyline": r.GetString("polyline"), + "domain": domain, + "iri": r.GetString("iri"), + "min_lat": bounds[0], + "max_lat": bounds[1], + "min_lon": bounds[2], + "max_lon": bounds[3], + "bounding_box_diagonal": diagonal, "_geo": map[string]float64{ "lat": r.GetFloat("lat"), "lng": r.GetFloat("lon"), diff --git a/db/util/subcategory.go b/db/util/subcategory.go new file mode 100644 index 00000000..7440077e --- /dev/null +++ b/db/util/subcategory.go @@ -0,0 +1,230 @@ +package util + +import ( + "fmt" + + "github.com/pocketbase/pocketbase/core" +) + +func ValidateSubcategoryRecord(app core.App, record *core.Record) error { + parentCategory := record.GetString("category") + if parentCategory == "" { + return fmt.Errorf("subcategory category is required") + } + + name := record.GetString("name") + normalizedName := NormalizeCategoryName(name) + + allSubcategories, err := app.FindAllRecords("subcategories") + if err != nil { + return err + } + + for _, existing := range allSubcategories { + if existing.Id == record.Id || existing.GetString("category") != parentCategory { + continue + } + + if NormalizeCategoryName(existing.GetString("name")) == normalizedName { + return fmt.Errorf("subcategory name %q collides with existing subcategory %q in the same category after normalization", name, existing.GetString("name")) + } + } + + if _, err := ParseCategoryTranslations(record.Get("translations")); err != nil { + return err + } + + return nil +} + +func ValidateTrailSubcategoryRecord(app core.App, record *core.Record, subcategoryExplicit bool) error { + subcategoryID := record.GetString("subcategory") + if subcategoryID == "" { + return nil + } + + categoryID := record.GetString("category") + if categoryID == "" { + if !subcategoryExplicit { + record.Set("subcategory", "") + return nil + } + + return fmt.Errorf("trail subcategory requires a category") + } + + subcategory, err := app.FindRecordById("subcategories", subcategoryID) + if err != nil { + if !subcategoryExplicit { + record.Set("subcategory", "") + return nil + } + + return fmt.Errorf("trail subcategory %q does not exist: %w", subcategoryID, err) + } + + parentCategory := subcategory.GetString("category") + if parentCategory != categoryID { + if !subcategoryExplicit { + record.Set("subcategory", "") + return nil + } + + return fmt.Errorf("trail subcategory %q belongs to category %q, not %q", subcategoryID, parentCategory, categoryID) + } + + return nil +} + +func FindSubcategoryByNormalizedName(app core.App, categoryID string, name string) (*core.Record, error) { + normalizedName := NormalizeCategoryName(name) + if categoryID == "" || normalizedName == "" { + return nil, nil + } + + subcategories, err := app.FindRecordsByFilter( + "subcategories", + "category = {:category}", + "", + 0, + 0, + map[string]any{"category": categoryID}, + ) + if err != nil { + return nil, err + } + + for _, subcategory := range subcategories { + if NormalizeCategoryName(subcategory.GetString("name")) == normalizedName { + return subcategory, nil + } + } + + return nil, nil +} + +func ResolveCategoryAndSubcategoryByNormalizedNames(app core.App, categoryName string, subcategoryName string) (*core.Record, *core.Record, error) { + category, err := FindCategoryByNormalizedName(app, categoryName) + if err != nil || category == nil { + return category, nil, err + } + + subcategory, err := FindSubcategoryByNormalizedName(app, category.Id, subcategoryName) + if err != nil { + return category, nil, err + } + + return category, subcategory, nil +} + +func BackfillRemoteTrailCategory(app core.App, category *core.Record) error { + if category == nil || category.Id == "" { + return nil + } + + subcategoriesByName, err := normalizedSubcategoriesByName(app, category.Id) + if err != nil { + return err + } + + trails, err := app.FindRecordsByFilter( + "trails", + "federated_category_name != '' && category = ''", + "", + 0, + 0, + nil, + ) + if err != nil { + return err + } + + normalizedCategoryName := NormalizeCategoryName(category.GetString("name")) + for _, trail := range trails { + if NormalizeCategoryName(trail.GetString("federated_category_name")) != normalizedCategoryName { + continue + } + + trail.Set("category", category.Id) + if subcategory, ok := subcategoriesByName[NormalizeCategoryName(trail.GetString("federated_subcategory_name"))]; ok { + trail.Set("subcategory", subcategory.Id) + } + + if err := app.Save(trail); err != nil { + return err + } + } + + return nil +} + +func BackfillRemoteTrailSubcategory(app core.App, subcategory *core.Record) error { + if subcategory == nil || subcategory.Id == "" { + return nil + } + + category, err := app.FindRecordById("categories", subcategory.GetString("category")) + if err != nil { + return err + } + + trails, err := app.FindRecordsByFilter( + "trails", + "federated_subcategory_name != '' && subcategory = '' && (category = {:category} || category = '')", + "", + 0, + 0, + map[string]any{"category": category.Id}, + ) + if err != nil { + return err + } + + normalizedCategoryName := NormalizeCategoryName(category.GetString("name")) + normalizedSubcategoryName := NormalizeCategoryName(subcategory.GetString("name")) + for _, trail := range trails { + categoryID := trail.GetString("category") + if categoryID == "" { + if NormalizeCategoryName(trail.GetString("federated_category_name")) != normalizedCategoryName { + continue + } + trail.Set("category", category.Id) + } + + if NormalizeCategoryName(trail.GetString("federated_subcategory_name")) != normalizedSubcategoryName { + continue + } + + trail.Set("subcategory", subcategory.Id) + if err := app.Save(trail); err != nil { + return err + } + } + + return nil +} + +func normalizedSubcategoriesByName(app core.App, categoryID string) (map[string]*core.Record, error) { + subcategories, err := app.FindRecordsByFilter( + "subcategories", + "category = {:category}", + "", + 0, + 0, + map[string]any{"category": categoryID}, + ) + if err != nil { + return nil, err + } + + byName := make(map[string]*core.Record, len(subcategories)) + for _, subcategory := range subcategories { + normalizedName := NormalizeCategoryName(subcategory.GetString("name")) + if normalizedName == "" { + continue + } + byName[normalizedName] = subcategory + } + + return byName, nil +} diff --git a/db/util/subcategory_defaults.go b/db/util/subcategory_defaults.go new file mode 100644 index 00000000..5395ce01 --- /dev/null +++ b/db/util/subcategory_defaults.go @@ -0,0 +1,284 @@ +package util + +import ( + "fmt" + + "github.com/pocketbase/pocketbase/core" +) + +type defaultSubcategorySeed struct { + parentCategory string + name string + shortName string + badgeIcon string + translations map[string]CategoryTranslation + aliases []string +} + +var defaultSubcategories = []defaultSubcategorySeed{ + {parentCategory: "Biking", name: "MTB", shortName: "MTB", badgeIcon: "mountain"}, + {parentCategory: "Biking", name: "Gravel", shortName: "GRVL"}, + { + parentCategory: "Biking", + name: "Touring", + shortName: "TOUR", + aliases: []string{"Touring Bike", "City Bike"}, + translations: subcategoryTranslations("Touring", "Tourenrad", "TOUR"), + }, + { + parentCategory: "Biking", + name: "Road", + shortName: "ROAD", + badgeIcon: "grip-lines-vertical", + translations: subcategoryTranslations("Road", "Rennrad", "ROAD"), + }, + {parentCategory: "Biking", name: "E-Bike", shortName: "EBIKE", badgeIcon: "bolt"}, + { + parentCategory: "Hiking", + name: "Winter", + shortName: "WINT", + badgeIcon: "snowflake", + aliases: []string{"Winter Hiking"}, + translations: map[string]CategoryTranslation{ + "de": {Name: "Winterwandern", ShortName: "WINT"}, + "en": {Name: "Winter", ShortName: "WINT"}, + }, + }, + { + parentCategory: "Hiking", + name: "Alpine", + shortName: "ALP", + badgeIcon: "mountain", + aliases: []string{"Alpine Hiking"}, + translations: subcategoryTranslations("Alpine", "Bergwandern", "ALP"), + }, + { + parentCategory: "Hiking", + name: "Long-distance", + shortName: "LONG", + aliases: []string{"Long-distance Hiking"}, + translations: subcategoryTranslations("Long-distance", "Fernwandern", "LONG"), + }, + { + parentCategory: "Hiking", + name: "Snowshoeing", + shortName: "SNOW", + badgeIcon: "snowflake", + translations: subcategoryTranslations("Snowshoeing", "Schneeschuhwandern", "SNOW"), + }, + { + parentCategory: "Hiking", + name: "Family", + shortName: "FAM", + badgeIcon: "child", + aliases: []string{"Family Hiking"}, + translations: subcategoryTranslations("Family", "Familienwandern", "FAM"), + }, + { + parentCategory: "Hiking", + name: "Pilgrimage", + shortName: "PILG", + badgeIcon: "cross", + translations: subcategoryTranslations("Pilgrimage", "Pilgern", "PILG"), + }, + { + parentCategory: "Running", + name: "Trail", + shortName: "TRAIL", + aliases: []string{"Trail Running"}, + translations: subcategoryTranslations("Trail", "Trailrunning", "TRAIL"), + }, + { + parentCategory: "Running", + name: "Road", + shortName: "ROAD", + badgeIcon: "grip-lines-vertical", + aliases: []string{"Road Running"}, + translations: subcategoryTranslations("Road", "Straßenlauf", "ROAD"), + }, + { + parentCategory: "Skiing", + name: "Cross-country", + shortName: "NORD", + aliases: []string{"Cross-country Skiing"}, + translations: subcategoryTranslations("Cross-country", "Langlauf", "NORD"), + }, + { + parentCategory: "Skiing", + name: "Skating", + shortName: "SKATE", + translations: subcategoryTranslations("Skating", "Skating", "SKATE"), + }, + { + parentCategory: "Skiing", + name: "Backcountry", + shortName: "BACK", + aliases: []string{"Backcountry Skiing"}, + translations: subcategoryTranslations("Backcountry", "Skitour", "BACK"), + }, +} + +func subcategoryTranslations(en string, de string, shortName string) map[string]CategoryTranslation { + return map[string]CategoryTranslation{ + "de": {Name: de, ShortName: shortName}, + "en": {Name: en, ShortName: shortName}, + } +} + +func SeedDefaultSubcategories(app core.App) error { + subcategoriesCollection, err := app.FindCollectionByNameOrId("subcategories") + if err != nil { + return err + } + + for _, seed := range defaultSubcategories { + categories, err := app.FindRecordsByFilter( + "categories", + "name = {:name}", + "", + 1, + 0, + map[string]any{"name": seed.parentCategory}, + ) + if err != nil { + return err + } + if len(categories) == 0 { + continue + } + category := categories[0] + + existing, err := app.FindRecordsByFilter( + "subcategories", + "category = {:category}", + "", + 0, + 0, + map[string]any{"category": category.Id}, + ) + if err != nil { + return err + } + if existingRecord := findDefaultSubcategory(existing, seed.name, seed.aliases); existingRecord != nil { + changed, err := applyDefaultSubcategorySeed(existingRecord, seed) + if err != nil { + return err + } + if changed { + if err := app.Save(existingRecord); err != nil { + return fmt.Errorf("failed to update seeded subcategory %q: %w", seed.name, err) + } + } + continue + } + + record := core.NewRecord(subcategoriesCollection) + record.Set("category", category.Id) + record.Set("name", seed.name) + record.Set("short_name", seed.shortName) + if seed.badgeIcon != "" { + record.Set("badge_icon", seed.badgeIcon) + } + if len(seed.translations) > 0 { + record.Set("translations", seed.translations) + } + if err := app.Save(record); err != nil { + return fmt.Errorf("failed to seed subcategory %q: %w", seed.name, err) + } + } + + return nil +} + +func applyDefaultSubcategorySeed(record *core.Record, seed defaultSubcategorySeed) (bool, error) { + changed := false + + if isDefaultSubcategoryAlias(record.GetString("name"), seed.aliases) { + record.Set("name", seed.name) + changed = true + } + + if record.GetString("short_name") == "" && seed.shortName != "" { + record.Set("short_name", seed.shortName) + changed = true + } + + if record.GetString("badge_icon") == "" && seed.badgeIcon != "" { + record.Set("badge_icon", seed.badgeIcon) + changed = true + } + + if len(seed.translations) > 0 { + currentTranslations, err := ParseCategoryTranslations(record.Get("translations")) + if err != nil { + return false, fmt.Errorf("invalid existing translations for subcategory %q: %w", record.GetString("name"), err) + } + + mergedTranslations, translationsChanged := mergeDefaultSubcategoryTranslations(seed.translations, currentTranslations) + if translationsChanged { + record.Set("translations", mergedTranslations) + changed = true + } + } + + return changed, nil +} + +func isDefaultSubcategoryAlias(name string, aliases []string) bool { + normalizedName := NormalizeCategoryName(name) + for _, alias := range aliases { + if normalizedName == NormalizeCategoryName(alias) { + return true + } + } + + return false +} + +func mergeDefaultSubcategoryTranslations(staticTranslations map[string]CategoryTranslation, currentTranslations map[string]CategoryTranslation) (map[string]CategoryTranslation, bool) { + if currentTranslations == nil { + currentTranslations = map[string]CategoryTranslation{} + } + + changed := false + for locale, staticTranslation := range staticTranslations { + translation := currentTranslations[locale] + localeChanged := false + if translation.Name == "" && staticTranslation.Name != "" { + translation.Name = staticTranslation.Name + localeChanged = true + } + if translation.ShortName == "" && staticTranslation.ShortName != "" { + translation.ShortName = staticTranslation.ShortName + localeChanged = true + } + if localeChanged { + changed = true + currentTranslations[locale] = translation + } + } + + return currentTranslations, changed +} + +func findDefaultSubcategory(records []*core.Record, name string, aliases []string) *core.Record { + normalizedName := NormalizeCategoryName(name) + for _, record := range records { + if NormalizeCategoryName(record.GetString("name")) == normalizedName { + return record + } + } + + normalizedAliases := map[string]struct{}{} + for _, alias := range aliases { + normalizedAliases[NormalizeCategoryName(alias)] = struct{}{} + } + + for _, record := range records { + if _, ok := normalizedAliases[NormalizeCategoryName(record.GetString("name"))]; ok { + return record + } + } + + return nil +} diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index aeed090d..1081c919 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -51,6 +51,10 @@ export default defineConfig({ label: 'Create/Edit a trail', link: '/use/create-a-trail/' }, + { + label: 'Categories', + link: '/use/categories/' + }, { label: 'Summit logs', link: '/use/summit-logs/' diff --git a/docs/package-lock.json b/docs/package-lock.json index f13f4b15..a2fe756f 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -9,19 +9,19 @@ "version": "0.19.2", "dependencies": { "@astrojs/check": "^0.9.9", - "@astrojs/node": "^10.1.2", - "@astrojs/starlight": "^0.39.2", + "@astrojs/node": "^11.0.0", + "@astrojs/starlight": "^0.41.1", "@astrojs/starlight-tailwind": "^5.0.0", - "@astrojs/svelte": "^8.1.2", + "@astrojs/svelte": "^9.0.0", "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.2.8", - "@tailwindcss/vite": "^4.3.0", - "astro": "^6.4.8", - "sharp": "^0.34.5", - "starlight-openapi": "^0.25.3", - "svelte": "^5.56.0", - "tailwindcss": "^4.1.10", - "typescript": "^5.9.3" + "@tailwindcss/vite": "^4.3.2", + "astro": "^7.0.4", + "sharp": "^0.35.2", + "starlight-openapi": "^0.26.0", + "svelte": "^5.56.4", + "tailwindcss": "^4.3.2", + "typescript": "^6.0.3" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -64,6 +64,182 @@ "integrity": "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==", "license": "MIT" }, + "node_modules/@astrojs/compiler-binding": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.3.0.tgz", + "integrity": "sha512-zlsOT5COD9hRwplJCgQhS21unxON5AKirf0vgt1ijXwuseYIaZdm2ZOpF8fsz+DY9EyXx+I/ukxtg7uoBep68A==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@astrojs/compiler-binding-darwin-arm64": "0.3.0", + "@astrojs/compiler-binding-darwin-x64": "0.3.0", + "@astrojs/compiler-binding-linux-arm64-gnu": "0.3.0", + "@astrojs/compiler-binding-linux-arm64-musl": "0.3.0", + "@astrojs/compiler-binding-linux-x64-gnu": "0.3.0", + "@astrojs/compiler-binding-linux-x64-musl": "0.3.0", + "@astrojs/compiler-binding-wasm32-wasi": "0.3.0", + "@astrojs/compiler-binding-win32-arm64-msvc": "0.3.0", + "@astrojs/compiler-binding-win32-x64-msvc": "0.3.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-arm64": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.3.0.tgz", + "integrity": "sha512-3n0uu+uJpnCq8b4JFi3uGDsIisAvHctxSmH+cIO9Gbei1H1Y1QXaYboXyiWJugUmprr3OEYP7+LdodzpVFzLMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-x64": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.3.0.tgz", + "integrity": "sha512-scxNGKjOBydMo1QR4LtK0FMgh7ubQomJDv953nz2msQFkPKke/0FpPv/cQM0T/kuZdReZQFU8Oz3iOrP/6WHEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.3.0.tgz", + "integrity": "sha512-NZrWLolVUANmrnl0zrFK/Sx5Sock1gEUT49ALfMTTCA5Ya2ec/BoJXMIg4KgE+wZcrdXJ8e+WyEhM7YLk/FJkA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.3.0.tgz", + "integrity": "sha512-PjwRmKgMFDsFhg82g0poXlIY8Qn3fMA3hXjaR0coJWJzTJsRH9ATU0j2ocigjtU1h3vL/yR7yLUxGj/lTCq73g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.3.0.tgz", + "integrity": "sha512-Dr69VJYlnSfyL8gzELW6S4mE41P7TDPn1IKjwMnjdZ7+dxgJI50oMLFSk1LVe26bHmWB3ktuh8fDVK1THI9e9A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-musl": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.3.0.tgz", + "integrity": "sha512-AEt+bRw8PfImCcyRH1lpXVB8CdmQ1K/wPo5u99iec4/U/XdNvQZ715YVuNzIJpbJXelgQeZ5H2+Ea7XwRyWY5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-wasm32-wasi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.3.0.tgz", + "integrity": "sha512-U80tA1j8V6LjhiTZzVCtG4E8hrNVVNXDGV5fCgJ94q8FU9CPH+XwdDDhLzBybfWhKfyItXmQiZNRPTiPCYTpVg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.3.0.tgz", + "integrity": "sha512-CpY1RII2r1XMpOUVD1VR/F2wtuRsiOCkFULS10Khyj8/DFZMtxVuUCAWGw+CW2Ka0h6eP3Xc1CA+glFlvXMPxA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.3.0.tgz", + "integrity": "sha512-qmFbs769oeeGrRebAnCW7aBk8m71vf85W/dX/jddfx5Z06/w0wf7TZCfJPOX1Fld2t+4N+iXzfGEJG+zJQ+bzg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-rs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.3.0.tgz", + "integrity": "sha512-J2qEVHtIDjEM9TxwmwuebOGmZNwhKu/dR7P7qBpnJKGmBBX0vdweQ/4cEXhj8fBbWVUB5V12xWChri3CgKNULQ==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler-binding": "0.3.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/@astrojs/internal-helpers": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.0.tgz", @@ -146,13 +322,26 @@ "vfile": "^6.0.3" } }, - "node_modules/@astrojs/mdx": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-5.0.6.tgz", - "integrity": "sha512-4dKe0ZMmqujofPNDHahzClkwinn9f8jHPcaXcgdGvPAlboD2mjzkUCofli2cBnxYAkdfhC6d50gBJ8i/cH8gHw==", + "node_modules/@astrojs/markdown-satteri": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.3.2.tgz", + "integrity": "sha512-feXuUPy41gVfeM7EHT1ciUim8ozGr+YHXab9uUBc1Hk8y60DQosO8ldL+AoPXnCAoGj1OChwHfvXmmJ6XVnY9A==", "license": "MIT", "dependencies": { - "@astrojs/markdown-remark": "7.1.2", + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "satteri": "^0.9.1" + } + }, + "node_modules/@astrojs/mdx": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-7.0.0.tgz", + "integrity": "sha512-LKwNA8nnLtEM0auoP6OfH/UnlKe1Ub59qZjbcYkZjPBGw6PkJewWkA/1qwLpECvV6gMDd6TR6eqV9p/VYZrcrQ==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/markdown-remark": "7.2.0", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", @@ -170,51 +359,19 @@ "node": ">=22.12.0" }, "peerDependencies": { - "astro": "^6.0.0" - } - }, - "node_modules/@astrojs/mdx/node_modules/@astrojs/internal-helpers": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.9.1.tgz", - "integrity": "sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ==", - "license": "MIT", - "dependencies": { - "picomatch": "^4.0.4" - } - }, - "node_modules/@astrojs/mdx/node_modules/@astrojs/markdown-remark": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.1.2.tgz", - "integrity": "sha512-caXZ4Dc2St2dW8luEg22GlP0gupLdztCTQE4EzZOxW1pqWXz9mbeJEuHUkgDYcKWW8tjIHkydYDhWLVoxJ327Q==", - "license": "MIT", - "dependencies": { - "@astrojs/internal-helpers": "0.9.1", - "@astrojs/prism": "4.0.2", - "github-slugger": "^2.0.0", - "hast-util-from-html": "^2.0.3", - "hast-util-to-text": "^4.0.2", - "js-yaml": "^4.1.1", - "mdast-util-definitions": "^6.0.0", - "rehype-raw": "^7.0.0", - "rehype-stringify": "^10.0.1", - "remark-gfm": "^4.0.1", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.2", - "remark-smartypants": "^3.0.2", - "retext-smartypants": "^6.2.0", - "shiki": "^4.0.0", - "smol-toml": "^1.6.0", - "unified": "^11.0.5", - "unist-util-remove-position": "^5.0.0", - "unist-util-visit": "^5.1.0", - "unist-util-visit-parents": "^6.0.2", - "vfile": "^6.0.3" + "@astrojs/markdown-satteri": "^0.3.1-alpha.0", + "astro": "^7.0.0-alpha.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-satteri": { + "optional": true + } } }, "node_modules/@astrojs/node": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-10.1.2.tgz", - "integrity": "sha512-6MtNb0iEdZw3m7Dva8V3qo1y9MVoVJKx9dQiNGvy4Ncg5yfaAwGa+9mrDJ3gWCWUtESZi8cvqVshy92sFufg1Q==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-11.0.0.tgz", + "integrity": "sha512-lmwSTG42fAs9/UxNAHt4noseEwXcDupwqMWVftw0j24BaHU9F1CEfi63p3pPlwGP9g0wLqitGQJP/P6x1vLdOQ==", "license": "MIT", "dependencies": { "@astrojs/internal-helpers": "0.10.0", @@ -222,7 +379,7 @@ "server-destroy": "^1.0.1" }, "peerDependencies": { - "astro": "^6.3.0" + "astro": "^7.0.0-alpha.2" } }, "node_modules/@astrojs/prism": { @@ -249,19 +406,19 @@ } }, "node_modules/@astrojs/starlight": { - "version": "0.39.2", - "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.39.2.tgz", - "integrity": "sha512-vlw+bwnjtf5buCTUtLU7JfV6D3knslxqnspr6LKs6hfRuFZiyr5hT44F7GyDqR9FKANUqFxnIzWM81F1k/kOUA==", + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.41.1.tgz", + "integrity": "sha512-avf2OmrVg6GdVU18juebjjIIuLa+uS3syHuJ/3yDaEFP/8it+YvcxRrYDSf7K6rC4v770UxIddba2hAqQyTeYA==", "license": "MIT", "dependencies": { - "@astrojs/markdown-remark": "^7.1.1", - "@astrojs/mdx": "^5.0.4", + "@astrojs/markdown-satteri": "^0.3.2", + "@astrojs/mdx": "^7.0.0", "@astrojs/sitemap": "^3.7.2", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", - "astro-expressive-code": "^0.42.0", + "astro-expressive-code": "^0.44.0", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.3", "hast-util-select": "^6.0.4", @@ -274,17 +431,24 @@ "mdast-util-directive": "^3.1.0", "mdast-util-to-markdown": "^2.1.2", "mdast-util-to-string": "^4.0.0", - "pagefind": "^1.3.0", + "pagefind": "^1.5.2", "rehype": "^13.0.2", "rehype-format": "^5.0.1", "remark-directive": "^4.0.0", + "satteri": "^0.9.1", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { - "astro": "^6.0.0" + "@astrojs/markdown-remark": "^7.2.0", + "astro": "^7.0.2" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } } }, "node_modules/@astrojs/starlight-tailwind": { @@ -298,21 +462,21 @@ } }, "node_modules/@astrojs/svelte": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@astrojs/svelte/-/svelte-8.1.2.tgz", - "integrity": "sha512-L4eBoTY+DdgyH1g8pmKJxidRSvdk+NbkyJt5+f8EtdyJtPdxogeCyG3eEDmlIqqXxzNqLg7k/tYCjfBQ4kDMRA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/svelte/-/svelte-9.0.0.tgz", + "integrity": "sha512-gn4yFrTNvabpNu/yDp6xsD8xBVy4T2UdvEouLnvMQdBL7KbdZwkeR8Y3RBNBdYyIpRp1+oBPcxqr9DFpUrJT8A==", "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@sveltejs/vite-plugin-svelte": "^7.1.0", "svelte2tsx": "^0.7.55", - "vite": "^7.3.2", + "vite": "^8.0.13", "vitefu": "^1.1.2" }, "engines": { "node": ">=22.12.0" }, "peerDependencies": { - "astro": "^6.0.0", + "astro": "^7.0.0-alpha.0", "svelte": "^5.43.6", "typescript": "^5.3.3 || ^6.0.0" } @@ -343,12 +507,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -357,30 +521,30 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -390,56 +554,178 @@ } }, "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@bruits/satteri-darwin-arm64": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.9.4.tgz", + "integrity": "sha512-W3MSUkr2mZRR8Stoe+lqNAyzQzRuFMU8WffV9IvFSxTok0LGWR0ZZQPLELU4QTRiUbhL2Y4VUP9vV7pj8rHjgg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@bruits/satteri-darwin-x64": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.9.4.tgz", + "integrity": "sha512-DXOuuaE1lsv7mpk2mOvGrzqoEWEvOIZEO/fXVa7zfM23Iob+CBjBkRAMwpHA4pmZ3j6Gj7WJzPKw0kQ7w741AQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@bruits/satteri-linux-arm64-gnu": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.9.4.tgz", + "integrity": "sha512-gJxU9rGGoqIznSEgEzpjxkry24jeHuMpoo1tCIAhHYh7WaD3j5F8zt3jmHxEaN1Uwa+K5+wFgIR2uIGOnMzEmw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-arm64-musl": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.9.4.tgz", + "integrity": "sha512-Wjzu9hmmAbfmDkBfPI1VdZygJtYz9uYZQnkEyrXi6S2JFi+2pXQ1A5irj38bqm0IZmWcTbk0cVG4NZnPdtVNJA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-gnu": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.9.4.tgz", + "integrity": "sha512-MR1Q+wMx65FQlbSV7cRqWW87Knp0zkoaIV55Dt+xZl028wJABXEPEEmG3670SLq7lVZvcGIDwCgSg2kCYxvRwA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-musl": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.9.4.tgz", + "integrity": "sha512-T4gxhXve3zyNAZesrXAd/rDZOGRkbfFIUFld4TGsw6BsjoIteCcDji6IMqeXyaWEVSykY2X8Eid2hr6aXGYAaw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-wasm32-wasi": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.9.4.tgz", + "integrity": "sha512-/CEG8LUlpaBEnhFnYVn0UnlHFLs51UhrkJBUPDUXLzkadzAcnR88iRA/nOl7Zwhjb4WhfBV4p3P5qeOJMtH0iA==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@bruits/satteri-win32-arm64-msvc": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.9.4.tgz", + "integrity": "sha512-E1ZPQbgCtFKiU7pFYVndynvY7ne4coeVDUgnVThErSFlJ2ceQCBZrfRTD1lzrIDy63Bbqo+g/cZY9duw+JYjIw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@bruits/satteri-win32-x64-msvc": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.9.4.tgz", + "integrity": "sha512-5I7SiarsNdAUuhJb50CXJPTwr/ECVrBoU+fymoLjChK5fW//+srhY4lstcNTzgFRtQSYfVtm4OQZz16CVMeTeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@capsizecss/unpack": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", - "integrity": "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz", + "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==", "license": "MIT", "dependencies": { - "fontkitten": "^1.0.0" + "fontkitten": "^1.0.3" }, "engines": { "node": ">=18" } }, "node_modules/@clack/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.1.0.tgz", - "integrity": "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.2.tgz", + "integrity": "sha512-0Ty/1Gfm+Kb07sXcuESjyKfwEhSy4Ns1AgeEisHb/bDY5fWme0tTeTkU14T1Gmcs17YIjB/teiDe4uaCghbYqQ==", "license": "MIT", "dependencies": { + "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" } }, "node_modules/@clack/prompts": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.1.0.tgz", - "integrity": "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.6.0.tgz", + "integrity": "sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==", "license": "MIT", "dependencies": { - "@clack/core": "1.1.0", + "@clack/core": "1.4.2", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" } }, "node_modules/@ctrl/tinycolor": { @@ -506,10 +792,31 @@ "integrity": "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==", "license": "MIT" }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, "dependencies": { @@ -517,9 +824,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -533,9 +840,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -549,9 +856,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -565,9 +872,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -581,9 +888,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -597,9 +904,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -613,9 +920,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -629,9 +936,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -645,9 +952,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -661,9 +968,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -677,9 +984,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -693,9 +1000,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -709,9 +1016,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -725,9 +1032,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -741,9 +1048,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -757,9 +1064,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -773,9 +1080,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -789,9 +1096,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -805,9 +1112,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -821,9 +1128,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -837,9 +1144,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -853,9 +1160,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -869,9 +1176,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -885,9 +1192,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -901,9 +1208,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -917,9 +1224,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -933,9 +1240,9 @@ } }, "node_modules/@expressive-code/core": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.42.0.tgz", - "integrity": "sha512-MN11+9nfmaC7sYu2BZJXAXqwkBRt8t1xTSqP+Ti1NfTEskgl6xUnzDxoaiQkg0BMzpglA0pys4dpDKquP/cyIw==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.0.tgz", + "integrity": "sha512-xgiF2P6tYUbrhi3+x0S8xHZWT1t3Bvb3U91tAtRbLb9HLejLvYc5GZUqKICKLaUN4iSGhhNJu2fM/aH8e5yCMg==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^4.0.4", @@ -950,31 +1257,31 @@ } }, "node_modules/@expressive-code/plugin-frames": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.42.0.tgz", - "integrity": "sha512-XtkPm+941Uta7Y+81Acv+OA/20F1NJmJhCX6UYGKpqEIGqplNh3PTOhcURp6tcruhlzJcWcvpWy6Oigz3SrjqA==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.0.tgz", + "integrity": "sha512-V6M6+zVc1GzqCvXkQHc2m5rcFOIVzJgMq5gnfrMnVf2gwtj/sg4H93c1f/mGeqHycubwkHFUDyParAOiGeDZeA==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.42.0" + "@expressive-code/core": "^0.44.0" } }, "node_modules/@expressive-code/plugin-shiki": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.42.0.tgz", - "integrity": "sha512-PMKey/kLmewttAHQezL+Y5Fx3vVssfDi3+FJOYQQS2mXP3tQspFELtKKAfsXfmSXdToZYgwoO69HJndqfE+09g==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.0.tgz", + "integrity": "sha512-RZsdaqlbGqyAQKuoX4myQXxjmiE2l5KBpJ/gKPh62tCdIdpWyjbzVqSo8+5XsezZxkfi8AJ/J6EUaBTPROFX/Q==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.42.0", + "@expressive-code/core": "^0.44.0", "shiki": "^4.0.2" } }, "node_modules/@expressive-code/plugin-text-markers": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.42.0.tgz", - "integrity": "sha512-l59lUx8fq1v5g6SpmbDjiU0+7IdfbiWnAyRmtTVSpfhyq+nZMN4UcmYyu2b9Mynhzt7Gr+O+cXyEPDNb2AVWVQ==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.0.tgz", + "integrity": "sha512-0/m3A5b+lz2upyNq+wzZ1S69HRoJmyFs5LsR42lVZ9pmGRlBiSBYQpvqlji4DBj1+Riamxc0AvcCr5kuzOQeWA==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.42.0" + "@expressive-code/core": "^0.44.0" } }, "node_modules/@fontsource/ibm-plex-mono": { @@ -1014,9 +1321,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz", + "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==", "cpu": [ "arm64" ], @@ -1026,19 +1333,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.1" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz", + "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==", "cpu": [ "x64" ], @@ -1048,19 +1355,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz", + "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.2" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz", + "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==", "cpu": [ "arm64" ], @@ -1074,9 +1400,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz", + "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==", "cpu": [ "x64" ], @@ -1090,9 +1416,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz", + "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==", "cpu": [ "arm" ], @@ -1106,9 +1432,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz", + "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==", "cpu": [ "arm64" ], @@ -1122,9 +1448,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz", + "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==", "cpu": [ "ppc64" ], @@ -1138,9 +1464,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz", + "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==", "cpu": [ "riscv64" ], @@ -1154,9 +1480,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz", + "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==", "cpu": [ "s390x" ], @@ -1170,9 +1496,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz", + "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==", "cpu": [ "x64" ], @@ -1186,9 +1512,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz", + "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==", "cpu": [ "arm64" ], @@ -1202,9 +1528,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz", + "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==", "cpu": [ "x64" ], @@ -1218,9 +1544,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz", + "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==", "cpu": [ "arm" ], @@ -1230,19 +1556,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.1" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz", + "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==", "cpu": [ "arm64" ], @@ -1252,19 +1578,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.1" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz", + "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==", "cpu": [ "ppc64" ], @@ -1274,19 +1600,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.1" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz", + "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==", "cpu": [ "riscv64" ], @@ -1296,19 +1622,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.1" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz", + "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==", "cpu": [ "s390x" ], @@ -1318,19 +1644,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.1" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz", + "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==", "cpu": [ "x64" ], @@ -1340,19 +1666,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.1" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz", + "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==", "cpu": [ "arm64" ], @@ -1362,19 +1688,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz", + "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==", "cpu": [ "x64" ], @@ -1384,38 +1710,54 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.1" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz", + "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@emnapi/runtime": "^1.11.1" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz", + "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.2" + }, + "engines": { + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz", + "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==", "cpu": [ "arm64" ], @@ -1425,16 +1767,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz", + "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==", "cpu": [ "ia32" ], @@ -1444,16 +1786,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz", + "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==", "cpu": [ "x64" ], @@ -1463,7 +1805,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1551,16 +1893,43 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@oslojs/encoding": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "license": "MIT" }, + "node_modules/@oxc-project/types": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pagefind/darwin-arm64": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.4.0.tgz", - "integrity": "sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.5.2.tgz", + "integrity": "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==", "cpu": [ "arm64" ], @@ -1571,9 +1940,9 @@ ] }, "node_modules/@pagefind/darwin-x64": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.4.0.tgz", - "integrity": "sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.5.2.tgz", + "integrity": "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==", "cpu": [ "x64" ], @@ -1584,15 +1953,15 @@ ] }, "node_modules/@pagefind/default-ui": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.4.0.tgz", - "integrity": "sha512-wie82VWn3cnGEdIjh4YwNESyS1G6vRHwL6cNjy9CFgNnWW/PGRjsLq300xjVH5sfPFK3iK36UxvIBymtQIEiSQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.5.2.tgz", + "integrity": "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==", "license": "MIT" }, "node_modules/@pagefind/freebsd-x64": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.4.0.tgz", - "integrity": "sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.5.2.tgz", + "integrity": "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==", "cpu": [ "x64" ], @@ -1603,9 +1972,9 @@ ] }, "node_modules/@pagefind/linux-arm64": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.4.0.tgz", - "integrity": "sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.5.2.tgz", + "integrity": "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==", "cpu": [ "arm64" ], @@ -1616,9 +1985,9 @@ ] }, "node_modules/@pagefind/linux-x64": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.4.0.tgz", - "integrity": "sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.5.2.tgz", + "integrity": "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==", "cpu": [ "x64" ], @@ -1628,10 +1997,23 @@ "linux" ] }, + "node_modules/@pagefind/windows-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-arm64/-/windows-arm64-1.5.2.tgz", + "integrity": "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@pagefind/windows-x64": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.4.0.tgz", - "integrity": "sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.5.2.tgz", + "integrity": "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==", "cpu": [ "x64" ], @@ -1690,10 +2072,258 @@ "node": ">=18" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -1718,339 +2348,14 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", - "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", - "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", - "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", - "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", - "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", - "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", - "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", - "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", - "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", - "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", - "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", - "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", - "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", - "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", - "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", - "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", - "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", - "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", - "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", - "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", - "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", - "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", - "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", - "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", - "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@shikijs/core": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", - "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.0.tgz", + "integrity": "sha512-EooU3i9F6IAE8kEu+AnGf9DFZWkQBZ+hJn3tLVbsH+61mtQiva5biai66fAA6nvFPXkLgvrh7BrR7YcJU83xQQ==", "license": "MIT", "dependencies": { - "@shikijs/primitive": "4.0.2", - "@shikijs/types": "4.0.2", + "@shikijs/primitive": "4.3.0", + "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" @@ -2060,26 +2365,26 @@ } }, "node_modules/@shikijs/engine-javascript": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", - "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.3.0.tgz", + "integrity": "sha512-hTv/KiFf2tpiqlACPiztGGurEARWIutB8YUhcrA1pUC7VzzwKO+g5crUocrLztrZ5ro5Z4hbXg7bYclETn3gSQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2", + "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.4" + "oniguruma-to-es": "^4.3.6" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", - "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.0.tgz", + "integrity": "sha512-1vMdN3gHfnKfLYwecUI2ITJI4RhHt96xEaJumVn7Heb0IlJ8WQMIH0Voak+2j22BpSNKdnOfB/pCTPnPm2gq7A==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2", + "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2" }, "engines": { @@ -2087,24 +2392,24 @@ } }, "node_modules/@shikijs/langs": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", - "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.3.0.tgz", + "integrity": "sha512-rnlqFbBRSys9bT4gl/5rw9RnS0W/I84ZldXPkO7cvlEMoV85TyF/aU01N7/NbSR776RNLjrJKjfFUXJR6wN1Cg==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2" + "@shikijs/types": "4.3.0" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/primitive": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", - "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.3.0.tgz", + "integrity": "sha512-CPkz64PTa5diRW1ggzMZH9VM/du4RNChYgVtgqrFcgruvIybmCvySv8GkiHSczUHXYuuR8TdKEwFx+UnZMpgdg==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2", + "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" }, @@ -2113,21 +2418,21 @@ } }, "node_modules/@shikijs/themes": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", - "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.3.0.tgz", + "integrity": "sha512-Avgt05YiT+Y3prjIc9lmQxhJzHBcCfR6cjiFW4OyaMBbt2A6trX5rfjUzx+Vj/mE9qpArYjatnqo9XPjQNW/AQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2" + "@shikijs/types": "4.3.0" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", - "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.3.0.tgz", + "integrity": "sha512-oc8b9U2SYvofKZk8e/737nIX0qwf6eV2vHFATeObAu7r+mUVpLs8Re0BmVkIjAWAYgkmG/CzLNo7rzuBzRu/wQ==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -2153,84 +2458,66 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", - "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", + "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", - "vitefu": "^1.1.1" + "vitefu": "^1.1.2" }, "engines": { "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { - "svelte": "^5.0.0", - "vite": "^6.3.0 || ^7.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz", - "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", - "license": "MIT", - "dependencies": { - "obug": "^2.1.0" - }, - "engines": { - "node": "^20.19 || ^22.12 || >=24" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", - "svelte": "^5.0.0", - "vite": "^6.3.0 || ^7.0.0" + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "node_modules/@tailwindcss/node": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", - "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.21.0", - "jiti": "^2.6.1", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.0" + "tailwindcss": "4.3.2" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", - "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-x64": "4.3.0", - "@tailwindcss/oxide-freebsd-x64": "4.3.0", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-x64-musl": "4.3.0", - "@tailwindcss/oxide-wasm32-wasi": "4.3.0", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", - "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", "cpu": [ "arm64" ], @@ -2244,9 +2531,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", - "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", "cpu": [ "arm64" ], @@ -2260,9 +2547,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", - "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", "cpu": [ "x64" ], @@ -2276,9 +2563,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", - "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", "cpu": [ "x64" ], @@ -2292,9 +2579,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", - "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", "cpu": [ "arm" ], @@ -2308,9 +2595,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", - "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", "cpu": [ "arm64" ], @@ -2324,9 +2611,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", - "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", "cpu": [ "arm64" ], @@ -2340,9 +2627,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", - "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", "cpu": [ "x64" ], @@ -2356,9 +2643,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", - "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", "cpu": [ "x64" ], @@ -2372,9 +2659,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", - "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -2389,81 +2676,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.10.0", - "@emnapi/runtime": "^1.10.0", - "@emnapi/wasi-threads": "^1.2.1", + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.1", + "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.10.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", - "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", "cpu": [ "arm64" ], @@ -2477,9 +2704,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", - "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", "cpu": [ "x64" ], @@ -2493,19 +2720,29 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", - "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.3.0", - "@tailwindcss/oxide": "4.3.0", - "tailwindcss": "4.3.0" + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "tailwindcss": "4.3.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -2516,9 +2753,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -2582,12 +2819,12 @@ } }, "node_modules/@types/node": { - "version": "24.12.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", - "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/sax": { @@ -2612,9 +2849,9 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", + "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", "license": "ISC" }, "node_modules/@volar/kit": { @@ -2701,6 +2938,12 @@ "vscode-uri": "^3.0.8" } }, + "node_modules/@vscode/emmet-helper/node_modules/jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", + "license": "MIT" + }, "node_modules/@vscode/l10n": { "version": "0.0.18", "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", @@ -2708,9 +2951,9 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2729,9 +2972,9 @@ } }, "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -2758,6 +3001,18 @@ } } }, + "node_modules/am-i-vibing": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.4.0.tgz", + "integrity": "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==", + "license": "MIT", + "dependencies": { + "process-ancestry": "^0.1.0" + }, + "bin": { + "am-i-vibing": "dist/cli.mjs" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2848,19 +3103,20 @@ } }, "node_modules/astro": { - "version": "6.4.8", - "resolved": "https://registry.npmjs.org/astro/-/astro-6.4.8.tgz", - "integrity": "sha512-KK5lX90uU9EeVaTjINyj3sy9/NFXVa59aowaqbWBDDKLXZh4rr7GwIaCFYVetE22MJtsCNFerQXn0vlCLmpP/Q==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/astro/-/astro-7.0.4.tgz", + "integrity": "sha512-6DSiJ4CT69vngNlwEKec7nGOJHcPzc95lX7E6qkUhAt6drGYzUdf0KTJToswu3vFp3lTCGnNDCzBi/5aW8tTPA==", "license": "MIT", "dependencies": { - "@astrojs/compiler": "^4.0.0", + "@astrojs/compiler-rs": "^0.3.0", "@astrojs/internal-helpers": "0.10.0", - "@astrojs/markdown-remark": "7.2.0", + "@astrojs/markdown-satteri": "0.3.2", "@astrojs/telemetry": "3.3.2", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", + "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", @@ -2871,7 +3127,7 @@ "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", - "esbuild": "^0.27.3", + "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", @@ -2903,7 +3159,7 @@ "unist-util-visit": "^5.1.0", "unstorage": "^1.17.5", "vfile": "^6.0.3", - "vite": "^7.3.2", + "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", @@ -2922,33 +3178,30 @@ "url": "https://opencollective.com/astrodotbuild" }, "optionalDependencies": { - "sharp": "^0.34.0" + "sharp": "^0.34.0 || ^0.35.0" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "7.2.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } } }, "node_modules/astro-expressive-code": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.42.0.tgz", - "integrity": "sha512-aiTePi2Cn0mJPYWZSzP1GcxCinX9mNtJyCCshVVPSg1yRwM7ADvFJOx0FnS440M9t65hp8JH//dc2qr22Bm4ag==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.0.tgz", + "integrity": "sha512-b1wN/ZvbJprzxlGKIpIes2kQrCY5KRLwys2tWbZAZyjGZcW5ZtgneZnBwzNRiBna9/48d4mQl19KLjcRuhO1hw==", "license": "MIT", "dependencies": { - "rehype-expressive-code": "^0.42.0" + "rehype-expressive-code": "^0.44.0", + "url-extras": "^0.1.0" }, "peerDependencies": { - "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" } }, - "node_modules/astro/node_modules/@astrojs/compiler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-4.0.0.tgz", - "integrity": "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==", - "license": "MIT" - }, - "node_modules/astro/node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3603,9 +3856,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.23.0.tgz", - "integrity": "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==", + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -3646,15 +3899,15 @@ } }, "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", + "integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==", "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3711,9 +3964,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3723,32 +3976,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -3785,9 +4038,9 @@ "license": "MIT" }, "node_modules/esrap": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz", - "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==", + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz", + "integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -3923,15 +4176,15 @@ "license": "MIT" }, "node_modules/expressive-code": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.42.0.tgz", - "integrity": "sha512-V5DtJLEKuj4wf9O6IRtPtRObkMVy2ggR+S0MdjrTw6m58krZnDioyhW1si3Y04c5YPeooP4nd85Yq9NwEVHS4g==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.0.tgz", + "integrity": "sha512-JXVWVNCKlLuZLMQH8cOiDUSosT0Bb+elwE/dbAkpwFwDFmyFyWlECoWZIohh2FkIF1iI67TQJ+Ts9k7oNDh2qA==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.42.0", - "@expressive-code/plugin-frames": "^0.42.0", - "@expressive-code/plugin-shiki": "^0.42.0", - "@expressive-code/plugin-text-markers": "^0.42.0" + "@expressive-code/core": "^0.44.0", + "@expressive-code/plugin-frames": "^0.44.0", + "@expressive-code/plugin-shiki": "^0.44.0", + "@expressive-code/plugin-text-markers": "^0.44.0" } }, "node_modules/extend": { @@ -3946,10 +4199,25 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", "funding": [ { "type": "github", @@ -3962,6 +4230,15 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4235,9 +4512,9 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4684,9 +4961,9 @@ } }, "node_modules/i18next": { - "version": "26.3.1", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz", - "integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==", + "version": "26.3.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.4.tgz", + "integrity": "sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==", "funding": [ { "type": "individual", @@ -4888,9 +5165,9 @@ } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -4903,9 +5180,19 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4921,9 +5208,9 @@ "license": "MIT" }, "node_modules/jsonc-parser": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", - "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, "node_modules/jsonpointer": { @@ -5228,9 +5515,9 @@ } }, "node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -5246,12 +5533,12 @@ } }, "node_modules/magicast": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", - "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", + "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } @@ -6405,9 +6692,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "funding": [ { "type": "github", @@ -6478,14 +6765,17 @@ } }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/ofetch": { "version": "1.5.1", @@ -6517,18 +6807,18 @@ } }, "node_modules/oniguruma-parser": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", - "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", "license": "MIT" }, "node_modules/oniguruma-to-es": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", - "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", "license": "MIT", "dependencies": { - "oniguruma-parser": "^0.12.1", + "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } @@ -6556,12 +6846,12 @@ } }, "node_modules/p-queue": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz", - "integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", + "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", "license": "MIT", "dependencies": { - "eventemitter3": "^5.0.1", + "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" }, "engines": { @@ -6584,26 +6874,27 @@ } }, "node_modules/package-manager-detector": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", - "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.7.0.tgz", + "integrity": "sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==", "license": "MIT" }, "node_modules/pagefind": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.4.0.tgz", - "integrity": "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.5.2.tgz", + "integrity": "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==", "license": "MIT", "bin": { "pagefind": "lib/runner/bin.cjs" }, "optionalDependencies": { - "@pagefind/darwin-arm64": "1.4.0", - "@pagefind/darwin-x64": "1.4.0", - "@pagefind/freebsd-x64": "1.4.0", - "@pagefind/linux-arm64": "1.4.0", - "@pagefind/linux-x64": "1.4.0", - "@pagefind/windows-x64": "1.4.0" + "@pagefind/darwin-arm64": "1.5.2", + "@pagefind/darwin-x64": "1.5.2", + "@pagefind/freebsd-x64": "1.5.2", + "@pagefind/linux-arm64": "1.5.2", + "@pagefind/linux-x64": "1.5.2", + "@pagefind/windows-arm64": "1.5.2", + "@pagefind/windows-x64": "1.5.2" } }, "node_modules/parse-entities": { @@ -6704,9 +6995,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "funding": [ { "type": "opencollective", @@ -6723,7 +7014,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6757,9 +7048,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -6770,9 +7061,9 @@ } }, "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", + "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -6793,10 +7084,19 @@ "node": ">=6" } }, + "node_modules/process-ancestry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/process-ancestry/-/process-ancestry-0.1.0.tgz", + "integrity": "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", "license": "MIT", "funding": { "type": "github", @@ -6810,12 +7110,16 @@ "license": "MIT" }, "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "license": "MIT", "engines": { "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/readdirp": { @@ -6939,12 +7243,12 @@ } }, "node_modules/rehype-expressive-code": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.42.0.tgz", - "integrity": "sha512-8rp/1YMEVVSYbtz+bFBx+uSx3vA4i4T8RwRm5Q/IWbucQnnQqQ0hDqtmKOr8tv+59Cik6cu5aH3WPo0I7csuTA==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.44.0.tgz", + "integrity": "sha512-5r74C5F2sMR3X+QJH8OKWgZBO/cqRw5W1fLT6GVlSfLqepk+4j8tGFkyPqZYWjwntsBHzKPDH2zI68sZ7ScLfA==", "license": "MIT", "dependencies": { - "expressive-code": "^0.42.0" + "expressive-code": "^0.44.0" } }, "node_modules/rehype-format": { @@ -7226,48 +7530,59 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/rollup": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", - "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "node_modules/rolldown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.137.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.0", - "@rollup/rollup-android-arm64": "4.60.0", - "@rollup/rollup-darwin-arm64": "4.60.0", - "@rollup/rollup-darwin-x64": "4.60.0", - "@rollup/rollup-freebsd-arm64": "4.60.0", - "@rollup/rollup-freebsd-x64": "4.60.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", - "@rollup/rollup-linux-arm-musleabihf": "4.60.0", - "@rollup/rollup-linux-arm64-gnu": "4.60.0", - "@rollup/rollup-linux-arm64-musl": "4.60.0", - "@rollup/rollup-linux-loong64-gnu": "4.60.0", - "@rollup/rollup-linux-loong64-musl": "4.60.0", - "@rollup/rollup-linux-ppc64-gnu": "4.60.0", - "@rollup/rollup-linux-ppc64-musl": "4.60.0", - "@rollup/rollup-linux-riscv64-gnu": "4.60.0", - "@rollup/rollup-linux-riscv64-musl": "4.60.0", - "@rollup/rollup-linux-s390x-gnu": "4.60.0", - "@rollup/rollup-linux-x64-gnu": "4.60.0", - "@rollup/rollup-linux-x64-musl": "4.60.0", - "@rollup/rollup-openbsd-x64": "4.60.0", - "@rollup/rollup-openharmony-arm64": "4.60.0", - "@rollup/rollup-win32-arm64-msvc": "4.60.0", - "@rollup/rollup-win32-ia32-msvc": "4.60.0", - "@rollup/rollup-win32-x64-gnu": "4.60.0", - "@rollup/rollup-win32-x64-msvc": "4.60.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" + } + }, + "node_modules/satteri": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.9.4.tgz", + "integrity": "sha512-BKob126Tay84diOZsnVNH/Q/c+3njPJTCad3w5zLKa6j8bVjxskPNHDtxrMwYK4bN/RlqUSdMnPwKY4k65EMOQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.5", + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "@types/unist": "^3.0.3" + }, + "optionalDependencies": { + "@bruits/satteri-darwin-arm64": "0.9.4", + "@bruits/satteri-darwin-x64": "0.9.4", + "@bruits/satteri-linux-arm64-gnu": "0.9.4", + "@bruits/satteri-linux-arm64-musl": "0.9.4", + "@bruits/satteri-linux-x64-gnu": "0.9.4", + "@bruits/satteri-linux-x64-musl": "0.9.4", + "@bruits/satteri-wasm32-wasi": "0.9.4", + "@bruits/satteri-win32-arm64-msvc": "0.9.4", + "@bruits/satteri-win32-x64-msvc": "0.9.4" } }, "node_modules/sax": { @@ -7286,9 +7601,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7336,61 +7651,61 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", + "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.4" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.2", + "@img/sharp-darwin-x64": "0.35.2", + "@img/sharp-freebsd-wasm32": "0.35.2", + "@img/sharp-libvips-darwin-arm64": "1.3.1", + "@img/sharp-libvips-darwin-x64": "1.3.1", + "@img/sharp-libvips-linux-arm": "1.3.1", + "@img/sharp-libvips-linux-arm64": "1.3.1", + "@img/sharp-libvips-linux-ppc64": "1.3.1", + "@img/sharp-libvips-linux-riscv64": "1.3.1", + "@img/sharp-libvips-linux-s390x": "1.3.1", + "@img/sharp-libvips-linux-x64": "1.3.1", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", + "@img/sharp-libvips-linuxmusl-x64": "1.3.1", + "@img/sharp-linux-arm": "0.35.2", + "@img/sharp-linux-arm64": "0.35.2", + "@img/sharp-linux-ppc64": "0.35.2", + "@img/sharp-linux-riscv64": "0.35.2", + "@img/sharp-linux-s390x": "0.35.2", + "@img/sharp-linux-x64": "0.35.2", + "@img/sharp-linuxmusl-arm64": "0.35.2", + "@img/sharp-linuxmusl-x64": "0.35.2", + "@img/sharp-webcontainers-wasm32": "0.35.2", + "@img/sharp-win32-arm64": "0.35.2", + "@img/sharp-win32-ia32": "0.35.2", + "@img/sharp-win32-x64": "0.35.2" } }, "node_modules/shiki": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", - "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.3.0.tgz", + "integrity": "sha512-NKKjWzR6LIGL3sXBrWDw9sDS9cxx42/DkysaNqJEeOWE8Kix5gpak0bc00OfDVEO4oyXSyz8+aRaqKoBD1yo7A==", "license": "MIT", "dependencies": { - "@shikijs/core": "4.0.2", - "@shikijs/engine-javascript": "4.0.2", - "@shikijs/engine-oniguruma": "4.0.2", - "@shikijs/langs": "4.0.2", - "@shikijs/themes": "4.0.2", - "@shikijs/types": "4.0.2", + "@shikijs/core": "4.3.0", + "@shikijs/engine-javascript": "4.3.0", + "@shikijs/engine-oniguruma": "4.3.0", + "@shikijs/langs": "4.3.0", + "@shikijs/themes": "4.3.0", + "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" }, @@ -7424,9 +7739,9 @@ } }, "node_modules/smol-toml": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", - "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", + "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -7476,9 +7791,9 @@ } }, "node_modules/starlight-openapi": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/starlight-openapi/-/starlight-openapi-0.25.3.tgz", - "integrity": "sha512-erFtGyU1NI1mHmlRrVo/SWLdVvjFr1Qsxwz1bnOss/rOSBH3Tgq39rSlkXr9HWbSTnbjIpZkitgYw+s73zk5pA==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/starlight-openapi/-/starlight-openapi-0.26.0.tgz", + "integrity": "sha512-+WJnS7nIW42KfgmtBvTdj0Sx0LR+3kYPCdXMoiiMxKWDTF9TBd8KhTRDqNiPPrksej+gItZV/ROKwsXoF2/qZQ==", "license": "MIT", "dependencies": { "@readme/openapi-parser": "^4.1.2", @@ -7490,9 +7805,9 @@ "node": ">=22.12.0" }, "peerDependencies": { - "@astrojs/markdown-remark": ">=7.0.0", - "@astrojs/starlight": ">=0.38.0", - "astro": ">=6.0.0" + "@astrojs/markdown-satteri": ">=0.3.2", + "@astrojs/starlight": ">=0.41.0", + "astro": ">=7.0.2" } }, "node_modules/statuses": { @@ -7605,9 +7920,9 @@ } }, "node_modules/svelte": { - "version": "5.56.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.0.tgz", - "integrity": "sha512-kTXr26t1bchFp28ROrb957LtbujpBmBDibmqMGziVpUs7awBi96TGgX6SovrA8BNoEUDVRK2Fb9FkeYlGspoVg==", + "version": "5.56.4", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz", + "integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -7621,7 +7936,7 @@ "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.9", + "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -7641,9 +7956,9 @@ } }, "node_modules/svelte2tsx": { - "version": "0.7.55", - "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.55.tgz", - "integrity": "sha512-JWzgeM3lqySRNfqcsesvVEh8LhTWBxQJ9RMjzJ+VepdmXtVnNd0SbtGctG6+/fbHq0N6mhwSd823gszw9JHeGQ==", + "version": "0.7.57", + "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.57.tgz", + "integrity": "sha512-nQo0xEfUpSVjfkxan2UmC6Wl9UZVe9+/pglUiyBNMJ7KDQ9DDooucmXdToBOvzSfgYjj/kMYwf6CTTDz/z048w==", "license": "MIT", "dependencies": { "dedent-js": "^1.0.1", @@ -7680,9 +7995,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", - "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", "license": "MIT" }, "node_modules/tapable": { @@ -7711,18 +8026,18 @@ "license": "MIT" }, "node_modules/tinyclip": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.12.tgz", - "integrity": "sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==", + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.15.tgz", + "integrity": "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==", "license": "MIT", "engines": { "node": "^16.14.0 || >= 17.3.0" } }, "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "license": "MIT", "engines": { "node": ">=18" @@ -7787,9 +8102,9 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7809,9 +8124,9 @@ } }, "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "license": "MIT" }, "node_modules/ultrahtml": { @@ -7827,9 +8142,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/unified": { @@ -8122,6 +8437,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/url-extras": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/url-extras/-/url-extras-0.1.0.tgz", + "integrity": "sha512-8tzwTeXFPuX/5PHuCDQE5Dd9Ts4rwoq2t9aIT+HS4iAVpmj5l4Ao7Q+BuuFjvWRqrLswBhQDk8O96ZicgCqQqw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/url-template": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.1.1.tgz", @@ -8180,17 +8507,16 @@ } }, "node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.2.tgz", + "integrity": "sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==", "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -8206,9 +8532,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -8221,15 +8548,18 @@ "@types/node": { "optional": true }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, "jiti": { "optional": true }, "less": { "optional": true }, - "lightningcss": { - "optional": true - }, "sass": { "optional": true }, @@ -8254,9 +8584,9 @@ } }, "node_modules/vitefu": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", - "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", "license": "MIT", "workspaces": [ "tests/deps/*", @@ -8264,7 +8594,7 @@ "tests/projects/workspace/packages/*" ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "vite": { @@ -8454,16 +8784,10 @@ "npm": ">=7.0.0" } }, - "node_modules/vscode-json-languageservice/node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "license": "MIT" - }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0.tgz", - "integrity": "sha512-+VvMmQPJhtvJ+8O+zu2JKIRiLxXF8NW7krWgyMGeOHrp4Cn23T5hc0v2LknNeopDOB70wghHAds7mKtcZ0I4Sg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz", + "integrity": "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -8482,12 +8806,12 @@ } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.0.tgz", - "integrity": "sha512-Zdz+kJ12Iz6tc11xfZyEo501bBATHXrCjmMfnaR3pMnf1CoqZBKIynba3P+/bi9VEdrMbNtAVKYpKhbODvqy+Q==", + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.2.tgz", + "integrity": "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg==", "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0", + "vscode-jsonrpc": "9.0.1", "vscode-languageserver-types": "3.18.0" } }, @@ -8592,9 +8916,9 @@ } }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -8647,9 +8971,9 @@ } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -8701,9 +9025,9 @@ "license": "MIT" }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/docs/package.json b/docs/package.json index 6495a685..e3389b82 100644 --- a/docs/package.json +++ b/docs/package.json @@ -12,18 +12,18 @@ }, "dependencies": { "@astrojs/check": "^0.9.9", - "@astrojs/node": "^10.1.2", - "@astrojs/starlight": "^0.39.2", + "@astrojs/node": "^11.0.0", + "@astrojs/starlight": "^0.41.1", "@astrojs/starlight-tailwind": "^5.0.0", - "@astrojs/svelte": "^8.1.2", + "@astrojs/svelte": "^9.0.0", "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.2.8", - "@tailwindcss/vite": "^4.3.0", - "astro": "^6.4.8", - "sharp": "^0.34.5", - "starlight-openapi": "^0.25.3", - "svelte": "^5.56.0", - "tailwindcss": "^4.1.10", - "typescript": "^5.9.3" + "@tailwindcss/vite": "^4.3.2", + "astro": "^7.0.4", + "sharp": "^0.35.2", + "starlight-openapi": "^0.26.0", + "svelte": "^5.56.4", + "tailwindcss": "^4.3.2", + "typescript": "^6.0.3" } } diff --git a/docs/src/assets/guides/pocketbase_categories.png b/docs/src/assets/guides/pocketbase_categories.png index 98730280..5ddeeb08 100644 Binary files a/docs/src/assets/guides/pocketbase_categories.png and b/docs/src/assets/guides/pocketbase_categories.png differ diff --git a/docs/src/assets/guides/pocketbase_subcategories.png b/docs/src/assets/guides/pocketbase_subcategories.png new file mode 100644 index 00000000..f3b2da06 Binary files /dev/null and b/docs/src/assets/guides/pocketbase_subcategories.png differ diff --git a/docs/src/assets/guides/wanderer_settings_categories.png b/docs/src/assets/guides/wanderer_settings_categories.png new file mode 100644 index 00000000..08dd44bc Binary files /dev/null and b/docs/src/assets/guides/wanderer_settings_categories.png differ diff --git a/docs/src/assets/guides/wanderer_trails_adjust.png b/docs/src/assets/guides/wanderer_trails_adjust.png new file mode 100644 index 00000000..cec4568b Binary files /dev/null and b/docs/src/assets/guides/wanderer_trails_adjust.png differ diff --git a/docs/src/assets/guides/wanderer_trails_category_filter.png b/docs/src/assets/guides/wanderer_trails_category_filter.png new file mode 100644 index 00000000..03d8cae5 Binary files /dev/null and b/docs/src/assets/guides/wanderer_trails_category_filter.png differ diff --git a/docs/src/content/docs/develop/plugin-system.md b/docs/src/content/docs/develop/plugin-system.md index c299ad1e..ebd53f6a 100644 --- a/docs/src/content/docs/develop/plugin-system.md +++ b/docs/src/content/docs/develop/plugin-system.md @@ -536,12 +536,14 @@ Supported host fields: | `privacy` | string | Trail import | `original` keeps provider visibility; `settings` uses the local user trail privacy setting. | | `merge.enabled` | boolean | Trail import | Runs auto-merge after creating imported trails. | | `createSummitLogForCompleted` | boolean | Trail import | Creates summit logs for completed imported trails. Defaults to `true`. | -| `categoryMapping` | object | Trail import | Maps plugin-provided `metadata.providerCategory` values to local category IDs or category names. | +| `categoryMapping` | object | Trail import | Maps plugin-provided `metadata.providerCategory` values to local category or subcategory targets. | | `connectors` | object | Host request/media policy | Concrete settings for configured connectors. | The settings UI lets users edit `categoryMapping` per plugin instance for trail -import plugins. Unknown or empty provider categories still fall back to the -host's activity-type mapping. +import plugins. A mapping value can be a string for broad category-only +compatibility, or an object with `category` and optional `subcategory`. Category +and subcategory values may be local record IDs or canonical names. Unknown or +empty provider categories still fall back to the host's activity-type mapping. Example: @@ -549,7 +551,14 @@ Example: { "hostConfig": { "categoryMapping": { - "Ride": "Biking", + "Ride": { + "category": "Biking", + "subcategory": "Road" + }, + "GravelRide": { + "category": "Biking", + "subcategory": "Gravel" + }, "Hike": "Hiking" } }, diff --git a/docs/src/content/docs/run/backend-configuration/custom-categories.md b/docs/src/content/docs/run/backend-configuration/custom-categories.md index 6de63a74..313f8267 100644 --- a/docs/src/content/docs/run/backend-configuration/custom-categories.md +++ b/docs/src/content/docs/run/backend-configuration/custom-categories.md @@ -1,11 +1,12 @@ --- title: Custom categories -description: How to create custom trail categories +description: How to configure trail categories and subcategories --- wanderer uses categories to classify what kind of activity a trail belongs to. -Out of the box you get: Biking, Canoeing, Climbing, Hiking, Skiing and Walking. -However, you can adapt these categories to your needs or add completely new ones. +Out of the box you get: Biking, Canoeing, Climbing, Hiking, Running, Skiing and Walking. +Some broad categories also have subcategories, for example Biking can be refined into MTB, Gravel, Road or E-Bike. +You can adapt this taxonomy to your needs in the PocketBase admin panel. ## Modifying categories @@ -15,7 +16,69 @@ In the PocketBase admin panel, click on the `categories` table in the list on th All existing categories will be listed here. To edit one simply click on the row, edit the data you want to change, and click "Save". To delete a category check the box at the beginning of the row and click "Delete selected". -To create a new category click the "New record" button in the top right corner, give your new category a name and a background image, and click "Save". +To create a new category click the "New record" button in the top right corner, give your new category a name, optionally fill in display metadata such as `short_name`, `icon`, or localized `translations`, and click "Save". + +The category `name` is the canonical, language-independent identity. +Use stable names such as `Hiking` or `Biking`; display labels in different languages should be stored in `translations`. +Incoming federated trails and integration imports match categories by a normalized version of `name`, so changing a category name can affect future matching. + +### Category fields + +| Field | Description | +| ----- | ----------- | +| `name` | Canonical category name. This is used for matching across imports and federation. | +| `short_name` | Optional compact label for space-constrained UI. | +| `icon` | Optional Font Awesome Free icon name without the `fa-` prefix, for example `person-hiking`. | +| `translations` | Optional localized display labels. | +| `settings` | Optional JSON settings for category-specific backend behavior. | + +`translations` uses supported base locale codes such as `de`, `en`, `fr`, or `pt` as keys. +Do not use region-specific keys such as `de-CH` or `pt-BR`; the frontend resolves user locales to their base locale before looking up category translations. + +Example: + +```json +{ + "de": { + "name": "Radfahren", + "short_name": "RAD" + }, + "en": { + "name": "Biking", + "short_name": "BIKE" + } +} +``` + +## Modifying subcategories + +Subcategories live in the `subcategories` table and act as optional refinements below a single parent category. Their names only need to be unique within that parent, so `Road` can exist under both Biking and Running at the same time. + +![Pocketbase Subcategories](../../../../assets/guides/pocketbase_subcategories.png) + +To add one, create a new record in `subcategories`, choose its parent `category`, set a canonical `name`, and optionally add display metadata. + +### Subcategory fields + +| Field | Description | +| ----- | ----------- | +| `category` | Required parent category. | +| `name` | Canonical subcategory name, unique within the parent category after normalization. | +| `short_name` | Compact label shown in icon-based filters, for example `MTB`, `GRVL`, or `ROAD`. | +| `icon` | Optional Font Awesome Free icon name. If empty, the parent category icon is used. | +| `badge_icon` | Optional Font Awesome Free overlay icon, for example `snowflake`, `mountain`, `bolt`, or `cross`. | +| `translations` | Optional localized display labels, using the same structure as category translations. | + +Most subcategories should reuse the parent category's icon and rely on `short_name` — plus a `badge_icon` where it helps — to set themselves apart, rather than each carrying a distinct full icon. You can browse available icon names at [fontawesome.com](https://fontawesome.com/search?ic=free-collection). + +:::note +Unknown remote categories and subcategories are not automatically created during federation. +Raw remote values are stored on the trail and can be matched later when an admin creates a compatible local category or subcategory. +::: + +## Migrating old custom categories + +If your instance already had custom categories such as `MTB` or `Gravel` that now overlap with a default subcategory, you can reassign the affected trails in bulk from the web UI. See [Categories](/use/categories/#editing-several-trails-at-once) for the step-by-step migration path. ## Category settings diff --git a/docs/src/content/docs/use/categories.md b/docs/src/content/docs/use/categories.md new file mode 100644 index 00000000..fb33f2f6 --- /dev/null +++ b/docs/src/content/docs/use/categories.md @@ -0,0 +1,58 @@ +--- +title: Categories +description: How to use trail categories, subcategories, and category visibility settings +--- + +Every trail has a category that describes its broad activity type — Hiking, Biking, Running, Skiing, and so on. Many categories can be narrowed down further with a subcategory, such as Biking / Gravel or Hiking / Snowshoeing, whenever you want to be more specific. + +## Choosing a category + +When you create or edit a trail, pick the activity type with the **Category** selector in the trail form. It lists the broad categories together with their subcategories, so you can stay general or get specific: + +- Choose **Hiking** for an ordinary hiking trail. +- Choose **Hiking / Snowshoeing** to mark it as a snowshoe route. +- Choose **Biking / Gravel** for a gravel ride. + +A broad category on its own is always enough; a subcategory is optional. On trail cards and in lists, the category icon carries a small badge for subcategories that need one — for example a snowflake for winter variants — so you can tell refinements apart at a glance. + +## Filtering trails + +The filter panel shows each category as an icon. Click an icon to add that category to the filter. + +![Trail category filter with the subcategory overlay open](../../../assets/guides/wanderer_trails_category_filter.png) + +Categories that have subcategories reveal a subcategory overlay when you hover or focus the icon; on touch devices, long-press it instead. From there you can filter by: + +- the whole category, +- only trails that have no subcategory, or +- one or more specific subcategories. + +When a subcategory filter is active, a small indicator appears on the category icon — that's how you tell "all Biking trails" apart from "only the Biking subcategories I picked". + +## Editing several trails at once + +To reclassify many trails in one go, select them in the trail list, open the action menu, and choose **Adjust**. The modal lets you set a new category, subcategory, or difficulty for the whole selection. + +![The bulk edit modal for adjusting selected trails](../../../assets/guides/wanderer_trails_adjust.png) + +This is handy after upgrading an instance, when an older standalone category overlaps with a new subcategory: every trail previously filed under `Gravel`, for instance, can be moved to Biking / Gravel in a single step. + +## Category preferences + +Open **Settings → Categories** to control how categories behave for your account. + +![The category preferences page in Settings](../../../assets/guides/wanderer_settings_categories.png) + +Each category has one visibility toggle: + +- **Show** controls whether the category is part of your exploration and planning. While it is on, the category appears in search and discovery and is offered in the category picker when you create or edit a trail. Turn it off to hide all trails in that category from those places, including federated trails from other instances. + +You can **reorder** categories by dragging them. This order carries over to pickers and filters, and it also decides which category is preselected when you create a new trail. New accounts start with Hiking as the first category unless an older favourite-sport setting is migrated. + +Categories with subcategories can be expanded. Inside the expanded section, each subcategory has its own visibility toggle and can be reordered by dragging. Hidden subcategories appear muted and drop out of your pickers and filters; hiding a parent category also hides its subcategories. When a category is collapsed, the compact badges below the category name show which subcategories belong to it. + +These settings are personal. They never delete categories, change other users' settings, or remove category assignments that already exist on trails. + +:::note +Categories and subcategories themselves are defined by the instance administrator in PocketBase. As a regular user you choose from the available taxonomy and set your own visibility preferences, but you cannot create global categories from the web UI. +::: diff --git a/docs/src/content/docs/use/create-a-trail.md b/docs/src/content/docs/use/create-a-trail.md index 47ffbc51..8a58ba5a 100644 --- a/docs/src/content/docs/use/create-a-trail.md +++ b/docs/src/content/docs/use/create-a-trail.md @@ -65,7 +65,7 @@ While drawing or editing a route, the anchor list shows the route's start, inter - **Distance / Duration / Elevation** – These are automatically calculated but can be manually adjusted if needed. - **Tags** – Add descriptive tags to help categorize and search for your trail (e.g. forest, sunset, dog-friendly). Start typing to add a tag and press Enter to confirm. - **Difficulty** – Select the trail's difficulty (e.g. Easy, Moderate, Hard) -- **Category** – Choose the activity type (e.g. Hiking, Cycling) +- **[Category](/use/categories/)** – Choose the activity type. You can select a broad category such as Hiking, or a more specific subcategory such as Biking / Gravel. #### Visibility @@ -83,7 +83,7 @@ Waypoints are points of interest along the trail. - When you are not editing the route, click on the map to open a popup with a **Create waypoint** button at that location. - While drawing or editing the route, right-click on the map to open the same popup without placing a route anchor. This works only while waypoint markers are visible (see the waypoint toggle in the route editing toolbar). - Each waypoint can have a name, description, icon, and photos. -- Use Font Awesome icons for map markers. You can browse them at [fontawesome.com](https://fontawesome.com/search?q=share&o=r&m=free). +- Use Font Awesome icons for map markers. You can browse them at [fontawesome.com](https://fontawesome.com/search?ic=free-collection). Alternatively, click **From Photos** to upload photos with GPS metadata. Waypoints will be created automatically based on the photo locations. diff --git a/plugins/README.md b/plugins/README.md index 724262e3..1510f3b4 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -204,10 +204,24 @@ Manifest `configSchema` defines plugin-owned settings that are passed to plugin | `merge.available` | Controls whether the UI offers auto-merge for this plugin. Defaults to `true`. | | `merge.enabled` | Runs auto-merge after trail import. | | `createSummitLogForCompleted` | Creates summit logs for completed imports. | -| `categoryMapping` | Maps `metadata.providerCategory` to local category IDs or names. | +| `categoryMapping` | Maps `metadata.providerCategory` to local category or subcategory targets. | | `connectors` | Provides host-owned base URL, TLS, private-network, and storage redirect settings for configured connectors. | The settings UI lets users edit `categoryMapping` per plugin instance for trail import plugins. +Mapping values can be strings for broad category-only compatibility, or objects +with `category` and optional `subcategory`, using local record IDs or canonical +names: + +```json +{ + "categoryMapping": { + "Ride": { "category": "Biking", "subcategory": "Road" }, + "GravelRide": { "category": "Biking", "subcategory": "Gravel" }, + "Hike": "Hiking" + } +} +``` + Plugins may describe provider-owned category values for the settings UI with `metadata.providerCategories`. This is display-only metadata; `categoryMapping` keys still use the raw provider category values emitted as diff --git a/plugins/hammerhead/hammerhead.go b/plugins/hammerhead/hammerhead.go index 69737d31..26a8b0bf 100644 --- a/plugins/hammerhead/hammerhead.go +++ b/plugins/hammerhead/hammerhead.go @@ -11,6 +11,8 @@ import ( "github.com/open-wanderer/wanderer/plugins/sdk" ) +const hammerheadJSONMaxBytes int64 = 16 * 1024 * 1024 + type hammerheadClient struct { userID string token string @@ -37,7 +39,7 @@ func login(email string, password string) (string, error) { }, Expect: sdk.ResponseExpect{ ContentTypes: []string{"application/json"}, - MaxBytes: 1048576, + MaxBytes: hammerheadJSONMaxBytes, }, } response, body, err := sdk.HostRequest(spec) @@ -92,7 +94,7 @@ func (c hammerheadClient) get(path string, query []sdk.QueryParam, out any) erro }, Expect: sdk.ResponseExpect{ ContentTypes: []string{"application/json"}, - MaxBytes: 1048576, + MaxBytes: hammerheadJSONMaxBytes, }, }) if err != nil { diff --git a/plugins/hammerhead/main.go b/plugins/hammerhead/main.go index a4d8c85a..28290df1 100644 --- a/plugins/hammerhead/main.go +++ b/plugins/hammerhead/main.go @@ -165,7 +165,7 @@ func prepareTrailSendV1() int32 { }, Expect: sdk.ResponseExpect{ ContentTypes: []string{"application/json"}, - MaxBytes: 1048576, + MaxBytes: hammerheadJSONMaxBytes, }, }, } diff --git a/plugins/hammerhead/plugin.json b/plugins/hammerhead/plugin.json index da0a5fef..ec15411d 100644 --- a/plugins/hammerhead/plugin.json +++ b/plugins/hammerhead/plugin.json @@ -81,7 +81,7 @@ ] }, "downloads": { - "maxBytes": 1048576, + "maxBytes": 16777216, "contentTypes": [ "application/json" ] diff --git a/plugins/komoot/plugin.json b/plugins/komoot/plugin.json index 7bff07e1..85f2057c 100644 --- a/plugins/komoot/plugin.json +++ b/plugins/komoot/plugin.json @@ -101,31 +101,31 @@ "hostConfig": { "categoryMapping": { "hike": "Hiking", - "mountaineering": "Hiking", - "racebike": "Biking", - "e_racebike": "Biking", - "touringbicycle": "Biking", - "e_touringbicycle": "Biking", - "mtb": "Biking", - "e_mtb": "Biking", - "mtb_easy": "Biking", - "e_mtb_easy": "Biking", - "mtb_advanced": "Biking", - "e_mtb_advanced": "Biking", - "downhillbike": "Biking", + "mountaineering": { "category": "Hiking", "subcategory": "Alpine" }, + "racebike": { "category": "Biking", "subcategory": "Road" }, + "e_racebike": { "category": "Biking", "subcategory": "E-Bike" }, + "touringbicycle": { "category": "Biking", "subcategory": "Touring" }, + "e_touringbicycle": { "category": "Biking", "subcategory": "Touring" }, + "mtb": { "category": "Biking", "subcategory": "MTB" }, + "e_mtb": { "category": "Biking", "subcategory": "MTB" }, + "mtb_easy": { "category": "Biking", "subcategory": "MTB" }, + "e_mtb_easy": { "category": "Biking", "subcategory": "MTB" }, + "mtb_advanced": { "category": "Biking", "subcategory": "MTB" }, + "e_mtb_advanced": { "category": "Biking", "subcategory": "MTB" }, + "downhillbike": { "category": "Biking", "subcategory": "MTB" }, "unicycle": "Biking", - "citybike": "Biking", - "jogging": "Walking", + "citybike": { "category": "Biking", "subcategory": "Touring" }, + "jogging": { "category": "Running", "subcategory": "Road" }, "nordicwalking": "Walking", "skaten": "Walking", "other": "Walking", "climbing": "Climbing", - "nordic": "Skiing", + "nordic": { "category": "Skiing", "subcategory": "Cross-country" }, "skialpin": "Skiing", - "skitour": "Skiing", + "skitour": { "category": "Skiing", "subcategory": "Backcountry" }, "sled": "Skiing", "snowboard": "Skiing", - "snowshoe": "Skiing" + "snowshoe": { "category": "Hiking", "subcategory": "Snowshoeing" } } }, "metadata": { diff --git a/plugins/schema/plugin.schema.json b/plugins/schema/plugin.schema.json index 02d671d6..016c039a 100644 --- a/plugins/schema/plugin.schema.json +++ b/plugins/schema/plugin.schema.json @@ -412,7 +412,17 @@ "type": "boolean" }, "categoryMapping": { - "$ref": "#/definitions/stringMap" + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/categoryMappingTarget" + } + ] + } }, "connectors": { "type": "object", @@ -476,6 +486,18 @@ "$ref": "#/definitions/stringMap" } } + }, + "categoryMappingTarget": { + "type": "object", + "additionalProperties": false, + "properties": { + "category": { + "type": "string" + }, + "subcategory": { + "type": "string" + } + } } } } diff --git a/plugins/strava/activity_type.go b/plugins/strava/activity_type.go new file mode 100644 index 00000000..e3d1cbe6 --- /dev/null +++ b/plugins/strava/activity_type.go @@ -0,0 +1,22 @@ +package main + +func activityTypeFromProvider(value string) string { + switch value { + case "AlpineSki", "BackcountrySki", "IceSkate", "NordicSki", "RollerSki", "Snowboard": + return "skiing" + case "Canoeing", "Kayaking", "Kitesurf", "Rowing", "Sail", "StandUpPaddling", "Surfing", "Windsurf": + return "canoeing" + case "Hike", "Snowshoe": + return "hiking" + case "Run", "TrailRun", "VirtualRun": + return "running" + case "Walk", "Golf", "Skateboard", "Wheelchair": + return "walking" + case "Ride", "EBikeRide", "Handcycle", "InlineSkate", "Velomobile", "VirtualRide": + return "biking" + case "RockClimbing": + return "climbing" + default: + return value + } +} diff --git a/plugins/strava/mapper.go b/plugins/strava/mapper.go index 3ef2445f..deff7b31 100644 --- a/plugins/strava/mapper.go +++ b/plugins/strava/mapper.go @@ -208,21 +208,5 @@ func providerActivityType(activity *detailedActivity) string { } func activityType(activity *detailedActivity) string { - value := providerActivityType(activity) - switch value { - case "AlpineSki", "BackcountrySki", "IceSkate", "NordicSki", "RollerSki", "Snowboard": - return "skiing" - case "Canoeing", "Kayaking", "Kitesurf", "Rowing", "Sail", "StandUpPaddling", "Surfing", "Windsurf": - return "canoeing" - case "Hike", "Snowshoe": - return "hiking" - case "Run", "VirtualRun", "Walk", "Golf", "Skateboard", "Wheelchair": - return "walking" - case "Ride", "EBikeRide", "Handcycle", "InlineSkate", "Velomobile", "VirtualRide": - return "biking" - case "RockClimbing": - return "climbing" - default: - return value - } + return activityTypeFromProvider(providerActivityType(activity)) } diff --git a/plugins/strava/mapper_test.go b/plugins/strava/mapper_test.go new file mode 100644 index 00000000..3ec6e3ee --- /dev/null +++ b/plugins/strava/mapper_test.go @@ -0,0 +1,18 @@ +package main + +import "testing" + +func TestActivityTypeMapsRunsToRunning(t *testing.T) { + cases := map[string]string{ + "Run": "running", + "TrailRun": "running", + "VirtualRun": "running", + "Walk": "walking", + } + + for providerType, want := range cases { + if got := activityTypeFromProvider(providerType); got != want { + t.Fatalf("activityTypeFromProvider(%q) = %q, want %q", providerType, got, want) + } + } +} diff --git a/plugins/strava/plugin.json b/plugins/strava/plugin.json index e05c1cfa..506b626c 100644 --- a/plugins/strava/plugin.json +++ b/plugins/strava/plugin.json @@ -102,7 +102,7 @@ "oauth_access_token" ], "downloads": { - "maxBytes": 1048576, + "maxBytes": 16777216, "contentTypes": [ "application/json", "application/gpx+xml", @@ -145,15 +145,15 @@ "route:1": "Biking", "route:2": "Walking", "AlpineSki": "Skiing", - "BackcountrySki": "Skiing", + "BackcountrySki": { "category": "Skiing", "subcategory": "Backcountry" }, "Badminton": "Other", "Canoeing": "Canoeing", - "Crossfit": "Workout", - "EBikeRide": "Biking", - "EMountainBikeRide": "Biking", - "Elliptical": "Workout", + "Crossfit": "Other", + "EBikeRide": { "category": "Biking", "subcategory": "E-Bike" }, + "EMountainBikeRide": { "category": "Biking", "subcategory": "E-Bike" }, + "Elliptical": "Other", "Golf": "Other", - "GravelRide": "Biking", + "GravelRide": { "category": "Biking", "subcategory": "Gravel" }, "Handcycle": "Biking", "HighIntensityIntervalTraining": "Other", "Hike": "Hiking", @@ -161,40 +161,40 @@ "InlineSkate": "Walking", "Kayaking": "Canoeing", "Kitesurf": "Canoeing", - "MountainBikeRide": "Biking", - "NordicSki": "Skiing", + "MountainBikeRide": { "category": "Biking", "subcategory": "MTB" }, + "NordicSki": { "category": "Skiing", "subcategory": "Cross-country" }, "Pickleball": "Other", "Pilates": "Other", "Racquetball": "Other", - "Ride": "Biking", + "Ride": { "category": "Biking", "subcategory": "Road" }, "RockClimbing": "Climbing", - "RollerSki": "Skiing", + "RollerSki": { "category": "Skiing", "subcategory": "Cross-country" }, "Rowing": "Canoeing", - "Run": "Walking", + "Run": { "category": "Running", "subcategory": "Road" }, "Sail": "Canoeing", "Skateboard": "Walking", "Snowboard": "Skiing", - "Snowshoe": "Hiking", + "Snowshoe": { "category": "Hiking", "subcategory": "Snowshoeing" }, "Soccer": "Other", "Squash": "Other", - "StairStepper": "Workout", + "StairStepper": "Other", "StandUpPaddling": "Canoeing", "Surfing": "Canoeing", "Swim": "Other", "TableTennis": "Other", "Tennis": "Other", - "TrailRun": "Other", + "TrailRun": { "category": "Running", "subcategory": "Trail" }, "Training": "Other", "Velomobile": "Biking", "VirtualRide": "Biking", "VirtualRow": "Other", - "VirtualRun": "Walking", + "VirtualRun": "Running", "Walk": "Walking", - "WeightTraining": "Workout", + "WeightTraining": "Other", "Wheelchair": "Walking", "Windsurf": "Canoeing", - "Workout": "Workout", - "Yoga": "Workout" + "Workout": "Other", + "Yoga": "Other" } }, "metadata": { diff --git a/plugins/strava/strava.go b/plugins/strava/strava.go index be049b34..f694f8ab 100644 --- a/plugins/strava/strava.go +++ b/plugins/strava/strava.go @@ -12,6 +12,8 @@ import ( "github.com/open-wanderer/wanderer/plugins/sdk" ) +const stravaDownloadMaxBytes int64 = 16 * 1024 * 1024 + // Strava is migrating its API host: the new host "https://www.api-v3.strava.com" // is available from 2027-01-04 and the old one is retired on 2027-06-01 (June // 2026 Developer Program update). We cut over on 2027-03-01 — after the new host @@ -132,7 +134,7 @@ func (c *stravaClient) request(path string, query []sdk.QueryParam, contentTypes }, Expect: sdk.ResponseExpect{ ContentTypes: contentTypes, - MaxBytes: 1048576, + MaxBytes: stravaDownloadMaxBytes, }, }) } diff --git a/web/package-lock.json b/web/package-lock.json index cc70e3ba..60367661 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,11 +9,11 @@ "version": "0.19.2", "dependencies": { "@felte/validator-zod": "^1.0.18", - "@fortawesome/fontawesome-free": "^7.1.0", - "@sveltejs/adapter-node": "^5.5.2", - "@tailwindcss/vite": "^4.2.4", - "@threlte/core": "^8.5.9", - "@threlte/extras": "^9.14.6", + "@fortawesome/fontawesome-free": "^7.3.0", + "@sveltejs/adapter-node": "^5.5.7", + "@tailwindcss/vite": "^4.3.2", + "@threlte/core": "^8.5.16", + "@threlte/extras": "^9.21.0", "@tiptap/core": "^3.27.1", "@tiptap/extension-heading": "^3.27.1", "@tiptap/extension-link": "^3.27.1", @@ -22,73 +22,77 @@ "@tiptap/pm": "^3.27.1", "@tiptap/starter-kit": "^3.27.1", "@tiptap/suggestion": "^3.27.1", - "@turf/destination": "^7.3.3", - "@turf/distance": "^7.3.3", + "@turf/destination": "^7.3.5", + "@turf/distance": "^7.3.5", "@types/chart.js": "^4.0.1", "@types/supercluster": "^7.1.3", - "@types/three": "^0.183.1", - "@xmldom/xmldom": "^0.8.12", + "@types/three": "^0.185.0", + "@xmldom/xmldom": "^0.9.10", "activitypub-types": "^1.1.0", - "autoprefixer": "^10.4.24", + "autoprefixer": "^10.5.2", "canvas-confetti": "^1.9.4", "canvg": "^4.0.3", "chart.js": "^4.5.1", "chartjs-plugin-crosshair": "^2.0.0", - "chartjs-plugin-zoom": "^2.1.0", + "chartjs-plugin-zoom": "^2.2.0", "chokidar": "^5.0.0", "crypto-random-string": "^5.0.0", "felte": "^1.3.0", "heic2any": "^0.0.4", "instead": "^1.0.3", "isomorphic-xml2js": "^0.1.3", - "json-diff-ts": "^4.8.2", + "json-diff-ts": "^4.10.4", "jspdf": "^4.2.1", "jszip": "^3.10.1", - "maplibre-gl": "^4.7.1", - "marked": "^17.0.4", - "meilisearch": "^0.57.0", + "maplibre-gl": "^5.24.0", + "marked": "^18.0.5", + "meilisearch": "^0.58.0", "ngeohash": "^0.6.3", - "nouislider": "^15.7.1", - "pdfkit": "^0.17.2", - "photoswipe": "^5.4.3", - "pocketbase": "^0.26.8", - "qrcode": "^1.4.4", + "nouislider": "^15.8.1", + "pdfkit": "^0.19.1", + "photoswipe": "^5.4.4", + "pocketbase": "^0.27.0", + "qrcode": "^1.5.4", "supercluster": "^8.0.1", - "svelte-i18n": "^4.0.0", - "tailwindcss": "^4.2.4", - "three": "^0.183.1", - "vitest": "^4.1.4", + "svelte-i18n": "^4.0.1", + "tailwindcss": "^4.3.2", + "three": "^0.185.0", + "vitest": "^4.1.9", "zod": "^3.24.1" }, "devDependencies": { - "@playwright/test": "^1.58.2", + "@playwright/test": "^1.61.1", "@sveltejs/adapter-auto": "^7.0.1", - "@sveltejs/enhanced-img": "^0.10.4", - "@sveltejs/kit": "^2.60.1", - "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tailwindcss/typography": "^0.5.19", + "@sveltejs/enhanced-img": "^0.11.0", + "@sveltejs/kit": "^2.68.0", + "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@tailwindcss/typography": "^0.5.20", "@types/canvas-confetti": "^1.9.0", - "@types/node": "^25.3.3", - "postcss": "^8.5.6", - "svelte": "^5.55.7", - "svelte-check": "^4.3.6", + "@types/node": "^26.0.1", + "postcss": "^8.5.16", + "svelte": "^5.56.4", + "svelte-check": "^4.7.1", "sveltekit-openapi-generator": "^0.1.6", - "tslib": "^2.4.1", - "typescript": "^5.9.3", - "vite": "^8.0.16" + "tslib": "^2.8.1", + "typescript": "^6.0.3", + "vite": "8.1.1" } }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", - "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", + "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", "dev": true, "license": "MIT", "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", + "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" } }, "node_modules/@apidevtools/openapi-schemas": { @@ -109,27 +113,27 @@ "license": "MIT" }, "node_modules/@apidevtools/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz", + "integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==", "dev": true, "license": "MIT", "dependencies": { - "@apidevtools/json-schema-ref-parser": "^9.0.6", - "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/json-schema-ref-parser": "14.0.1", + "@apidevtools/openapi-schemas": "^2.1.0", "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "call-me-maybe": "^1.0.1", - "z-schema": "^5.0.1" + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" }, "peerDependencies": { "openapi-types": ">=7" } }, "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -142,20 +146,20 @@ "license": "Apache-2.0" }, "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "license": "MIT", "optional": true, "dependencies": { @@ -163,9 +167,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, "dependencies": { @@ -288,9 +292,9 @@ } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz", - "integrity": "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.3.0.tgz", + "integrity": "sha512-Yw4Qa43P6e4Xwh0TwEUkHQNRJInWaqIBo73VJmns3j2AIlZPAvUcR4yGIxCPqPRCgyJ5KIVIalF/I1GxhZ/Kgw==", "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "engines": { "node": ">=6" @@ -849,50 +853,31 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true, - "license": "MIT" - }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, - "node_modules/@mapbox/geojson-rewind": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", - "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", - "license": "ISC", - "dependencies": { - "get-stream": "^6.0.1", - "minimist": "^1.2.6" - }, - "bin": { - "geojson-rewind": "geojson-rewind" - } - }, "node_modules/@mapbox/jsonlint-lines-primitives": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", - "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.3.tgz", + "integrity": "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==", + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 22" } }, "node_modules/@mapbox/point-geometry": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", - "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", "license": "ISC" }, "node_modules/@mapbox/tiny-sdf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.1.0.tgz", - "integrity": "sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz", + "integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==", "license": "BSD-2-Clause" }, "node_modules/@mapbox/unitbezier": { @@ -902,12 +887,14 @@ "license": "BSD-2-Clause" }, "node_modules/@mapbox/vector-tile": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", - "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.5.tgz", + "integrity": "sha512-pXj8m7KTsqZt+1jsE0xIpGvqTSbblfkuEJL/NJmNePMtEwxO8V3XMDo9WMSfDeqHvCtBI9Lmt4mGcGR10zecmw==", "license": "BSD-3-Clause", "dependencies": { - "@mapbox/point-geometry": "~0.1.0" + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^4.0.2" } }, "node_modules/@mapbox/whoots-js": { @@ -919,18 +906,26 @@ "node": ">=6.0.0" } }, + "node_modules/@maplibre/geojson-vt": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.1.tgz", + "integrity": "sha512-FVMOcmSP/yqol45t7StApEyTL5/vmqBCuFhH9n+fFuINenhaX+YgHHIt1yJ86S8kln3uJLcMvmEU2cfn6E2eCQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.1.0" + } + }, "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz", - "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==", + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.10.0.tgz", + "integrity": "sha512-lichxSiagMEBBrqHF0trtMQH9RKh+9jUlIJl0qW0QHvt2H/tbvUWdE+ZzI2Jd0/pT7j/iavLonlPu7EQ/ixTOw==", "license": "ISC", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", - "@mapbox/unitbezier": "^0.0.1", + "@mapbox/unitbezier": "^1.0.0", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", - "quickselect": "^2.0.0", - "rw": "^1.3.3", + "quickselect": "^3.0.0", "tinyqueue": "^3.0.0" }, "bin": { @@ -939,20 +934,52 @@ "gl-style-validate": "dist/gl-style-validate.mjs" } }, - "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/quickselect": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", - "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", - "license": "ISC" + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz", + "integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@maplibre/mlt": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.12.tgz", + "integrity": "sha512-ZeK5w2TTeHOajcLaEQs1KZXw2V9wIKo1PmThlxlsHoXsQsYlBqLJzPOd6tJHRtGTChUY3DPPmjXRArYVvAbmZw==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.2.tgz", + "integrity": "sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^5.1.0" + } + }, + "node_modules/@maplibre/vt-pbf/node_modules/pbf": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.0.tgz", + "integrity": "sha512-Wv0yo0+uZepnoNEKsquhar1F18LogB8oeEikIhUXG16udbiXG7JecHGySwoo6kuMgjmbQYzdrTZlO+/K9t8eZg==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -963,10 +990,34 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -984,13 +1035,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", - "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.59.1" + "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -1006,9 +1057,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", "cpu": [ "arm64" ], @@ -1022,9 +1073,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", "cpu": [ "arm64" ], @@ -1038,9 +1089,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", "cpu": [ "x64" ], @@ -1054,9 +1105,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", "cpu": [ "x64" ], @@ -1070,9 +1121,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", "cpu": [ "arm" ], @@ -1086,9 +1137,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", "cpu": [ "arm64" ], @@ -1102,9 +1153,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", "cpu": [ "arm64" ], @@ -1118,9 +1169,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", "cpu": [ "ppc64" ], @@ -1134,9 +1185,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", "cpu": [ "s390x" ], @@ -1150,9 +1201,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", "cpu": [ "x64" ], @@ -1166,9 +1217,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", "cpu": [ "x64" ], @@ -1182,9 +1233,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", "cpu": [ "arm64" ], @@ -1198,27 +1249,27 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", "cpu": [ "wasm32" ], "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", "cpu": [ "arm64" ], @@ -1232,9 +1283,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", "cpu": [ "x64" ], @@ -1254,9 +1305,9 @@ "license": "MIT" }, "node_modules/@rollup/plugin-commonjs": { - "version": "29.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz", - "integrity": "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==", + "version": "29.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.3.tgz", + "integrity": "sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg==", "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -1323,10 +1374,31 @@ } } }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", + "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -1346,9 +1418,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", - "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", "cpu": [ "arm" ], @@ -1359,9 +1431,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", - "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", "cpu": [ "arm64" ], @@ -1372,9 +1444,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", - "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", "cpu": [ "arm64" ], @@ -1385,9 +1457,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", - "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", "cpu": [ "x64" ], @@ -1398,9 +1470,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", - "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", "cpu": [ "arm64" ], @@ -1411,9 +1483,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", - "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", "cpu": [ "x64" ], @@ -1424,9 +1496,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", - "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", "cpu": [ "arm" ], @@ -1437,9 +1509,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", - "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", "cpu": [ "arm" ], @@ -1450,9 +1522,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", - "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], @@ -1463,9 +1535,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", - "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", "cpu": [ "arm64" ], @@ -1476,9 +1548,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", - "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", "cpu": [ "loong64" ], @@ -1489,9 +1561,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", - "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", "cpu": [ "loong64" ], @@ -1502,9 +1574,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", - "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ "ppc64" ], @@ -1515,9 +1587,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", - "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", "cpu": [ "ppc64" ], @@ -1528,9 +1600,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", - "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ "riscv64" ], @@ -1541,9 +1613,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", - "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", "cpu": [ "riscv64" ], @@ -1554,9 +1626,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", - "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ "s390x" ], @@ -1567,9 +1639,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", - "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", "cpu": [ "x64" ], @@ -1580,9 +1652,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", - "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", "cpu": [ "x64" ], @@ -1593,9 +1665,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", - "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", "cpu": [ "x64" ], @@ -1606,9 +1678,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", - "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], @@ -1619,9 +1691,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", - "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ "arm64" ], @@ -1632,9 +1704,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", - "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ "ia32" ], @@ -1645,9 +1717,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", - "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -1658,9 +1730,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", - "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", "cpu": [ "x64" ], @@ -1677,9 +1749,9 @@ "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", - "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", + "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -1696,14 +1768,15 @@ } }, "node_modules/@sveltejs/adapter-node": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.4.tgz", - "integrity": "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==", + "version": "5.5.7", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.7.tgz", + "integrity": "sha512-uOfc9eVlI3A37RRSaKcgrheBYPrfJwC9VMqDp8x/O6tlKdcLLvHThSWD0KNIbjQ/d+7bwLGx3vx6aowAcRfd2g==", "license": "MIT", "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-replace": "^6.0.3", "rollup": "^4.59.0" }, "peerDependencies": { @@ -1711,9 +1784,9 @@ } }, "node_modules/@sveltejs/enhanced-img": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.10.4.tgz", - "integrity": "sha512-Am5nmAKUo7Nboqq7Dhtfn7dcXA087d7gIz6Vecn1opB41aJ680+0q9U9KvEcMgduOyeiwckTIOQOx4Mmq9GcvA==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.11.0.tgz", + "integrity": "sha512-TN7VzGoqwFqvA4Faj1brUPHLtcEkyFa08nrn9bfm98yVrzr0g4bOZHjlBoPYHHX+sZNGE+cxznhTVgarECPQsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1730,15 +1803,15 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.60.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.60.1.tgz", - "integrity": "sha512-mQjlkNo+rJvpln7V2IGY2j99BqhcFbS4UN0AQNKNYfhBAFZTuCDAdW3a1sgf330mvtNvsBXn3HpAhcmvdJTcIQ==", + "version": "2.68.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.68.0.tgz", + "integrity": "sha512-PdKiWsqinAoubVsSiRgVFkg3MHzGhQPnwQ8VxnGQKpZYijpapZ3UHHBje0GeByt2TvfjHPw+kxV+dNK2RIZg9g==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", - "@sveltejs/acorn-typescript": "^1.0.5", + "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", - "acorn": "^8.14.1", + "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", @@ -1770,10 +1843,20 @@ } } }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", + "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz", - "integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", + "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", "license": "MIT", "dependencies": { "deepmerge": "^4.3.1", @@ -1790,56 +1873,56 @@ } }, "node_modules/@swc/helpers": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", - "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" } }, "node_modules/@tailwindcss/node": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", - "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.2.4" + "tailwindcss": "4.3.2" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", - "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-x64": "4.2.4", - "@tailwindcss/oxide-freebsd-x64": "4.2.4", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-x64-musl": "4.2.4", - "@tailwindcss/oxide-wasm32-wasi": "4.2.4", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", - "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", "cpu": [ "arm64" ], @@ -1853,9 +1936,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", - "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", "cpu": [ "arm64" ], @@ -1869,9 +1952,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", - "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", "cpu": [ "x64" ], @@ -1885,9 +1968,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", - "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", "cpu": [ "x64" ], @@ -1901,9 +1984,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", - "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", "cpu": [ "arm" ], @@ -1917,9 +2000,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", - "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", "cpu": [ "arm64" ], @@ -1933,9 +2016,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", - "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", "cpu": [ "arm64" ], @@ -1949,9 +2032,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", - "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", "cpu": [ "x64" ], @@ -1965,9 +2048,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", - "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", "cpu": [ "x64" ], @@ -1981,9 +2064,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", - "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -1998,11 +2081,11 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "engines": { @@ -2010,9 +2093,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", - "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", "cpu": [ "arm64" ], @@ -2026,9 +2109,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", - "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", "cpu": [ "x64" ], @@ -2042,27 +2125,27 @@ } }, "node_modules/@tailwindcss/typography": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", - "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.20.tgz", + "integrity": "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==", "dev": true, "license": "MIT", "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders" } }, "node_modules/@tailwindcss/vite": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", - "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.2.4", - "@tailwindcss/oxide": "4.2.4", - "tailwindcss": "4.2.4" + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "tailwindcss": "4.3.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" @@ -2084,9 +2167,9 @@ } }, "node_modules/@threlte/core": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/@threlte/core/-/core-8.5.9.tgz", - "integrity": "sha512-EncyIKITwXnOUQpzstq+7AxqjW7sWhrg662BdHi/iSMXaIvjUaF3VPLBzGvVm0MCsokxFG69jfW39AG5GBbHbg==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/@threlte/core/-/core-8.5.16.tgz", + "integrity": "sha512-+aa9Zgz0/Qat82H0hTm1pTEtS2BIP1Nfu7pKaei+3+TvDqER0YnZ4hdgI9fYU9Z5r1lrzY3JxMyrdISp4B0kfw==", "license": "MIT", "peerDependencies": { "svelte": ">=5", @@ -2094,9 +2177,9 @@ } }, "node_modules/@threlte/extras": { - "version": "9.14.8", - "resolved": "https://registry.npmjs.org/@threlte/extras/-/extras-9.14.8.tgz", - "integrity": "sha512-0QVIYoJPo+Nc4S4Il9asHVzpt4iHX88jRgyrWi7Uk5iWK+xSAFLyHkyL0kSvdFoBXSJZLE0LG6qP78n69//scA==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@threlte/extras/-/extras-9.21.0.tgz", + "integrity": "sha512-ZkBx4IH+7vcxVM09Rex9XJNMKKbQt4JZ5J/3TnBd/Fge8IzSgl0ydgn70riDf/8tDLMDID6VdZPPrlr5vCHHJA==", "license": "MIT", "dependencies": { "@threejs-kit/instanced-sprite-mesh": "^2.5.1", @@ -2586,9 +2669,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "license": "MIT", "optional": true, "dependencies": { @@ -2635,9 +2718,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/geojson": { @@ -2646,15 +2729,6 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, - "node_modules/@types/geojson-vt": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", - "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/hammerjs": { "version": "2.0.46", "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", @@ -2675,30 +2749,13 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mapbox__point-geometry": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", - "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", - "license": "MIT" - }, - "node_modules/@types/mapbox__vector-tile": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", - "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*", - "@types/mapbox__point-geometry": "*", - "@types/pbf": "*" - } - }, "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "undici-types": "~8.3.0" } }, "node_modules/@types/pako": { @@ -2707,12 +2764,6 @@ "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", "license": "MIT" }, - "node_modules/@types/pbf": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", - "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", - "license": "MIT" - }, "node_modules/@types/raf": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", @@ -2741,18 +2792,17 @@ } }, "node_modules/@types/three": { - "version": "0.183.1", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz", - "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==", + "version": "0.185.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.0.tgz", + "integrity": "sha512-O2Uy8Cj4Nonr8dWUUbifMdPe8B0Mq7EdOHb89S4+kjUw/KhbjTZrUuYlrQ1bpUKG+EP9QJnN7qNxbHGlGoLHMA==", "license": "MIT", "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", - "@webgpu/types": "*", "fflate": "~0.8.2", - "meshoptimizer": "~1.0.1" + "meshoptimizer": "~1.1.1" } }, "node_modules/@types/trusted-types": { @@ -2777,15 +2827,15 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", - "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2794,12 +2844,12 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", - "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.4", + "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2829,9 +2879,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", - "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", "license": "MIT", "dependencies": { "tinyrainbow": "^3.1.0" @@ -2841,12 +2891,12 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", - "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.4", + "@vitest/utils": "4.1.9", "pathe": "^2.0.3" }, "funding": { @@ -2854,13 +2904,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", - "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2869,21 +2919,21 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", - "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", - "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.4", + "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -2891,25 +2941,19 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@webgpu/types": { - "version": "0.1.69", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", - "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", - "license": "BSD-3-Clause" - }, "node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=14.6" } }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2924,6 +2968,38 @@ "integrity": "sha512-06XrXpzW0Y7kS9ZS61qtV12s0oTCdcuWBO2/cPjPzGqlSBYqDvP8oiTIkSIrrynIrox5S5+/t8atDbi0Wo1xKA==", "license": "MIT" }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -2988,9 +3064,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", - "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", "funding": [ { "type": "opencollective", @@ -3007,8 +3083,8 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -3070,9 +3146,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", - "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -3091,9 +3167,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -3125,9 +3201,9 @@ "license": "(MIT AND Zlib)" }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", "funding": [ { "type": "opencollective", @@ -3144,10 +3220,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -3187,9 +3263,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001788", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", - "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", "funding": [ { "type": "opencollective", @@ -3439,13 +3515,6 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "license": "MIT" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3494,35 +3563,6 @@ "node": ">= 8" } }, - "node_modules/cross-spawn/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", - "license": "MIT" - }, "node_modules/crypto-random-string": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-5.0.0.tgz", @@ -3645,9 +3685,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz", - "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==", + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", "license": "(MPL-2.0 OR Apache-2.0)", "optional": true, "optionalDependencies": { @@ -3668,9 +3708,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.340", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz", - "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==", + "version": "1.5.383", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.383.tgz", + "integrity": "sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -3681,13 +3721,13 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -3703,9 +3743,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", + "integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==", "license": "MIT" }, "node_modules/es5-ext": { @@ -3791,9 +3831,9 @@ } }, "node_modules/esrap": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.5.tgz", - "integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==", + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz", + "integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -3834,9 +3874,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "license": "Apache-2.0", "engines": { "node": ">=12.0.0" @@ -3868,6 +3908,23 @@ "pako": "^2.1.0" } }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3901,9 +3958,9 @@ } }, "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "license": "MIT" }, "node_modules/find-up": { @@ -3966,13 +4023,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -3996,12 +4046,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/geojson-vt": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", - "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", - "license": "ISC" - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4011,18 +4055,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gl-matrix": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", @@ -4051,20 +4083,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/global-prefix": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", - "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", - "license": "MIT", - "dependencies": { - "ini": "^4.1.3", - "kind-of": "^6.0.3", - "which": "^4.0.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/globalyzer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", @@ -4093,9 +4111,9 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4124,26 +4142,6 @@ "node": ">=8.0.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/imagetools-core": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/imagetools-core/-/imagetools-core-9.1.0.tgz", @@ -4160,33 +4158,12 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ini": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", - "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/instead": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/instead/-/instead-1.0.3.tgz", @@ -4215,12 +4192,12 @@ "license": "MIT" }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -4266,13 +4243,11 @@ "license": "MIT" }, "node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" }, "node_modules/isomorphic-xml2js": { "version": "0.1.3", @@ -4301,26 +4276,35 @@ } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/jpeg-exif": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", - "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "node_modules/js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==", "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4335,6 +4319,13 @@ "integrity": "sha512-/Ipg6migsLtNQQz+I3jm+hnPGZx4x47cVXTr4tgvxepQJKIB5iDmyguxicZ/59wIg3OlGgIGwIMftn+LqT8lNQ==", "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-pretty-compact": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", @@ -4407,20 +4398,11 @@ "license": "(MIT AND Zlib)" }, "node_modules/kdbush": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", - "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz", + "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==", "license": "ISC" }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -4738,22 +4720,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", @@ -4797,37 +4763,30 @@ } }, "node_modules/maplibre-gl": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", - "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz", + "integrity": "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==", "license": "BSD-3-Clause", "dependencies": { - "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", - "@mapbox/point-geometry": "^0.1.0", - "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/tiny-sdf": "^2.1.0", "@mapbox/unitbezier": "^0.0.1", - "@mapbox/vector-tile": "^1.3.1", + "@mapbox/vector-tile": "^2.0.4", "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^20.3.1", - "@types/geojson": "^7946.0.14", - "@types/geojson-vt": "3.2.5", - "@types/mapbox__point-geometry": "^0.1.4", - "@types/mapbox__vector-tile": "^1.3.4", - "@types/pbf": "^3.0.5", - "@types/supercluster": "^7.1.3", - "earcut": "^3.0.0", - "geojson-vt": "^4.0.2", - "gl-matrix": "^3.4.3", - "global-prefix": "^4.0.0", + "@maplibre/geojson-vt": "^6.1.0", + "@maplibre/maplibre-gl-style-spec": "^24.8.1", + "@maplibre/mlt": "^1.1.8", + "@maplibre/vt-pbf": "^4.3.0", + "@types/geojson": "^7946.0.16", + "earcut": "^3.0.2", + "gl-matrix": "^3.4.4", "kdbush": "^4.0.2", "murmurhash-js": "^1.0.0", - "pbf": "^3.3.0", - "potpack": "^2.0.0", + "pbf": "^4.0.1", + "potpack": "^2.1.0", "quickselect": "^3.0.0", - "supercluster": "^8.0.1", - "tinyqueue": "^3.0.0", - "vt-pbf": "^3.1.3" + "tinyqueue": "^3.0.0" }, "engines": { "node": ">=16.14.0", @@ -4838,15 +4797,15 @@ } }, "node_modules/maplibre-gl/node_modules/earcut": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", - "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.0.tgz", + "integrity": "sha512-qy0wTgkOKsKC0cHVd4cKFfaO5u7egzs0F2bQ/9l5Pvm0EJM4XDdUMFoCJrxmWc2Fc3sRFa8l6gds8tx+6BcN8g==", "license": "ISC" }, "node_modules/marked": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.6.tgz", - "integrity": "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -4856,9 +4815,9 @@ } }, "node_modules/meilisearch": { - "version": "0.57.0", - "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.57.0.tgz", - "integrity": "sha512-J3ZdX6MjguEIG9Yo+duRZeVsbMdRuwM2OFvJUs8wZSeBADYZ2qc1eajlClKLRqS/mAhGh2bLiU5SbmhKXKuWSw==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.58.0.tgz", + "integrity": "sha512-MXFlU+JVG2I3tDI4brS3UHxEv2fjOROP24TPP16c8dGHTulb1kxMgXvKP0x4ImvUcsVVJFi4QXP0JNDS/TaWaw==", "license": "MIT", "engines": { "node": "^20.19.0 || >=22.12.0" @@ -4884,9 +4843,9 @@ } }, "node_modules/meshoptimizer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", - "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", "license": "MIT" }, "node_modules/minimatch": { @@ -4949,9 +4908,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "funding": [ { "type": "github", @@ -4982,10 +4941,13 @@ } }, "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", - "license": "MIT" + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/nouislider": { "version": "15.8.1", @@ -4994,23 +4956,16 @@ "license": "MIT" }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" + "license": "MIT", + "engines": { + "node": ">=12.20.0" } }, "node_modules/openapi-merge": { @@ -5083,9 +5038,19 @@ "license": "BlueOak-1.0.0" }, "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "(MIT AND Zlib)" }, "node_modules/path-exists": { @@ -5097,16 +5062,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5147,12 +5102,11 @@ "license": "MIT" }, "node_modules/pbf": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", - "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.2.tgz", + "integrity": "sha512-J0ajxARhZfpUEebxYs1vhMGMuLSXtBe1e+fFPDrf2uA2hgo+UshKfNUWOz92HJNz6/NFEXseQPddnHkTreWRqg==", "license": "BSD-3-Clause", "dependencies": { - "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" }, "bin": { @@ -5160,16 +5114,17 @@ } }, "node_modules/pdfkit": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz", - "integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.19.1.tgz", + "integrity": "sha512-6Gzk+wDwTs4VSxsR5rCMTnIl5nlmkye1oWB0l2hDB1EX6ZNSIBroKQEv+2+fPPn+stVjyqzmsqRJVDfB9fo5DA==", "license": "MIT", "dependencies": { - "crypto-js": "^4.2.0", + "@noble/ciphers": "^1.0.0", + "@noble/hashes": "^1.6.0", "fontkit": "^2.0.4", - "jpeg-exif": "^1.1.4", + "js-md5": "^0.8.3", "linebreak": "^1.1.0", - "png-js": "^1.0.0" + "png-js": "^1.1.0" } }, "node_modules/performance-now": { @@ -5206,13 +5161,13 @@ } }, "node_modules/playwright": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", - "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.59.1" + "playwright-core": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -5225,9 +5180,9 @@ } }, "node_modules/playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5255,15 +5210,15 @@ } }, "node_modules/pocketbase": { - "version": "0.26.8", - "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.26.8.tgz", - "integrity": "sha512-aQ/ewvS7ncvAE8wxoW10iAZu6ElgbeFpBhKPnCfvRovNzm2gW8u/sQNPGN6vNgVEagz44kK//C61oKjfa+7Low==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.27.0.tgz", + "integrity": "sha512-K5N6d93UP/BNMbMnlZ6BUfy9VPCIvLyqhJFOsNI8OsZwzvKWEAfyD36boi5K4ECIOl5HMlo0TzuaeGdKpMwizQ==", "license": "MIT" }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "funding": [ { "type": "opencollective", @@ -5449,12 +5404,12 @@ } }, "node_modules/prosemirror-view": { - "version": "1.41.8", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz", - "integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==", + "version": "1.41.9", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.9.tgz", + "integrity": "sha512-clTunTX+eaLbr87L1V1QPheRlEQJyTlL3gXe9x3jQIk3rL0RVWxviDGz8tFaydwIVm+hKhYCyr+R/zBtWr9s6A==", "license": "MIT", "dependencies": { - "prosemirror-model": "^1.20.0", + "prosemirror-model": "^1.25.8", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } @@ -5602,12 +5557,12 @@ } }, "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -5617,30 +5572,30 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" } }, "node_modules/rollup": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", - "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -5650,31 +5605,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.2", - "@rollup/rollup-android-arm64": "4.60.2", - "@rollup/rollup-darwin-arm64": "4.60.2", - "@rollup/rollup-darwin-x64": "4.60.2", - "@rollup/rollup-freebsd-arm64": "4.60.2", - "@rollup/rollup-freebsd-x64": "4.60.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", - "@rollup/rollup-linux-arm-musleabihf": "4.60.2", - "@rollup/rollup-linux-arm64-gnu": "4.60.2", - "@rollup/rollup-linux-arm64-musl": "4.60.2", - "@rollup/rollup-linux-loong64-gnu": "4.60.2", - "@rollup/rollup-linux-loong64-musl": "4.60.2", - "@rollup/rollup-linux-ppc64-gnu": "4.60.2", - "@rollup/rollup-linux-ppc64-musl": "4.60.2", - "@rollup/rollup-linux-riscv64-gnu": "4.60.2", - "@rollup/rollup-linux-riscv64-musl": "4.60.2", - "@rollup/rollup-linux-s390x-gnu": "4.60.2", - "@rollup/rollup-linux-x64-gnu": "4.60.2", - "@rollup/rollup-linux-x64-musl": "4.60.2", - "@rollup/rollup-openbsd-x64": "4.60.2", - "@rollup/rollup-openharmony-arm64": "4.60.2", - "@rollup/rollup-win32-arm64-msvc": "4.60.2", - "@rollup/rollup-win32-ia32-msvc": "4.60.2", - "@rollup/rollup-win32-x64-gnu": "4.60.2", - "@rollup/rollup-win32-x64-msvc": "4.60.2", + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" } }, @@ -5684,12 +5639,6 @@ "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", "license": "MIT" }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -5718,9 +5667,9 @@ } }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -5737,9 +5686,9 @@ "license": "ISC" }, "node_modules/set-cookie-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", - "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.1.tgz", + "integrity": "sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA==", "license": "MIT" }, "node_modules/setimmediate": { @@ -6014,14 +5963,14 @@ } }, "node_modules/svelte": { - "version": "5.55.7", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.7.tgz", - "integrity": "sha512-ymI5ykLPwIHW839E053FQbI1G+jnRFJEw3Kv5Y4njixVWywQBx+NUFpkkKyk5LIb36Fg9DVXSYpqiGekLD0hyw==", + "version": "5.56.4", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz", + "integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", + "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", @@ -6030,7 +5979,7 @@ "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.4", + "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -6041,13 +5990,14 @@ } }, "node_modules/svelte-check": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.6.tgz", - "integrity": "sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.1.tgz", + "integrity": "sha512-FGUOmAqxXdN/H9Zm8slrqO7SLtFisXRB7rfOsHNJ3MLTD2po/+Stg8XyErkpumPHbuUiYTcqrEIzxpVWKTLqtg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.0", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", @@ -6575,70 +6525,141 @@ } }, "node_modules/swagger-jsdoc": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", - "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.3.0.tgz", + "integrity": "sha512-I+iQjVGV3t28pOkQUJv2MncthvOtkEactOn8R76SvSYhxgtIn7FoqfDHwQaN+GBnQdXQLrhgDXseKitmJcHMsA==", "dev": true, "license": "MIT", "dependencies": { + "@apidevtools/swagger-parser": "^12.1.0", "commander": "6.2.0", "doctrine": "3.0.0", - "glob": "7.1.6", + "glob": "11.1.0", "lodash.mergewith": "^4.6.2", - "swagger-parser": "^10.0.3", "yaml": "2.0.0-1" }, "bin": { "swagger-jsdoc": "bin/swagger-jsdoc.js" }, "engines": { - "node": ">=12.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/swagger-jsdoc/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/swagger-jsdoc/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/swagger-jsdoc/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/swagger-jsdoc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/swagger-jsdoc/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "@isaacs/cliui": "^9.0.0" }, "engines": { - "node": "*" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/swagger-jsdoc/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/swagger-jsdoc/node_modules/yaml": { @@ -6651,23 +6672,10 @@ "node": ">= 6" } }, - "node_modules/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "10.0.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/tailwindcss": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", - "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", "license": "MIT" }, "node_modules/tapable": { @@ -6694,9 +6702,9 @@ } }, "node_modules/three": { - "version": "0.183.2", - "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", - "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", + "version": "0.185.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.0.tgz", + "integrity": "sha512-+yRrcRO2iZa8uzvNNl0d7cL4huhgKgBvVJ0njcTe8xFqZ6DMAFZdCKDP91SEAuj25bNAj7k1QQdf+srZywVK6w==", "license": "MIT" }, "node_modules/three-instanced-uniforms-mesh": { @@ -6712,9 +6720,9 @@ } }, "node_modules/three-mesh-bvh": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.9.9.tgz", - "integrity": "sha512-FJKitcjvbALmeQRK+Sc+nLGorCpkrZBrbgJZFzhdyWboak37DZikn46hvQkNqSbJPm227ahYmS6k3N/GXaAyXw==", + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.9.10.tgz", + "integrity": "sha512-UOlTgPIeqUURcwaG8knxvBaruwZlC4X3/WSHEFO7rYvMVv/YNUrkfFEszvfj36pXV88dCHoHNnIp0PifkirnTQ==", "license": "MIT", "peerDependencies": { "three": ">= 0.159.0" @@ -6778,9 +6786,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", - "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "license": "MIT", "engines": { "node": ">=18" @@ -6897,9 +6905,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -6911,9 +6919,9 @@ } }, "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "license": "MIT" }, "node_modules/unicode-properties": { @@ -6995,26 +7003,16 @@ "base64-arraybuffer": "^1.0.2" } }, - "node_modules/validator": { - "version": "13.15.35", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", - "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.1.tgz", + "integrity": "sha512-X/05/cT+VITy2AeDc1der6smvGWWREtL4hPbPTaVbjSBuuWkmNOjR6HP3NzqcQA2nF6VHGUPaFRJyft/2AE9Kg==", "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "bin": { @@ -7031,7 +7029,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -7131,18 +7129,18 @@ } }, "node_modules/vitest": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", - "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.4", - "@vitest/mocker": "4.1.4", - "@vitest/pretty-format": "4.1.4", - "@vitest/runner": "4.1.4", - "@vitest/snapshot": "4.1.4", - "@vitest/spy": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -7170,12 +7168,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.4", - "@vitest/browser-preview": "4.1.4", - "@vitest/browser-webdriverio": "4.1.4", - "@vitest/coverage-istanbul": "4.1.4", - "@vitest/coverage-v8": "4.1.4", - "@vitest/ui": "4.1.4", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -7219,17 +7217,6 @@ } } }, - "node_modules/vt-pbf": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", - "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", - "license": "MIT", - "dependencies": { - "@mapbox/point-geometry": "0.1.0", - "@mapbox/vector-tile": "^1.3.1", - "pbf": "^3.2.1" - } - }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -7243,18 +7230,19 @@ "license": "MIT" }, "node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^2.0.0" }, "bin": { - "node-which": "bin/which.js" + "node-which": "bin/node-which" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": ">= 8" } }, "node_modules/which-module": { @@ -7377,13 +7365,6 @@ "node": ">=8" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/xml2js": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", @@ -7488,38 +7469,6 @@ "node": ">=8" } }, - "node_modules/z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" - }, - "bin": { - "z-schema": "bin/z-schema" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "commander": "^9.4.1" - } - }, - "node_modules/z-schema/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/zimmerframe": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", diff --git a/web/package.json b/web/package.json index 8695224b..07d4ad51 100644 --- a/web/package.json +++ b/web/package.json @@ -15,30 +15,30 @@ "test:unit": "vitest --passWithNoTests" }, "devDependencies": { - "@playwright/test": "^1.58.2", + "@playwright/test": "^1.61.1", "@sveltejs/adapter-auto": "^7.0.1", - "@sveltejs/enhanced-img": "^0.10.4", - "@sveltejs/kit": "^2.60.1", - "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tailwindcss/typography": "^0.5.19", + "@sveltejs/enhanced-img": "^0.11.0", + "@sveltejs/kit": "^2.68.0", + "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@tailwindcss/typography": "^0.5.20", "@types/canvas-confetti": "^1.9.0", - "@types/node": "^25.3.3", - "postcss": "^8.5.6", - "svelte": "^5.55.7", - "svelte-check": "^4.3.6", + "@types/node": "^26.0.1", + "postcss": "^8.5.16", + "svelte": "^5.56.4", + "svelte-check": "^4.7.1", "sveltekit-openapi-generator": "^0.1.6", - "tslib": "^2.4.1", - "typescript": "^5.9.3", - "vite": "^8.0.16" + "tslib": "^2.8.1", + "typescript": "^6.0.3", + "vite": "8.1.1" }, "type": "module", "dependencies": { "@felte/validator-zod": "^1.0.18", - "@fortawesome/fontawesome-free": "^7.1.0", - "@sveltejs/adapter-node": "^5.5.2", - "@tailwindcss/vite": "^4.2.4", - "@threlte/core": "^8.5.9", - "@threlte/extras": "^9.14.6", + "@fortawesome/fontawesome-free": "^7.3.0", + "@sveltejs/adapter-node": "^5.5.7", + "@tailwindcss/vite": "^4.3.2", + "@threlte/core": "^8.5.16", + "@threlte/extras": "^9.21.0", "@tiptap/core": "^3.27.1", "@tiptap/extension-heading": "^3.27.1", "@tiptap/extension-link": "^3.27.1", @@ -47,42 +47,42 @@ "@tiptap/pm": "^3.27.1", "@tiptap/starter-kit": "^3.27.1", "@tiptap/suggestion": "^3.27.1", - "@turf/destination": "^7.3.3", - "@turf/distance": "^7.3.3", + "@turf/destination": "^7.3.5", + "@turf/distance": "^7.3.5", "@types/chart.js": "^4.0.1", "@types/supercluster": "^7.1.3", - "@types/three": "^0.183.1", - "@xmldom/xmldom": "^0.8.12", + "@types/three": "^0.185.0", + "@xmldom/xmldom": "^0.9.10", "activitypub-types": "^1.1.0", - "autoprefixer": "^10.4.24", + "autoprefixer": "^10.5.2", "canvas-confetti": "^1.9.4", "canvg": "^4.0.3", "chart.js": "^4.5.1", "chartjs-plugin-crosshair": "^2.0.0", - "chartjs-plugin-zoom": "^2.1.0", + "chartjs-plugin-zoom": "^2.2.0", "chokidar": "^5.0.0", "crypto-random-string": "^5.0.0", "felte": "^1.3.0", "heic2any": "^0.0.4", "instead": "^1.0.3", "isomorphic-xml2js": "^0.1.3", - "json-diff-ts": "^4.8.2", + "json-diff-ts": "^4.10.4", "jspdf": "^4.2.1", "jszip": "^3.10.1", - "maplibre-gl": "^4.7.1", - "marked": "^17.0.4", - "meilisearch": "^0.57.0", + "maplibre-gl": "^5.24.0", + "marked": "^18.0.5", + "meilisearch": "^0.58.0", "ngeohash": "^0.6.3", - "nouislider": "^15.7.1", - "pdfkit": "^0.17.2", - "photoswipe": "^5.4.3", - "pocketbase": "^0.26.8", - "qrcode": "^1.4.4", + "nouislider": "^15.8.1", + "pdfkit": "^0.19.1", + "photoswipe": "^5.4.4", + "pocketbase": "^0.27.0", + "qrcode": "^1.5.4", "supercluster": "^8.0.1", - "svelte-i18n": "^4.0.0", - "tailwindcss": "^4.2.4", - "three": "^0.183.1", - "vitest": "^4.1.4", + "svelte-i18n": "^4.0.1", + "tailwindcss": "^4.3.2", + "three": "^0.185.0", + "vitest": "^4.1.9", "zod": "^3.24.1" }, "overrides": { diff --git a/web/src/css/components.css b/web/src/css/components.css index dc1ea2dc..fc24ae1b 100644 --- a/web/src/css/components.css +++ b/web/src/css/components.css @@ -1,6 +1,15 @@ @import 'tailwindcss'; @reference "./app.css"; +:root { + --tooltip-background: rgba(36, 39, 52, 0.75); + --tooltip-border-radius: 4px; + --tooltip-color: #fff; + --tooltip-font-size: 12px; + --tooltip-offset-top: 24px; + --tooltip-padding: 6px 10px; +} + .btn-primary { @apply min-h-10 text-white rounded-lg px-4 py-2 bg-primary font-semibold transition-all hover:bg-primary-hover focus:ring-4 ring-input-ring } @@ -58,13 +67,13 @@ } .tooltip:before { - background: rgba(36, 39, 52, 0.75); - border-radius: 4px; - color: #fff; + background: var(--tooltip-background); + border-radius: var(--tooltip-border-radius); + color: var(--tooltip-color); content: attr(data-title); - font-size: 12px; - padding: 6px 10px; - top: 24px; + font-size: var(--tooltip-font-size); + padding: var(--tooltip-padding); + top: var(--tooltip-offset-top); white-space: nowrap; z-index: 10 } diff --git a/web/src/lib/components/base/calendar.svelte b/web/src/lib/components/base/calendar.svelte index fd886e59..ba026068 100644 --- a/web/src/lib/components/base/calendar.svelte +++ b/web/src/lib/components/base/calendar.svelte @@ -1,8 +1,9 @@ - -
- -
-
- {$_(category.name)} -
-
- - diff --git a/web/src/lib/components/confirm_modal.svelte b/web/src/lib/components/confirm_modal.svelte index 5970b252..1641b6c4 100644 --- a/web/src/lib/components/confirm_modal.svelte +++ b/web/src/lib/components/confirm_modal.svelte @@ -1,5 +1,6 @@ - + {#snippet content()} -

{text}

+ {#if children} + {@render children()} + {:else} +

{text}

+ {/if} {/snippet} {#snippet footer()}
diff --git a/web/src/lib/components/profile/feed_card.svelte b/web/src/lib/components/profile/feed_card.svelte index fa8d761e..4acce1ca 100644 --- a/web/src/lib/components/profile/feed_card.svelte +++ b/web/src/lib/components/profile/feed_card.svelte @@ -10,7 +10,13 @@ formatTimeHHMM, formatTimeSince, } from "$lib/util/format_util"; - import { _ } from "svelte-i18n"; + import { + displayCategoryIcon, + displayCategoryName, + displaySubcategoryIcon, + displaySubcategoryLabel, + } from "$lib/util/category_util"; + import { _, locale } from "svelte-i18n"; import TrailDropdown from "../trail/trail_dropdown.svelte"; interface Props { feedItem: FeedItem; @@ -26,11 +32,24 @@ const photos = $derived((feedItem.expand.item as Trail).photos); const location = $derived((feedItem.expand.item as Trail).location); - const category = $derived((feedItem.expand.item as Trail).expand?.category?.name); + const category = $derived( + (feedItem.expand.item as Trail).expand?.category, + ); + const subcategory = $derived( + (feedItem.expand.item as Trail).expand?.subcategory, + ); const trails = $derived((feedItem.expand.item as List).trails); const author = $derived(feedItem.expand.item.expand?.author); + + function feedCategoryIcon() { + if (subcategory) { + return displaySubcategoryIcon(subcategory, category); + } + + return displayCategoryIcon(category); + }
@@ -78,7 +97,20 @@
{#if category}

- {$_(category)} + {displayCategoryName( + category, + $locale, + )} + {#if subcategory} + + / {displaySubcategoryLabel( + subcategory, + $locale, + )} + + {/if}

{/if} {#if location} diff --git a/web/src/lib/components/settings/plugins/plugin_instance_settings_modal.svelte b/web/src/lib/components/settings/plugins/plugin_instance_settings_modal.svelte index be1e10de..ad9f7916 100644 --- a/web/src/lib/components/settings/plugins/plugin_instance_settings_modal.svelte +++ b/web/src/lib/components/settings/plugins/plugin_instance_settings_modal.svelte @@ -6,11 +6,18 @@ import TextField from "$lib/components/base/text_field.svelte"; import Toggle from "$lib/components/base/toggle.svelte"; import PluginMergeSettings from "$lib/components/settings/plugins/plugin_merge_settings.svelte"; + import CategoryPicker from "$lib/components/trail/category_picker.svelte"; import type { Category } from "$lib/models/category"; import type { PluginInstance } from "$lib/models/plugin_instance"; import type { ConfigField, PluginProvider } from "$lib/models/plugin_provider"; + import type { Subcategory } from "$lib/models/subcategory"; import { plugin_auth_validate, plugin_oauth_start } from "$lib/stores/plugin_instance_store"; import { show_toast } from "$lib/stores/toast_store.svelte"; + import { + categoryMappingTargetFromUnknown, + categoryMappingTargetToPickerValue, + type CategoryMappingTarget, + } from "$lib/util/category_util"; import { translatePluginAPIError } from "$lib/util/plugin_error_i18n"; import { configFieldDescription, @@ -34,12 +41,13 @@ interface Props { plugin: PluginProvider; categories?: Category[]; + subcategories?: Subcategory[]; instance?: PluginInstance; onbeforecategorymappingsave?: (instance: PluginInstanceForm) => Promise | boolean; onsave?: (instance: Partial) => Promise | PluginInstance | void; } - let { plugin, categories = [], instance, onbeforecategorymappingsave, onsave }: Props = $props(); + let { plugin, categories = [], subcategories = [], instance, onbeforecategorymappingsave, onsave }: Props = $props(); let modal: Modal; let auth: Record = $state(initialAuth()); @@ -73,14 +81,6 @@ ); let mergeAvailable = $derived((hostConfig().merge as any)?.available !== false); let supportsCategoryMapping = $derived(supportsPlanned || supportsCompleted); - let categorySelectItems: SelectItem[] = $derived( - categories - .map((category) => ({ - text: $_(category.name), - value: category.id, - })) - .sort((a, b) => a.text.localeCompare(b.text, $locale ?? undefined)), - ); let providerCategorySelectItems: SelectItem[] = $derived(providerCategoryItems()); let canAddCategoryMappingRow = $derived( categoryMappingRows.every((row) => row.providerCategory && row.category) && @@ -143,52 +143,108 @@ function initialCategoryMappingRows(): CategoryMappingRow[] { return Object.entries(categoryMapping()) - .filter(([, category]) => category !== "") - .map(([providerCategory, category]) => ({ + .filter(([, target]) => !isBlankCategoryMappingTarget(target)) + .map(([providerCategory, target]) => ({ providerCategory, - category: categoryTargetValue(category), + category: categoryPickerValue(target), })); } - function manifestCategoryMapping(): Record { + function manifestCategoryMapping(): Record { const raw = plugin.hostConfig?.categoryMapping; if (!raw || typeof raw !== "object" || Array.isArray(raw)) { return {}; } - return stringMapping(raw as Record); + return categoryTargetMapping(raw as Record); } - function categoryMapping(): Record { + function categoryMapping(): Record { const raw = hostConfig().categoryMapping; if (!raw || typeof raw !== "object" || Array.isArray(raw)) { return {}; } - return stringMapping(raw as Record); + return categoryTargetMapping(raw as Record); } function categoryMappingsEqual( - left: Record, - right: Record, + left: Record, + right: Record, ) { const leftKeys = Object.keys(left).sort(); const rightKeys = Object.keys(right).sort(); if (leftKeys.length !== rightKeys.length) { return false; } - return leftKeys.every((key, index) => key === rightKeys[index] && left[key] === right[key]); - } - - function stringMapping(raw: Record): Record { - return Object.fromEntries( - Object.entries(raw) - .filter(([, value]) => typeof value === "string") - .map(([key, value]) => [key, value as string]), + return leftKeys.every( + (key, index) => + key === rightKeys[index] && + normalizedCategoryMappingTarget(left[key]) === normalizedCategoryMappingTarget(right[key]), ); } - function categoryTargetValue(value: string): string { - const match = categories.find((category) => category.id === value || category.name === value); - return match?.id ?? value; + function categoryTargetMapping(raw: Record): Record { + return Object.fromEntries( + Object.entries(raw) + .map(([key, value]) => [ + key, + categoryMappingTargetFromUnknown(value), + ]) + .filter(([, value]) => value !== undefined), + ); + } + + function isBlankCategoryMappingTarget(target: CategoryMappingTarget): boolean { + if (typeof target === "string") { + return target.trim() === ""; + } + return !target.category?.trim() && !target.subcategory?.trim(); + } + + function normalizedCategoryMappingTarget(target: CategoryMappingTarget): string { + if (typeof target === "string") { + return target.trim(); + } + return JSON.stringify({ + category: target.category?.trim() ?? "", + subcategory: target.subcategory?.trim() ?? "", + }); + } + + function categoryPickerValue(target: CategoryMappingTarget): string { + return categoryMappingTargetToPickerValue( + target, + categories, + subcategories, + ); + } + + function categoryMappingTargetFromPickerValue(value: string): CategoryMappingTarget { + if (value.startsWith("subcategory:")) { + const subcategoryId = value.replace("subcategory:", ""); + const subcategory = subcategories.find((candidate) => candidate.id === subcategoryId); + return { + category: subcategory?.category ?? "", + subcategory: subcategoryId, + }; + } + if (value.startsWith("category:")) { + return { + category: value.replace("category:", ""), + subcategory: "", + }; + } + return ""; + } + + function categoryPickerCurrentCategoryId(value: string): string | null { + if (value.startsWith("category:")) { + return value.replace("category:", ""); + } + if (value.startsWith("subcategory:")) { + const subcategoryId = value.replace("subcategory:", ""); + return subcategories.find((candidate) => candidate.id === subcategoryId)?.category ?? null; + } + return null; } function providerCategoryItems(): SelectItem[] { @@ -389,7 +445,7 @@ ...currentMergeConfig, enabled: mergeAvailable && mergeEnabled, }; - const categoryMappingConfig: Record = {}; + const categoryMappingConfig: Record = {}; const assignedProviderCategories = new Set(); for (const row of categoryMappingRows) { const providerCategory = row.providerCategory.trim(); @@ -397,7 +453,7 @@ continue; } assignedProviderCategories.add(providerCategory); - categoryMappingConfig[providerCategory] = row.category; + categoryMappingConfig[providerCategory] = categoryMappingTargetFromPickerValue(row.category); } for (const providerCategory of [ ...Object.keys(manifestCategoryMapping()), @@ -651,7 +707,7 @@ {/if} {/each} - {#if supportsCategoryMapping && categorySelectItems.length > 0} + {#if supportsCategoryMapping && categories.length > 0}
@@ -671,7 +727,7 @@ {#if categoryMappingRows.length > 0}
- + currentCategoryId={categoryPickerCurrentCategoryId(row.category)} + fixedDropdown + onchange={(selection) => { + row.category = selection.subcategory + ? `subcategory:${selection.subcategory}` + : `category:${selection.category}`; + }} + > + {#if open} +
    + {#each items as item} +
  • + +
  • + {/each} +
+ {/if} +
diff --git a/web/src/lib/components/trail/trail_bulk_edit_modal.svelte b/web/src/lib/components/trail/trail_bulk_edit_modal.svelte new file mode 100644 index 00000000..d70bba1e --- /dev/null +++ b/web/src/lib/components/trail/trail_bulk_edit_modal.svelte @@ -0,0 +1,167 @@ + + + + + + {#snippet content()} +

+ {$_("bulk-edit-selected-trails", { + values: { n: selectedCount }, + })} +

+ +
+ + {#if applyCategory} + + {/if} + + + {#if applyDifficulty} + + {/if} +
+ {/snippet} + + {#snippet footer({ closeModal })} +
+ + +
+ {/snippet} +
diff --git a/web/src/lib/components/trail/trail_card.svelte b/web/src/lib/components/trail/trail_card.svelte index 12b3e957..aa4ea69e 100644 --- a/web/src/lib/components/trail/trail_card.svelte +++ b/web/src/lib/components/trail/trail_card.svelte @@ -10,7 +10,13 @@ formatElevation, formatTimeHHMM, } from "$lib/util/format_util"; - import { _ } from "svelte-i18n"; + import { + displayCategoryName, + displaySubcategoryLabel, + displayTrailCategoryBadgeIcon, + displayTrailCategoryIcon, + } from "$lib/util/category_util"; + import { _, locale } from "svelte-i18n"; import type { MouseEventHandler } from "svelte/elements"; import Chip from "../base/chip.svelte"; @@ -63,6 +69,7 @@ e.stopPropagation(); expandedTags = !expandedTags; } +
{#if trail.expand?.category?.name || trail.category}

- {$_( - trail.expand?.category?.name ?? - trail.category ?? - "-", - )} + + + {#if displayTrailCategoryBadgeIcon(trail)} + + {/if} + {displayCategoryName( + trail.expand?.category ?? { name: trail.category ?? "" }, + $locale, + ) || "-"} + {#if trail.expand?.subcategory} + + / {displaySubcategoryLabel( + trail.expand.subcategory, + $locale, + )} + + {/if}

{/if} {#if trail.location} diff --git a/web/src/lib/components/trail/trail_category_filter.svelte b/web/src/lib/components/trail/trail_category_filter.svelte new file mode 100644 index 00000000..a1df3259 --- /dev/null +++ b/web/src/lib/components/trail/trail_category_filter.svelte @@ -0,0 +1,693 @@ + + +{#snippet categoryButton(category: CategorySelectItem)} + {@const selected = filter.category.includes(category.value)} + {@const hasSubcategories = $subcategories.some( + (subcategory) => subcategory.category === category.value, + )} + {@const selectedSubcategoriesForCategory = selectedSubcategoryIds.filter( + (id) => + noSubcategoryFilterCategory(id) === category.value || + $subcategories.some( + (subcategory) => + subcategory.id === id && + subcategory.category === category.value, + ), + )} + {@const noSubcategorySelected = selectedSubcategoryIds.includes( + noSubcategoryFilterValue(category.value), + )} + {@const noSubcategoryInherited = + selected && selectedSubcategoriesForCategory.length === 0} + {@const noSubcategoryActive = + noSubcategorySelected || noSubcategoryInherited} + +{/snippet} + +
+

{$_("categories")}

+
+ {#each visibleCategoryItems as category} + {@render categoryButton(category)} + {/each} + + {#if overflowCategoryItems.length} +
{ + const nextTarget = e.relatedTarget; + if ( + nextTarget instanceof Node && + (e.currentTarget as HTMLElement).contains(nextTarget) + ) { + return; + } + + closeOverflow(); + }} + > + + + {#if overflowExpanded} +
+
+ {#each overflowCategoryItems as category} + {@render categoryButton(category)} + {/each} +
+
+ {/if} +
+ {/if} +
+ {#if categoryTooltip} +
+ {categoryTooltip} +
+ {/if} +
+{#if activeHiddenCategories.length} +
+ + {$_("category-filter-hidden-active", { + values: { + categories: activeHiddenCategories.join(", "), + }, + })} +
+{/if} diff --git a/web/src/lib/components/trail/trail_dropdown.svelte b/web/src/lib/components/trail/trail_dropdown.svelte index 2dc9fb5e..91c3fb99 100644 --- a/web/src/lib/components/trail/trail_dropdown.svelte +++ b/web/src/lib/components/trail/trail_dropdown.svelte @@ -26,6 +26,9 @@ import ConfirmModal from "../confirm_modal.svelte"; import ListSearchModal from "../list/list_search_modal.svelte"; import TrailExportModal from "./trail_export_modal.svelte"; + import TrailBulkEditModal, { + type TrailBulkEditChanges, + } from "./trail_bulk_edit_modal.svelte"; import TrailSendModal from "./trail_send_modal.svelte"; import TrailShareModal from "./trail_share_modal.svelte"; import { @@ -51,7 +54,7 @@ toggle?: Snippet<[any]>; onDelete?: () => void; onShare?: () => void; - onUpdate?: () => void; + onUpdate?: (updatedTrails?: Trail[]) => void; onMerge?: (result: MergeResult) => void; } @@ -63,6 +66,7 @@ let trailSendModal: TrailSendModal; let trailShareModal: TrailShareModal; let trailMergeModal: TrailMergeModal; + let trailBulkEditModal: TrailBulkEditModal; let lists: List[] = $state([]); @@ -190,6 +194,81 @@ return true; } + function allowBulkEdit(): boolean { + return allowPublish(); + } + + function selectedCategorySelection(): + | TrailBulkEditChanges["category"] + | undefined { + if (!trails || trails.size === 0) { + return undefined; + } + + const selectedTrails = [...trails]; + const firstCategory = + selectedTrails[0]?.category ?? + selectedTrails[0]?.expand?.category?.id ?? + selectedTrails[0]?.expand?.subcategory?.category ?? + ""; + const firstSubcategory = + selectedTrails[0]?.subcategory ?? + selectedTrails[0]?.expand?.subcategory?.id ?? + ""; + + if (!firstCategory) { + return undefined; + } + + const allSame = selectedTrails.every((candidate) => { + const category = + candidate.category ?? + candidate.expand?.category?.id ?? + candidate.expand?.subcategory?.category ?? + ""; + const subcategory = + candidate.subcategory ?? candidate.expand?.subcategory?.id ?? ""; + + return ( + category === firstCategory && + subcategory === firstSubcategory + ); + }); + + if (!allSame) { + return undefined; + } + + return { + category: firstCategory, + subcategory: firstSubcategory, + }; + } + + function selectedDifficulty(): + | TrailBulkEditChanges["difficulty"] + | undefined { + if (!trails || trails.size === 0) { + return undefined; + } + + const selectedTrails = [...trails]; + const firstDifficulty = selectedTrails[0]?.difficulty; + if (!firstDifficulty) { + return undefined; + } + + if ( + !selectedTrails.every( + (candidate) => candidate.difficulty === firstDifficulty, + ) + ) { + return undefined; + } + + return firstDifficulty; + } + function dropdownItems(): DropdownItem[] { const separator = (value: string): DropdownItem => ({ text: "", @@ -202,10 +281,20 @@ if (isMultiselectMode()) { return [ + ...(allowBulkEdit() + ? [ + { + text: $_("adjust"), + value: "bulk-edit", + icon: "pen", + }, + ] + : []), ...(allowMerge() ? [{ text: $_("link"), value: "merge", icon: "link" }] : []), - ...(allowMerge() && (canExport() || allowListManagement || allowPublish() || allowDelete()) + ...((allowBulkEdit() || allowMerge()) && + (canExport() || allowListManagement || allowPublish() || allowDelete()) ? [separator("sep-multi-actions")] : []), ...(canExport() @@ -288,14 +377,16 @@ ] : []), ...(allowFindSimilarTrails() - ? [{ - text: $_("find-similar-trails"), - value: "find-similar-trails", - icon: "link", - }] + ? [ + { + text: $_("find-similar-trails"), + value: "find-similar-trails", + icon: "link", + }, + ] : []), - ...(allowCopy() - ? [ + ...(allowCopy() + ? [ { text: $_("duplicate"), value: "copy", @@ -500,6 +591,8 @@ } } else if (ddVal == "publish") { updateTrailsVisibility(); + } else if (ddVal == "bulk-edit") { + trailBulkEditModal.openModal(); } else if (ddVal == "delete") { confirmModal.openModal(); } else if (item.value == "merge") { @@ -604,6 +697,66 @@ onUpdate?.(); } + async function updateTrailsBulk(changes: TrailBulkEditChanges) { + loading = true; + let updatedCount = 0; + const updatedTrails: Trail[] = []; + + for (const cTrail of trails ?? []) { + if (!cTrail || !canEditTrail(cTrail)) continue; + if (!cTrail.expand?.author?.id) continue; + + const origTrail: Trail = { + ...cTrail, + author: cTrail.expand.author.id, + }; + const updatedTrail: Trail = { + ...origTrail, + ...(changes.category + ? { + category: changes.category.category, + subcategory: changes.category.subcategory, + } + : {}), + ...(changes.difficulty + ? { difficulty: changes.difficulty } + : {}), + }; + + try { + const updated = await trails_update( + origTrail, + updatedTrail, + undefined, + undefined, + ["tags"], + ); + updatedTrails.push(updated); + updatedCount += 1; + } catch (e) { + console.error(e); + + show_toast({ + type: "error", + icon: "close", + text: `${$_("error-saving-trail")}: ${cTrail.name}`, + }); + } + } + + loading = false; + if (updatedCount > 0) { + show_toast({ + type: "success", + icon: "check", + text: $_("bulk-edit-updated-trails", { + values: { n: updatedCount }, + }), + }); + } + onUpdate?.(updatedTrails); + } + async function exportTrails(exportSettings: { fileFormat: "gpx" | "json"; photos: boolean; @@ -850,5 +1003,12 @@ bind:this={trailMergeModal} onmerge={(settings, selection) => mergeTrails(settings, selection)} > + updateTrailsBulk(changes)} +> diff --git a/web/src/lib/components/trail/trail_filter_panel.svelte b/web/src/lib/components/trail/trail_filter_panel.svelte index 7979edfd..bd11b54e 100644 --- a/web/src/lib/components/trail/trail_filter_panel.svelte +++ b/web/src/lib/components/trail/trail_filter_panel.svelte @@ -18,6 +18,7 @@ import Search, { type SearchItem } from "../base/search.svelte"; import type { SelectItem } from "../base/select.svelte"; import Slider from "../base/slider.svelte"; + import TrailCategoryFilter from "./trail_category_filter.svelte"; interface Props { categories: Category[]; @@ -37,13 +38,6 @@ onupdate, }: Props = $props(); - let categorySelectItems = $derived( - categories.map((c) => ({ - value: c.name, - text: c.name, - })), - ); - const radioGroupCompletenessItems: RadioItem[] = [ { text: $_("completed"), value: "completed" }, { text: $_("not-completed"), value: "not_completed" }, @@ -66,12 +60,6 @@ onupdate?.(filter); } - function setCategoryFilter(categories: SelectItem[]) { - filter.category = categories.map((c) => c.value); - - update(); - } - function setAuthorFilter(item: SearchItem) { filter.author = item.value.id; update(); @@ -200,15 +188,11 @@ {#if showTrailSearch}
{/if} - setCategoryFilter(value)} - value={categorySelectItems.filter((i) => - filter.category.includes(i.value), - )} - label={$_("categories")} - items={categorySelectItems} - placeholder={`${$_("filter-categories")}...`} - > +
initTrail)); + function trailCategoryIcon() { + if (trail.expand?.subcategory) { + return displaySubcategoryIcon( + trail.expand.subcategory, + trail.expand?.category, + ); + } + + return displayCategoryIcon(trail.expand?.category); + } + const tabs = [ $_("summit-book"), $_("photos"), @@ -747,10 +764,20 @@ >{#if mode == "overview"} {$_("category")} {:else} - + {/if} - {$_(trail.expand.category.name)} + + {displayCategoryName(trail.expand.category, $locale)} + {#if trail.expand?.subcategory} + + / {displaySubcategoryLabel( + trail.expand.subcategory, + $locale, + )} + + {/if} +
{/if} diff --git a/web/src/lib/components/trail/trail_list.svelte b/web/src/lib/components/trail/trail_list.svelte index 249b739f..7bcfcf10 100644 --- a/web/src/lib/components/trail/trail_list.svelte +++ b/web/src/lib/components/trail/trail_list.svelte @@ -31,7 +31,7 @@ let { filter = $bindable(null), - trails, + trails = $bindable([]), pagination = { page: 1, totalPages: 1, @@ -307,7 +307,40 @@ onupdate?.(filter, selection); } - async function handleTrailsEditDone(resetSelection: boolean = false) { + async function handleTrailsEditDone( + resetSelection: boolean = false, + updatedTrails: Trail[] = [], + ) { + if (updatedTrails.length > 0) { + const updatedByID = new Map( + updatedTrails + .filter((trail) => trail.id) + .map((trail) => [trail.id, trail]), + ); + // Keep the richer expand fields from the existing (search-derived) + // trail (e.g. author, likes) that trails_update does not return, + // and only overlay the changed values and their expands. + const mergeUpdated = (trail: Trail) => { + const updated = updatedByID.get(trail.id); + if (!updated) return trail; + return { + ...trail, + ...updated, + expand: { ...trail.expand, ...updated.expand }, + }; + }; + trails = trails.map(mergeUpdated); + if (resetSelection) { + selection = new Set(); + hoveredTrail = undefined; + } else if (selection) { + selection = new Set([...selection].map(mergeUpdated)); + } + // The updated trails already carry the fresh data; skip the refetch + // because the search index is updated asynchronously and would + // return stale categories, overwriting the local update. + return; + } if (resetSelection) { selection = new Set(); hoveredTrail = undefined; @@ -356,7 +389,8 @@ onDelete={() => handleTrailsEditDone(true)} onShare={() => handleTrailsEditDone(false)} onMerge={() => handleTrailsMergeDone(true)} - onUpdate={() => handleTrailsEditDone(true)} + onUpdate={(updatedTrails) => + handleTrailsEditDone(true, updatedTrails)} />
{/if} diff --git a/web/src/lib/components/trail/trail_list_item.svelte b/web/src/lib/components/trail/trail_list_item.svelte index 7d6524af..9c58f3b5 100644 --- a/web/src/lib/components/trail/trail_list_item.svelte +++ b/web/src/lib/components/trail/trail_list_item.svelte @@ -11,7 +11,13 @@ formatHTMLAsText, formatTimeHHMM, } from "$lib/util/format_util"; - import { _ } from "svelte-i18n"; + import { + displayCategoryName, + displaySubcategoryLabel, + displayTrailCategoryBadgeIcon, + displayTrailCategoryIcon, + } from "$lib/util/category_util"; + import { _, locale } from "svelte-i18n"; import ShareInfo from "../share_info.svelte"; import { handleFromRecordWithIRI } from "$lib/util/activitypub_util"; import Chip from "../base/chip.svelte"; @@ -57,6 +63,7 @@ e.stopPropagation(); expandedTags = !expandedTags; } +
  • {#if trail.expand?.category?.name || trail.category}

    - {$_( - trail.expand?.category?.name ?? trail.category ?? "-", - )} + + + {#if displayTrailCategoryBadgeIcon(trail)} + + {/if} + {displayCategoryName( + trail.expand?.category ?? { name: trail.category ?? "" }, + $locale, + ) || "-"} + {#if trail.expand?.subcategory} + + / {displaySubcategoryLabel( + trail.expand.subcategory, + $locale, + )} + + {/if}

    {/if} {#if trail.location} diff --git a/web/src/lib/components/trail/trail_picker.svelte b/web/src/lib/components/trail/trail_picker.svelte index deb2c46d..c322c27e 100644 --- a/web/src/lib/components/trail/trail_picker.svelte +++ b/web/src/lib/components/trail/trail_picker.svelte @@ -36,7 +36,7 @@ attributionControl: false, dragPan: false, scrollZoom: false, - preserveDrawingBuffer: true, + canvasContextAttributes: { preserveDrawingBuffer: true } }); } diff --git a/web/src/lib/i18n/locales/de.json b/web/src/lib/i18n/locales/de.json index 105966f2..5f2ffdec 100644 --- a/web/src/lib/i18n/locales/de.json +++ b/web/src/lib/i18n/locales/de.json @@ -3,9 +3,10 @@ "Canoeing": "Kanufahren", "Climbing": "Klettern", "Hiking": "Wandern", + "Running": "Laufen", "Other": "Sonstiges", "Skiing": "Skifahren", - "Walking": "Laufen", + "Walking": "Spazieren", "about": "Über", "account-delete-confirm": "Du bist dabei, dein Konto zu löschen. Alle deine Routen werden ebenfalls gelöscht. Möchtest du fortfahren?", "account-privacy": "Privatsphäre des Kontos", @@ -17,6 +18,7 @@ "add-waypoint": "Wegpunkt hinzufügen", "added-trail-to": "Route hinzugefügt zu", "added-trails-to": "Routen hinzugefügt zu", + "adjust": "Anpassen", "after": "Nach", "all-activities": "Alle Aktivitäten", "allow-auto-geolocate": "Beginne das Zeichnen einer neuen Route am aktuellen Standort", @@ -31,6 +33,7 @@ "append-waypoint-description": "Kommentar anhängen", "append-waypoint-photos": "Fotos hinzufügen", "append-waypoint-title": "Titel anhängen", + "apply": "Anwenden", "apply-user-settings": "Benutzereinstellungen anwenden", "attraction": "Sehenswürdigkeit", "author": "Autor", @@ -49,6 +52,8 @@ "bicycle-rental": "Fahrradverleih", "bicycle-shop": "Fahrrad-Reparatur", "bike-type": "Fahrradtyp", + "bulk-edit-selected-trails": "{n, plural, =1 {1 ausgewählte Route} other {# ausgewählte Routen}}", + "bulk-edit-updated-trails": "{n, plural, =1 {1 Route aktualisiert} other {# Routen aktualisiert}}", "bus-stop": "Bushaltestelle", "by": "von", "calendar": { @@ -70,6 +75,24 @@ "card": "{n, plural, =1 {Karte} other {Karten}}", "categories": "Kategorien", "category": "Kategorie", + "category-preference-visible": "Anzeigen", + "category-preferences": "Kategorie-Einstellungen", + "category-preferences-description": "Definiere, welche Kategorien und Unterkategorien für deine Routen relevant sind und in welcher Reihenfolge Kategorien erscheinen sollen.", + "confirm-disable-category-with-trails-title": "Kategorie ausblenden?", + "confirm-disable-category-intro": "Diese Kategorie wird noch an einigen Stellen verwendet. Prüfe die Konflikte, bevor du sie ausblendest.", + "confirm-disable-category-with-trails": "{count, plural, =1 {1 deiner eigenen Routen verwendet „{name}“.} other {# deiner eigenen Routen verwenden „{name}“.}}", + "confirm-disable-category-active-plugin-mappings": "Aktive Plugins referenzieren diese Kategorie in ihren Zuordnungen: {plugins}.", + "confirm-disable-category-inactive-plugin-mappings": "Vorbereitete, aber inaktive Plugins referenzieren diese Kategorie ebenfalls: {plugins}.", + "confirm-disable-category-anyway": "Diese Kategorie trotzdem ausblenden?", + "confirm-disable-subcategory-with-trails-title": "Unterkategorie ausblenden?", + "confirm-disable-subcategory-intro": "Diese Unterkategorie wird noch an einigen Stellen verwendet. Prüfe die Konflikte, bevor du sie ausblendest.", + "confirm-disable-subcategory-with-trails": "{count, plural, =1 {1 deiner eigenen Routen verwendet „{name}“.} other {# deiner eigenen Routen verwenden „{name}“.}}", + "confirm-disable-subcategory-active-plugin-mappings": "Aktive Plugins referenzieren diese Unterkategorie in ihren Zuordnungen: {plugins}.", + "confirm-disable-subcategory-inactive-plugin-mappings": "Vorbereitete, aber inaktive Plugins referenzieren diese Unterkategorie ebenfalls: {plugins}.", + "confirm-disable-subcategory-anyway": "Diese Unterkategorie trotzdem ausblenden?", + "conflicts": "Konflikte", + "collapse-subcategories": "Unterkategorien einklappen", + "category-filter-hidden-active": "{categories} ist in deinen Kategorie-Einstellungen ausgeblendet.", "category-mapping": "Kategorie-Zuordnung", "category-mapping-help": "Entfernte Provider-Kategorien werden bewusst nicht zugeordnet und erhalten beim Import keine Kategorie.", "change": "Ändern", @@ -175,6 +198,7 @@ "error-printing-map": "Fehler beim Drucken der Karte", "error-reading-file": "Fehler beim Lesen der Datei", "error-saving-list": "Fehler beim Speichern der Liste", + "error-saving-settings": "Fehler beim Speichern der Einstellungen", "error-saving-trail": "Fehler beim Speichern der Route", "error-setting-up-plugin": "Fehler beim Einrichten des {provider}-Plugins", "error-starting-oauth": "Fehler beim Starten der OAuth-Verbindung", @@ -185,9 +209,14 @@ "error-uploading-trail-to-hammerhead": "Fehler beim Hochladen der Route zu Hammerhead", "est-duration": "Gesch. Dauer", "everyone-with-the-link": "Jeder mit dem Link", + "expand-subcategories": "Unterkategorien ausklappen", "expand-trail-list": "", "expiration": "", "expires": "", + "exclude-federated": "Föderierte ausschließen", + "exclude-search": "Von Suche ausschließen", + "include-federated": "Föderierte einschließen", + "include-search": "In Suche einschließen", "explore": "Erkunden", "explore-some-trails": "Erkunde einige Routen", "export": "Exportieren", @@ -223,6 +252,9 @@ "get-started": "Los geht’s", "grid": "Gitter", "grocery-store": "Lebensmittelgeschäft", + "hide-design": "Im Routeneditor ausblenden", + "show-design": "Im Routeneditor auswählbar", + "hammerhead-integration-after-date-hint": "Wenn Ihr Hammerhead Konto bereits mit anderen Trail-Datenbanken wie komoot oder Strava synchronisiert ist, kann die zusätzliche Synchronisierung Ihrer Hammerhead-Daten zu Duplikaten führen. Um dies zu vermeiden, können Sie unten ein Startdatum festlegen, sodass nur Aktivitäten synchronisiert werden, die nach diesem Datum aufgezeichnet wurden.", "heading": "Überschrift", "height": "Höhe", "help": "Hilfe", @@ -362,6 +394,7 @@ "no-preference": "Keine Präferenz", "no-results": "Keine Ergebnisse gefunden", "no-routes-added": "Keine Routen hinzugefügt", + "no-subcategory": "Keine Unterkategorie", "no-waypoints-yet": "Noch keine Wegpunkte", "norwegian": "Norwegisch", "not-a-valid-email-address": "Keine gültige Email-Adresse", @@ -407,6 +440,7 @@ "profile": "Profil", "provider-category": "Provider-Kategorie", "public": "Öffentlich", + "priority": "Priorität", "public-access": "Öffentlicher Zugriff", "public-share-everyone": "Jeder im Internet mit dem Link kann diese Route sehen", "public-share-limited": "Nur Leute mit Zugriff können diese Route sehen", @@ -503,6 +537,7 @@ "stop-drawing": "Zeichnen beenden", "stop-editing": "Bearbeiten beenden", "subway-stop": "U-Bahn Eingang", + "subcategory": "Unterkategorie", "summit": "Gipfel", "summit-book": "Gipfelbuch", "summit-log": "{n, plural, =1 {Gipfelbuch-Eintrag} other {Gipfelbuch-Einträge}}", @@ -588,6 +623,7 @@ "tram-stop": "Tram Haltestelle", "unchanged": "unverändert", "units": "Einheiten", + "unprioritized": "Nicht priorisiert", "unlink": "Trennen", "upload-file": "Datei hochladen", "upload-gpx": "GPX hochladen", @@ -602,6 +638,7 @@ "username": "Nutzername", "username-not-unique": "Dieser Nutzername ist bereits vergeben. Bitte versuche es mit einem anderen.", "view": "Ansehen", + "view-affected-trails": "Betroffene Routen ansehen", "viewpoint": "Aussichtspunkt", "visibilty": "", "visibilty-status": "Sichtbarkeit", diff --git a/web/src/lib/i18n/locales/en.json b/web/src/lib/i18n/locales/en.json index f49ea388..7c6dd870 100644 --- a/web/src/lib/i18n/locales/en.json +++ b/web/src/lib/i18n/locales/en.json @@ -3,6 +3,7 @@ "Canoeing": "Canoeing", "Climbing": "Climbing", "Hiking": "Hiking", + "Running": "Running", "Other": "Other", "Skiing": "", "Walking": "Walking", @@ -17,6 +18,7 @@ "add-waypoint": "Add Waypoint", "added-trail-to": "Added trail to", "added-trails-to": "Added trails to", + "adjust": "Adjust", "after": "After", "all-activities": "All activities", "allow-auto-geolocate": "Begin drawing a new trail from your current location", @@ -31,6 +33,7 @@ "append-waypoint-description": "Append description", "append-waypoint-photos": "Add photos", "append-waypoint-title": "Append title", + "apply": "Apply", "apply-user-settings": "Apply user settings", "attraction": "Attraction", "author": "Author", @@ -49,6 +52,8 @@ "bicycle-rental": "Bicycle Rental", "bicycle-shop": "Bicycle Shop", "bike-type": "Bike Type", + "bulk-edit-selected-trails": "{n, plural, =1 {1 selected trail} other {# selected trails}}", + "bulk-edit-updated-trails": "{n, plural, =1 {1 trail updated} other {# trails updated}}", "bus-stop": "Bus stop", "by": "by", "calendar": { @@ -70,6 +75,24 @@ "card": "{n, plural, =1 {Card} other {Cards}}", "categories": "Categories", "category": "Category", + "category-preference-visible": "Show", + "category-preferences": "Category preferences", + "category-preferences-description": "Define which categories and subcategories are relevant to your trails and the order in which categories appear.", + "confirm-disable-category-with-trails-title": "Hide category?", + "confirm-disable-category-intro": "This category is still used in a few places. Review the conflicts before hiding it.", + "confirm-disable-category-with-trails": "{count, plural, =1 {1 of your own trails uses “{name}”.} other {# of your own trails use “{name}”.}}", + "confirm-disable-category-active-plugin-mappings": "Active plugins reference this category in their mappings: {plugins}.", + "confirm-disable-category-inactive-plugin-mappings": "Prepared but inactive plugins also reference this category: {plugins}.", + "confirm-disable-category-anyway": "Hide this category anyway?", + "confirm-disable-subcategory-with-trails-title": "Hide subcategory?", + "confirm-disable-subcategory-intro": "This subcategory is still used in a few places. Review the conflicts before hiding it.", + "confirm-disable-subcategory-with-trails": "{count, plural, =1 {1 of your own trails uses “{name}”.} other {# of your own trails use “{name}”.}}", + "confirm-disable-subcategory-active-plugin-mappings": "Active plugins reference this subcategory in their mappings: {plugins}.", + "confirm-disable-subcategory-inactive-plugin-mappings": "Prepared but inactive plugins also reference this subcategory: {plugins}.", + "confirm-disable-subcategory-anyway": "Hide this subcategory anyway?", + "conflicts": "Conflicts", + "collapse-subcategories": "Collapse subcategories", + "category-filter-hidden-active": "{categories} is hidden in your category preferences.", "category-mapping": "Category mapping", "category-mapping-help": "Removed provider categories are intentionally left unmapped and will be imported without a category.", "change": "Change", @@ -175,6 +198,7 @@ "error-printing-map": "Error printing map", "error-reading-file": "Error reading file", "error-saving-list": "Error saving list", + "error-saving-settings": "Error saving settings", "error-saving-trail": "Error saving trail", "error-setting-up-plugin": "Error setting up {provider} plugin", "error-starting-oauth": "Error starting OAuth connection", @@ -185,9 +209,14 @@ "error-uploading-trail-to-hammerhead": "Error uploading trail to Hammerhead", "est-duration": "Est. duration", "everyone-with-the-link": "Everyone with the link", + "expand-subcategories": "Expand subcategories", "expand-trail-list": "Expand trail list", "expiration": "Expiration", "expires": "Expires", + "exclude-federated": "Exclude federated", + "exclude-search": "Exclude from search", + "include-federated": "Include federated", + "include-search": "Include in search", "explore": "Explore", "explore-some-trails": "Explore some trails", "export": "Export", @@ -223,6 +252,9 @@ "get-started": "Get started", "grid": "Grid", "grocery-store": "Grocery store", + "hide-design": "Hide in trail editor", + "show-design": "Available in trail editor", + "hammerhead-integration-after-date-hint": "If your hammerhead account is already synced with other trail databases, such as komoot or Strava, start syncing your Hammerhead data may result in duplicates. To avoid this, you can set an start date below, meaning only activities recorded after this date will be synced.", "heading": "Heading", "height": "Height", "help": "Help", @@ -362,6 +394,7 @@ "no-preference": "No preference", "no-results": "No results found", "no-routes-added": "No routes added", + "no-subcategory": "No subcategory", "no-waypoints-yet": "No waypoints yet", "norwegian": "Norwegian", "not-a-valid-email-address": "Not a valid email address", @@ -407,6 +440,7 @@ "profile": "Profile", "provider-category": "Provider category", "public": "Public", + "priority": "Priority", "public-access": "Public access", "public-share-everyone": "Everyone on the internet with the link can see this trail", "public-share-limited": "Only people with access can open the link", @@ -503,6 +537,7 @@ "stop-drawing": "Stop drawing", "stop-editing": "Stop editing", "subway-stop": "Subway entrance", + "subcategory": "Subcategory", "summit": "Summit", "summit-book": "Summit Book", "summit-log": "{n, plural, =1 {Summit log} other {Summit logs}}", @@ -588,6 +623,7 @@ "tram-stop": "Tram stop", "unchanged": "unchanged", "units": "Units", + "unprioritized": "Unprioritized", "unlink": "Unlink", "upload-file": "Upload file", "upload-gpx": "Upload GPX", @@ -602,6 +638,7 @@ "username": "Username", "username-not-unique": "This username is already taken. Please try another.", "view": "View", + "view-affected-trails": "View affected trails", "viewpoint": "Viewpoint", "visibilty": "Visibility", "visibilty-status": "Visibility status", diff --git a/web/src/lib/models/api/category_preference_schema.ts b/web/src/lib/models/api/category_preference_schema.ts new file mode 100644 index 00000000..3ad52055 --- /dev/null +++ b/web/src/lib/models/api/category_preference_schema.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +const UserCategoryPreferenceUpsertSchema = z.object({ + category: z.string().length(15), + visible: z.boolean(), +}); + +const UserCategoryPreferenceReorderSchema = z.object({ + categories: z.array(z.string().length(15)), +}); + +export { + UserCategoryPreferenceReorderSchema, + UserCategoryPreferenceUpsertSchema, +}; diff --git a/web/src/lib/models/api/openapi_schemas.ts b/web/src/lib/models/api/openapi_schemas.ts index aa2a218a..7f18af3d 100644 --- a/web/src/lib/models/api/openapi_schemas.ts +++ b/web/src/lib/models/api/openapi_schemas.ts @@ -158,6 +158,195 @@ * type: string * description: Tag name * + * CategoryTranslation: + * type: object + * properties: + * name: + * type: string + * short_name: + * type: string + * + * Category: + * type: object + * required: + * - id + * - name + * properties: + * id: + * type: string + * description: Category ID (15 chars) + * name: + * type: string + * short_name: + * type: string + * nullable: true + * icon: + * type: string + * nullable: true + * translations: + * type: object + * nullable: true + * additionalProperties: + * $ref: '#/components/schemas/CategoryTranslation' + * settings: + * type: object + * nullable: true + * properties: + * wp_merge_enabled: + * type: boolean + * wp_merge_radius: + * type: number + * created: + * type: string + * format: date-time + * updated: + * type: string + * format: date-time + * + * Subcategory: + * type: object + * required: + * - id + * - category + * - name + * properties: + * id: + * type: string + * description: Subcategory ID (15 chars) + * category: + * type: string + * description: Parent category ID (15 chars) + * name: + * type: string + * short_name: + * type: string + * nullable: true + * icon: + * type: string + * nullable: true + * badge_icon: + * type: string + * nullable: true + * translations: + * type: object + * nullable: true + * additionalProperties: + * $ref: '#/components/schemas/CategoryTranslation' + * created: + * type: string + * format: date-time + * updated: + * type: string + * format: date-time + * + * UserCategoryPreference: + * type: object + * required: + * - id + * - user + * - category + * - visible + * properties: + * id: + * type: string + * description: Preference ID (15 chars) + * user: + * type: string + * description: User ID (15 chars) + * category: + * type: string + * description: Category ID (15 chars) + * visible: + * type: boolean + * priority: + * type: integer + * nullable: true + * created: + * type: string + * format: date-time + * updated: + * type: string + * format: date-time + * + * UserCategoryPreferenceUpsertInput: + * type: object + * required: + * - category + * - visible + * properties: + * category: + * type: string + * description: Category ID (15 chars) + * visible: + * type: boolean + * + * UserCategoryPreferenceReorderInput: + * type: object + * required: + * - categories + * properties: + * categories: + * type: array + * items: + * type: string + * description: Category ID (15 chars) + * + * UserSubcategoryPreference: + * type: object + * required: + * - id + * - user + * - subcategory + * - visible + * properties: + * id: + * type: string + * description: Preference ID (15 chars) + * user: + * type: string + * description: User ID (15 chars) + * subcategory: + * type: string + * description: Subcategory ID (15 chars) + * visible: + * type: boolean + * priority: + * type: integer + * nullable: true + * created: + * type: string + * format: date-time + * updated: + * type: string + * format: date-time + * + * UserSubcategoryPreferenceUpsertInput: + * type: object + * required: + * - subcategory + * - visible + * properties: + * subcategory: + * type: string + * description: Subcategory ID (15 chars) + * visible: + * type: boolean + * + * UserSubcategoryPreferenceReorderInput: + * type: object + * required: + * - category + * - subcategories + * properties: + * category: + * type: string + * description: Category ID (15 chars) + * subcategories: + * type: array + * items: + * type: string + * description: Subcategory ID (15 chars) + * * Trail: * type: object * required: @@ -220,6 +409,9 @@ * category: * type: string * description: Category ID (15 chars) + * subcategory: + * type: string + * description: Subcategory ID (15 chars) * tags: * type: array * items: @@ -290,6 +482,8 @@ * default: 0 * category: * type: string + * subcategory: + * type: string * tags: * type: array * items: @@ -348,6 +542,8 @@ * default: 0 * category: * type: string + * subcategory: + * type: string * tags: * type: array * items: diff --git a/web/src/lib/models/api/subcategory_preference_schema.ts b/web/src/lib/models/api/subcategory_preference_schema.ts new file mode 100644 index 00000000..ac36e940 --- /dev/null +++ b/web/src/lib/models/api/subcategory_preference_schema.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +const UserSubcategoryPreferenceUpsertSchema = z.object({ + subcategory: z.string().length(15), + visible: z.boolean(), +}); + +const UserSubcategoryPreferenceReorderSchema = z.object({ + category: z.string().length(15), + subcategories: z.array(z.string().length(15)), +}); + +export { + UserSubcategoryPreferenceReorderSchema, + UserSubcategoryPreferenceUpsertSchema, +}; diff --git a/web/src/lib/models/api/trail_schema.ts b/web/src/lib/models/api/trail_schema.ts index 609b6380..5824daf8 100644 --- a/web/src/lib/models/api/trail_schema.ts +++ b/web/src/lib/models/api/trail_schema.ts @@ -21,6 +21,7 @@ const TrailCreateSchema = z.object({ thumbnail: z.number().int().nonnegative().optional(), like_count: z.number().int().min(0).optional().default(0), category: z.string().length(15).optional().or(z.literal('')), + subcategory: z.string().length(15).optional().or(z.literal('')), tags: z.array(z.string()).default([]), gpx: z.string().optional(), author: z.string().length(15), @@ -47,6 +48,7 @@ const TrailUpdateSchema = z.object({ thumbnail: z.number().int().nonnegative().optional(), like_count: z.number().int().min(0).optional(), category: z.string().optional(), + subcategory: z.string().optional(), tags: z.array(z.string()).optional(), gpx: z.string().optional(), }) satisfies ZodType> diff --git a/web/src/lib/models/category.ts b/web/src/lib/models/category.ts index cd39a7ea..f3c98956 100644 --- a/web/src/lib/models/category.ts +++ b/web/src/lib/models/category.ts @@ -1,14 +1,22 @@ interface Category { id: string; name: string; - img: string; + short_name?: string | null; + icon?: string | null; + translations?: Record | null; settings?: Settings | null; } +interface CategoryTranslation { + name?: string; + short_name?: string; +} + interface Settings { wp_merge_enabled?: boolean; wp_merge_radius?: number; } export type {Category} +export type {CategoryTranslation} export type {Settings} diff --git a/web/src/lib/models/category_preference.ts b/web/src/lib/models/category_preference.ts new file mode 100644 index 00000000..e1a2d97a --- /dev/null +++ b/web/src/lib/models/category_preference.ts @@ -0,0 +1,9 @@ +interface UserCategoryPreference { + id?: string; + user: string; + category: string; + visible?: boolean; + priority?: number | null; +} + +export type { UserCategoryPreference }; diff --git a/web/src/lib/models/settings.ts b/web/src/lib/models/settings.ts index 529c2a46..58a0286f 100644 --- a/web/src/lib/models/settings.ts +++ b/web/src/lib/models/settings.ts @@ -24,7 +24,6 @@ class Settings { bio?: string | null; mapFocus?: "trails" | "location"; location?: { name: string, lat: number, lon: number } | null; - category?: string; tilesets?: ({ name: string, url: string }[]) | null terrain?: { terrain?: string, hillshading?: string } | null; user?: string; @@ -39,7 +38,6 @@ class Settings { user: string, params?: { location?: { name: string, lat: number, lon: number } - category?: string tilesets?: { name: string, url: string }[] terrain?: { terrain: string, hillshading: string }; } @@ -49,7 +47,6 @@ class Settings { this.mapFocus = mapFocus; this.user = user; this.location = params?.location; - this.category = params?.category; this.tilesets = params?.tilesets ?? []; this.terrain = params?.terrain; } diff --git a/web/src/lib/models/subcategory.ts b/web/src/lib/models/subcategory.ts new file mode 100644 index 00000000..aa911ab6 --- /dev/null +++ b/web/src/lib/models/subcategory.ts @@ -0,0 +1,16 @@ +import type { Category, CategoryTranslation } from "./category"; + +interface Subcategory { + id: string; + category: string; + name: string; + short_name?: string | null; + icon?: string | null; + badge_icon?: string | null; + translations?: Record | null; + expand?: { + category?: Category; + }; +} + +export type { Subcategory }; diff --git a/web/src/lib/models/subcategory_preference.ts b/web/src/lib/models/subcategory_preference.ts new file mode 100644 index 00000000..fad335a1 --- /dev/null +++ b/web/src/lib/models/subcategory_preference.ts @@ -0,0 +1,9 @@ +interface UserSubcategoryPreference { + id?: string; + user: string; + subcategory: string; + visible?: boolean; + priority?: number | null; +} + +export type { UserSubcategoryPreference }; diff --git a/web/src/lib/models/trail.ts b/web/src/lib/models/trail.ts index d1c7f58b..4da19cf4 100644 --- a/web/src/lib/models/trail.ts +++ b/web/src/lib/models/trail.ts @@ -3,6 +3,7 @@ import type { Actor } from "./activitypub/actor"; import type { Category } from "./category"; import type { Comment } from "./comment"; import type GPX from "./gpx/gpx"; +import type { Subcategory } from "./subcategory"; import type { SummitLog } from "./summit_log"; import type { Tag } from "./tag"; import type { TrailLike } from "./trail_like"; @@ -29,6 +30,7 @@ class Trail { created?: string; updated?: string; category?: string; + subcategory?: string; tags: string[]; polyline?: string; domain?: string; @@ -38,6 +40,7 @@ class Trail { expand?: { tags?: Tag[] category?: Category; + subcategory?: Subcategory; waypoints_via_trail?: Waypoint[] summit_logs_via_trail?: SummitLog[] author?: Actor @@ -70,6 +73,7 @@ class Trail { gpx?: string, gpx_data?: string, category?: Category, + subcategory?: Subcategory, waypoints?: Waypoint[], summit_logs?: SummitLog[], comments?: Comment[], @@ -98,10 +102,13 @@ class Trail { this.photos = params?.photos ?? []; this.tags = []; this.gpx = params?.gpx; + this.category = params?.category?.id; + this.subcategory = params?.subcategory?.id; this.bounding_box_diagonal = params?.bounding_box_diagonal ?? 0; this.like_count = 0 this.expand = { category: params?.category, + subcategory: params?.subcategory, waypoints_via_trail: params?.waypoints ?? [], summit_logs_via_trail: params?.summit_logs ?? [], comments_via_trail: params?.comments ?? [], @@ -129,6 +136,7 @@ class Trail { public: orig.public, tags: orig.expand?.tags, category: orig.expand?.category, + subcategory: orig.expand?.subcategory, gpx_data: orig.expand?.gpx_data, waypoints: orig.expand?.waypoints_via_trail?.map(wp => new Waypoint(wp.lat, wp.lon, { id: cryptoRandomString({ length: 15 }), @@ -143,6 +151,7 @@ class Trail { interface TrailFilter { q: string, category: string[], + subcategory: string[], tags: string[], difficulty: (0 | 1 | 2)[] author?: string; @@ -187,6 +196,7 @@ interface TrailBoundingBox { min_lat: number, max_lon: number, min_lon: number, + has_trails?: boolean, } @@ -204,6 +214,12 @@ interface TrailSearchResult { duration: number; difficulty: 0 | 1 | 2; category: string; + category_id?: string | null; + category_icon?: string; + subcategory_id?: string | null; + is_federated?: boolean; + federated_category_name?: string | null; + federated_subcategory_name?: string | null; completed: boolean; date: number; created: number; @@ -238,6 +254,12 @@ export const defaultTrailSearchAttributes = [ "duration", "difficulty", "category", + "category_id", + "category_icon", + "subcategory_id", + "is_federated", + "federated_category_name", + "federated_subcategory_name", "completed", "date", "created", diff --git a/web/src/lib/server/category_preference_filter.ts b/web/src/lib/server/category_preference_filter.ts new file mode 100644 index 00000000..c2688c96 --- /dev/null +++ b/web/src/lib/server/category_preference_filter.ts @@ -0,0 +1,159 @@ +import type { UserCategoryPreference } from "$lib/models/category_preference"; +import type { UserSubcategoryPreference } from "$lib/models/subcategory_preference"; +import { Collection } from "$lib/util/api_util"; +import type { RequestEvent } from "@sveltejs/kit"; + +type MeiliFilter = string | string[] | undefined; + +type TrailPreferenceCache = { + categories?: Promise; + subcategories?: Promise; +}; + +const preferenceCache = new WeakMap(); + +function quotedList(ids: string[]) { + return `[${ids.map((id) => `'${id}'`).join(", ")}]`; +} + +function meiliFilterParts(filter: unknown): string[] { + if (!filter) { + return []; + } + if (typeof filter === "string") { + return filter.trim() ? [filter] : []; + } + if (Array.isArray(filter)) { + return filter.flatMap((item) => meiliFilterParts(item)); + } + + return []; +} + +function isIdOnlyDetailQuery(parts: string[]) { + return parts.length > 0 && parts.every((part) => /\bid\s+IN\b/i.test(part)); +} + +async function userCategoryPreferences(event: RequestEvent) { + if (!event.locals.user) { + return []; + } + + const cache = cachedTrailPreferences(event); + cache.categories ??= event.locals.pb + .collection(Collection.user_category_preferences) + .getFullList({ + filter: event.locals.pb.filter("user = {:user}", { + user: event.locals.user.id, + }), + requestKey: null, + }); + + return cache.categories; +} + +async function userSubcategoryPreferences(event: RequestEvent) { + if (!event.locals.user) { + return []; + } + + const cache = cachedTrailPreferences(event); + cache.subcategories ??= event.locals.pb + .collection(Collection.user_subcategory_preferences) + .getFullList({ + filter: event.locals.pb.filter("user = {:user}", { + user: event.locals.user.id, + }), + requestKey: null, + }); + + return cache.subcategories; +} + +function cachedTrailPreferences(event: RequestEvent) { + let cache = preferenceCache.get(event); + if (!cache) { + cache = {}; + preferenceCache.set(event, cache); + } + + return cache; +} + +async function trailPreferenceExclusions(event: RequestEvent) { + const [preferences, subcategoryPreferences] = await Promise.all([ + userCategoryPreferences(event), + userSubcategoryPreferences(event), + ]); + + return { + hiddenCategoryIds: preferences + .filter((preference) => preference.visible === false) + .map((preference) => preference.category), + hiddenSubcategoryIds: subcategoryPreferences + .filter((preference) => preference.visible === false) + .map((preference) => preference.subcategory), + }; +} + +export async function withTrailPreferenceMeiliFilter( + event: RequestEvent, + filter: MeiliFilter, +): Promise { + const parts = meiliFilterParts(filter); + if (!event.locals.user || isIdOnlyDetailQuery(parts)) { + return filter; + } + + const { hiddenCategoryIds, hiddenSubcategoryIds } = + await trailPreferenceExclusions(event); + + const preferenceParts: string[] = []; + if (hiddenCategoryIds.length) { + preferenceParts.push( + `(category_id IS NULL OR category_id NOT IN ${quotedList(hiddenCategoryIds)})`, + ); + } + if (hiddenSubcategoryIds.length) { + preferenceParts.push( + `(subcategory_id IS NULL OR subcategory_id NOT IN ${quotedList(hiddenSubcategoryIds)})`, + ); + } + + const nextParts = [...parts, ...preferenceParts]; + if (!nextParts.length) { + return undefined; + } + + return nextParts; +} + +function pbNotEqualsAll(field: string, ids: string[]) { + return ids.map((id) => `${field} != "${id}"`).join(" && "); +} + +export async function withTrailPreferencePocketBaseFilter( + event: RequestEvent, + filter?: string, +) { + if (!event.locals.user) { + return filter; + } + + const { hiddenCategoryIds, hiddenSubcategoryIds } = + await trailPreferenceExclusions(event); + + const parts = filter?.trim() ? [filter] : []; + if (hiddenCategoryIds.length) { + parts.push( + `(category = "" || (${pbNotEqualsAll("category", hiddenCategoryIds)}))`, + ); + } + if (hiddenSubcategoryIds.length) { + parts.push( + `(subcategory = "" || (${pbNotEqualsAll("subcategory", hiddenSubcategoryIds)}))`, + ); + } + + return parts.length ? parts.join(" && ") : undefined; +} diff --git a/web/src/lib/stores/category_preference_store.ts b/web/src/lib/stores/category_preference_store.ts new file mode 100644 index 00000000..41523c8f --- /dev/null +++ b/web/src/lib/stores/category_preference_store.ts @@ -0,0 +1,58 @@ +import type { UserCategoryPreference } from "$lib/models/category_preference"; +import { APIError } from "$lib/util/api_util"; +import { writable, type Writable } from "svelte/store"; + +export const categoryPreferences: Writable = writable( + [], +); + +export async function category_preferences_index( + f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch, +) { + const r = await f("/api/v1/user-category-preference", { + method: "GET", + }); + if (!r.ok) { + const response = await r.json(); + throw new APIError(r.status, response.message, response.detail); + } + + const response = (await r.json()) as UserCategoryPreference[]; + categoryPreferences.set(response); + + return response; +} + +export async function category_preferences_save( + preference: Pick, +) { + const r = await fetch("/api/v1/user-category-preference", { + method: "PUT", + body: JSON.stringify(preference), + }); + if (!r.ok) { + const response = await r.json(); + throw new APIError(r.status, response.message, response.detail); + } + + const response = (await r.json()) as UserCategoryPreference; + categoryPreferences.update((preferences) => [ + ...preferences.filter((item) => item.id !== response.id), + response, + ]); + + return response; +} + +export async function category_preferences_reorder(categories: string[]) { + const r = await fetch("/api/v1/user-category-preference/reorder", { + method: "POST", + body: JSON.stringify({ categories }), + }); + if (!r.ok) { + const response = await r.json(); + throw new APIError(r.status, response.message, response.detail); + } + + return await r.json(); +} diff --git a/web/src/lib/stores/list_store.ts b/web/src/lib/stores/list_store.ts index 6266fceb..7d6233b1 100644 --- a/web/src/lib/stores/list_store.ts +++ b/web/src/lib/stores/list_store.ts @@ -79,7 +79,7 @@ export async function lists_search_filter(filter: ListFilter, page: number = 1, export async function lists_show(id: string, handle?: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch) { const r = await f(`/api/v1/list/${id}?` + new URLSearchParams({ - expand: "author,trails,trails.author,trails.category,trails.tags,list_share_via_list.actor", + expand: "author,trails,trails.author,trails.category,trails.subcategory,trails.subcategory.category,trails.tags,list_share_via_list.actor", ...(handle ? { handle } : {}) }), { method: 'GET', @@ -291,4 +291,4 @@ export async function searchResultToLists(hits: Hits): Promise } return lists -} \ No newline at end of file +} diff --git a/web/src/lib/stores/profile_store.ts b/web/src/lib/stores/profile_store.ts index 66da7b0f..f4e79444 100644 --- a/web/src/lib/stores/profile_store.ts +++ b/web/src/lib/stores/profile_store.ts @@ -118,7 +118,7 @@ export async function profile_stats_index(handle: string, filter: SummitLogFilte const r = await f(`/api/v1/profile/${handle}/stats?` + new URLSearchParams({ filter: filterText, - expand: "trail.category,author", + expand: "trail.category,trail.subcategory,trail.subcategory.category,author", sort: "+date", }), { method: 'GET', @@ -155,4 +155,4 @@ export async function profile_follows_index(handle: string, type: "followers" | follows = result; return { ...fetchedFollows, items: result }; -} \ No newline at end of file +} diff --git a/web/src/lib/stores/subcategory_preference_store.ts b/web/src/lib/stores/subcategory_preference_store.ts new file mode 100644 index 00000000..91f108d0 --- /dev/null +++ b/web/src/lib/stores/subcategory_preference_store.ts @@ -0,0 +1,60 @@ +import type { UserSubcategoryPreference } from "$lib/models/subcategory_preference"; +import { APIError } from "$lib/util/api_util"; +import { writable, type Writable } from "svelte/store"; + +export const subcategoryPreferences: Writable = + writable([]); + +export async function subcategory_preferences_index( + f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch, +) { + const r = await f("/api/v1/user-subcategory-preference", { + method: "GET", + }); + if (!r.ok) { + const response = await r.json(); + throw new APIError(r.status, response.message, response.detail); + } + + const response = (await r.json()) as UserSubcategoryPreference[]; + subcategoryPreferences.set(response); + + return response; +} + +export async function subcategory_preferences_save( + preference: Pick, +) { + const r = await fetch("/api/v1/user-subcategory-preference", { + method: "PUT", + body: JSON.stringify(preference), + }); + if (!r.ok) { + const response = await r.json(); + throw new APIError(r.status, response.message, response.detail); + } + + const response = (await r.json()) as UserSubcategoryPreference; + subcategoryPreferences.update((preferences) => [ + ...preferences.filter((item) => item.id !== response.id), + response, + ]); + + return response; +} + +export async function subcategory_preferences_reorder( + category: string, + subcategories: string[], +) { + const r = await fetch("/api/v1/user-subcategory-preference/reorder", { + method: "POST", + body: JSON.stringify({ category, subcategories }), + }); + if (!r.ok) { + const response = await r.json(); + throw new APIError(r.status, response.message, response.detail); + } + + return await r.json(); +} diff --git a/web/src/lib/stores/subcategory_store.ts b/web/src/lib/stores/subcategory_store.ts new file mode 100644 index 00000000..17d0a6df --- /dev/null +++ b/web/src/lib/stores/subcategory_store.ts @@ -0,0 +1,31 @@ +import type { Subcategory } from "$lib/models/subcategory"; +import { APIError } from "$lib/util/api_util"; +import { type ListResult } from "pocketbase"; +import { writable, type Writable } from "svelte/store"; + +export const subcategories: Writable = writable([]); + +export async function subcategories_index( + f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch, +) { + const r = await f( + "/api/v1/subcategory?" + + new URLSearchParams({ + perPage: "-1", + expand: "category", + sort: "category,name", + }), + { + method: "GET", + }, + ); + if (!r.ok) { + const response = await r.json(); + throw new APIError(r.status, response.message, response.detail); + } + + const response: ListResult = await r.json(); + subcategories.set(response.items); + + return response.items as Subcategory[]; +} diff --git a/web/src/lib/stores/summit_log_store.ts b/web/src/lib/stores/summit_log_store.ts index 94665169..d7962c4b 100644 --- a/web/src/lib/stores/summit_log_store.ts +++ b/web/src/lib/stores/summit_log_store.ts @@ -13,7 +13,7 @@ export async function summit_logs_index(filter?: SummitLogFilter, handle?: strin const r = await f('/api/v1/summit-log?' + new URLSearchParams({ ...(filter ? { filter: buildFilterText(filter) } : {}), perPage: "-1", - expand: "trail.category,author", + expand: "trail.category,trail.subcategory,trail.subcategory.category,author", sort: "+date", ...(handle ? { handle } : {}) }), { diff --git a/web/src/lib/stores/trail_store.ts b/web/src/lib/stores/trail_store.ts index 8da25cc5..af7bb196 100644 --- a/web/src/lib/stores/trail_store.ts +++ b/web/src/lib/stores/trail_store.ts @@ -1,16 +1,19 @@ import type { SummitLog } from "$lib/models/summit_log"; import type { Tag } from "$lib/models/tag"; import { MAP_MAX_POLYLINES } from "$lib/config/map"; -import { defaultTrailSearchAttributes, Trail, type TrailFilter, type TrailFilterValues, type TrailSearchResult } from "$lib/models/trail"; +import { defaultTrailSearchAttributes, Trail, type TrailBoundingBox, type TrailFilter, type TrailFilterValues, type TrailSearchResult } from "$lib/models/trail"; import type { Waypoint } from "$lib/models/waypoint"; import { APIError } from "$lib/util/api_util"; import { deepEqual } from "$lib/util/deep_util"; import { getFileURL, objectToFormData } from "$lib/util/file_util"; +import { noSubcategoryFilterCategory } from "$lib/util/trail_filter_util"; import * as M from "maplibre-gl"; import type { Hits } from "meilisearch"; import { type AuthRecord, type ListResult, type RecordModel } from "pocketbase"; import { get, writable, type Writable } from "svelte/store"; import { summit_logs_create, summit_logs_delete, summit_logs_update } from "./summit_log_store"; +import { categories } from "./category_store"; +import { subcategories } from "./subcategory_store"; import { tags_create } from "./tag_store"; import { currentUser } from "./user_store"; import { waypoints_create, waypoints_delete, waypoints_update } from "./waypoint_store"; @@ -18,7 +21,7 @@ import { waypoints_create, waypoints_delete, waypoints_update } from "./waypoint export async function trails_index(perPage: number = 21, random: boolean = false, f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch) { const r = await f('/api/v1/trail?' + new URLSearchParams({ "perPage": perPage.toString(), - expand: "category,waypoints_via_trail,summit_logs_via_trail,tags", + expand: "category,subcategory,subcategory.category,waypoints_via_trail,summit_logs_via_trail,tags", sort: random ? "@random" : "", }), { method: 'GET', @@ -322,7 +325,7 @@ export async function trails_search_bounding_box( export async function trails_show(id: string, handle?: string, share?: string, loadGPX?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch) { const r = await f(`/api/v1/trail/${id}?` + new URLSearchParams({ - expand: "category,waypoints_via_trail,summit_logs_via_trail,summit_logs_via_trail.author,trail_share_via_trail.actor,trail_like_via_trail,tags,author", + expand: "category,subcategory,subcategory.category,waypoints_via_trail,summit_logs_via_trail,summit_logs_via_trail.author,trail_share_via_trail.actor,trail_like_via_trail,tags,author", ...(handle ? { handle } : {}), ...(share ? { share } : {}) }), { @@ -393,7 +396,7 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl } let r = await f(`/api/v1/trail/form?` + new URLSearchParams({ - expand: "category,waypoints_via_trail,summit_logs_via_trail,trail_share_via_trail,tags", + expand: "category,subcategory,subcategory.category,waypoints_via_trail,summit_logs_via_trail,trail_share_via_trail,tags", }), { method: 'PUT', body: formData, @@ -520,7 +523,7 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F const updateUrl = `/api/v1/trail/form/${newTrail.id}?` + new URLSearchParams({ - expand: "category,waypoints_via_trail,summit_logs_via_trail,trail_share_via_trail,tags", + expand: "category,subcategory,subcategory.category,waypoints_via_trail,summit_logs_via_trail,trail_share_via_trail,tags", }); let r = await fetch(updateUrl, { @@ -625,7 +628,7 @@ export async function trails_get_filter_values(f: (url: RequestInfo | URL, confi } -export async function trails_get_bounding_box(f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch): Promise { +export async function trails_get_bounding_box(f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch): Promise { const r = await f('/api/v1/trail/bounding-box', { method: 'GET', }) @@ -687,9 +690,18 @@ export async function fetchGPX(trail: { gpx?: string } & Record, f: export async function searchResultToTrailList(hits: Hits): Promise { const trails: Trail[] = [] + const categoryById = new Map(get(categories).map((category) => [category.id, category])); + const subcategoryById = new Map( + get(subcategories).map((subcategory) => [subcategory.id, subcategory]), + ); + for (const h of hits) { const created = Number(h.created || 0); const date = Number(h.date || 0); + const category = h.category_id ? categoryById.get(h.category_id) : undefined; + const subcategory = h.subcategory_id + ? subcategoryById.get(h.subcategory_id) + : undefined; const t: Trail & RecordModel = { collectionId: "trails", collectionName: "trails", @@ -702,7 +714,8 @@ export async function searchResultToTrailList(hits: Hits): Pr summit_logs: [], waypoints: [], tags: h.tags ?? [], - category: h.category, + category: h.category_id ?? "", + subcategory: h.subcategory_id ?? "", created: new Date(created * 1000).toISOString(), date: new Date(date * 1000).toISOString(), description: h.description, @@ -723,6 +736,14 @@ export async function searchResultToTrailList(hits: Hits): Pr thumbnail: 0, like_count: h.like_count, expand: { + category: category ?? (h.category + ? { + id: h.category_id ?? "", + name: h.category, + icon: h.category_icon, + } + : undefined), + subcategory, author: { collectionId: "activitypub_actors", is_local: (h.domain?.length ?? 0) == 0, @@ -838,9 +859,52 @@ function buildFilterText(user: AuthRecord, filter: TrailFilter, includeGeo: bool filterText += ` AND date <= ${new Date(filter.endDate).getTime() / 1000}` } - if (filter.category.length > 0) { - const categoryValues = filter.category.map(category => `'${category}'`).join(", "); - filterText += ` AND category IN [${categoryValues}]`; + const selectedSubcategoryIds = filter.subcategory ?? []; + if (filter.category.length > 0 || selectedSubcategoryIds.length > 0) { + const selectedNoSubcategoryCategoryIds = selectedSubcategoryIds + .map(noSubcategoryFilterCategory) + .filter((category): category is string => category !== undefined); + const selectedRealSubcategoryIds = selectedSubcategoryIds.filter( + (id) => noSubcategoryFilterCategory(id) === undefined, + ); + const selectedSubcategoryParentIds = new Set( + get(subcategories) + .filter((subcategory) => selectedRealSubcategoryIds.includes(subcategory.id)) + .map((subcategory) => subcategory.category), + ); + for (const categoryId of selectedNoSubcategoryCategoryIds) { + selectedSubcategoryParentIds.add(categoryId); + } + const categoriesWithoutSubcategoryFilter = filter.category.filter( + (category) => !selectedSubcategoryParentIds.has(category), + ); + const categoryParts: string[] = []; + + if (categoriesWithoutSubcategoryFilter.length > 0) { + const categoryValues = categoriesWithoutSubcategoryFilter + .map(category => `'${category}'`) + .join(", "); + categoryParts.push(`category_id IN [${categoryValues}]`); + } + + if (selectedNoSubcategoryCategoryIds.length > 0) { + const noSubcategoryParts = selectedNoSubcategoryCategoryIds.map( + (category) => + `(category_id = '${category}' AND subcategory_id IS NULL)`, + ); + categoryParts.push(...noSubcategoryParts); + } + + if (selectedRealSubcategoryIds.length > 0) { + const subcategoryValues = selectedRealSubcategoryIds + .map(subcategory => `'${subcategory}'`) + .join(", "); + categoryParts.push(`subcategory_id IN [${subcategoryValues}]`); + } + + if (categoryParts.length > 0) { + filterText += ` AND (${categoryParts.join(" OR ")})`; + } } if (filter.tags.length > 0) { diff --git a/web/src/lib/util/api_util.ts b/web/src/lib/util/api_util.ts index a13aeebd..4899cb3b 100644 --- a/web/src/lib/util/api_util.ts +++ b/web/src/lib/util/api_util.ts @@ -31,6 +31,9 @@ export enum Collection { notifications = "notifications", profile_feed = "profile_feed", settings = "settings", + subcategories = "subcategories", + user_category_preferences = "user_category_preferences", + user_subcategory_preferences = "user_subcategory_preferences", summit_logs = "summit_logs", trail_like = "trail_like", trail_share = "trail_share", @@ -162,7 +165,14 @@ export function handleError(e: any) { return json({ ...e.response, message: e.message, detail: e.originalError.data }, { status: e.status }) } else if (e instanceof SyntaxError) { return json({ message: "invalid_json" }, { status: 400 }) + } else if (e instanceof Error) { + return json({ message: e.message }, { status: 500 }) } else { - return json({ message: e }, { status: 500 }) + const message = typeof e?.message === "string" + ? e.message + : typeof e === "string" + ? e + : "internal_server_error"; + return json({ message, detail: e }, { status: 500 }) } } diff --git a/web/src/lib/util/category_util.ts b/web/src/lib/util/category_util.ts new file mode 100644 index 00000000..d3c6ab5c --- /dev/null +++ b/web/src/lib/util/category_util.ts @@ -0,0 +1,411 @@ +import type { Category } from "$lib/models/category"; +import type { UserCategoryPreference } from "$lib/models/category_preference"; +import type { Subcategory } from "$lib/models/subcategory"; +import type { UserSubcategoryPreference } from "$lib/models/subcategory_preference"; + +type CategoryDisplayEntity = Pick; +type CategoryShortNameEntity = Pick< + Category | Subcategory, + "name" | "short_name" | "translations" +>; + +export type CategoryMappingTarget = + | string + | { + category?: string; + subcategory?: string; + }; + +export type ResolvedCategoryMappingTarget = { + categoryId?: string; + subcategoryId?: string; +}; + +export function normalizeCategoryName(name: string): string { + // Best-effort mirror of the backend normalization for resolving ?category= links. + // Full Unicode casefold parity would require a dedicated frontend casefold implementation. + return name + .normalize("NFD") + .replace(/\p{Mn}/gu, "") + .toLowerCase() + .replace(/[\s_-]+/g, " ") + .trim(); +} + +export function categoryMappingTargetFromUnknown( + value: unknown, +): CategoryMappingTarget | undefined { + if (typeof value === "string") { + return value; + } + if (!value || typeof value !== "object" || Array.isArray(value)) { + return undefined; + } + + const raw = value as Record; + return { + category: typeof raw.category === "string" ? raw.category : "", + subcategory: typeof raw.subcategory === "string" ? raw.subcategory : "", + }; +} + +function resolveCategoryTargetValue( + value: string, + categories: Category[], + subcategories: Subcategory[], +): ResolvedCategoryMappingTarget { + const trimmed = value.trim(); + if (!trimmed) { + return {}; + } + + const directCategory = categories.find((category) => category.id === trimmed); + if (directCategory) { + return { categoryId: directCategory.id }; + } + + const directSubcategory = subcategories.find( + (subcategory) => subcategory.id === trimmed, + ); + if (directSubcategory) { + return { + categoryId: directSubcategory.category, + subcategoryId: directSubcategory.id, + }; + } + + if (trimmed.includes("/")) { + const [rawCategory, rawSubcategory] = trimmed.split("/", 2); + const category = categories.find( + (candidate) => + normalizeCategoryName(candidate.name) === + normalizeCategoryName(rawCategory), + ); + const subcategory = subcategories.find( + (candidate) => + candidate.category === category?.id && + normalizeCategoryName(candidate.name) === + normalizeCategoryName(rawSubcategory), + ); + if (subcategory) { + return { + categoryId: subcategory.category, + subcategoryId: subcategory.id, + }; + } + } + + const namedCategory = categories.find( + (category) => + normalizeCategoryName(category.name) === normalizeCategoryName(trimmed), + ); + return namedCategory ? { categoryId: namedCategory.id } : {}; +} + +export function resolveCategoryMappingTarget( + target: CategoryMappingTarget, + categories: Category[], + subcategories: Subcategory[], +): ResolvedCategoryMappingTarget { + if (typeof target === "string") { + return resolveCategoryTargetValue(target, categories, subcategories); + } + + const categoryValue = resolveCategoryTargetValue( + target.category ?? "", + categories, + subcategories, + ); + const subcategoryValue = resolveCategoryTargetValue( + target.subcategory ?? "", + categories, + subcategories, + ); + if (subcategoryValue.subcategoryId) { + return subcategoryValue; + } + + if (target.subcategory?.trim() && categoryValue.categoryId) { + const subcategory = subcategories.find( + (candidate) => + candidate.category === categoryValue.categoryId && + normalizeCategoryName(candidate.name) === + normalizeCategoryName(target.subcategory ?? ""), + ); + if (subcategory) { + return { + categoryId: subcategory.category, + subcategoryId: subcategory.id, + }; + } + } + + return categoryValue; +} + +export function categoryMappingTargetToPickerValue( + target: CategoryMappingTarget, + categories: Category[], + subcategories: Subcategory[], +): string { + const resolved = resolveCategoryMappingTarget(target, categories, subcategories); + if (resolved.subcategoryId) { + return `subcategory:${resolved.subcategoryId}`; + } + if (resolved.categoryId) { + return `category:${resolved.categoryId}`; + } + return ""; +} + +function localeCandidates(locale?: string | null): string[] { + if (!locale) { + return []; + } + + const normalized = locale.trim(); + if (!normalized) { + return []; + } + + const lower = normalized.toLowerCase(); + const base = lower.split("-")[0]; + + return [...new Set([normalized, lower, base])]; +} + +export function displayCategoryName( + category?: CategoryDisplayEntity | null, + locale?: string | null, +): string { + if (!category) { + return ""; + } + + for (const candidate of localeCandidates(locale)) { + const translatedName = category.translations?.[candidate]?.name; + if (translatedName) { + return translatedName; + } + } + + // Fall back to the English translation before the raw canonical name. + return category.translations?.["en"]?.name || category.name || ""; +} + +export function displayCategoryShortName( + category?: CategoryShortNameEntity | null, + locale?: string | null, +): string { + if (!category) { + return ""; + } + + for (const candidate of localeCandidates(locale)) { + const translatedShortName = category.translations?.[candidate]?.short_name; + if (translatedShortName?.trim()) { + return translatedShortName; + } + } + + return category.short_name?.trim() || displayCategoryName(category, locale); +} + +export function displayCategoryIcon( + category?: Pick | null, +): string { + const icon = category?.icon?.trim().replace(/^fa-/, ""); + return icon ? `fa-${icon}` : "fa-shapes"; +} + +export function displaySubcategoryIcon( + subcategory?: Pick | null, + parentCategory?: Pick | null, +): string { + return displayCategoryIcon(subcategory?.icon ? subcategory : parentCategory); +} + +export function displaySubcategoryBadgeIcon( + subcategory?: Pick | null, +): string { + const icon = subcategory?.badge_icon?.trim().replace(/^fa-/, ""); + return icon ? `fa-${icon}` : ""; +} + +type TrailCategoryIconEntity = { + expand?: { + category?: Pick | null; + subcategory?: Pick | null; + } | null; +}; + +export function displayTrailCategoryIcon( + trail?: TrailCategoryIconEntity | null, +): string { + const subcategory = trail?.expand?.subcategory; + if (subcategory) { + return displaySubcategoryIcon(subcategory, trail?.expand?.category); + } + return displayCategoryIcon(trail?.expand?.category); +} + +export function displayTrailCategoryBadgeIcon( + trail?: TrailCategoryIconEntity | null, +): string { + return displaySubcategoryBadgeIcon(trail?.expand?.subcategory); +} + +export function displaySubcategoryName( + subcategory?: Subcategory | null, + locale?: string | null, +): string { + return displayCategoryName(subcategory, locale); +} + +export function displaySubcategoryLabel( + subcategory?: Subcategory | null, + locale?: string | null, +): string { + return displaySubcategoryName(subcategory, locale); +} + +export function displaySubcategoryShortBadge( + subcategory?: Subcategory | null, + locale?: string | null, +): string { + if (!subcategory) { + return ""; + } + + const shortName = displayCategoryShortName(subcategory, locale); + const label = displaySubcategoryLabel(subcategory, locale); + + if (shortName && shortName !== label) { + return shortName.toUpperCase(); + } + + if (subcategory.short_name?.trim()) { + return subcategory.short_name.trim().toUpperCase(); + } + + const normalizedLabel = label.trim(); + if (normalizedLabel.length <= 5) { + return normalizedLabel.toUpperCase(); + } + + const words = normalizedLabel.match(/[\p{L}\p{N}]+/gu) ?? []; + if (words.length > 1) { + return words + .map((word) => word.at(0)) + .join("") + .slice(0, 5) + .toUpperCase(); + } + + return normalizedLabel.slice(0, 4).toUpperCase(); +} + +export function preferenceForCategory( + preferences: UserCategoryPreference[], + categoryId?: string | null, +): UserCategoryPreference | undefined { + return preferences.find((preference) => preference.category === categoryId); +} + +export function preferenceForSubcategory( + preferences: UserSubcategoryPreference[], + subcategoryId?: string | null, +): UserSubcategoryPreference | undefined { + return preferences.find( + (preference) => preference.subcategory === subcategoryId, + ); +} + +export function subcategoryVisible( + subcategoryId: string | undefined | null, + preferences: UserSubcategoryPreference[], +): boolean { + return preferenceForSubcategory(preferences, subcategoryId)?.visible !== false; +} + +export function sortedCategoriesByPreference( + categories: Category[], + preferences: UserCategoryPreference[], + locale?: string | null, +): Category[] { + return [...categories].sort((a, b) => { + const aPriority = preferenceForCategory(preferences, a.id)?.priority; + const bPriority = preferenceForCategory(preferences, b.id)?.priority; + const aPrioritized = typeof aPriority === "number" && aPriority > 0; + const bPrioritized = typeof bPriority === "number" && bPriority > 0; + + if (aPrioritized && bPrioritized) { + return aPriority - bPriority; + } + if (aPrioritized) { + return -1; + } + if (bPrioritized) { + return 1; + } + + return displayCategoryName(a, locale).localeCompare( + displayCategoryName(b, locale), + locale ?? undefined, + { sensitivity: "base" }, + ); + }); +} + +export function sortedSubcategoriesByPreference( + subcategories: Subcategory[], + preferences: UserSubcategoryPreference[], + locale?: string | null, +): Subcategory[] { + return [...subcategories].sort((a, b) => { + const aPriority = preferenceForSubcategory(preferences, a.id)?.priority; + const bPriority = preferenceForSubcategory(preferences, b.id)?.priority; + const aPrioritized = typeof aPriority === "number" && aPriority > 0; + const bPrioritized = typeof bPriority === "number" && bPriority > 0; + + if (aPrioritized && bPrioritized && aPriority !== bPriority) { + return aPriority - bPriority; + } + if (aPrioritized && !bPrioritized) { + return -1; + } + if (bPrioritized && !aPrioritized) { + return 1; + } + + return displaySubcategoryLabel(a, locale).localeCompare( + displaySubcategoryLabel(b, locale), + locale ?? undefined, + { sensitivity: "base" }, + ); + }); +} + +export function categoryVisibleInDesign( + category: Category, + preferences: UserCategoryPreference[], + currentCategoryId?: string | null, +): boolean { + if (category.id === currentCategoryId) { + return true; + } + + return preferenceForCategory(preferences, category.id)?.visible !== false; +} + +export function designSelectableCategories( + categories: Category[], + preferences: UserCategoryPreference[], + locale?: string | null, + currentCategoryId?: string | null, +): Category[] { + return sortedCategoriesByPreference(categories, preferences, locale).filter( + (category) => + categoryVisibleInDesign(category, preferences, currentCategoryId), + ); +} diff --git a/web/src/lib/util/trail_filter_util.ts b/web/src/lib/util/trail_filter_util.ts index dd8f9a7f..96a5ef0b 100644 --- a/web/src/lib/util/trail_filter_util.ts +++ b/web/src/lib/util/trail_filter_util.ts @@ -1,5 +1,17 @@ import type { TrailFilter } from "$lib/models/trail"; +const NO_SUBCATEGORY_FILTER_PREFIX = "__no_subcategory__:"; + +export function noSubcategoryFilterValue(categoryId: string): string { + return `${NO_SUBCATEGORY_FILTER_PREFIX}${categoryId}`; +} + +export function noSubcategoryFilterCategory(value: string): string | undefined { + return value.startsWith(NO_SUBCATEGORY_FILTER_PREFIX) + ? value.substring(NO_SUBCATEGORY_FILTER_PREFIX.length) + : undefined; +} + const TRAIL_SORT_OPTIONS = new Set([ "name", "distance", @@ -95,6 +107,7 @@ export function sanitizeTrailFilter( ...defaultFilter, q: getString(source.q, defaultFilter.q), category: getStringArray(source.category, defaultFilter.category), + subcategory: getStringArray(source.subcategory, defaultFilter.subcategory), tags: getStringArray(source.tags, defaultFilter.tags), difficulty: parseDifficulty(source.difficulty, defaultFilter.difficulty), author: getString(source.author, defaultFilter.author), diff --git a/web/src/routes/+page.ts b/web/src/routes/+page.ts index cf85734d..6dbf4baf 100644 --- a/web/src/routes/+page.ts +++ b/web/src/routes/+page.ts @@ -1,12 +1,14 @@ import type { Trail } from "$lib/models/trail"; import { categories_index } from "$lib/stores/category_store"; import { feed_index } from "$lib/stores/feed_store"; +import { subcategories_index } from "$lib/stores/subcategory_store"; import { trails_recommend } from "$lib/stores/trail_store"; import type { Load } from "@sveltejs/kit"; export const load: Load = async ({ fetch }) => { try { await categories_index(fetch) + await subcategories_index(fetch) const feed = await feed_index(1, 10, fetch); @@ -19,4 +21,4 @@ export const load: Load = async ({ fetch }) => { } } return { trails: [], feed: { items: [], page: 1, perPage: 1, totalItems: 0, totalPages: 0 } } -}; \ No newline at end of file +}; diff --git a/web/src/routes/api/v1/activitypub/user/[handle]/inbox/+server.ts b/web/src/routes/api/v1/activitypub/user/[handle]/inbox/+server.ts index 768f3fb9..2954ac67 100644 --- a/web/src/routes/api/v1/activitypub/user/[handle]/inbox/+server.ts +++ b/web/src/routes/api/v1/activitypub/user/[handle]/inbox/+server.ts @@ -36,7 +36,8 @@ export async function POST(event: RequestEvent) { try { - const activity: APActivity = await event.request.json() + const bodyText = await event.request.text() + const activity: APActivity = JSON.parse(bodyText) if (!activity.actor) { return json("Bad request", { status: 400 }); } @@ -54,7 +55,7 @@ export async function POST(event: RequestEvent) { method: "POST", fetch: event.fetch, headers: originalHeaders, - body: JSON.stringify(activity) + body: bodyText }) if (success === false) { diff --git a/web/src/routes/api/v1/category/+server.ts b/web/src/routes/api/v1/category/+server.ts index 7da67808..8b7808f9 100644 --- a/web/src/routes/api/v1/category/+server.ts +++ b/web/src/routes/api/v1/category/+server.ts @@ -33,7 +33,20 @@ import { json, type RequestEvent } from "@sveltejs/kit"; * content: * application/json: * schema: - * $ref: '#/components/schemas/ListResult' + * type: object + * properties: + * page: + * type: integer + * perPage: + * type: integer + * totalItems: + * type: integer + * totalPages: + * type: integer + * items: + * type: array + * items: + * $ref: '#/components/schemas/Category' * 400: * description: Bad Request * 500: @@ -48,4 +61,4 @@ export async function GET(event: RequestEvent) { return handleError(e) } -} \ No newline at end of file +} diff --git a/web/src/routes/api/v1/profile/[handle]/trails/+server.ts b/web/src/routes/api/v1/profile/[handle]/trails/+server.ts index 44e4185e..006398fc 100644 --- a/web/src/routes/api/v1/profile/[handle]/trails/+server.ts +++ b/web/src/routes/api/v1/profile/[handle]/trails/+server.ts @@ -1,3 +1,4 @@ +import { withTrailPreferenceMeiliFilter } from '$lib/server/category_preference_filter'; import type { TrailSearchResult } from '$lib/models/trail'; import { getActorResponseForHandle } from '$lib/util/activitypub_server_util'; import { handleError } from '$lib/util/api_util'; @@ -59,7 +60,11 @@ export async function POST(event: RequestEvent) { let r: SearchResponse; if (actor.is_local) { - r = await event.locals.ms.index("trails").search(data.q, { ...data.options, filter: `author = ${actor.id}` }); + const filter = await withTrailPreferenceMeiliFilter(event, [ + data.options?.filter, + `author = ${actor.id}`, + ]); + r = await event.locals.ms.index("trails").search(data.q, { ...data.options, filter }); } else { const origin = new URL(actor.iri).origin const url = `${origin}/api/v1/profile/${actor.preferred_username}/trails?` + event.url.searchParams diff --git a/web/src/routes/api/v1/search/[index]/+server.ts b/web/src/routes/api/v1/search/[index]/+server.ts index 663b8b38..bad3e895 100644 --- a/web/src/routes/api/v1/search/[index]/+server.ts +++ b/web/src/routes/api/v1/search/[index]/+server.ts @@ -1,3 +1,4 @@ +import { withTrailPreferenceMeiliFilter } from "$lib/server/category_preference_filter"; import { error, json, type RequestEvent } from "@sveltejs/kit"; /** @@ -43,6 +44,15 @@ export async function POST(event: RequestEvent) { const data = await event.request.json() try { + if (event.params.index === "trails") { + data.options = { + ...(data.options ?? {}), + filter: await withTrailPreferenceMeiliFilter( + event, + data.options?.filter, + ), + }; + } const r = await event.locals.ms.index(event.params.index as string).search(data.q, data.options); return json(r); } catch (e: any) { @@ -50,4 +60,4 @@ export async function POST(event: RequestEvent) { throw error(e.httpStatus || 500, e) } -} \ No newline at end of file +} diff --git a/web/src/routes/api/v1/search/multi/+server.ts b/web/src/routes/api/v1/search/multi/+server.ts index 205d3ae8..b70d2ab9 100644 --- a/web/src/routes/api/v1/search/multi/+server.ts +++ b/web/src/routes/api/v1/search/multi/+server.ts @@ -1,3 +1,4 @@ +import { withTrailPreferenceMeiliFilter } from "$lib/server/category_preference_filter"; import { error, json, type RequestEvent } from "@sveltejs/kit"; /** @@ -37,6 +38,19 @@ export async function POST(event: RequestEvent) { const data = await event.request.json() try { + data.queries = await Promise.all( + data.queries.map(async (query: any) => + query.indexUid === "trails" + ? { + ...query, + filter: await withTrailPreferenceMeiliFilter( + event, + query.filter, + ), + } + : query, + ), + ); const r = await event.locals.ms.multiSearch({ queries: data.queries }); @@ -45,4 +59,4 @@ export async function POST(event: RequestEvent) { console.error(e); throw error(e.httpStatus || 500, e) } -} \ No newline at end of file +} diff --git a/web/src/routes/api/v1/search/trails/cluster/+server.ts b/web/src/routes/api/v1/search/trails/cluster/+server.ts index e5887b78..5ac522e8 100644 --- a/web/src/routes/api/v1/search/trails/cluster/+server.ts +++ b/web/src/routes/api/v1/search/trails/cluster/+server.ts @@ -1,3 +1,4 @@ +import { withTrailPreferenceMeiliFilter } from "$lib/server/category_preference_filter"; import { error, json, type RequestEvent } from "@sveltejs/kit"; import Supercluster from "supercluster"; import { MAP_MAX_POLYLINES } from "$lib/config/map"; @@ -33,7 +34,10 @@ export async function POST(event: RequestEvent) { const summaryQuery = { indexUid: "trails", q, - filter: [geoFilter, filterText].filter(f => f && f !== ""), + filter: await withTrailPreferenceMeiliFilter( + event, + [geoFilter, filterText].filter(f => f && f !== ""), + ), attributesToRetrieve: ["id", "_geo", "bounding_box_diagonal"], limit: 10000, }; diff --git a/web/src/routes/api/v1/subcategory/+server.ts b/web/src/routes/api/v1/subcategory/+server.ts new file mode 100644 index 00000000..4d2e9ddb --- /dev/null +++ b/web/src/routes/api/v1/subcategory/+server.ts @@ -0,0 +1,66 @@ +import type { Subcategory } from "$lib/models/subcategory"; +import { Collection, handleError, list } from "$lib/util/api_util"; +import { json, type RequestEvent } from "@sveltejs/kit"; + +/** + * @swagger + * /api/v1/subcategory: + * get: + * summary: List subcategories + * description: Retrieves a paginated list of trail subcategories + * tags: + * - Categories + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * - in: query + * name: perPage + * schema: + * type: integer + * - in: query + * name: sort + * schema: + * type: string + * - in: query + * name: filter + * schema: + * type: string + * - in: query + * name: expand + * schema: + * type: string + * responses: + * 200: + * description: List of subcategories + * content: + * application/json: + * schema: + * type: object + * properties: + * page: + * type: integer + * perPage: + * type: integer + * totalItems: + * type: integer + * totalPages: + * type: integer + * items: + * type: array + * items: + * $ref: '#/components/schemas/Subcategory' + * 400: + * description: Bad Request + * 500: + * description: Internal Server Error + */ +export async function GET(event: RequestEvent) { + try { + const r = await list(event, Collection.subcategories); + return json(r); + } catch (e) { + return handleError(e); + } +} diff --git a/web/src/routes/api/v1/trail/+server.ts b/web/src/routes/api/v1/trail/+server.ts index ca4a1755..92ab998b 100644 --- a/web/src/routes/api/v1/trail/+server.ts +++ b/web/src/routes/api/v1/trail/+server.ts @@ -1,6 +1,8 @@ +import { RecordListOptionsSchema } from '$lib/models/api/base_schema'; import { TrailCreateSchema } from '$lib/models/api/trail_schema'; import type { Trail } from '$lib/models/trail'; -import { Collection, create, handleError, list } from '$lib/util/api_util'; +import { withTrailPreferencePocketBaseFilter } from '$lib/server/category_preference_filter'; +import { Collection, create, handleError } from '$lib/util/api_util'; import { json, type RequestEvent } from '@sveltejs/kit'; /** @@ -42,7 +44,31 @@ import { json, type RequestEvent } from '@sveltejs/kit'; */ export async function GET(event: RequestEvent) { try { - const r = await list(event, Collection.trails); + const searchParams = Object.fromEntries(event.url.searchParams); + const safeSearchParams = RecordListOptionsSchema.parse(searchParams); + const { perPage, page, ...opts } = safeSearchParams; + const filter = await withTrailPreferencePocketBaseFilter( + event, + safeSearchParams.filter, + ); + const listOptions = { ...opts, filter }; + const r = (perPage ?? 0) < 0 + ? { + items: await event.locals.pb + .collection(Collection.trails) + .getFullList(listOptions), + perPage: -1, + page: 1, + totalItems: 0, + totalPages: 1, + } + : await event.locals.pb + .collection(Collection.trails) + .getList(page, perPage, listOptions); + + if (perPage && perPage < 0) { + r.totalItems = r.items.length; + } for (const t of r.items) { if (!t.author || !event.locals.pb.authStore.record) { diff --git a/web/src/routes/api/v1/trail/bounding-box/+server.ts b/web/src/routes/api/v1/trail/bounding-box/+server.ts index 10e91724..1d76080a 100644 --- a/web/src/routes/api/v1/trail/bounding-box/+server.ts +++ b/web/src/routes/api/v1/trail/bounding-box/+server.ts @@ -1,6 +1,6 @@ -import { type TrailBoundingBox, type TrailFilterValues } from '$lib/models/trail'; -import { handleError } from '$lib/util/api_util'; -import { error, json, type RequestEvent } from '@sveltejs/kit'; +import { withTrailPreferenceMeiliFilter } from "$lib/server/category_preference_filter"; +import type { TrailBoundingBox } from "$lib/models/trail"; +import { error, json, type RequestEvent } from "@sveltejs/kit"; /** * @swagger @@ -37,13 +37,78 @@ export async function GET(event: RequestEvent) { max_lat: 0, min_lat: 0, max_lon: 0, - min_lon: 0 + min_lon: 0, + has_trails: false, }); } try { - const r = await event.locals.pb.collection('trails_bounding_box').getOne(event.locals.user.actor!) - return json(r) + const filter = await withTrailPreferenceMeiliFilter(event, undefined); + const attributesToRetrieve = ["min_lat", "max_lat", "min_lon", "max_lon"]; + const r = await event.locals.ms.multiSearch({ + queries: [ + { + indexUid: "trails", + q: "", + filter, + attributesToRetrieve, + sort: ["min_lat:asc"], + limit: 1, + }, + { + indexUid: "trails", + q: "", + filter, + attributesToRetrieve, + sort: ["max_lat:desc"], + limit: 1, + }, + { + indexUid: "trails", + q: "", + filter, + attributesToRetrieve, + sort: ["min_lon:asc"], + limit: 1, + }, + { + indexUid: "trails", + q: "", + filter, + attributesToRetrieve, + sort: ["max_lon:desc"], + limit: 1, + }, + ], + }); + + const [minLatResult, maxLatResult, minLonResult, maxLonResult] = r.results; + const hasTrails = + minLatResult.hits.length > 0 && + maxLatResult.hits.length > 0 && + minLonResult.hits.length > 0 && + maxLonResult.hits.length > 0; + + if (!hasTrails) { + return json({ + min_lat: 0, + max_lat: 0, + min_lon: 0, + max_lon: 0, + has_trails: false, + }); + } + + const boundingBox: TrailBoundingBox = { + min_lat: minLatResult.hits[0].min_lat, + max_lat: maxLatResult.hits[0].max_lat, + min_lon: minLonResult.hits[0].min_lon, + max_lon: maxLonResult.hits[0].max_lon, + has_trails: true, + }; + + return json(boundingBox) } catch (e: any) { - return handleError(e); + console.error(e); + throw error(e.httpStatus || 500, e.message ?? "Unable to get trail bounding box"); } } diff --git a/web/src/routes/api/v1/trail/recommend/+server.ts b/web/src/routes/api/v1/trail/recommend/+server.ts index 936a1289..2b9e0565 100644 --- a/web/src/routes/api/v1/trail/recommend/+server.ts +++ b/web/src/routes/api/v1/trail/recommend/+server.ts @@ -1,4 +1,5 @@ import { TrailRecommendSchema } from '$lib/models/api/trail_schema'; +import { withTrailPreferenceMeiliFilter } from '$lib/server/category_preference_filter'; import { handleError } from '$lib/util/api_util'; import { json, type RequestEvent } from '@sveltejs/kit'; @@ -35,12 +36,25 @@ export async function GET(event: RequestEvent) { const searchParams = Object.fromEntries(event.url.searchParams); const safeSearchParams = TrailRecommendSchema.parse(searchParams); - const numberOfTrails = (await event.locals.ms.index("trails").search("", {limit: 1})).estimatedTotalHits - const randomOffset = (safeSearchParams.size ?? 0) > numberOfTrails ? 0 : Math.floor(Math.random() * (numberOfTrails - 1) + 1) - const response = await event.locals.ms.index("trails").search("", {limit: safeSearchParams.size, offset: randomOffset}) + const size = safeSearchParams.size ?? 10; + const response = await recommend(event, size); return json(response.hits) } catch (e: any) { return handleError(e); } } + +async function recommend(event: RequestEvent, size: number) { + const filter = await withTrailPreferenceMeiliFilter(event, undefined); + const countResponse = await event.locals.ms.index("trails").search("", { limit: 1, filter }); + const numberOfTrails = countResponse.estimatedTotalHits ?? countResponse.totalHits ?? 0; + + if (numberOfTrails === 0 || size === 0) { + return { hits: [] }; + } + + const maxOffset = Math.max(0, numberOfTrails - size); + const randomOffset = Math.floor(Math.random() * (maxOffset + 1)); + return event.locals.ms.index("trails").search("", { limit: size, offset: randomOffset, filter }); +} diff --git a/web/src/routes/api/v1/user-category-preference/+server.ts b/web/src/routes/api/v1/user-category-preference/+server.ts new file mode 100644 index 00000000..8cb68816 --- /dev/null +++ b/web/src/routes/api/v1/user-category-preference/+server.ts @@ -0,0 +1,120 @@ +import { + UserCategoryPreferenceUpsertSchema, +} from "$lib/models/api/category_preference_schema"; +import type { UserCategoryPreference } from "$lib/models/category_preference"; +import { Collection, handleError } from "$lib/util/api_util"; +import { json, type RequestEvent } from "@sveltejs/kit"; + +/** + * @swagger + * /api/v1/user-category-preference: + * get: + * summary: List current user's category preferences + * tags: + * - Categories + * responses: + * 200: + * description: Category preferences for the authenticated user, or an empty list for anonymous requests + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/UserCategoryPreference' + * 500: + * description: Internal Server Error + */ +export async function GET(event: RequestEvent) { + try { + if (!event.locals.user) { + return json([]); + } + + const preferences = await event.locals.pb + .collection(Collection.user_category_preferences) + .getFullList({ + filter: event.locals.pb.filter("user = {:user}", { + user: event.locals.user.id, + }), + requestKey: null, + }); + + return json(preferences); + } catch (e) { + return handleError(e); + } +} + +/** + * @swagger + * /api/v1/user-category-preference: + * put: + * summary: Create or update a category preference + * tags: + * - Categories + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserCategoryPreferenceUpsertInput' + * responses: + * 200: + * description: Saved category preference + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserCategoryPreference' + * 400: + * description: Bad Request + * 401: + * description: Unauthorized + * 500: + * description: Internal Server Error + */ +export async function PUT(event: RequestEvent) { + try { + if (!event.locals.user) { + return json({ message: "Unauthorized" }, { status: 401 }); + } + + const data = await event.request.json(); + const safeData = UserCategoryPreferenceUpsertSchema.parse(data); + + const payload = { + ...safeData, + user: event.locals.user.id, + }; + + let preference: UserCategoryPreference | undefined; + try { + preference = await event.locals.pb + .collection(Collection.user_category_preferences) + .getFirstListItem( + event.locals.pb.filter("user = {:user} && category = {:category}", { + user: event.locals.user.id, + category: safeData.category, + }), + { requestKey: null }, + ); + } catch { + preference = undefined; + } + + const saved = preference?.id + ? await event.locals.pb + .collection(Collection.user_category_preferences) + .update(preference.id, payload, { + requestKey: null, + }) + : await event.locals.pb + .collection(Collection.user_category_preferences) + .create(payload, { + requestKey: null, + }); + + return json(saved); + } catch (e) { + return handleError(e); + } +} diff --git a/web/src/routes/api/v1/user-category-preference/reorder/+server.ts b/web/src/routes/api/v1/user-category-preference/reorder/+server.ts new file mode 100644 index 00000000..bedd304e --- /dev/null +++ b/web/src/routes/api/v1/user-category-preference/reorder/+server.ts @@ -0,0 +1,61 @@ +import { + UserCategoryPreferenceReorderSchema, +} from "$lib/models/api/category_preference_schema"; +import { handleError } from "$lib/util/api_util"; +import { json, type RequestEvent } from "@sveltejs/kit"; + +/** + * @swagger + * /api/v1/user-category-preference/reorder: + * post: + * summary: Reorder current user's category preferences + * tags: + * - Categories + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserCategoryPreferenceReorderInput' + * responses: + * 200: + * description: Reorder acknowledged + * content: + * application/json: + * schema: + * type: object + * required: + * - acknowledged + * properties: + * acknowledged: + * type: boolean + * 400: + * description: Bad Request + * 401: + * description: Unauthorized + * 500: + * description: Internal Server Error + */ +export async function POST(event: RequestEvent) { + try { + if (!event.locals.user) { + return json({ message: "Unauthorized" }, { status: 401 }); + } + + const data = await event.request.json(); + const safeData = UserCategoryPreferenceReorderSchema.parse(data); + const response = await event.locals.pb.send( + "/category-preferences/reorder", + { + method: "POST", + body: JSON.stringify(safeData), + fetch: event.fetch, + requestKey: null, + }, + ); + + return json(response); + } catch (e) { + return handleError(e); + } +} diff --git a/web/src/routes/api/v1/user-subcategory-preference/+server.ts b/web/src/routes/api/v1/user-subcategory-preference/+server.ts new file mode 100644 index 00000000..844196c2 --- /dev/null +++ b/web/src/routes/api/v1/user-subcategory-preference/+server.ts @@ -0,0 +1,121 @@ +import { UserSubcategoryPreferenceUpsertSchema } from "$lib/models/api/subcategory_preference_schema"; +import type { UserSubcategoryPreference } from "$lib/models/subcategory_preference"; +import { Collection, handleError } from "$lib/util/api_util"; +import { json, type RequestEvent } from "@sveltejs/kit"; + +/** + * @swagger + * /api/v1/user-subcategory-preference: + * get: + * summary: List current user's subcategory preferences + * tags: + * - Categories + * responses: + * 200: + * description: Subcategory preferences for the authenticated user, or an empty list for anonymous requests + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/UserSubcategoryPreference' + * 500: + * description: Internal Server Error + */ +export async function GET(event: RequestEvent) { + try { + if (!event.locals.user) { + return json([]); + } + + const preferences = await event.locals.pb + .collection(Collection.user_subcategory_preferences) + .getFullList({ + filter: event.locals.pb.filter("user = {:user}", { + user: event.locals.user.id, + }), + requestKey: null, + }); + + return json(preferences); + } catch (e) { + return handleError(e); + } +} + +/** + * @swagger + * /api/v1/user-subcategory-preference: + * put: + * summary: Create or update a subcategory preference + * tags: + * - Categories + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserSubcategoryPreferenceUpsertInput' + * responses: + * 200: + * description: Saved subcategory preference + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserSubcategoryPreference' + * 400: + * description: Bad Request + * 401: + * description: Unauthorized + * 500: + * description: Internal Server Error + */ +export async function PUT(event: RequestEvent) { + try { + if (!event.locals.user) { + return json({ message: "Unauthorized" }, { status: 401 }); + } + + const data = await event.request.json(); + const safeData = UserSubcategoryPreferenceUpsertSchema.parse(data); + + const payload = { + ...safeData, + user: event.locals.user.id, + }; + + let preference: UserSubcategoryPreference | undefined; + try { + preference = await event.locals.pb + .collection(Collection.user_subcategory_preferences) + .getFirstListItem( + event.locals.pb.filter( + "user = {:user} && subcategory = {:subcategory}", + { + user: event.locals.user.id, + subcategory: safeData.subcategory, + }, + ), + { requestKey: null }, + ); + } catch { + preference = undefined; + } + + const saved = preference?.id + ? await event.locals.pb + .collection(Collection.user_subcategory_preferences) + .update(preference.id, payload, { + requestKey: null, + }) + : await event.locals.pb + .collection(Collection.user_subcategory_preferences) + .create(payload, { + requestKey: null, + }); + + return json(saved); + } catch (e) { + return handleError(e); + } +} diff --git a/web/src/routes/api/v1/user-subcategory-preference/reorder/+server.ts b/web/src/routes/api/v1/user-subcategory-preference/reorder/+server.ts new file mode 100644 index 00000000..4fdd780f --- /dev/null +++ b/web/src/routes/api/v1/user-subcategory-preference/reorder/+server.ts @@ -0,0 +1,61 @@ +import { + UserSubcategoryPreferenceReorderSchema, +} from "$lib/models/api/subcategory_preference_schema"; +import { handleError } from "$lib/util/api_util"; +import { json, type RequestEvent } from "@sveltejs/kit"; + +/** + * @swagger + * /api/v1/user-subcategory-preference/reorder: + * post: + * summary: Reorder current user's subcategory preferences for one category + * tags: + * - Categories + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserSubcategoryPreferenceReorderInput' + * responses: + * 200: + * description: Reorder acknowledged + * content: + * application/json: + * schema: + * type: object + * required: + * - acknowledged + * properties: + * acknowledged: + * type: boolean + * 400: + * description: Bad Request + * 401: + * description: Unauthorized + * 500: + * description: Internal Server Error + */ +export async function POST(event: RequestEvent) { + try { + if (!event.locals.user) { + return json({ message: "Unauthorized" }, { status: 401 }); + } + + const data = await event.request.json(); + const safeData = UserSubcategoryPreferenceReorderSchema.parse(data); + const response = await event.locals.pb.send( + "/subcategory-preferences/reorder", + { + method: "POST", + body: JSON.stringify(safeData), + fetch: event.fetch, + requestKey: null, + }, + ); + + return json(response); + } catch (e) { + return handleError(e); + } +} diff --git a/web/src/routes/map/+page.svelte b/web/src/routes/map/+page.svelte index 2c2e555e..d3fa8c7f 100644 --- a/web/src/routes/map/+page.svelte +++ b/web/src/routes/map/+page.svelte @@ -290,10 +290,11 @@ } else if ( settings && settings.mapFocus == "trails" && - (maxBoundingBox.min_lon != 0 || - maxBoundingBox.max_lat != 0 || - maxBoundingBox.max_lon != 0 || - maxBoundingBox.min_lat != 0) + (maxBoundingBox.has_trails ?? + (maxBoundingBox.min_lon != 0 || + maxBoundingBox.max_lat != 0 || + maxBoundingBox.max_lon != 0 || + maxBoundingBox.min_lat != 0)) ) { if ( maxBoundingBox.min_lon == maxBoundingBox.max_lon && diff --git a/web/src/routes/map/+page.ts b/web/src/routes/map/+page.ts index 35ffe098..e8e4638a 100644 --- a/web/src/routes/map/+page.ts +++ b/web/src/routes/map/+page.ts @@ -1,5 +1,8 @@ import type { TrailFilter } from "$lib/models/trail"; import { categories_index } from "$lib/stores/category_store"; +import { category_preferences_index } from "$lib/stores/category_preference_store"; +import { subcategory_preferences_index } from "$lib/stores/subcategory_preference_store"; +import { subcategories_index } from "$lib/stores/subcategory_store"; import { trails_get_bounding_box, trails_get_filter_values } from "$lib/stores/trail_store"; import type { ServerLoad } from "@sveltejs/kit"; @@ -10,6 +13,7 @@ export const load: ServerLoad = async ({ fetch }) => { const filter: TrailFilter = { q: "", category: [], + subcategory: [], tags: [], difficulty: [0, 1, 2], author: "", @@ -34,6 +38,9 @@ export const load: ServerLoad = async ({ fetch }) => { }; await categories_index(fetch) + await subcategories_index(fetch) + await category_preferences_index(fetch) + await subcategory_preferences_index(fetch) return { filter: filter, boundingBox: boundingBox } -}; \ No newline at end of file +}; diff --git a/web/src/routes/map/trail/[handle]/[id]/print/+page.svelte b/web/src/routes/map/trail/[handle]/[id]/print/+page.svelte index 5e12e58c..26c95785 100644 --- a/web/src/routes/map/trail/[handle]/[id]/print/+page.svelte +++ b/web/src/routes/map/trail/[handle]/[id]/print/+page.svelte @@ -666,7 +666,7 @@ bind:map {showGrid} elevationProfileContainer="elevation-profile" - mapOptions={{ preserveDrawingBuffer: true }} + mapOptions={{ canvasContextAttributes: { preserveDrawingBuffer: true } }} >
  • diff --git a/web/src/routes/profile/[handle]/stats/+page.svelte b/web/src/routes/profile/[handle]/stats/+page.svelte index 57cdebf8..e56ac44f 100644 --- a/web/src/routes/profile/[handle]/stats/+page.svelte +++ b/web/src/routes/profile/[handle]/stats/+page.svelte @@ -9,8 +9,14 @@ import SummitLogTable from "$lib/components/summit_log/summit_log_table.svelte"; import type { SummitLog } from "$lib/models/summit_log.js"; import { categories } from "$lib/stores/category_store.js"; + import { categoryPreferences } from "$lib/stores/category_preference_store"; import { profile_stats_index } from "$lib/stores/profile_store.js"; import { show_toast } from "$lib/stores/toast_store.svelte.js"; + import { + displayCategoryName, + preferenceForCategory, + sortedCategoriesByPreference, + } from "$lib/util/category_util"; import { formatDistance, formatElevation, @@ -30,7 +36,7 @@ Tooltip, } from "chart.js"; import { untrack } from "svelte"; - import { _ } from "svelte-i18n"; + import { _, locale } from "svelte-i18n"; let { data } = $props(); @@ -48,10 +54,22 @@ const filter = $state(untrack(() => data.filter)); - const categorySelectItems: SelectItem[] = $categories.map((c) => ({ - value: c.id, - text: c.name, - })); + let categorySelectItems = $derived( + sortedCategoriesByPreference( + $categories, + $categoryPreferences, + $locale, + ) + .filter( + (c) => + preferenceForCategory($categoryPreferences, c.id)?.visible !== + false || filter.category.includes(c.id), + ) + .map((c) => ({ + value: c.id, + text: displayCategoryName(c, $locale), + })), + ); const barChartSelectItems: SelectItem[] = [ { @@ -93,8 +111,11 @@ summitLogs.reduce( (acc, log) => { const cat = - log.expand?.trail?.expand?.category?.name ?? "unknown"; - acc[$_(cat)] = (acc[$_(cat)] || 0) + 1; + log.expand?.trail?.expand?.category; + const key = cat + ? displayCategoryName(cat, $locale) + : "-"; + acc[key] = (acc[key] || 0) + 1; return acc; }, {} as Record, diff --git a/web/src/routes/profile/[handle]/stats/+page.ts b/web/src/routes/profile/[handle]/stats/+page.ts index ac98339a..6309e15f 100644 --- a/web/src/routes/profile/[handle]/stats/+page.ts +++ b/web/src/routes/profile/[handle]/stats/+page.ts @@ -1,5 +1,6 @@ import type { SummitLogFilter } from "$lib/models/summit_log"; import { categories_index } from "$lib/stores/category_store"; +import { category_preferences_index } from "$lib/stores/category_preference_store"; import { profile_stats_index } from "$lib/stores/profile_store"; import { error, type Load } from "@sveltejs/kit"; @@ -16,7 +17,8 @@ export const load: Load = async ({ params, fetch, parent }) => { const firstDay = new Date(y, m, 2); const lastDay = new Date(y, m + 1, 1); - const categories = await categories_index(fetch) + await categories_index(fetch) + await category_preferences_index(fetch) const filter: SummitLogFilter = { startDate: firstDay.toISOString().slice(0, 10), @@ -30,4 +32,4 @@ export const load: Load = async ({ params, fetch, parent }) => { } catch (e) { return { logs: [], filter } } -}; \ No newline at end of file +}; diff --git a/web/src/routes/profile/[handle]/trails/+page.svelte b/web/src/routes/profile/[handle]/trails/+page.svelte index 66e9b4f0..61388f35 100644 --- a/web/src/routes/profile/[handle]/trails/+page.svelte +++ b/web/src/routes/profile/[handle]/trails/+page.svelte @@ -71,7 +71,7 @@ {pagination} {loading} fullWidthCards={true} - trails={trails.items} + bind:trails={trails.items} {filter} onupdate={handleFilterUpdate} onpagination={paginate} diff --git a/web/src/routes/profile/[handle]/trails/+page.ts b/web/src/routes/profile/[handle]/trails/+page.ts index 07adeb7a..bf911ea6 100644 --- a/web/src/routes/profile/[handle]/trails/+page.ts +++ b/web/src/routes/profile/[handle]/trails/+page.ts @@ -1,5 +1,8 @@ import type { TrailFilter } from "$lib/models/trail"; import { profile_trails_index } from "$lib/stores/profile_store"; +import { categories_index } from "$lib/stores/category_store"; +import { subcategory_preferences_index } from "$lib/stores/subcategory_preference_store"; +import { subcategories_index } from "$lib/stores/subcategory_store"; import { error, type Load } from "@sveltejs/kit"; export const load: Load = async ({ params, fetch, parent }) => { @@ -11,6 +14,7 @@ export const load: Load = async ({ params, fetch, parent }) => { const filter: TrailFilter = { q: "", category: [], + subcategory: [], tags: [], difficulty: [0, 1, 2], author: actor.id, @@ -34,9 +38,12 @@ export const load: Load = async ({ params, fetch, parent }) => { sortOrder: "+", }; try { + await categories_index(fetch) + await subcategories_index(fetch) + await subcategory_preferences_index(fetch) const trails = await profile_trails_index(params.handle, filter, 1, 12, fetch) return { trails, filter } } catch (e) { return { trails: { hits: {}, items: [], page: 1, totalPages: 0 }, filter } } -}; \ No newline at end of file +}; diff --git a/web/src/routes/settings/+layout.svelte b/web/src/routes/settings/+layout.svelte index d79fb789..d6cbab12 100644 --- a/web/src/routes/settings/+layout.svelte +++ b/web/src/routes/settings/+layout.svelte @@ -21,6 +21,7 @@ }, { text: $_("notifications"), value: "/settings/notifications" }, { text: $_("map"), value: "/settings/map" }, + { text: $_("categories"), value: "/settings/categories" }, { text: $_("plugins"), value: "/settings/plugins" }, { text: $_("similar-trails"), value: "/settings/maintenance/similar-trails" }, { text: `${$_("import")}/${$_("export")}`, value: "/settings/export" }, diff --git a/web/src/routes/settings/categories/+page.server.ts b/web/src/routes/settings/categories/+page.server.ts new file mode 100644 index 00000000..4ff4b600 --- /dev/null +++ b/web/src/routes/settings/categories/+page.server.ts @@ -0,0 +1,43 @@ +import { Collection } from "$lib/util/api_util"; +import type { ServerLoad } from "@sveltejs/kit"; + +type TrailCategoryUsageRecord = { + category?: string; + subcategory?: string; +}; + +function increment(counts: Record, id?: string) { + if (!id) { + return; + } + + counts[id] = (counts[id] ?? 0) + 1; +} + +export const load: ServerLoad = async ({ locals }) => { + const trailUsage = { + categories: {} as Record, + subcategories: {} as Record, + }; + + if (!locals.user?.actor) { + return { trailUsage }; + } + + const trails = await locals.pb + .collection(Collection.trails) + .getFullList({ + filter: locals.pb.filter("author = {:author}", { + author: locals.user.actor, + }), + fields: "category,subcategory", + requestKey: null, + }); + + for (const trail of trails) { + increment(trailUsage.categories, trail.category); + increment(trailUsage.subcategories, trail.subcategory); + } + + return { trailUsage }; +}; diff --git a/web/src/routes/settings/categories/+page.svelte b/web/src/routes/settings/categories/+page.svelte new file mode 100644 index 00000000..1dc4a070 --- /dev/null +++ b/web/src/routes/settings/categories/+page.svelte @@ -0,0 +1,1213 @@ + + + + {$_("settings")} | wanderer + + +

    {$_("category-preferences")}

    +

    + {$_("category-preferences-description")} +

    +
    + +
      + {#each orderedCategories as category, index} + {@const currentPreference = preference(category)} + {@const categoryVisible = currentPreference?.visible !== false} + {@const categoryName = displayCategoryName(category, $locale)} + {@const childSubcategories = subcategoriesForCategory(category)} + {@const categoryExpanded = isCategoryExpanded(category)} +
    1. +
      + + + + + + + +
      +

      + {categoryName} +

      + {#if childSubcategories.length > 0 && !categoryExpanded} +
      + {#each childSubcategories as subcategory (subcategory.id)} + {@const subcategoryLabel = displaySubcategoryLabel( + subcategory, + $locale, + )} + {@const visibleSubcategory = + categoryVisible && + isSubcategoryVisible(subcategory)} + {@const badgeListIcon = + subcategoryBadgeListIcon(subcategory)} + + {#if badgeListIcon} + + {/if} + + {subcategoryLabel} + + + {/each} +
      + {/if} +
      + +
      + {#if childSubcategories.length > 0} + + {/if} + {#key `${category.id}-${categoryVisible}-${categoryToggleResetKey}`} + + toggleCategoryVisibility(category, value)} + > + {/key} +
      +
      + + {#if childSubcategories.length > 0 && categoryExpanded} +
      +
        + {#each childSubcategories as subcategory, subcategoryIndex (subcategory.id)} + {@const subcategoryLabel = displaySubcategoryLabel( + subcategory, + $locale, + )} + {@const visibleSubcategory = + categoryVisible && + isSubcategoryVisible(subcategory)} + {@const badgeIcon = + displaySubcategoryBadgeIcon(subcategory)} + {@const badge = displaySubcategoryShortBadge( + subcategory, + $locale, + )} +
      1. + + + + + {#if badgeIcon} + + {/if} + {#if badge} + + {badge} + + {/if} + +

        + {subcategoryLabel} +

        +
        + {#key `${subcategory.id}-${visibleSubcategory}-${categoryToggleResetKey}`} + + toggleSubcategoryVisibility( + subcategory, + )} + > + {/key} +
        +
      2. + {/each} +
      +
      + {/if} +
    2. + {/each} +
    + + +
    + {#if pendingDisable} + {@const ownTrailCount = ownTrailCountForPendingDisable()} + {@const activePlugins = activePluginNamesForPendingDisable()} + {@const inactivePlugins = inactivePluginNamesForPendingDisable()} +

    {disableIntroTextForPendingDisable()}

    + +
    +

    + {$_("conflicts")} +

    +
      + {#if ownTrailCount > 0} +
    • + + + +
      +

      {ownTrailMessageForPendingDisable()}

      +
      + +
    • + {/if} + {#if activePlugins.length > 0} +
    • + + + +

      {activePluginMessageForPendingDisable()}

      +
    • + {/if} + {#if inactivePlugins.length > 0} +
    • + + + +

      {inactivePluginMessageForPendingDisable()}

      +
    • + {/if} +
    +
    + +

    {disableAnywayTextForPendingDisable()}

    + {/if} +
    +
    + + diff --git a/web/src/routes/settings/categories/+page.ts b/web/src/routes/settings/categories/+page.ts new file mode 100644 index 00000000..03658786 --- /dev/null +++ b/web/src/routes/settings/categories/+page.ts @@ -0,0 +1,37 @@ +import { categories_index } from "$lib/stores/category_store"; +import { category_preferences_index } from "$lib/stores/category_preference_store"; +import { plugin_instances_index } from "$lib/stores/plugin_instance_store"; +import { plugins_index } from "$lib/stores/plugin_store"; +import { subcategory_preferences_index } from "$lib/stores/subcategory_preference_store"; +import { subcategories_index } from "$lib/stores/subcategory_store"; +import { type Load } from "@sveltejs/kit"; + +export const load: Load = async ({ fetch, data, parent }) => { + const parentData = await parent(); + const [ + categories, + categoryPreferences, + subcategories, + subcategoryPreferences, + pluginInstances, + pluginProviders, + ] = await Promise.all([ + categories_index(fetch), + category_preferences_index(fetch), + subcategories_index(fetch), + subcategory_preferences_index(fetch), + plugin_instances_index(fetch), + plugins_index(fetch), + ]); + + return { + categories, + categoryPreferences, + subcategories, + subcategoryPreferences, + pluginInstances, + pluginProviders, + user: parentData.user, + trailUsage: data?.trailUsage ?? { categories: {}, subcategories: {} }, + }; +}; diff --git a/web/src/routes/settings/plugins/+page.svelte b/web/src/routes/settings/plugins/+page.svelte index afd1d669..417b35a2 100644 --- a/web/src/routes/settings/plugins/+page.svelte +++ b/web/src/routes/settings/plugins/+page.svelte @@ -5,6 +5,7 @@ import type { Category } from "$lib/models/category.js"; import type { PluginInstance } from "$lib/models/plugin_instance.js"; import type { PluginProvider } from "$lib/models/plugin_provider.js"; + import type { Subcategory } from "$lib/models/subcategory.js"; import { plugin_category_remap_apply, plugin_category_remap_preview, @@ -30,6 +31,7 @@ untrack(() => data.pluginProviders ?? []), ); let categories: Category[] = $state(untrack(() => data.categories ?? [])); + let subcategories: Subcategory[] = $state(untrack(() => data.subcategories ?? [])); let pluginSettingsModal: PluginInstanceSettingsModal | undefined = $state(); let categoryRemapConfirmModal: ConfirmModal | undefined = $state(); @@ -554,6 +556,7 @@ plugin={selectedPlugin} instance={instanceForPlugin(selectedPlugin)} categories={categories} + subcategories={subcategories} onbeforecategorymappingsave={confirmCategoryMappingSave} onsave={savePluginInstance} > diff --git a/web/src/routes/settings/plugins/+page.ts b/web/src/routes/settings/plugins/+page.ts index 0956b4fe..d794c501 100644 --- a/web/src/routes/settings/plugins/+page.ts +++ b/web/src/routes/settings/plugins/+page.ts @@ -1,13 +1,19 @@ import { plugin_instances_index } from "$lib/stores/plugin_instance_store"; import { plugins_index } from "$lib/stores/plugin_store"; +import { category_preferences_index } from "$lib/stores/category_preference_store"; import { categories_index } from "$lib/stores/category_store"; +import { subcategory_preferences_index } from "$lib/stores/subcategory_preference_store"; +import { subcategories_index } from "$lib/stores/subcategory_store"; import { type Load } from "@sveltejs/kit"; export const load: Load = async ({ fetch }) => { - const [pluginInstances, pluginProviders, categories] = await Promise.all([ + const [pluginInstances, pluginProviders, categories, subcategories] = await Promise.all([ plugin_instances_index(fetch), plugins_index(fetch), categories_index(fetch), + subcategories_index(fetch), + category_preferences_index(fetch), + subcategory_preferences_index(fetch), ]); - return { pluginInstances, pluginProviders, categories }; + return { pluginInstances, pluginProviders, categories, subcategories }; }; diff --git a/web/src/routes/settings/profile/+page.svelte b/web/src/routes/settings/profile/+page.svelte index c6a7549e..f42fc9e9 100644 --- a/web/src/routes/settings/profile/+page.svelte +++ b/web/src/routes/settings/profile/+page.svelte @@ -1,31 +1,31 @@ @@ -124,11 +118,24 @@

    {$_("favourite-sport")}

    - +
    + {#if favouriteCategory} + + + {displayCategoryName(favouriteCategory, $locale)} + + {:else} + + {/if} + + + +
    {/if} diff --git a/web/src/routes/settings/profile/+page.ts b/web/src/routes/settings/profile/+page.ts index 85b2a1f1..cdc88d37 100644 --- a/web/src/routes/settings/profile/+page.ts +++ b/web/src/routes/settings/profile/+page.ts @@ -1,8 +1,10 @@ import { categories_index } from "$lib/stores/category_store"; +import { category_preferences_index } from "$lib/stores/category_preference_store"; import { type Load } from "@sveltejs/kit"; -export const load: Load = async ({ params, fetch }) => { - const categories = await categories_index(fetch) +export const load: Load = async ({ fetch }) => { + const categories = await categories_index(fetch); + const categoryPreferences = await category_preferences_index(fetch); - return { categories: categories } -}; \ No newline at end of file + return { categories, categoryPreferences }; +}; diff --git a/web/src/routes/trail/edit/[id]/+page.svelte b/web/src/routes/trail/edit/[id]/+page.svelte index 847add27..3e326d12 100644 --- a/web/src/routes/trail/edit/[id]/+page.svelte +++ b/web/src/routes/trail/edit/[id]/+page.svelte @@ -27,7 +27,6 @@ import type { OverpassPopupActionFactory } from "$lib/vendor/maplibre-layer-manager/overpass-layer"; import { type OverpassPopupAction } from "$lib/util/maplibre_util"; import { Waypoint } from "$lib/models/waypoint"; - import { categories } from "$lib/stores/category_store"; import { lists_add_trail, lists_remove_trail, @@ -89,6 +88,7 @@ import { tags_index } from "$lib/stores/tag_store.js"; import { theme } from "$lib/stores/theme_store.js"; import { currentUser } from "$lib/stores/user_store.js"; + import { designSelectableCategories } from "$lib/util/category_util"; import { getIconForLocation } from "$lib/util/icon_util.js"; import { createAnchorMarker, @@ -105,13 +105,14 @@ import { createForm } from "felte"; import * as M from "maplibre-gl"; import { onMount, untrack } from "svelte"; - import { _ } from "svelte-i18n"; + import { _, locale } from "svelte-i18n"; import { backInOut } from "svelte/easing"; import { fly } from "svelte/transition"; import { z } from "zod"; import Track from "$lib/models/gpx/track.js"; import TrackSegment from "$lib/models/gpx/track-segment.js"; import ConfirmModal from "$lib/components/confirm_modal.svelte"; + import CategoryPicker from "$lib/components/trail/category_picker.svelte"; let { data } = $props(); @@ -155,7 +156,6 @@ let searchDropdownItems: SearchItem[] = $state([]); let selectedSearchLocation: SearchItem | null = $state(null); - let cropStartMarker: FontawesomeMarker; let cropEndMarker: FontawesomeMarker; @@ -191,15 +191,29 @@ let tagItems: ComboboxItem[] = $state([]); + function defaultCategoryId() { + const existingCategory = data.trail.category; + if (existingCategory) { + return existingCategory; + } + + // Pre-select the highest-priority visible category for new trails. + return ( + designSelectableCategories( + data.categories, + data.categoryPreferences, + $locale, + )[0]?.id ?? data.categories[0]?.id ?? "" + ); + } + const getInitialFormValues = () => ({ ...data.trail, public: data.trail.id ? data.trail.public : page.data.settings?.privacy?.trails === "public", - category: - data.trail.category || - page.data.settings?.category || - $categories[0].id, + category: defaultCategoryId(), + subcategory: data.trail.subcategory || "", }); const { @@ -297,6 +311,22 @@ }, }); + let categorySelectValue = $derived( + $formData.subcategory + ? `subcategory:${$formData.subcategory}` + : $formData.category + ? `category:${$formData.category}` + : "", + ); + + function handleCategoryChange(selection: { + category: string; + subcategory: string; + }) { + setFields("category", selection.category); + setFields("subcategory", selection.subcategory); + } + onMount(async () => { clearAnchors(); clearRoute(); @@ -405,8 +435,9 @@ if (!replaceExistingRoute) { setFields( "category", - page.data.settings.category || $categories[0].id, + defaultCategoryId(), ); + setFields("subcategory", ""); setFields( "public", page.data.settings?.privacy?.trails === "public", @@ -1993,14 +2024,13 @@ { text: $_("difficult"), value: "difficult" }, ]} > - +
    handleMapContextMenu(target)} onsegmentclick={(data) => handleSegmentClick(data)} onsegmentdragend={(data) => handleSegmentDragEnd(data)} - mapOptions={{ preserveDrawingBuffer: true }} + mapOptions={{ canvasContextAttributes: { preserveDrawingBuffer: true } }} {buildPoiAnchorAction} >
    diff --git a/web/src/routes/trail/edit/[id]/+page.ts b/web/src/routes/trail/edit/[id]/+page.ts index acb09052..5f0858ce 100644 --- a/web/src/routes/trail/edit/[id]/+page.ts +++ b/web/src/routes/trail/edit/[id]/+page.ts @@ -1,10 +1,15 @@ import { Trail } from "$lib/models/trail"; import { categories_index } from "$lib/stores/category_store"; +import { category_preferences_index } from "$lib/stores/category_preference_store"; import { lists_index } from "$lib/stores/list_store"; +import { subcategory_preferences_index } from "$lib/stores/subcategory_preference_store"; +import { subcategories_index } from "$lib/stores/subcategory_store"; import { trails_show } from "$lib/stores/trail_store"; import { currentUser } from "$lib/stores/user_store"; +import { designSelectableCategories } from "$lib/util/category_util"; import { error, type Load } from "@sveltejs/kit"; import { get } from "svelte/store"; +import { locale } from "svelte-i18n"; export const load: Load = async ({ params, fetch, url }) => { const user = get(currentUser) @@ -13,6 +18,9 @@ export const load: Load = async ({ params, fetch, url }) => { return error(400, "Bad Request") } const categories = await categories_index(fetch) + const categoryPreferences = await category_preferences_index(fetch) + await subcategories_index(fetch) + await subcategory_preferences_index(fetch) const lists = await lists_index({ q: "", author: user?.actor ?? "" }, 1, -1, fetch) let trail: Trail; @@ -23,11 +31,25 @@ export const load: Load = async ({ params, fetch, url }) => { const originalTrail = await trails_show(originalId, undefined, undefined, true, fetch); trail = Trail.from(originalTrail) } else { - trail = new Trail("", { category: categories[0] }); + const defaultCategory = + designSelectableCategories( + categories, + categoryPreferences, + get(locale), + )[0] ?? categories[0]; + + trail = defaultCategory + ? new Trail("", { category: defaultCategory }) + : new Trail(""); } } else { trail = await trails_show(params.id, undefined, url.searchParams.get("share") ?? undefined, true, fetch); } - return { trail: trail, lists: lists } -}; \ No newline at end of file + return { + trail: trail, + lists: lists, + categories: categories, + categoryPreferences: categoryPreferences, + } +}; diff --git a/web/src/routes/trails/+page.svelte b/web/src/routes/trails/+page.svelte index d9919ccd..e602b3ef 100644 --- a/web/src/routes/trails/+page.svelte +++ b/web/src/routes/trails/+page.svelte @@ -18,6 +18,13 @@ if (!browser) { return defaultFilter; } + if ( + page.url.searchParams.has("category") || + page.url.searchParams.has("subcategory") || + page.url.searchParams.has("author") + ) { + return defaultFilter; + } const stored = localStorage.getItem(TRAIL_LIST_FILTER_STORAGE_KEY); if (!stored) { @@ -227,7 +234,7 @@ handleFilterUpdate(false)} onpagination={paginate} diff --git a/web/src/routes/trails/+page.ts b/web/src/routes/trails/+page.ts index 97e1d269..793c3adb 100644 --- a/web/src/routes/trails/+page.ts +++ b/web/src/routes/trails/+page.ts @@ -1,14 +1,19 @@ import type { TrailFilter } from "$lib/models/trail"; import { categories_index } from "$lib/stores/category_store"; +import { category_preferences_index } from "$lib/stores/category_preference_store"; +import { subcategory_preferences_index } from "$lib/stores/subcategory_preference_store"; +import { subcategories_index } from "$lib/stores/subcategory_store"; import { trails_get_filter_values } from "$lib/stores/trail_store"; -import type { ServerLoad } from "@sveltejs/kit"; +import { normalizeCategoryName } from "$lib/util/category_util"; +import type { Load } from "@sveltejs/kit"; -export const load: ServerLoad = async ({ params, locals, url, fetch }) => { +export const load: Load = async ({ url, fetch }) => { const filterValues = await trails_get_filter_values(fetch); const filter: TrailFilter = { q: "", category: [], + subcategory: [], tags: [], difficulty: [0, 1, 2], author: "", @@ -31,14 +36,46 @@ export const load: ServerLoad = async ({ params, locals, url, fetch }) => { sort: "created", sortOrder: "+", }; + const categories = await categories_index(fetch) + const subcategories = await subcategories_index(fetch) + + const paramAuthor = url.searchParams.get("author"); + if (paramAuthor) { + filter.author = paramAuthor; + filter.shared = undefined; + } + const paramCategory = url.searchParams.get("category"); if (paramCategory) { - filter.category.push(paramCategory); + const normalizedCategory = normalizeCategoryName(paramCategory); + const category = categories.find( + (item) => + item.id === paramCategory || + normalizeCategoryName(item.name) === normalizedCategory, + ); + if (category) { + filter.category.push(category.id); + } } - const categories = await categories_index(fetch) + + const paramSubcategory = url.searchParams.get("subcategory"); + if (paramSubcategory) { + const normalizedSubcategory = normalizeCategoryName(paramSubcategory); + const subcategory = subcategories.find( + (item) => + item.id === paramSubcategory || + normalizeCategoryName(item.name) === normalizedSubcategory, + ); + if (subcategory) { + filter.subcategory.push(subcategory.id); + } + } + + await category_preferences_index(fetch) + await subcategory_preferences_index(fetch) return { categories, filter: filter }; -}; \ No newline at end of file +};