@jskit-ai/users-web 0.1.36 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/package.descriptor.mjs +8 -22
  2. package/package.json +7 -6
  3. package/src/client/components/MembersAdminClientElement.vue +5 -5
  4. package/src/client/components/WorkspaceMembersClientElement.vue +16 -16
  5. package/src/client/components/WorkspacesClientElement.vue +2 -2
  6. package/src/client/composables/accountSettingsAvatarUploadRuntime.js +26 -172
  7. package/src/client/composables/accountSettingsRuntimeConstants.js +0 -4
  8. package/src/client/composables/accountSettingsRuntimeHelpers.js +1 -1
  9. package/src/client/composables/addEditUiRuntime.js +11 -2
  10. package/src/client/composables/crudLookupFieldLabelSupport.js +36 -4
  11. package/src/client/composables/crudLookupFieldRuntime.js +5 -2
  12. package/src/client/composables/crudSchemaFormHelpers.js +23 -3
  13. package/src/client/composables/listQueryParamSupport.js +459 -0
  14. package/src/client/composables/listUiRuntime.js +18 -6
  15. package/src/client/composables/routeTemplateHelpers.js +122 -0
  16. package/src/client/composables/useAddEdit.js +10 -0
  17. package/src/client/composables/useList.js +242 -2
  18. package/src/client/composables/usePagedCollection.js +55 -4
  19. package/src/client/composables/useView.js +4 -1
  20. package/src/client/composables/viewUiRuntime.js +11 -2
  21. package/src/client/lib/bootstrap.js +1 -1
  22. package/src/client/lib/menuIcons.js +27 -6
  23. package/templates/src/components/WorkspaceNotFoundCard.vue +2 -1
  24. package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +1 -1
  25. package/test/addEditUiRuntime.test.js +18 -0
  26. package/test/crudLookupFieldRuntime.test.js +51 -1
  27. package/test/listQueryParamSupport.test.js +190 -0
  28. package/test/listUiRuntime.test.js +21 -0
  29. package/test/menuIcons.test.js +2 -0
  30. package/test/routeTemplateHelpers.test.js +56 -0
  31. package/test/usePagedCollection.test.js +53 -0
  32. package/test/viewUiRuntime.test.js +35 -0
@@ -2,6 +2,24 @@ import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validato
2
2
  import { asPlainObject } from "./scopeHelpers.js";
3
3
  import { toRouteParamValue } from "./routeTemplateHelpers.js";
4
4
 
