@eric-emg/symphiq-components 1.2.376 → 1.2.377

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.
@@ -88683,6 +88683,8 @@ class ShopProfileQuestionAnswerComponent {
88683
88683
  return this.selectedAnswerTexts().length === 0 || this.addOtherAnswersExpanded();
88684
88684
  }, ...(ngDevMode ? [{ debugName: "saveButtonsDisabled" }] : []));
88685
88685
  this.stickyState = false;
88686
+ this.lastStickyChange = 0;
88687
+ this.stickyChangeCount = 0;
88686
88688
  // Initialize selected answers from existing profile answers
88687
88689
  effect(() => {
88688
88690
  const questionId = this.question().id;
@@ -88704,35 +88706,47 @@ class ShopProfileQuestionAnswerComponent {
88704
88706
  const header = this.stickyHeader?.nativeElement;
88705
88707
  const title = this.questionTitle?.nativeElement;
88706
88708
  if (!sentinel || !scrollRoot || !header) {
88709
+ console.log('[StickyDebug] Setup failed - missing elements:', { sentinel: !!sentinel, scrollRoot: !!scrollRoot, header: !!header });
88707
88710
  return;
88708
88711
  }
88712
+ console.log('[StickyDebug] Setting up IntersectionObserver');
88709
88713
  this.ngZone.runOutsideAngular(() => {
88710
88714
  this.intersectionObserver = new IntersectionObserver((entries) => {
88711
88715
  entries.forEach((entry) => {
88716
+ const now = Date.now();
88717
+ const timeSinceLastChange = now - this.lastStickyChange;
88712
88718
  const newStickyState = !entry.isIntersecting;
88719
+ this.stickyChangeCount++;
88720
+ console.log(`[StickyDebug #${this.stickyChangeCount}] isIntersecting: ${entry.isIntersecting}, boundingClientRect.top: ${entry.boundingClientRect.top.toFixed(1)}, intersectionRatio: ${entry.intersectionRatio.toFixed(3)}, currentSticky: ${this.stickyState}, newSticky: ${newStickyState}, timeSinceLastChange: ${timeSinceLastChange}ms`);
88721
+ // Debounce: ignore changes within 250ms (longer than the 200ms CSS transition) to prevent flip-flopping
88722
+ if (timeSinceLastChange < 250 && this.stickyState !== newStickyState) {
88723
+ console.log(`[StickyDebug] DEBOUNCED - ignoring change from ${this.stickyState} to ${newStickyState} (only ${timeSinceLastChange}ms since last change)`);
88724
+ return;
88725
+ }
88713
88726
  if (this.stickyState === newStickyState) {
88727
+ console.log(`[StickyDebug] No state change needed (already ${this.stickyState})`);
88714
88728
  return;
88715
88729
  }
88716
88730
  this.stickyState = newStickyState;
88731
+ this.lastStickyChange = now;
88717
88732
  if (newStickyState) {
88718
- // Lock header height BEFORE applying sticky to prevent layout shift
88719
- const currentHeight = header.offsetHeight;
88720
- header.style.minHeight = `${currentHeight}px`;
88721
- header.classList.add('shadow-md', 'is-sticky');
88733
+ console.log('[StickyDebug] Applying sticky classes');
88734
+ header.classList.add('shadow-md');
88735
+ header.classList.add('is-sticky');
88722
88736
  title?.classList.add('is-sticky');
88723
88737
  }
88724
88738
  else {
88725
- header.classList.remove('shadow-md', 'is-sticky');
88739
+ console.log('[StickyDebug] Removing sticky classes');
88740
+ header.classList.remove('shadow-md');
88741
+ header.classList.remove('is-sticky');
88726
88742
  title?.classList.remove('is-sticky');
88727
- // Release height lock after transition completes
88728
- setTimeout(() => {
88729
- if (!this.stickyState) {
88730
- header.style.minHeight = '';
88731
- }
88732
- }, 200);
88733
88743
  }
88734
88744
  });
88735
- }, { root: scrollRoot, threshold: 0 });
88745
+ }, {
88746
+ root: scrollRoot,
88747
+ threshold: 0,
88748
+ rootMargin: '-20px 0px 0px 0px'
88749
+ });
88736
88750
  this.intersectionObserver.observe(sentinel);
88737
88751
  });
88738
88752
  }
