jupiter-dynamic-forms 1.20.4 → 1.20.5

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.
package/dist/index.mjs CHANGED
@@ -2493,6 +2493,10 @@ class Parser {
2493
2493
  this.next();
2494
2494
  return { kind: "number", value: Number(token.value) };
2495
2495
  }
2496
+ if (token.type === "STRING") {
2497
+ this.next();
2498
+ return { kind: "string", value: token.value };
2499
+ }
2496
2500
  if (token.type === "VARIABLE") {
2497
2501
  this.next();
2498
2502
  return { kind: "variable", name: token.value };
@@ -2520,20 +2524,30 @@ class Parser {
2520
2524
  function lookup(bindings, name) {
2521
2525
  return bindings.get(name);
2522
2526
  }
2523
- function evalSum(variable, bindings) {
2527
+ function evalSum(variable, bindings, test) {
2524
2528
  const binding = lookup(bindings, variable);
2525
2529
  if (binding === void 0)
2526
2530
  return 0;
2527
- if (Array.isArray(binding))
2528
- return binding.reduce((total, value) => total + value, 0);
2529
- return binding;
2531
+ const values = Array.isArray(binding) ? binding : [binding];
2532
+ return values.reduce((total, value) => {
2533
+ if (typeof value === "string") {
2534
+ throw new FormulaExpressionError(
2535
+ `Variable "$${variable}" resolved to a non-numeric value ("${value}") and cannot be used in sum(...)`,
2536
+ test,
2537
+ "unsupported-syntax"
2538
+ );
2539
+ }
2540
+ return total + value;
2541
+ }, 0);
2530
2542
  }
2531
2543
  function evalValue(node, bindings, test) {
2532
2544
  switch (node.kind) {
2533
2545
  case "number":
2534
2546
  return node.value;
2547
+ case "string":
2548
+ return node.value;
2535
2549
  case "sum":
2536
- return evalSum(node.variable, bindings);
2550
+ return evalSum(node.variable, bindings, test);
2537
2551
  case "variable": {
2538
2552
  const binding = lookup(bindings, node.name);
2539
2553
  if (binding === void 0) {
@@ -2553,7 +2567,17 @@ function evalValue(node, bindings, test) {
2553
2567
  return binding;
2554
2568
  }
2555
2569
  case "additive":
2556
- return node.terms.reduce((total, { sign, term }) => total + sign * evalValue(term, bindings, test), 0);
2570
+ return node.terms.reduce((total, { sign, term }) => {
2571
+ const value = evalValue(term, bindings, test);
2572
+ if (typeof value === "string") {
2573
+ throw new FormulaExpressionError(
2574
+ `Non-numeric value ("${value}") cannot be used in arithmetic (+ / -)`,
2575
+ test,
2576
+ "unsupported-syntax"
2577
+ );
2578
+ }
2579
+ return total + sign * value;
2580
+ }, 0);
2557
2581
  }
2558
2582
  }
2559
2583
  function evalBool(node, bindings, test, fallbackUsage) {
@@ -2582,6 +2606,9 @@ function evalBool(node, bindings, test, fallbackUsage) {
2582
2606
  case "comparison": {
2583
2607
  const left = evalValue(node.left, bindings, test);
2584
2608
  const right = evalValue(node.right, bindings, test);
2609
+ if (typeof left === "string" || typeof right === "string") {
2610
+ return String(left) === String(right);
2611
+ }
2585
2612
  return Math.abs(left - right) < CALCULATION_TOLERANCE;
2586
2613
  }
2587
2614
  }
@@ -2721,7 +2748,8 @@ function indexConceptTree(concept, conceptByQName, conceptById, conceptIdByQName
2721
2748
  const info = {
2722
2749
  conceptId: concept.id,
2723
2750
  qname: concept.name,
2724
- balance: concept.balance
2751
+ balance: concept.balance,
2752
+ effectivePeriodRole: concept.effectivePeriodRole
2725
2753
  };
2726
2754
  conceptById.set(concept.id, info);
2727
2755
  (conceptByQName.get(concept.name) ?? conceptByQName.set(concept.name, []).get(concept.name)).push(info);
@@ -2738,10 +2766,13 @@ function indexConceptTree(concept, conceptByQName, conceptById, conceptIdByQName
2738
2766
  );
2739
2767
  children.forEach((child) => indexConceptTree(child, conceptByQName, conceptById, conceptIdByQName, conceptIdByQNameHasChildren, childrenIndex));
2740
2768
  }
2741
- function indexMemberTree(member, memberQNameToId) {
2769
+ function indexMemberTree(member, memberQNameToId, memberChildrenByQName) {
2742
2770
  if (!memberQNameToId.has(member.conceptName))
2743
2771
  memberQNameToId.set(member.conceptName, member.id);
2744
- (member.children || []).forEach((child) => indexMemberTree(child, memberQNameToId));
2772
+ if (!memberChildrenByQName.has(member.conceptName)) {
2773
+ memberChildrenByQName.set(member.conceptName, (member.children || []).map((child) => child.id));
2774
+ }
2775
+ (member.children || []).forEach((child) => indexMemberTree(child, memberQNameToId, memberChildrenByQName));
2745
2776
  }
2746
2777
  const PRESENTATION_TOTAL_GROUP_ACCESSORS = {
2747
2778
  getId: (concept) => concept.id,
@@ -2749,12 +2780,14 @@ const PRESENTATION_TOTAL_GROUP_ACCESSORS = {
2749
2780
  isAbstract: (concept) => concept.abstract === true,
2750
2781
  getChildren: (concept) => concept.children || []
2751
2782
  };
2752
- function buildFormulaResolutionContext(formData, columns, sections, hypercubeRoles, periodStartDate, periodEndDate) {
2783
+ function buildFormulaResolutionContext(formData, columns, sections, hypercubeRoles, periodStartDate, periodEndDate, typedMemberData) {
2753
2784
  const conceptByQName = /* @__PURE__ */ new Map();
2754
2785
  const conceptById = /* @__PURE__ */ new Map();
2755
2786
  const conceptIdByQNamePerRole = /* @__PURE__ */ new Map();
2756
2787
  const childrenByRole = /* @__PURE__ */ new Map();
2757
2788
  const totalGroupChildrenByRole = /* @__PURE__ */ new Map();
2789
+ const columnsByRole = /* @__PURE__ */ new Map();
2790
+ const roleURIByConceptId = /* @__PURE__ */ new Map();
2758
2791
  sections.forEach((section2) => {
2759
2792
  const childrenIndex = /* @__PURE__ */ new Map();
2760
2793
  const conceptIdByQName = /* @__PURE__ */ new Map();
@@ -2763,16 +2796,22 @@ function buildFormulaResolutionContext(formData, columns, sections, hypercubeRol
2763
2796
  childrenByRole.set(section2.roleURI, childrenIndex);
2764
2797
  conceptIdByQNamePerRole.set(section2.roleURI, conceptIdByQName);
2765
2798
  totalGroupChildrenByRole.set(section2.roleURI, buildTotalGroupChildrenIndex(section2.concepts, PRESENTATION_TOTAL_GROUP_ACCESSORS));
2799
+ for (const conceptId of childrenIndex.keys()) {
2800
+ roleURIByConceptId.set(conceptId, section2.roleURI);
2801
+ }
2802
+ if (section2.columns)
2803
+ columnsByRole.set(section2.roleURI, section2.columns);
2766
2804
  });
2767
2805
  const dimensionQNameToId = /* @__PURE__ */ new Map();
2768
2806
  const memberQNameToId = /* @__PURE__ */ new Map();
2807
+ const memberChildrenByQName = /* @__PURE__ */ new Map();
2769
2808
  (hypercubeRoles || []).forEach((role) => {
2770
2809
  role.items.forEach((item) => {
2771
2810
  item.dimensions.forEach((dimension) => {
2772
2811
  if (!dimensionQNameToId.has(dimension.conceptName)) {
2773
2812
  dimensionQNameToId.set(dimension.conceptName, dimension.id);
2774
2813
  }
2775
- (dimension.members || []).forEach((member) => indexMemberTree(member, memberQNameToId));
2814
+ (dimension.members || []).forEach((member) => indexMemberTree(member, memberQNameToId, memberChildrenByQName));
2776
2815
  });
2777
2816
  });
2778
2817
  });
@@ -2788,7 +2827,11 @@ function buildFormulaResolutionContext(formData, columns, sections, hypercubeRol
2788
2827
  totalGroupChildrenByRole,
2789
2828
  dimensionQNameToId,
2790
2829
  memberQNameToId,
2791
- allCandidates: collectAllCandidates(formData)
2830
+ memberChildrenByQName,
2831
+ columnsByRole,
2832
+ roleURIByConceptId,
2833
+ allCandidates: collectAllCandidates(formData),
2834
+ typedMemberData: typedMemberData ?? {}
2792
2835
  };
2793
2836
  }
2794
2837
  function collectDescendantConceptIds(childrenIndex, rootConceptId) {
@@ -2809,10 +2852,13 @@ function candidateKey(candidate) {
2809
2852
  return `${candidate.conceptId}::${candidate.columnId}`;
2810
2853
  }
2811
2854
  function resolveMatchedCandidates(variable, ctx) {
2812
- return intersectCandidates(variable.filters.map((filter2) => resolveFilter(filter2, ctx.allCandidates, ctx)));
2855
+ return intersectCandidates(variable.filters.map((filter2) => resolveFilter(filter2, ctx.allCandidates, ctx, variable.filters)));
2813
2856
  }
2814
- function getColumn(ctx, columnId) {
2815
- return ctx.columns.find((column2) => column2.id === columnId);
2857
+ function getColumn(ctx, candidate) {
2858
+ var _a;
2859
+ const roleURI = ctx.roleURIByConceptId.get(candidate.conceptId);
2860
+ const roleColumn = roleURI ? (_a = ctx.columnsByRole.get(roleURI)) == null ? void 0 : _a.find((column2) => column2.id === candidate.columnId) : void 0;
2861
+ return roleColumn ?? ctx.columns.find((column2) => column2.id === candidate.columnId);
2816
2862
  }
2817
2863
  function columnMatchesExplicitDimension(column2, axisId, memberId) {
2818
2864
  if (!(column2 == null ? void 0 : column2.dimensionData))
@@ -2824,13 +2870,22 @@ function columnMatchesExplicitDimension(column2, axisId, memberId) {
2824
2870
  (combo) => combo.axisId === axisId && (memberId === void 0 || combo.memberId === memberId)
2825
2871
  );
2826
2872
  }
2827
- function columnMatchesTypedDimension(column2, axisId) {
2873
+ function columnDeclaresTypedDimension(column2, axisId) {
2828
2874
  if (!(column2 == null ? void 0 : column2.dimensionData))
2829
2875
  return false;
2830
2876
  if (column2.dimensionData.typedMemberId !== void 0 && column2.dimensionData.axisId === axisId)
2831
2877
  return true;
2832
2878
  return (column2.dimensionData.typedMembers || []).some((typedMember) => typedMember.axisId === axisId);
2833
2879
  }
2880
+ function columnMatchesTypedDimension(ctx, candidate, axisId) {
2881
+ const column2 = getColumn(ctx, candidate);
2882
+ if (!columnDeclaresTypedDimension(column2, axisId))
2883
+ return false;
2884
+ const chosenValues = ctx.typedMemberData[candidate.columnId];
2885
+ if (chosenValues === void 0)
2886
+ return true;
2887
+ return !!chosenValues[axisId];
2888
+ }
2834
2889
  function paramTokenName(rawDate) {
2835
2890
  if (!rawDate)
2836
2891
  return void 0;
@@ -2875,7 +2930,28 @@ function unionCandidates(lists) {
2875
2930
  lists.forEach((list) => list.forEach((candidate) => merged.set(candidateKey(candidate), candidate)));
2876
2931
  return Array.from(merged.values());
2877
2932
  }
2878
- function matchFilter(filter2, universe, ctx) {
2933
+ function resolveChildMemberAxisFilter(memberQName, axisId, universe, ctx, siblingFilters) {
2934
+ var _a, _b;
2935
+ const childMemberIds = ctx.memberChildrenByQName.get(memberQName) || [];
2936
+ if (childMemberIds.length > 0) {
2937
+ const matches = universe.filter(
2938
+ (candidate) => childMemberIds.some((childId) => columnMatchesExplicitDimension(getColumn(ctx, candidate), axisId, childId))
2939
+ );
2940
+ if (matches.length > 0)
2941
+ return matches;
2942
+ }
2943
+ const conceptRelationFilter = siblingFilters.find(
2944
+ (sibling) => sibling.type === "CONCEPT_RELATION" && sibling.attributes["axis"] === "descendant"
2945
+ );
2946
+ const linkrole = conceptRelationFilter == null ? void 0 : conceptRelationFilter.attributes["linkrole"];
2947
+ const anchorQName = conceptRelationFilter == null ? void 0 : conceptRelationFilter.attributes["qname"];
2948
+ const anchorConceptId = anchorQName && linkrole ? (_a = ctx.conceptIdByQNamePerRole.get(linkrole)) == null ? void 0 : _a.get(anchorQName) : void 0;
2949
+ const directChildren = new Set(anchorConceptId && linkrole ? ((_b = ctx.childrenByRole.get(linkrole)) == null ? void 0 : _b.get(anchorConceptId)) || [] : []);
2950
+ if (directChildren.size === 0)
2951
+ return [];
2952
+ return universe.filter((candidate) => !directChildren.has(candidate.conceptId));
2953
+ }
2954
+ function matchFilter(filter2, universe, ctx, siblingFilters) {
2879
2955
  var _a, _b;
2880
2956
  switch (filter2.type) {
2881
2957
  case "CONCEPT_NAME": {
@@ -2915,33 +2991,38 @@ function matchFilter(filter2, universe, ctx) {
2915
2991
  const dimensionQName = filter2.attributes["dimension.qname"];
2916
2992
  const memberQName = filter2.attributes["member.qname"];
2917
2993
  const axisId = dimensionQName ? ctx.dimensionQNameToId.get(dimensionQName) : void 0;
2918
- const memberId = memberQName ? ctx.memberQNameToId.get(memberQName) : void 0;
2919
2994
  if (!axisId)
2920
2995
  return [];
2921
- return universe.filter((candidate) => columnMatchesExplicitDimension(getColumn(ctx, candidate.columnId), axisId, memberId));
2996
+ if (memberQName && filter2.attributes["member.axis"] === "child") {
2997
+ return resolveChildMemberAxisFilter(memberQName, axisId, universe, ctx, siblingFilters);
2998
+ }
2999
+ const memberId = memberQName ? ctx.memberQNameToId.get(memberQName) : void 0;
3000
+ return universe.filter((candidate) => columnMatchesExplicitDimension(getColumn(ctx, candidate), axisId, memberId));
2922
3001
  }
2923
3002
  case "TYPED_DIMENSION": {
2924
3003
  const dimensionQName = filter2.attributes["dimension.qname"];
2925
3004
  const axisId = dimensionQName ? ctx.dimensionQNameToId.get(dimensionQName) : void 0;
2926
3005
  if (!axisId)
2927
3006
  return [];
2928
- return universe.filter((candidate) => columnMatchesTypedDimension(getColumn(ctx, candidate.columnId), axisId));
3007
+ return universe.filter((candidate) => columnMatchesTypedDimension(ctx, candidate, axisId));
2929
3008
  }
2930
3009
  case "PERIOD_INSTANT":
2931
3010
  case "PERIOD_END": {
2932
3011
  const dateAttribute = filter2.attributes["date"];
2933
- return universe.filter((candidate) => columnMatchesPeriod(getColumn(ctx, candidate.columnId), dateAttribute, ctx));
3012
+ return universe.filter((candidate) => columnMatchesPeriod(getColumn(ctx, candidate), dateAttribute, ctx));
2934
3013
  }
2935
3014
  case "OR_FILTER":
2936
- return unionCandidates(filter2.children.map((child) => resolveFilter(child, universe, ctx)));
3015
+ return unionCandidates(filter2.children.map((child) => resolveFilter(child, universe, ctx, siblingFilters)));
2937
3016
  case "ASPECT_COVER":
2938
3017
  return universe;
3018
+ case "UNKNOWN":
3019
+ return universe;
2939
3020
  default:
2940
3021
  return [];
2941
3022
  }
2942
3023
  }
2943
- function resolveFilter(filter2, universe, ctx) {
2944
- const matched = matchFilter(filter2, universe, ctx);
3024
+ function resolveFilter(filter2, universe, ctx, siblingFilters) {
3025
+ const matched = matchFilter(filter2, universe, ctx, siblingFilters);
2945
3026
  if (!filter2.complement)
2946
3027
  return matched;
2947
3028
  const matchedKeys = new Set(matched.map(candidateKey));
@@ -2955,12 +3036,63 @@ function intersectCandidates(sets) {
2955
3036
  return acc.filter((candidate) => setKeys.has(candidateKey(candidate)));
2956
3037
  });
2957
3038
  }
3039
+ function resolveRawValue(raw) {
3040
+ if (raw === void 0 || raw === null || raw === "")
3041
+ return void 0;
3042
+ const numeric = parseFloat(String(raw));
3043
+ return Number.isNaN(numeric) ? String(raw) : numeric;
3044
+ }
3045
+ function hasExplicitPeriodFilter(filters) {
3046
+ return filters.some((filter2) => {
3047
+ if (filter2.type === "PERIOD_INSTANT" || filter2.type === "PERIOD_END")
3048
+ return true;
3049
+ if (filter2.type === "OR_FILTER")
3050
+ return hasExplicitPeriodFilter(filter2.children);
3051
+ return false;
3052
+ });
3053
+ }
3054
+ function narrowToCurrentPeriod(candidates, variable, ctx) {
3055
+ if (candidates.length <= 1 || hasExplicitPeriodFilter(variable.filters))
3056
+ return candidates;
3057
+ const currentPeriodOnly = candidates.filter(
3058
+ (candidate) => columnEffectivePeriod(getColumn(ctx, candidate), ctx).end === ctx.periodEndDate
3059
+ );
3060
+ return currentPeriodOnly.length > 0 ? currentPeriodOnly : candidates;
3061
+ }
3062
+ function narrowByPreferredOccurrence(candidates, ctx) {
3063
+ const groups = /* @__PURE__ */ new Map();
3064
+ candidates.forEach((candidate) => {
3065
+ var _a;
3066
+ const qname = ((_a = ctx.conceptById.get(candidate.conceptId)) == null ? void 0 : _a.qname) ?? candidate.conceptId;
3067
+ const key = `${candidate.columnId}::${qname}`;
3068
+ const group = groups.get(key);
3069
+ if (group)
3070
+ group.push(candidate);
3071
+ else
3072
+ groups.set(key, [candidate]);
3073
+ });
3074
+ const result = [];
3075
+ groups.forEach((group) => {
3076
+ if (group.length <= 1) {
3077
+ result.push(...group);
3078
+ return;
3079
+ }
3080
+ const roles = group.map((candidate) => {
3081
+ var _a;
3082
+ return (_a = ctx.conceptById.get(candidate.conceptId)) == null ? void 0 : _a.effectivePeriodRole;
3083
+ });
3084
+ const endLabelOnly = group.filter((_candidate, index) => roles[index] === "periodEndLabel");
3085
+ const isAmbiguous = new Set(roles).size > 1;
3086
+ result.push(...isAmbiguous && endLabelOnly.length > 0 ? endLabelOnly : group);
3087
+ });
3088
+ return result;
3089
+ }
2958
3090
  function resolveFormulaVariable(variable, ctx) {
2959
- const matched = resolveMatchedCandidates(variable, ctx);
3091
+ const matched = narrowByPreferredOccurrence(narrowToCurrentPeriod(resolveMatchedCandidates(variable, ctx), variable, ctx), ctx);
2960
3092
  const values = matched.map((candidate) => {
2961
3093
  var _a;
2962
- return parseFloat((_a = ctx.formData[candidate.conceptId]) == null ? void 0 : _a[candidate.columnId]);
2963
- }).filter((value) => !Number.isNaN(value));
3094
+ return resolveRawValue((_a = ctx.formData[candidate.conceptId]) == null ? void 0 : _a[candidate.columnId]);
3095
+ }).filter((value) => value !== void 0);
2964
3096
  if (variable.bindAsSequence) {
2965
3097
  if (values.length === 0 && variable.fallbackValue === "()")
2966
3098
  return [];
@@ -2980,6 +3112,15 @@ function resolveFormulaVariable(variable, ctx) {
2980
3112
  function resolveFormulaVariableMatchCount(variable, ctx) {
2981
3113
  return resolveMatchedCandidates(variable, ctx).length;
2982
3114
  }
3115
+ function resolveFormulaVariableSetMatchCount(variables, ctx) {
3116
+ const merged = /* @__PURE__ */ new Map();
3117
+ for (const variable of variables) {
3118
+ for (const candidate of resolveMatchedCandidates(variable, ctx)) {
3119
+ merged.set(candidateKey(candidate), candidate);
3120
+ }
3121
+ }
3122
+ return merged.size;
3123
+ }
2983
3124
  function resolveFormulaVariableUsedFallback(variable, ctx) {
2984
3125
  const matchCount = resolveFormulaVariableMatchCount(variable, ctx);
2985
3126
  if (matchCount > 0)
@@ -3006,7 +3147,7 @@ function resolveVariableConceptQNames(variable) {
3006
3147
  }
3007
3148
  function resolveVariableColumnsForConcept(variable, targetConceptId, ctx) {
3008
3149
  const universe = ctx.columns.map((column2) => ({ conceptId: targetConceptId, columnId: column2.id }));
3009
- const resolved = intersectCandidates(variable.filters.map((filter2) => resolveFilter(filter2, universe, ctx)));
3150
+ const resolved = intersectCandidates(variable.filters.map((filter2) => resolveFilter(filter2, universe, ctx, variable.filters)));
3010
3151
  return resolved.map((candidate) => candidate.columnId);
3011
3152
  }
3012
3153
  function buildAssertionBindings(assertion, ctx) {
@@ -3029,6 +3170,14 @@ function buildAssertionFallbackUsage(assertion, ctx) {
3029
3170
  }
3030
3171
  return fallbackUsage;
3031
3172
  }
3173
+ function checkAllVariablesBound(assertion, bindings) {
3174
+ for (const variable of assertion.variables) {
3175
+ if (bindings.get(variable.name) === void 0) {
3176
+ return { satisfied: false, skipReason: "unresolvable" };
3177
+ }
3178
+ }
3179
+ return { satisfied: true };
3180
+ }
3032
3181
  function evaluateAssertionPreconditions(preconditions, bindings, fallbackUsage = /* @__PURE__ */ new Map()) {
3033
3182
  if (preconditions.length === 0)
3034
3183
  return { satisfied: true };
@@ -3099,13 +3248,19 @@ class FormulaValidationService {
3099
3248
  _evaluateAssertion(assertion, role, ctx, language) {
3100
3249
  try {
3101
3250
  const bindings = buildAssertionBindings(assertion, ctx);
3251
+ if (assertion.type === "VALUE_ASSERTION") {
3252
+ const bindingCheck = checkAllVariablesBound(assertion, bindings);
3253
+ if (!bindingCheck.satisfied) {
3254
+ return this._skippedResult(assertion, role, bindingCheck.skipReason ?? "unresolvable");
3255
+ }
3256
+ }
3102
3257
  const fallbackUsage = buildAssertionFallbackUsage(assertion, ctx);
3103
3258
  const precondition = evaluateAssertionPreconditions(assertion.preconditions, bindings, fallbackUsage);
3104
3259
  if (!precondition.satisfied) {
3105
3260
  return this._skippedResult(assertion, role, precondition.skipReason ?? "unresolvable");
3106
3261
  }
3107
3262
  if (assertion.type === "EXISTENCE_ASSERTION") {
3108
- const matchedFactCount = resolveFormulaVariableMatchCount(assertion.variables[0], ctx);
3263
+ const matchedFactCount = assertion.variables.length === 1 ? resolveFormulaVariableMatchCount(assertion.variables[0], ctx) : resolveFormulaVariableSetMatchCount(assertion.variables, ctx);
3109
3264
  const satisfied2 = evaluateExistenceTest(assertion.test, matchedFactCount);
3110
3265
  return this._evaluatedResult(assertion, role, ctx, satisfied2, language, matchedFactCount);
3111
3266
  }
@@ -6245,15 +6400,33 @@ JupiterConceptTree.styles = css`
6245
6400
  /* Row-level highlight while a field in this row has focus. Applied to every
6246
6401
  cell so the row stays identifiable even after the user scrolls the table
6247
6402
  horizontally away from the focused input. */
6248
- .concept-name-cell.row-focused,
6249
6403
  .field-cell.row-focused {
6250
6404
  background: var(--jupiter-row-focus-background, rgba(25, 118, 210, 0.14));
6251
6405
  }
6252
6406
 
6407
+ /* .concept-name-cell is position:sticky with left:0, so it renders on top of
6408
+ field-cells that have scrolled underneath it as the user tabs across columns.
6409
+ It must stay fully opaque, or that translucent tint lets the scrolled-under
6410
+ cells' content (inputs, icons) show through and visually overlap with this
6411
+ cell's label/badge. Layer the tint as a background-image over an explicit
6412
+ opaque background-color instead of replacing the background outright. */
6253
6413
  .concept-name-cell.row-focused {
6414
+ background-image: linear-gradient(
6415
+ var(--jupiter-row-focus-background, rgba(25, 118, 210, 0.14)),
6416
+ var(--jupiter-row-focus-background, rgba(25, 118, 210, 0.14))
6417
+ );
6418
+ background-color: var(--jupiter-concept-background, #f8f9fa);
6254
6419
  box-shadow: inset 3px 0 0 0 var(--jupiter-primary-color, #1976d2);
6255
6420
  }
6256
6421
 
6422
+ .concept-name-cell.abstract.row-focused {
6423
+ background-color: var(--bg-color-1, var(--jupiter-abstract-background, #f0f2f5));
6424
+ }
6425
+
6426
+ .concept-name-cell.leaf.row-focused {
6427
+ background-color: var(--bg-color-2, var(--jupiter-leaf-background, #fff));
6428
+ }
6429
+
6257
6430
  .concept-info-btn {
6258
6431
  flex-shrink: 0;
6259
6432
  width: 22px;
@@ -11516,7 +11689,7 @@ let JupiterDynamicForm = class extends LitElement {
11516
11689
  // resolution identified, via `columnId` rather than re-deriving it from dimensions.
11517
11690
  _handleFormulaConceptClick(event) {
11518
11691
  this._showFormulaValidationDialog = false;
11519
- this.scrollToConcept(event.detail.conceptQName, void 0, void 0, event.detail.columnId);
11692
+ this.scrollToConcept(event.detail.conceptQName, void 0, void 0, event.detail.columnId, event.detail.conceptId);
11520
11693
  }
11521
11694
  _handleRoleFilterApply(event) {
11522
11695
  var _a, _b;
@@ -12716,7 +12889,8 @@ let JupiterDynamicForm = class extends LitElement {
12716
12889
  var _a2;
12717
12890
  return {
12718
12891
  roleURI: ((_a2 = section2.metadata) == null ? void 0 : _a2.roleURI) || section2.id,
12719
- concepts: section2.concepts
12892
+ concepts: section2.concepts,
12893
+ columns: section2.columns
12720
12894
  };
12721
12895
  });
12722
12896
  const ctx = buildFormulaResolutionContext(
@@ -12725,7 +12899,8 @@ let JupiterDynamicForm = class extends LitElement {
12725
12899
  conceptSections,
12726
12900
  (_h = (_g = (_f = this.xbrlInput) == null ? void 0 : _f.hypercubes) == null ? void 0 : _g[0]) == null ? void 0 : _h.roles,
12727
12901
  this.periodStartDate,
12728
- this.periodEndDate
12902
+ this.periodEndDate,
12903
+ this._typedMemberData
12729
12904
  );
12730
12905
  const scopedXbrlInput = {
12731
12906
  ...this.xbrlInput,
@@ -14879,6 +15054,59 @@ let JupiterDynamicForm = class extends LitElement {
14879
15054
  }
14880
15055
  return null;
14881
15056
  }
15057
+ // Same taxonomy concept can be placed at multiple points in a role's presentation tree
15058
+ // with different preferred labels (e.g. a rollforward's periodStartLabel/periodEndLabel
15059
+ // rows) — each such occurrence gets its own ConceptTree row/id. Unlike _findConceptByName,
15060
+ // this collects every matching row (DFS order) so the caller can disambiguate between them
15061
+ // instead of always taking the first one found.
15062
+ _findAllConceptsByName(concepts, name) {
15063
+ var _a;
15064
+ const localName = name.includes(":") ? name.split(":").pop() : name;
15065
+ const matches = [];
15066
+ for (const c2 of concepts) {
15067
+ const cLocal = c2.name.includes(":") ? c2.name.split(":").pop() : c2.name;
15068
+ if (c2.name === name || cLocal === localName)
15069
+ matches.push(c2);
15070
+ if ((_a = c2.children) == null ? void 0 : _a.length) {
15071
+ matches.push(...this._findAllConceptsByName(c2.children, name));
15072
+ }
15073
+ }
15074
+ return matches;
15075
+ }
15076
+ _findConceptById(concepts, id) {
15077
+ var _a;
15078
+ for (const c2 of concepts) {
15079
+ if (c2.id === id)
15080
+ return c2;
15081
+ if ((_a = c2.children) == null ? void 0 : _a.length) {
15082
+ const hit = this._findConceptById(c2.children, id);
15083
+ if (hit)
15084
+ return hit;
15085
+ }
15086
+ }
15087
+ return null;
15088
+ }
15089
+ // Loose value comparison used to disambiguate rows that share a concept name: exact
15090
+ // string match, numeric equality, or numeric equality up to a power-of-ten scale
15091
+ // (the form may store a display value while the caller passes the raw XBRL value).
15092
+ _valueLooselyMatches(fieldValue, targetValue) {
15093
+ const fv = String(fieldValue ?? "");
15094
+ if (fv === targetValue)
15095
+ return true;
15096
+ const fNum = Number(fv);
15097
+ const tNum = Number(targetValue);
15098
+ if (isNaN(fNum) || isNaN(tNum))
15099
+ return false;
15100
+ if (fNum === tNum)
15101
+ return true;
15102
+ if (fNum !== 0 && tNum !== 0) {
15103
+ const ratio = Math.abs(tNum / fNum);
15104
+ const log = Math.log10(ratio);
15105
+ if (Math.abs(log - Math.round(log)) < 1e-4)
15106
+ return true;
15107
+ }
15108
+ return false;
15109
+ }
14882
15110
  _findColumnByDimensions(columns, dims) {
14883
15111
  var _a;
14884
15112
  return (_a = columns.find((col) => {
@@ -14895,9 +15123,9 @@ let JupiterDynamicForm = class extends LitElement {
14895
15123
  return dims.length === 1 && this._normalizeAxisId(col.dimensionData.axisId ?? "") === this._normalizeAxisId(dims[0].axis) && this._normalizeMemberId(col.dimensionData.memberId ?? "") === this._normalizeMemberId(dims[0].member);
14896
15124
  })) == null ? void 0 : _a.id;
14897
15125
  }
14898
- async scrollToConcept(conceptName, dimensions, match, columnId) {
15126
+ async scrollToConcept(conceptName, dimensions, match, columnId, exactConceptId) {
14899
15127
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
14900
- console.log(`[scrollToConcept] ▶ START conceptName=${conceptName} value=${match == null ? void 0 : match.value} dims=${JSON.stringify(dimensions)} columnId=${columnId}`);
15128
+ console.log(`[scrollToConcept] ▶ START conceptName=${conceptName} value=${match == null ? void 0 : match.value} dims=${JSON.stringify(dimensions)} columnId=${columnId} exactConceptId=${exactConceptId}`);
14901
15129
  let targetSection = null;
14902
15130
  let targetConcept = null;
14903
15131
  const sectionsToSearch = [
@@ -14908,11 +15136,49 @@ let JupiterDynamicForm = class extends LitElement {
14908
15136
  })
14909
15137
  ];
14910
15138
  console.log(`[scrollToConcept] Searching ${sectionsToSearch.length} sections`);
14911
- for (const section2 of sectionsToSearch) {
14912
- const found = this._findConceptByName(section2.concepts, conceptName);
14913
- if (found) {
15139
+ const hasValueMatch = (match == null ? void 0 : match.value) !== void 0 && (match == null ? void 0 : match.value) !== null;
15140
+ const targetValue = hasValueMatch ? String(match.value) : null;
15141
+ if (exactConceptId) {
15142
+ for (const section2 of sectionsToSearch) {
15143
+ const found = this._findConceptById(section2.concepts, exactConceptId);
15144
+ if (found) {
15145
+ console.log(`[scrollToConcept] Section "${section2.id}" matched exactConceptId="${exactConceptId}"`);
15146
+ targetSection = section2;
15147
+ targetConcept = found;
15148
+ break;
15149
+ }
15150
+ }
15151
+ }
15152
+ if (!targetSection || !targetConcept) {
15153
+ for (const section2 of sectionsToSearch) {
15154
+ const candidates = this._findAllConceptsByName(section2.concepts, conceptName);
15155
+ if (!candidates.length)
15156
+ continue;
15157
+ const cols = section2.columns ?? this._columns;
15158
+ const resolvedColumnId = columnId ?? ((dimensions == null ? void 0 : dimensions.length) ? this._findColumnByDimensions(cols, dimensions) : void 0);
15159
+ let picked;
15160
+ if (candidates.length > 1) {
15161
+ picked = candidates.find((c2) => {
15162
+ const rowData = this._formData[c2.id];
15163
+ if (!rowData)
15164
+ return false;
15165
+ if (resolvedColumnId) {
15166
+ const cellValue = rowData[resolvedColumnId];
15167
+ if (cellValue === void 0 || cellValue === null || cellValue === "")
15168
+ return false;
15169
+ return !hasValueMatch || this._valueLooselyMatches(cellValue, targetValue);
15170
+ }
15171
+ if (hasValueMatch) {
15172
+ return Object.values(rowData).some(
15173
+ (v) => v !== void 0 && v !== null && v !== "" && this._valueLooselyMatches(v, targetValue)
15174
+ );
15175
+ }
15176
+ return false;
15177
+ });
15178
+ console.log(`[scrollToConcept] Section "${section2.id}" has ${candidates.length} rows named "${conceptName}" | disambiguated=${(picked == null ? void 0 : picked.id) ?? "none (using first)"}`);
15179
+ }
15180
+ const found = picked ?? candidates[0];
14914
15181
  if (columnId) {
14915
- const cols = section2.columns ?? this._columns;
14916
15182
  const hasColumn = cols.some((c2) => c2.id === columnId);
14917
15183
  console.log(`[scrollToConcept] Section "${section2.id}" has concept "${found.id}" | columnId="${columnId}" present=${hasColumn}`);
14918
15184
  if (hasColumn) {
@@ -14924,10 +15190,8 @@ let JupiterDynamicForm = class extends LitElement {
14924
15190
  targetConcept = found;
14925
15191
  }
14926
15192
  } else if (dimensions == null ? void 0 : dimensions.length) {
14927
- const cols = section2.columns ?? this._columns;
14928
- const colId = this._findColumnByDimensions(cols, dimensions);
14929
- console.log(`[scrollToConcept] Section "${section2.id}" has concept "${found.id}" | colMatch=${colId ?? "null"} | cols=${cols.map((c2) => c2.id).join(",")}`);
14930
- if (colId) {
15193
+ console.log(`[scrollToConcept] Section "${section2.id}" has concept "${found.id}" | colMatch=${resolvedColumnId ?? "null"} | cols=${cols.map((c2) => c2.id).join(",")}`);
15194
+ if (resolvedColumnId) {
14931
15195
  targetSection = section2;
14932
15196
  targetConcept = found;
14933
15197
  break;
@@ -14954,8 +15218,6 @@ let JupiterDynamicForm = class extends LitElement {
14954
15218
  }
14955
15219
  const columns = targetSection.columns ?? this._columns;
14956
15220
  let targetColumnId = null;
14957
- const hasValueMatch = (match == null ? void 0 : match.value) !== void 0 && (match == null ? void 0 : match.value) !== null;
14958
- const targetValue = hasValueMatch ? String(match.value) : null;
14959
15221
  if (columnId) {
14960
15222
  targetColumnId = columnId;
14961
15223
  } else if (dimensions == null ? void 0 : dimensions.length) {