5
+ const EMPTY_FIELD_ERROR_LIST = Object.freeze([]);
6
+ const fieldErrorListCache = new Map();
7
+
8
+ function resolveStableFieldErrorList(fieldKey, message) {
9
+ if (!message) {
10
+ return EMPTY_FIELD_ERROR_LIST;
11
+ }
12
+
13
+ const cacheKey = `${fieldKey}::${message}`;
14
+ if (fieldErrorListCache.has(cacheKey)) {
15
+ return fieldErrorListCache.get(cacheKey);
16
+ }
17
+
18
+ const nextValue = Object.freeze([message]);
19
+ fieldErrorListCache.set(cacheKey, nextValue);
20
+ return nextValue;
21
+ }
22
+
5
23
  function normalizeCrudFormFields(fields = []) {
6
24
  const normalizedFields = [];
7
25
  const seenKeys = new Set();
@@ -150,11 +168,12 @@ function resolveCrudFieldErrors(fieldErrors = {}, fieldKey = "") {
150
168
 
151
169
  const source = asPlainObject(fieldErrors);
152
170
  const message = String(source[key] || "").trim();
171
+
153
172
  if (!message) {
154
- return [];
173
+ return resolveStableFieldErrorList(key, "");
155
174
  }
156
175
 
157
- return [message];
176
+ return resolveStableFieldErrorList(key, message);
158
177
  }
159
178
 
160
179
  function parseCrudResourceOperationInput({
@@ -167,12 +186,13 @@ function parseCrudResourceOperationInput({
167
186
  const operations = asPlainObject(asPlainObject(resource).operations);
168
187
  const operation = asPlainObject(operations[normalizedOperationName]);
169
188
 
170
- return validateOperationSection({
189
+ const parsed = validateOperationSection({
171
190
  operation,
172
191
  section: "bodyValidator",
173
192
  value: rawPayload,
174
193
  context
175
194
  });
195
+ return parsed;
176
196
  }
177
197
 
178
198
  export {
@@ -0,0 +1,459 @@
1
+ import { isRef, unref } from "vue";
2
+ import {
3
+ normalizeBoolean,
4
+ normalizeText,
5
+ normalizeUniqueTextList
6
+ } from "@jskit-ai/kernel/shared/support/normalize";
7
+ import { asPlainObject } from "./scopeHelpers.js";
8
+
9
+ const QUERY_PARAM_BINDING_TYPE_TEXT = "text";
10
+ const QUERY_PARAM_BINDING_TYPE_BOOLEAN = "boolean";
11
+ const QUERY_PARAM_BINDING_TYPE_NUMBER = "number";
12
+ const QUERY_PARAM_BINDING_TYPE_ARRAY = "array";
13
+ const QUERY_PARAM_BINDING_TYPE_DATE = "date";
14
+
15
+ function normalizeListSyncToRouteConfig(syncToRoute = false, { defaultSearchParam = "q" } = {}) {
16
+ const source = syncToRoute === true ? {} : asPlainObject(syncToRoute);
17
+ const requested = syncToRoute === true || Object.keys(source).length > 0;
18
+ const queryParamBlacklist = Object.freeze(normalizeUniqueTextList(source.queryParamBlacklist));
19
+ if (!requested || source.enabled === false) {
20
+ return Object.freeze({
21
+ enabled: false,
22
+ mode: "replace",
23
+ syncSearch: false,
24
+ syncQueryParams: false,
25
+ hydrateFromRoute: false,
26
+ searchParam: normalizeText(defaultSearchParam) || "q",
27
+ queryParamBlacklist
28
+ });
29
+ }
30
+
31
+ const mode = normalizeText(source.mode).toLowerCase() === "push" ? "push" : "replace";
32
+ const searchParam = normalizeText(source.searchParam) || normalizeText(defaultSearchParam) || "q";
33
+
34
+ return Object.freeze({
35
+ enabled: true,
36
+ mode,
37
+ syncSearch: source.search !== false,
38
+ syncQueryParams: source.queryParams !== false,
39
+ hydrateFromRoute: source.hydrateFromRoute !== false,
40
+ searchParam,
41
+ queryParamBlacklist
42
+ });
43
+ }
44
+
45
+ function normalizeQueryParamKey(value) {
46
+ return normalizeText(value);
47
+ }
48
+
49
+ function normalizeQueryParamValues(value) {
50
+ const list = Array.isArray(value) ? value : [value];
51
+ const normalizedValues = [];
52
+
53
+ for (const entry of list) {
54
+ const resolvedEntry = unref(entry);
55
+ if (resolvedEntry == null) {
56
+ continue;
57
+ }
58
+
59
+ if (typeof resolvedEntry === "boolean") {
60
+ if (resolvedEntry) {
61
+ normalizedValues.push("1");
62
+ }
63
+ continue;
64
+ }
65
+
66
+ if (typeof resolvedEntry === "number") {
67
+ if (Number.isFinite(resolvedEntry)) {
68
+ normalizedValues.push(String(resolvedEntry));
69
+ }
70
+ continue;
71
+ }
72
+
73
+ if (resolvedEntry instanceof Date) {
74
+ if (!Number.isNaN(resolvedEntry.getTime())) {
75
+ normalizedValues.push(resolvedEntry.toISOString());
76
+ }
77
+ continue;
78
+ }
79
+
80
+ const normalizedText = normalizeText(resolvedEntry);
81
+ if (normalizedText) {
82
+ normalizedValues.push(normalizedText);
83
+ }
84
+ }
85
+
86
+ return normalizedValues;
87
+ }
88
+
89
+ function resolveQueryParamsInput(queryParams, context = {}) {
90
+ if (typeof queryParams === "function") {
91
+ return asPlainObject(queryParams(context));
92
+ }
93
+ return asPlainObject(unref(queryParams));
94
+ }
95
+
96
+ function resolveQueryParamBindingType(value) {
97
+ if (Array.isArray(value)) {
98
+ return QUERY_PARAM_BINDING_TYPE_ARRAY;
99
+ }
100
+ if (typeof value === "boolean") {
101
+ return QUERY_PARAM_BINDING_TYPE_BOOLEAN;
102
+ }
103
+ if (typeof value === "number") {
104
+ return QUERY_PARAM_BINDING_TYPE_NUMBER;
105
+ }
106
+ if (value instanceof Date) {
107
+ return QUERY_PARAM_BINDING_TYPE_DATE;
108
+ }
109
+
110
+ return QUERY_PARAM_BINDING_TYPE_TEXT;
111
+ }
112
+
113
+ function resolveArrayQueryParamItemType(values = []) {
114
+ const list = Array.isArray(values) ? values : [];
115
+ for (const entry of list) {
116
+ if (entry == null) {
117
+ continue;
118
+ }
119
+
120
+ if (typeof entry === "boolean") {
121
+ return QUERY_PARAM_BINDING_TYPE_BOOLEAN;
122
+ }
123
+ if (typeof entry === "number") {
124
+ return QUERY_PARAM_BINDING_TYPE_NUMBER;
125
+ }
126
+ if (entry instanceof Date) {
127
+ return QUERY_PARAM_BINDING_TYPE_DATE;
128
+ }
129
+
130
+ return QUERY_PARAM_BINDING_TYPE_TEXT;
131
+ }
132
+
133
+ return QUERY_PARAM_BINDING_TYPE_TEXT;
134
+ }
135
+
136
+ function createWritableQueryParamBinding({
137
+ source = {},
138
+ rawKey = "",
139
+ rawValue = null,
140
+ key = ""
141
+ } = {}) {
142
+ const valueSourceIsRef = isRef(rawValue);
143
+ const read = valueSourceIsRef
144
+ ? () => rawValue.value
145
+ : () => source[rawKey];
146
+ const write = valueSourceIsRef
147
+ ? (nextValue) => {
148
+ rawValue.value = nextValue;
149
+ }
150
+ : (nextValue) => {
151
+ source[rawKey] = nextValue;
152
+ };
153
+
154
+ const currentValue = read();
155
+ const valueType = resolveQueryParamBindingType(currentValue);
156
+ return {
157
+ key,
158
+ valueType,
159
+ arrayItemType: valueType === QUERY_PARAM_BINDING_TYPE_ARRAY
160
+ ? resolveArrayQueryParamItemType(currentValue)
161
+ : QUERY_PARAM_BINDING_TYPE_TEXT,
162
+ get: read,
163
+ set: write
164
+ };
165
+ }
166
+
167
+ function resolveQueryParamDescriptors(queryParams, context = {}) {
168
+ const source = resolveQueryParamsInput(queryParams, context);
169
+ const descriptorsByKey = new Map();
170
+ const canWriteToSource = typeof queryParams !== "function";
171
+
172
+ for (const [rawKey, rawValue] of Object.entries(source)) {
173
+ const key = normalizeQueryParamKey(rawKey);
174
+ if (!key) {
175
+ continue;
176
+ }
177
+
178
+ const values = normalizeQueryParamValues(rawValue);
179
+ const current = descriptorsByKey.get(key) || {
180
+ key,
181
+ values: [],
182
+ binding: null
183
+ };
184
+ if (values.length > 0) {
185
+ current.values.push(...values);
186
+ }
187
+ if (!current.binding && canWriteToSource) {
188
+ current.binding = createWritableQueryParamBinding({
189
+ source,
190
+ rawKey,
191
+ rawValue,
192
+ key
193
+ });
194
+ }
195
+
196
+ descriptorsByKey.set(key, current);
197
+ }
198
+
199
+ return [...descriptorsByKey.values()].sort((left, right) => left.key.localeCompare(right.key));
200
+ }
201
+
202
+ function resolveActiveQueryParamEntries(descriptors = []) {
203
+ const source = Array.isArray(descriptors) ? descriptors : [];
204
+ return source
205
+ .filter((descriptor) => Array.isArray(descriptor?.values) && descriptor.values.length > 0)
206
+ .map((descriptor) => ({
207
+ key: descriptor.key,
208
+ values: [...descriptor.values]
209
+ }));
210
+ }
211
+
212
+ function resolveWritableQueryParamBindings(descriptors = []) {
213
+ const source = Array.isArray(descriptors) ? descriptors : [];
214
+ return source
215
+ .map((descriptor) => descriptor?.binding || null)
216
+ .filter(Boolean);
217
+ }
218
+
219
+ function buildQueryParamEntriesToken(entries = []) {
220
+ const normalizedEntries = Array.isArray(entries) ? entries : [];
221
+ if (normalizedEntries.length < 1) {
222
+ return "";
223
+ }
224
+
225
+ return normalizedEntries
226
+ .map((entry) => {
227
+ const key = normalizeQueryParamKey(entry?.key);
228
+ const values = Array.isArray(entry?.values)
229
+ ? entry.values.map((value) => normalizeText(value)).filter(Boolean)
230
+ : [];
231
+ if (!key || values.length < 1) {
232
+ return "";
233
+ }
234
+ return `${key}=${values.join(",")}`;
235
+ })
236
+ .filter(Boolean)
237
+ .join("&");
238
+ }
239
+
240
+ function firstRouteQueryValue(value) {
241
+ if (Array.isArray(value)) {
242
+ return value[0];
243
+ }
244
+ return value;
245
+ }
246
+
247
+ function normalizeRouteQueryValues(value) {
248
+ const values = Array.isArray(value) ? value : [value];
249
+ return values
250
+ .map((entry) => normalizeText(entry))
251
+ .filter(Boolean);
252
+ }
253
+
254
+ function parseRouteBooleanValue(value, fallback = false) {
255
+ if (value === undefined) {
256
+ return fallback;
257
+ }
258
+ if (value === null || value === "") {
259
+ return true;
260
+ }
261
+
262
+ try {
263
+ return normalizeBoolean(firstRouteQueryValue(value));
264
+ } catch {
265
+ return fallback;
266
+ }
267
+ }
268
+
269
+ function parseRouteNumberValue(value, fallback = null) {
270
+ if (value === undefined) {
271
+ return null;
272
+ }
273
+
274
+ const normalized = normalizeText(firstRouteQueryValue(value));
275
+ if (!normalized) {
276
+ return null;
277
+ }
278
+
279
+ const parsed = Number(normalized);
280
+ if (!Number.isFinite(parsed)) {
281
+ return fallback;
282
+ }
283
+
284
+ return parsed;
285
+ }
286
+
287
+ function parseRouteDateValue(value, fallback = null) {
288
+ if (value === undefined) {
289
+ return null;
290
+ }
291
+
292
+ const normalized = normalizeText(firstRouteQueryValue(value));
293
+ if (!normalized) {
294
+ return null;
295
+ }
296
+
297
+ const parsed = new Date(normalized);
298
+ if (Number.isNaN(parsed.getTime())) {
299
+ return fallback;
300
+ }
301
+
302
+ return parsed;
303
+ }
304
+
305
+ function parseRouteQueryItemValue(value, itemType = QUERY_PARAM_BINDING_TYPE_TEXT, fallback = null) {
306
+ if (itemType === QUERY_PARAM_BINDING_TYPE_BOOLEAN) {
307
+ try {
308
+ return normalizeBoolean(value);
309
+ } catch {
310
+ return fallback;
311
+ }
312
+ }
313
+ if (itemType === QUERY_PARAM_BINDING_TYPE_NUMBER) {
314
+ const numeric = Number(value);
315
+ return Number.isFinite(numeric) ? numeric : fallback;
316
+ }
317
+ if (itemType === QUERY_PARAM_BINDING_TYPE_DATE) {
318
+ const date = new Date(value);
319
+ return Number.isNaN(date.getTime()) ? fallback : date;
320
+ }
321
+
322
+ const normalized = normalizeText(value);
323
+ return normalized || fallback;
324
+ }
325
+
326
+ function parseRouteBindingValue(binding, routeQueryValue) {
327
+ const valueType = normalizeText(binding?.valueType).toLowerCase();
328
+ if (valueType === QUERY_PARAM_BINDING_TYPE_BOOLEAN) {
329
+ return parseRouteBooleanValue(routeQueryValue, false);
330
+ }
331
+ if (valueType === QUERY_PARAM_BINDING_TYPE_NUMBER) {
332
+ const fallback = typeof binding?.get === "function" ? binding.get() : null;
333
+ return parseRouteNumberValue(routeQueryValue, fallback);
334
+ }
335
+ if (valueType === QUERY_PARAM_BINDING_TYPE_DATE) {
336
+ const fallback = typeof binding?.get === "function" ? binding.get() : null;
337
+ return parseRouteDateValue(routeQueryValue, fallback);
338
+ }
339
+ if (valueType === QUERY_PARAM_BINDING_TYPE_ARRAY) {
340
+ const itemType = normalizeText(binding?.arrayItemType).toLowerCase() || QUERY_PARAM_BINDING_TYPE_TEXT;
341
+ return normalizeRouteQueryValues(routeQueryValue)
342
+ .map((value) => parseRouteQueryItemValue(value, itemType, null))
343
+ .filter((value) => value != null && !(typeof value === "string" && !value));
344
+ }
345
+
346
+ return normalizeText(firstRouteQueryValue(routeQueryValue));
347
+ }
348
+
349
+ function areQueryParamBindingItemsEqual(left, right) {
350
+ if (left === right) {
351
+ return true;
352
+ }
353
+ if (left instanceof Date && right instanceof Date) {
354
+ return left.getTime() === right.getTime();
355
+ }
356
+
357
+ return Object.is(left, right);
358
+ }
359
+
360
+ function areQueryParamBindingValuesEqual(left, right) {
361
+ if (left === right) {
362
+ return true;
363
+ }
364
+ if (left instanceof Date && right instanceof Date) {
365
+ return left.getTime() === right.getTime();
366
+ }
367
+ if (!Array.isArray(left) || !Array.isArray(right)) {
368
+ return Object.is(left, right);
369
+ }
370
+ if (left.length !== right.length) {
371
+ return false;
372
+ }
373
+
374
+ for (let index = 0; index < left.length; index += 1) {
375
+ if (!areQueryParamBindingItemsEqual(left[index], right[index])) {
376
+ return false;
377
+ }
378
+ }
379
+
380
+ return true;
381
+ }
382
+
383
+ function buildRouteQueryCompareToken(query = {}) {
384
+ const source = asPlainObject(query);
385
+ const keys = Object.keys(source).sort();
386
+ const parts = [];
387
+ for (const key of keys) {
388
+ const normalizedKey = normalizeQueryParamKey(key);
389
+ if (!normalizedKey) {
390
+ continue;
391
+ }
392
+
393
+ const normalizedValues = normalizeRouteQueryValues(source[key]);
394
+ if (normalizedValues.length < 1) {
395
+ continue;
396
+ }
397
+
398
+ parts.push(`${normalizedKey}=${normalizedValues.join(",")}`);
399
+ }
400
+
401
+ return parts.join("&");
402
+ }
403
+
404
+ function mergeManagedQueryParamKeyHistory(history = [], keys = []) {
405
+ const merged = new Set();
406
+ for (const key of Array.isArray(history) ? history : []) {
407
+ const normalized = normalizeQueryParamKey(key);
408
+ if (normalized) {
409
+ merged.add(normalized);
410
+ }
411
+ }
412
+ for (const key of Array.isArray(keys) ? keys : []) {
413
+ const normalized = normalizeQueryParamKey(key);
414
+ if (normalized) {
415
+ merged.add(normalized);
416
+ }
417
+ }
418
+
419
+ return [...merged].sort((left, right) => left.localeCompare(right));
420
+ }
421
+
422
+ function resolveRouteSyncManagedKeys({
423
+ searchEnabled = false,
424
+ searchParam = "q",
425
+ syncSearch = false,
426
+ syncQueryParams = false,
427
+ declaredKeys = [],
428
+ keyHistory = []
429
+ } = {}) {
430
+ const managed = new Set();
431
+
432
+ if (syncSearch === true && searchEnabled === true) {
433
+ const normalizedSearchParam = normalizeQueryParamKey(searchParam);
434
+ if (normalizedSearchParam) {
435
+ managed.add(normalizedSearchParam);
436
+ }
437
+ }
438
+
439
+ if (syncQueryParams === true) {
440
+ for (const key of mergeManagedQueryParamKeyHistory(keyHistory, declaredKeys)) {
441
+ managed.add(key);
442
+ }
443
+ }
444
+
445
+ return [...managed].sort((left, right) => left.localeCompare(right));
446
+ }
447
+
448
+ export {
449
+ normalizeListSyncToRouteConfig,
450
+ resolveQueryParamDescriptors,
451
+ resolveActiveQueryParamEntries,
452
+ resolveWritableQueryParamBindings,
453
+ buildQueryParamEntriesToken,
454
+ parseRouteBindingValue,
455
+ areQueryParamBindingValuesEqual,
456
+ buildRouteQueryCompareToken,
457
+ mergeManagedQueryParamKeyHistory,
458
+ resolveRouteSyncManagedKeys
459
+ };
@@ -4,7 +4,7 @@ import {
4
4
  normalizeRouteParamName,
5
5
  toRouteParamValue,
6
6
  resolveRouteParamsSource,
7
- resolveRoutePathnameSource,
7
+ resolveScopedRoutePathname,
8
8
  resolveRouteTemplateLocation
9
9
  } from "./routeTemplateHelpers.js";
10
10
 
@@ -23,6 +23,7 @@ function createListUiRuntime({
23
23
  recordIdParam = "recordId",
24
24
  recordIdSelector = null,
25
25
  routeParams = null,
26
+ routeParamNames = null,
26
27
  routePath = "",
27
28
  viewUrlTemplate = "",
28
29
  editUrlTemplate = ""
@@ -44,12 +45,23 @@ function createListUiRuntime({
44
45
  return "";
45
46
  }
46
47
 
48
+ const currentRouteParams = resolveRouteParamsSource(routeParams);
49
+ const sourceParams = {
50
+ ...currentRouteParams,
51
+ ...asPlainObject(extraParams)
52
+ };
53
+ const currentPathname = resolveScopedRoutePathname({
54
+ currentPathname: routePath,
55
+ params: currentRouteParams,
56
+ orderedParamNames: routeParamNames,
57
+ anchorParamName: normalizedRecordIdParam,
58
+ anchorParamValue: currentRouteParams[normalizedRecordIdParam],
59
+ anchorMode: "before"
60
+ });
61
+
47
62
  return resolveRouteTemplateLocation(normalizedTemplate, {
48
- params: {
49
- ...resolveRouteParamsSource(routeParams),
50
- ...asPlainObject(extraParams)
51
- },
52
- currentPathname: resolveRoutePathnameSource(routePath)
63
+ params: sourceParams,
64
+ currentPathname
53
65
  });
54
66
  }
55
67
 
@@ -40,6 +40,17 @@ function resolveRouteParamsSource(source = null) {
40
40
  return asPlainObject(resolveRouteSourceValue(source));
41
41
  }
42
42
 
43
+ function normalizeRouteParamNameList(value = []) {
44
+ const source = Array.isArray(value) ? value : [];
45
+ return source
46
+ .map((entry) => String(entry || "").trim())
47
+ .filter(Boolean);
48
+ }
49
+
50
+ function resolveRouteParamNamesSource(source = []) {
51
+ return normalizeRouteParamNameList(resolveRouteSourceValue(source));
52
+ }
53
+
43
54
  function normalizeRoutePathname(value = "") {
44
55
  const rawPathname = String(value || "").trim();
45
56
  const sanitizedPathname = rawPathname.split(/[?#]/u, 1)[0] || "";
@@ -54,6 +65,115 @@ function resolveRoutePathnameSource(source = "") {
54
65
  return normalizeRoutePathname(resolveRouteSourceValue(source));
55
66
  }
56
67
 
68
+ function segmentMatchesParamValue(segment = "", paramValue = "") {
69
+ const normalizedSegment = String(segment || "").trim();
70
+ const normalizedParamValue = toRouteParamValue(paramValue);
71
+ if (!normalizedSegment || !normalizedParamValue) {
72
+ return false;
73
+ }
74
+
75
+ const encodedParamValue = encodeURIComponent(normalizedParamValue);
76
+ if (normalizedSegment === encodedParamValue) {
77
+ return true;
78
+ }
79
+
80
+ try {
81
+ return decodeURIComponent(normalizedSegment) === normalizedParamValue;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ function findRouteParamSegmentIndex(segments = [], paramValue = "", fromIndex = 0) {
88
+ const source = Array.isArray(segments) ? segments : [];
89
+ const cursor = Number.isInteger(fromIndex) && fromIndex > 0 ? fromIndex : 0;
90
+ for (let index = cursor; index < source.length; index += 1) {
91
+ if (segmentMatchesParamValue(source[index], paramValue)) {
92
+ return index;
93
+ }
94
+ }
95
+ return -1;
96
+ }
97
+
98
+ function normalizePathPrefix(segments = [], endIndex = -1) {
99
+ const source = Array.isArray(segments) ? segments : [];
100
+ if (!Number.isInteger(endIndex) || endIndex < 0) {
101
+ return "/";
102
+ }
103
+ return `/${source.slice(0, endIndex + 1).join("/")}`;
104
+ }
105
+
106
+ function resolveAnchorEndIndex(segmentIndex = -1, totalSegments = 0, anchorMode = "at") {
107
+ const normalizedSegmentIndex = Number.isInteger(segmentIndex) ? segmentIndex : -1;
108
+ if (normalizedSegmentIndex < 0) {
109
+ return -1;
110
+ }
111
+
112
+ const normalizedTotalSegments = Number.isInteger(totalSegments) && totalSegments > 0 ? totalSegments : 0;
113
+ const normalizedMode = String(anchorMode || "at").trim().toLowerCase();
114
+ if (normalizedMode === "before") {
115
+ return normalizedSegmentIndex - 1;
116
+ }
117
+ if (normalizedMode === "after") {
118
+ return normalizedTotalSegments > 0
119
+ ? Math.min(normalizedSegmentIndex + 1, normalizedTotalSegments - 1)
120
+ : normalizedSegmentIndex + 1;
121
+ }
122
+
123
+ return normalizedSegmentIndex;
124
+ }
125
+
126
+ function resolveScopedRoutePathname({
127
+ currentPathname = "/",
128
+ params = {},
129
+ orderedParamNames = [],
130
+ anchorParamName = "",
131
+ anchorParamValue = "",
132
+ anchorMode = "at"
133
+ } = {}) {
134
+ const normalizedCurrentPathname = resolveRoutePathnameSource(currentPathname);
135
+ const normalizedAnchorParamName = String(anchorParamName || "").trim();
136
+ if (!normalizedAnchorParamName) {
137
+ return normalizedCurrentPathname;
138
+ }
139
+
140
+ const sourceParams = asPlainObject(params);
141
+ const segments = normalizedCurrentPathname.split("/").filter(Boolean);
142
+ if (segments.length < 1) {
143
+ return normalizedCurrentPathname;
144
+ }
145
+
146
+ const paramNames = resolveRouteParamNamesSource(orderedParamNames);
147
+ let cursor = 0;
148
+ for (const paramName of paramNames) {
149
+ const segmentIndex = findRouteParamSegmentIndex(segments, sourceParams[paramName], cursor);
150
+ if (segmentIndex < 0) {
151
+ continue;
152
+ }
153
+
154
+ if (paramName === normalizedAnchorParamName) {
155
+ const endIndex = resolveAnchorEndIndex(segmentIndex, segments.length, anchorMode);
156
+ return normalizePathPrefix(segments, endIndex);
157
+ }
158
+
159
+ cursor = segmentIndex + 1;
160
+ }
161
+
162
+ const fallbackAnchorValue = toRouteParamValue(anchorParamValue) ||
163
+ toRouteParamValue(sourceParams[normalizedAnchorParamName]);
164
+ if (!fallbackAnchorValue) {
165
+ return normalizedCurrentPathname;
166
+ }
167
+
168
+ const fallbackSegmentIndex = findRouteParamSegmentIndex(segments, fallbackAnchorValue, 0);
169
+ if (fallbackSegmentIndex < 0) {
170
+ return normalizedCurrentPathname;
171
+ }
172
+
173
+ const fallbackEndIndex = resolveAnchorEndIndex(fallbackSegmentIndex, segments.length, anchorMode);
174
+ return normalizePathPrefix(segments, fallbackEndIndex);
175
+ }
176
+
57
177
  function resolveRouteTemplatePath(routeTemplate = "", params = {}) {
58
178
  const normalizedTemplate = String(routeTemplate || "").trim();
59
179
  if (!normalizedTemplate) {
@@ -155,7 +275,9 @@ export {
155
275
  normalizeRouteParamName,
156
276
  toRouteParamValue,
157
277
  resolveRouteParamsSource,
278
+ resolveRouteParamNamesSource,
158
279
  resolveRoutePathnameSource,
280
+ resolveScopedRoutePathname,
159
281
  resolveRouteTemplatePath,
160
282
  resolveRouteTemplateLocation,
161
283
  extractRouteParamNames,