@@ -89060,172 +89074,172 @@ class ShopProfileQuestionAnswerComponent {
89060
89074
  ])
89061
89075
  ])
89062
89076
  ],
89063
- template: `
89064
- <div class="flex flex-col h-full">
89065
- <!-- Scrollable Content -->
89066
- <div #scrollContainer class="flex-1 overflow-y-auto">
89067
- <!-- Unanswered Questions Status -->
89068
- <div [ngClass]="statusHeaderClasses()" class="px-6 py-4 border-b">
89069
- <h4 [ngClass]="statusTitleClasses()" class="text-sm font-semibold mb-3">
89070
- Unanswered questions status
89071
- </h4>
89072
- <div class="flex items-center gap-3">
89073
- <div class="flex-1">
89074
- <div [ngClass]="progressBarContainerClasses()" class="h-2 rounded-full overflow-hidden">
89075
- <div
89076
- [ngClass]="progressBarFillClasses()"
89077
- class="h-full transition-all duration-500 ease-out rounded-full"
89078
- [style.width.%]="progressPercentage()"></div>
89079
- </div>
89080
- </div>
89081
- <div [ngClass]="progressTextClasses()" class="text-sm font-medium whitespace-nowrap">
89082
- {{ answeredCount() }} / {{ totalCount() }} answered
89083
- </div>
89084
- </div>
89085
- </div>
89086
-
89087
- <!-- Sentinel for intersection observer -->
89088
- <div #stickySentinel class="h-0"></div>
89089
-
89090
- <!-- Sticky Question Header -->
89091
- <div #stickyHeader [ngClass]="stickyHeaderClasses()" class="sticky top-0 z-10 px-6 py-4 transition-all duration-200">
89092
- <!-- Breadcrumb - hide for All Questions mode -->
89093
- @if (viewType() !== 'all') {
89094
- <div [ngClass]="breadcrumbClasses()" class="text-sm italic mb-2">
89095
- Shop > {{ breadcrumbPath() }}
89096
- </div>
89097
- }
89098
-
89099
- <!-- Question - animates from text-2xl to text-lg when sticky -->
89100
- <h2 #questionTitle [ngClass]="questionTextClasses()" class="font-bold text-2xl [&.is-sticky]:text-lg transition-all duration-200">
89101
- {{ question().question }}
89102
- </h2>
89103
- </div>
89104
-
89105
- <!-- Context and Focus Areas - scroll under sticky header -->
89106
- <div class="px-6">
89107
- <!-- Context -->
89108
- @if (question().context) {
89109
- <p [ngClass]="contextTextClasses()" class="text-sm leading-relaxed mt-3">
89110
- {{ question().context }}
89111
- </p>
89112
- }
89113
-
89114
- <!-- Related Focus Areas -->
89115
- @if (relatedFocusAreas().length > 0) {
89116
- <div class="mt-4">
89117
- <div class="flex items-center gap-2 mb-2">
89118
- <span [ngClass]="relatedLabelClasses()" class="text-xs font-semibold">
89119
- Related to {{ relatedFocusAreas().length }} focus area(s)
89120
- </span>
89121
- <svg class="w-4 h-4" [ngClass]="relatedLabelClasses()" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89122
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
89123
- </svg>
89124
- </div>
89125
- <div class="flex flex-wrap gap-2">
89126
- @for (focusArea of relatedFocusAreas(); track focusArea) {
89127
- <span [ngClass]="focusAreaChipClasses()" class="px-3 py-1.5 rounded-full text-xs font-medium">
89128
- {{ getFocusAreaTitle(focusArea) }}
89129
- </span>
89130
- }
89131
- </div>
89132
- </div>
89133
- }
89134
- </div>
89135
-
89136
- <div class="px-6 py-6">
89137
-
89138
- <!-- Answer Options -->
89139
- <div class="space-y-3 mb-4">
89140
- @for (option of answerOptions(); track option.tempId) {
89141
- <label
89142
- [ngClass]="checkboxRowClasses()"
89143
- class="flex items-start gap-3 p-4 rounded-lg cursor-pointer transition-all duration-200 hover:scale-[1.01]">
89144
- <input
89145
- type="checkbox"
89146
- [checked]="option.isSelected"
89147
- (change)="toggleAnswer(option.text)"
89148
- [ngClass]="checkboxClasses()"
89149
- class="mt-0.5 w-5 h-5 rounded border-2 cursor-pointer transition-all duration-200 focus:ring-2 focus:ring-offset-2" />
89150
- <span [ngClass]="checkboxLabelClasses()" class="text-base flex-1">
89151
- {{ option.text }}
89152
- </span>
89153
- </label>
89154
- }
89155
- </div>
89156
-
89157
- <!-- Add Other Answers -->
89158
- <div class="mb-6">
89159
- <div
89160
- class="grid transition-[grid-template-rows] duration-300 ease-in-out"
89161
- [style.grid-template-rows]="addOtherAnswersExpanded() ? '1fr' : '0fr'">
89162
- <div class="overflow-hidden min-h-0">
89163
- <div class="pb-4">
89164
- <textarea
89165
- [(ngModel)]="customAnswerText"
89166
- [ngClass]="textareaClasses()"
89167
- class="w-full px-4 py-3 rounded-lg border-2 text-sm resize-y transition-colors duration-200 focus:ring-2 focus:ring-offset-2"
89168
- rows="5"
89169
- placeholder="Enter your answer(s) here, separated by returns."></textarea>
89170
- <div class="flex items-center gap-3 mt-3">
89171
- <button
89172
- type="button"
89173
- (click)="cancelCustomAnswers()"
89174
- [ngClass]="cancelButtonClasses()"
89175
- class="px-5 py-2.5 rounded-lg font-medium text-sm transition-all duration-200 hover:scale-105 active:scale-95">
89176
- Cancel
89177
- </button>
89178
- <button
89179
- type="button"
89180
- (click)="addCustomAnswers()"
89181
- [disabled]="!canAddCustomAnswers()"
89182
- [ngClass]="addButtonClasses()"
89183
- class="flex-1 px-5 py-2.5 rounded-lg font-medium text-sm transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100">
89184
- Add
89185
- </button>
89186
- </div>
89187
- </div>
89188
- </div>
89189
- </div>
89190
-
89191
- @if (!addOtherAnswersExpanded()) {
89192
- <button
89193
- type="button"
89194
- (click)="toggleAddOtherAnswers()"
89195
- [ngClass]="addOtherAnswersButtonClasses()"
89196
- class="w-full px-5 py-3 rounded-lg font-medium text-sm transition-all duration-200 border-2 hover:scale-[1.01] active:scale-[0.99]">
89197
- Add other answers
89198
- </button>
89199
- }
89200
- </div>
89201
- </div>
89202
- </div>
89203
-
89204
- <!-- Sticky Footer -->
89205
- <div [ngClass]="footerClasses()" class="px-6 py-4 border-t sticky bottom-0 z-10">
89206
- <div class="flex items-center gap-4">
89207
- <button
89208
- type="button"
89209
- (click)="onSave()"
89210
- [disabled]="saveButtonsDisabled()"
89211
- [ngClass]="saveButtonClasses()"
89212
- class="px-5 py-2.5 rounded-lg font-medium text-sm transition-all duration-200 border disabled:opacity-50 disabled:cursor-not-allowed hover:scale-[1.02] active:scale-95 disabled:hover:scale-100">
89213
- Save
89214
- </button>
89215
- <button
89216
- type="button"
89217
- (click)="onSaveAndNext()"
89218
- [disabled]="saveButtonsDisabled()"
89219
- [ngClass]="saveAndNextButtonClasses()"
89220
- class="flex-1 px-5 py-2.5 rounded-lg font-medium text-sm transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:scale-[1.01] active:scale-[0.99] disabled:hover:scale-100">
89221
- <span>Save & next unanswered</span>
89222
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89223
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
89224
- </svg>
89225
- </button>
89226
- </div>
89227
- </div>
89228
- </div>
89077
+ template: `
89078
+ <div class="flex flex-col h-full">
89079
+ <!-- Scrollable Content -->
89080
+ <div #scrollContainer class="flex-1 overflow-y-auto">
89081
+ <!-- Unanswered Questions Status -->
89082
+ <div [ngClass]="statusHeaderClasses()" class="px-6 py-4 border-b">
89083
+ <h4 [ngClass]="statusTitleClasses()" class="text-sm font-semibold mb-3">
89084
+ Unanswered questions status
89085
+ </h4>
89086
+ <div class="flex items-center gap-3">
89087
+ <div class="flex-1">
89088
+ <div [ngClass]="progressBarContainerClasses()" class="h-2 rounded-full overflow-hidden">
89089
+ <div
89090
+ [ngClass]="progressBarFillClasses()"
89091
+ class="h-full transition-all duration-500 ease-out rounded-full"
89092
+ [style.width.%]="progressPercentage()"></div>
89093
+ </div>
89094
+ </div>
89095
+ <div [ngClass]="progressTextClasses()" class="text-sm font-medium whitespace-nowrap">
89096
+ {{ answeredCount() }} / {{ totalCount() }} answered
89097
+ </div>
89098
+ </div>
89099
+ </div>
89100
+
89101
+ <!-- Sentinel for intersection observer -->
89102
+ <div #stickySentinel class="h-0"></div>
89103
+
89104
+ <!-- Sticky Question Header -->
89105
+ <div #stickyHeader [ngClass]="stickyHeaderClasses()" class="sticky top-0 z-10 px-6 py-4 transition-all duration-200">
89106
+ <!-- Breadcrumb - hide for All Questions mode -->
89107
+ @if (viewType() !== 'all') {
89108
+ <div [ngClass]="breadcrumbClasses()" class="text-sm italic mb-2">
89109
+ Shop > {{ breadcrumbPath() }}
89110
+ </div>
89111
+ }
89112
+
89113
+ <!-- Question - animates from text-2xl to text-lg when sticky -->
89114
+ <h2 #questionTitle [ngClass]="questionTextClasses()" class="font-bold text-2xl [&.is-sticky]:text-lg transition-all duration-200">
89115
+ {{ question().question }}
89116
+ </h2>
89117
+ </div>
89118
+
89119
+ <!-- Context and Focus Areas - scroll under sticky header -->
89120
+ <div class="px-6">
89121
+ <!-- Context -->
89122
+ @if (question().context) {
89123
+ <p [ngClass]="contextTextClasses()" class="text-sm leading-relaxed mt-3">
89124
+ {{ question().context }}
89125
+ </p>
89126
+ }
89127
+
89128
+ <!-- Related Focus Areas -->
89129
+ @if (relatedFocusAreas().length > 0) {
89130
+ <div class="mt-4">
89131
+ <div class="flex items-center gap-2 mb-2">
89132
+ <span [ngClass]="relatedLabelClasses()" class="text-xs font-semibold">
89133
+ Related to {{ relatedFocusAreas().length }} focus area(s)
89134
+ </span>
89135
+ <svg class="w-4 h-4" [ngClass]="relatedLabelClasses()" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89136
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
89137
+ </svg>
89138
+ </div>
89139
+ <div class="flex flex-wrap gap-2">
89140
+ @for (focusArea of relatedFocusAreas(); track focusArea) {
89141
+ <span [ngClass]="focusAreaChipClasses()" class="px-3 py-1.5 rounded-full text-xs font-medium">
89142
+ {{ getFocusAreaTitle(focusArea) }}
89143
+ </span>
89144
+ }
89145
+ </div>
89146
+ </div>
89147
+ }
89148
+ </div>
89149
+
89150
+ <div class="px-6 py-6">
89151
+
89152
+ <!-- Answer Options -->
89153
+ <div class="space-y-3 mb-4">
89154
+ @for (option of answerOptions(); track option.tempId) {
89155
+ <label
89156
+ [ngClass]="checkboxRowClasses()"
89157
+ class="flex items-start gap-3 p-4 rounded-lg cursor-pointer transition-all duration-200 hover:scale-[1.01]">
89158
+ <input
89159
+ type="checkbox"
89160
+ [checked]="option.isSelected"
89161
+ (change)="toggleAnswer(option.text)"
89162
+ [ngClass]="checkboxClasses()"
89163
+ class="mt-0.5 w-5 h-5 rounded border-2 cursor-pointer transition-all duration-200 focus:ring-2 focus:ring-offset-2" />
89164
+ <span [ngClass]="checkboxLabelClasses()" class="text-base flex-1">
89165
+ {{ option.text }}
89166
+ </span>
89167
+ </label>
89168
+ }
89169
+ </div>
89170
+
89171
+ <!-- Add Other Answers -->
89172
+ <div class="mb-6">
89173
+ <div
89174
+ class="grid transition-[grid-template-rows] duration-300 ease-in-out"
89175
+ [style.grid-template-rows]="addOtherAnswersExpanded() ? '1fr' : '0fr'">
89176
+ <div class="overflow-hidden min-h-0">
89177
+ <div class="pb-4">
89178
+ <textarea
89179
+ [(ngModel)]="customAnswerText"
89180
+ [ngClass]="textareaClasses()"
89181
+ class="w-full px-4 py-3 rounded-lg border-2 text-sm resize-y transition-colors duration-200 focus:ring-2 focus:ring-offset-2"
89182
+ rows="5"
89183
+ placeholder="Enter your answer(s) here, separated by returns."></textarea>
89184
+ <div class="flex items-center gap-3 mt-3">
89185
+ <button
89186
+ type="button"
89187
+ (click)="cancelCustomAnswers()"
89188
+ [ngClass]="cancelButtonClasses()"
89189
+ class="px-5 py-2.5 rounded-lg font-medium text-sm transition-all duration-200 hover:scale-105 active:scale-95">
89190
+ Cancel
89191
+ </button>
89192
+ <button
89193
+ type="button"
89194
+ (click)="addCustomAnswers()"
89195
+ [disabled]="!canAddCustomAnswers()"
89196
+ [ngClass]="addButtonClasses()"
89197
+ class="flex-1 px-5 py-2.5 rounded-lg font-medium text-sm transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100">
89198
+ Add
89199
+ </button>
89200
+ </div>
89201
+ </div>
89202
+ </div>
89203
+ </div>
89204
+
89205
+ @if (!addOtherAnswersExpanded()) {
89206
+ <button
89207
+ type="button"
89208
+ (click)="toggleAddOtherAnswers()"
89209
+ [ngClass]="addOtherAnswersButtonClasses()"
89210
+ class="w-full px-5 py-3 rounded-lg font-medium text-sm transition-all duration-200 border-2 hover:scale-[1.01] active:scale-[0.99]">
89211
+ Add other answers
89212
+ </button>
89213
+ }
89214
+ </div>
89215
+ </div>
89216
+ </div>
89217
+
89218
+ <!-- Sticky Footer -->
89219
+ <div [ngClass]="footerClasses()" class="px-6 py-4 border-t sticky bottom-0 z-10">
89220
+ <div class="flex items-center gap-4">
89221
+ <button
89222
+ type="button"
89223
+ (click)="onSave()"
89224
+ [disabled]="saveButtonsDisabled()"
89225
+ [ngClass]="saveButtonClasses()"
89226
+ class="px-5 py-2.5 rounded-lg font-medium text-sm transition-all duration-200 border disabled:opacity-50 disabled:cursor-not-allowed hover:scale-[1.02] active:scale-95 disabled:hover:scale-100">
89227
+ Save
89228
+ </button>
89229
+ <button
89230
+ type="button"
89231
+ (click)="onSaveAndNext()"
89232
+ [disabled]="saveButtonsDisabled()"
89233
+ [ngClass]="saveAndNextButtonClasses()"
89234
+ class="flex-1 px-5 py-2.5 rounded-lg font-medium text-sm transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:scale-[1.01] active:scale-[0.99] disabled:hover:scale-100">
89235
+ <span>Save & next unanswered</span>
89236
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89237
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
89238
+ </svg>
89239
+ </button>
89240
+ </div>
89241
+ </div>
89242
+ </div>
89229
89243
  `
89230
89244
  }]
89231
89245
  }], () => [], { question: [{ type: i0.Input, args: [{ isSignal: true, alias: "question", required: true }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", required: false }] }], viewType: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewType", required: false }] }], selectedCategoryId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedCategoryId", required: false }] }], selectedFocusAreaId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedFocusAreaId", required: false }] }], filteredQuestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "filteredQuestions", required: false }] }], profileShopAnswers: [{ type: i0.Input, args: [{ isSignal: true, alias: "profileShopAnswers", required: false }] }], backClick: [{ type: i0.Output, args: ["backClick"] }], saveClick: [{ type: i0.Output, args: ["saveClick"] }], saveAndNextClick: [{ type: i0.Output, args: ["saveAndNextClick"] }], scrollContainer: [{