ps-helix 6.2.0 → 6.2.2

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A comprehensive Angular component library built with Angular 22+ featuring modern design patterns, accessibility-first development, and optimal developer experience.
4
4
 
5
- [![npm version](https://img.shields.io/badge/npm-6.2.0-blue.svg)](https://www.npmjs.com/package/ps-helix)
5
+ [![npm version](https://img.shields.io/badge/npm-6.2.2-blue.svg)](https://www.npmjs.com/package/ps-helix)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![Angular](https://img.shields.io/badge/Angular-22.0.0-red.svg)](https://angular.dev/)
8
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-6.0.0-blue.svg)](https://www.typescriptlang.org/)
@@ -106,7 +106,7 @@ After installation, verify that ps-helix is in your `package.json`:
106
106
  ```json
107
107
  {
108
108
  "dependencies": {
109
- "ps-helix": "^6.2.0"
109
+ "ps-helix": "^6.2.2"
110
110
  }
111
111
  }
112
112
  ```
@@ -1182,7 +1182,7 @@ Copyright (c) 2025 PACK Solutions
1182
1182
 
1183
1183
  ---
1184
1184
 
1185
- **Version**: 6.2.0
1185
+ **Version**: 6.2.2
1186
1186
  **Built with**: Angular 22.0.0, TypeScript 6.0.0, Phosphor Icons 2.0.3
1187
1187
  **Author**: Fabrice PEREZ | Design Engineer at PACK Solutions
1188
1188
  **Last Updated**: January 2026
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, output, computed, ChangeDetectionStrategy, Component, model, inject, ElementRef, signal, PLATFORM_ID, ViewEncapsulation, InjectionToken, viewChild, effect, Injectable, DestroyRef, Directive, ViewContainerRef, ChangeDetectorRef, ViewChild, Injector, afterNextRender, HostListener, Renderer2, contentChild, linkedSignal, EventEmitter, Output, Input, contentChildren } from '@angular/core';
2
+ import { input, output, computed, ChangeDetectionStrategy, Component, model, inject, ElementRef, signal, PLATFORM_ID, ViewEncapsulation, InjectionToken, viewChild, isDevMode, Injectable, DestroyRef, Directive, ViewContainerRef, ChangeDetectorRef, effect, ViewChild, Injector, afterNextRender, HostListener, Renderer2, contentChild, linkedSignal, EventEmitter, Output, Input, contentChildren } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
4
  import { CommonModule, DOCUMENT, isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
5
5
  import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
@@ -1210,6 +1210,9 @@ class PshCheckboxComponent {
1210
1210
  this.config = inject(CHECKBOX_CONFIG);
1211
1211
  this.checkboxInput = viewChild('checkboxInput', /* @ts-ignore */
1212
1212
  ...(ngDevMode ? [{ debugName: "checkboxInput" }] : /* istanbul ignore next */ []));
1213
+ /** Holds the `label` input or the projected content — whichever provides the visible label. */
1214
+ this.labelSlot = viewChild('labelSlot', /* @ts-ignore */
1215
+ ...(ngDevMode ? [{ debugName: "labelSlot" }] : /* istanbul ignore next */ []));
1213
1216
  this.uniqueId = `psh-cb-${++checkboxIdCounter}`;
1214
1217
  this.onChange = (_) => { };
1215
1218
  this.onTouched = () => { };
@@ -1261,11 +1264,17 @@ class PshCheckboxComponent {
1261
1264
  return this.checked() ? 'checked' : 'unchecked';
1262
1265
  }, /* @ts-ignore */
1263
1266
  ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
1264
- effect(() => {
1265
- if (!this.label() && !this.ariaLabel()) {
1266
- console.warn('[psh-checkbox] Label manquant pour l\'accessibilité.');
1267
- }
1268
- });
1267
+ }
1268
+ ngAfterViewInit() {
1269
+ if (!isDevMode())
1270
+ return;
1271
+ // Read the rendered label slot rather than the `label` input alone: a projected
1272
+ // label (`<psh-checkbox>Accept terms</psh-checkbox>`) is just as accessible, and
1273
+ // `contentChild` cannot see a bare text node. Checked once, after the first render.
1274
+ const hasVisibleLabel = !!this.labelSlot()?.nativeElement.textContent?.trim();
1275
+ if (!hasVisibleLabel && !this.ariaLabel()) {
1276
+ console.warn('[psh-checkbox] No accessible label provided. Please use label input, ariaLabel input, or projected content.');
1277
+ }
1269
1278
  }
1270
1279
  toggle() {
1271
1280
  if (!this.disabled()) {
@@ -1296,7 +1305,7 @@ class PshCheckboxComponent {
1296
1305
  provide: NG_VALUE_ACCESSOR,
1297
1306
  useExisting: PshCheckboxComponent,
1298
1307
  multi: true
1299
- }], viewQueries: [{ propertyName: "checkboxInput", first: true, predicate: ["checkboxInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"checkbox-container\">\r\n <label class=\"checkbox-label\" [class.label-left]=\"labelPosition() === 'left'\">\r\n <input\r\n #checkboxInput\r\n type=\"checkbox\"\r\n class=\"checkbox-input\"\r\n [checked]=\"checked()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required()\"\r\n [attr.aria-label]=\"computedAriaLabel()\"\r\n [attr.aria-checked]=\"ariaChecked()\"\r\n [attr.aria-required]=\"required() || null\"\r\n [attr.aria-invalid]=\"error() ? 'true' : null\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\"\r\n (change)=\"toggle()\"\r\n (keydown)=\"handleKeydown($event)\"\r\n />\r\n \r\n <span class=\"checkbox-control\">\r\n @if (checked() && !indeterminate()) {\r\n <i class=\"ph ph-check\" aria-hidden=\"true\"></i>\r\n }\r\n @if (indeterminate()) {\r\n <i class=\"ph ph-minus\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n\r\n <span class=\"checkbox-text\">\r\n @if (label()) {\r\n {{ label() }}\r\n } @else {\r\n <ng-content></ng-content>\r\n }\r\n </span>\r\n </label>\r\n\r\n @if (error()) {\r\n <div [id]=\"uniqueId + '-error'\" class=\"checkbox-message error\" role=\"alert\">\r\n {{ error() }}\r\n </div>\r\n } @else if (success()) {\r\n <div [id]=\"uniqueId + '-success'\" class=\"checkbox-message success\" role=\"status\">\r\n {{ success() }}\r\n </div>\r\n }\r\n</div>", styles: [":host{display:inline-flex;flex-direction:column}.checkbox-container{display:flex;flex-direction:column;gap:var(--form-field-gap)}.checkbox-label{display:inline-flex;align-items:center;gap:var(--form-label-gap);cursor:pointer;-webkit-user-select:none;user-select:none;color:var(--text-color)}.checkbox-label.label-left{flex-direction:row-reverse;justify-content:space-between}.checkbox-input{position:absolute;opacity:0;width:0;height:0}.checkbox-control{display:flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:.125rem solid var(--surface-border);border-radius:var(--border-radius);background:var(--surface-card);transition:all var(--animation-duration-fast) var(--animation-easing-default);color:#fff;position:relative;flex-shrink:0}:host.checkbox-checked .checkbox-control,:host.checkbox-indeterminate .checkbox-control{background:var(--primary-color);border-color:var(--primary-color)}.checkbox-input:focus-visible+.checkbox-control{outline:.125rem solid var(--focus-outline-color);outline-offset:.125rem}.checkbox-control i{font-size:.875rem;line-height:1}:host.checkbox-small .checkbox-control{width:1rem;height:1rem}:host.checkbox-large .checkbox-control{width:1.5rem;height:1.5rem}:host.checkbox-error .checkbox-control{border-color:var(--danger-color)}:host.checkbox-error.checkbox-checked .checkbox-control{background:var(--danger-color)}:host.checkbox-success .checkbox-control{border-color:var(--success-color)}:host.checkbox-success.checkbox-checked .checkbox-control{background:var(--success-color)}:host.checkbox-disabled{opacity:var(--opacity-disabled);cursor:not-allowed}:host.checkbox-disabled .checkbox-label{cursor:not-allowed}.checkbox-message{font-size:.875rem}.checkbox-message.error{color:var(--danger-color)}.checkbox-message.success{color:var(--success-color)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1308
+ }], viewQueries: [{ propertyName: "checkboxInput", first: true, predicate: ["checkboxInput"], descendants: true, isSignal: true }, { propertyName: "labelSlot", first: true, predicate: ["labelSlot"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"checkbox-container\">\r\n <label class=\"checkbox-label\" [class.label-left]=\"labelPosition() === 'left'\">\r\n <input\r\n #checkboxInput\r\n type=\"checkbox\"\r\n class=\"checkbox-input\"\r\n [checked]=\"checked()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required()\"\r\n [attr.aria-label]=\"computedAriaLabel()\"\r\n [attr.aria-checked]=\"ariaChecked()\"\r\n [attr.aria-required]=\"required() || null\"\r\n [attr.aria-invalid]=\"error() ? 'true' : null\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\"\r\n (change)=\"toggle()\"\r\n (keydown)=\"handleKeydown($event)\"\r\n />\r\n \r\n <span class=\"checkbox-control\">\r\n @if (checked() && !indeterminate()) {\r\n <i class=\"ph ph-check\" aria-hidden=\"true\"></i>\r\n }\r\n @if (indeterminate()) {\r\n <i class=\"ph ph-minus\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n\r\n <span class=\"checkbox-text\" #labelSlot>\r\n @if (label()) {\r\n {{ label() }}\r\n } @else {\r\n <ng-content></ng-content>\r\n }\r\n </span>\r\n </label>\r\n\r\n @if (error()) {\r\n <div [id]=\"uniqueId + '-error'\" class=\"checkbox-message error\" role=\"alert\">\r\n {{ error() }}\r\n </div>\r\n } @else if (success()) {\r\n <div [id]=\"uniqueId + '-success'\" class=\"checkbox-message success\" role=\"status\">\r\n {{ success() }}\r\n </div>\r\n }\r\n</div>", styles: [":host{display:inline-flex;flex-direction:column}.checkbox-container{display:flex;flex-direction:column;gap:var(--form-field-gap)}.checkbox-label{display:inline-flex;align-items:center;gap:var(--form-label-gap);cursor:pointer;-webkit-user-select:none;user-select:none;color:var(--text-color)}.checkbox-label.label-left{flex-direction:row-reverse;justify-content:space-between}.checkbox-input{position:absolute;opacity:0;width:0;height:0}.checkbox-control{display:flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:.125rem solid var(--surface-border);border-radius:var(--border-radius);background:var(--surface-card);transition:all var(--animation-duration-fast) var(--animation-easing-default);color:#fff;position:relative;flex-shrink:0}:host.checkbox-checked .checkbox-control,:host.checkbox-indeterminate .checkbox-control{background:var(--primary-color);border-color:var(--primary-color)}.checkbox-input:focus-visible+.checkbox-control{outline:.125rem solid var(--focus-outline-color);outline-offset:.125rem}.checkbox-control i{font-size:.875rem;line-height:1}:host.checkbox-small .checkbox-control{width:1rem;height:1rem}:host.checkbox-large .checkbox-control{width:1.5rem;height:1.5rem}:host.checkbox-error .checkbox-control{border-color:var(--danger-color)}:host.checkbox-error.checkbox-checked .checkbox-control{background:var(--danger-color)}:host.checkbox-success .checkbox-control{border-color:var(--success-color)}:host.checkbox-success.checkbox-checked .checkbox-control{background:var(--success-color)}:host.checkbox-disabled{opacity:var(--opacity-disabled);cursor:not-allowed}:host.checkbox-disabled .checkbox-label{cursor:not-allowed}.checkbox-message{font-size:.875rem}.checkbox-message.error{color:var(--danger-color)}.checkbox-message.success{color:var(--success-color)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1300
1309
  }
1301
1310
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PshCheckboxComponent, decorators: [{
1302
1311
  type: Component,
@@ -1314,8 +1323,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
1314
1323
  '[class.checkbox-indeterminate]': 'indeterminate()',
1315
1324
  '[attr.data-state]': 'state()',
1316
1325
  '[attr.aria-checked]': 'ariaChecked()'
1317
- }, template: "<div class=\"checkbox-container\">\r\n <label class=\"checkbox-label\" [class.label-left]=\"labelPosition() === 'left'\">\r\n <input\r\n #checkboxInput\r\n type=\"checkbox\"\r\n class=\"checkbox-input\"\r\n [checked]=\"checked()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required()\"\r\n [attr.aria-label]=\"computedAriaLabel()\"\r\n [attr.aria-checked]=\"ariaChecked()\"\r\n [attr.aria-required]=\"required() || null\"\r\n [attr.aria-invalid]=\"error() ? 'true' : null\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\"\r\n (change)=\"toggle()\"\r\n (keydown)=\"handleKeydown($event)\"\r\n />\r\n \r\n <span class=\"checkbox-control\">\r\n @if (checked() && !indeterminate()) {\r\n <i class=\"ph ph-check\" aria-hidden=\"true\"></i>\r\n }\r\n @if (indeterminate()) {\r\n <i class=\"ph ph-minus\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n\r\n <span class=\"checkbox-text\">\r\n @if (label()) {\r\n {{ label() }}\r\n } @else {\r\n <ng-content></ng-content>\r\n }\r\n </span>\r\n </label>\r\n\r\n @if (error()) {\r\n <div [id]=\"uniqueId + '-error'\" class=\"checkbox-message error\" role=\"alert\">\r\n {{ error() }}\r\n </div>\r\n } @else if (success()) {\r\n <div [id]=\"uniqueId + '-success'\" class=\"checkbox-message success\" role=\"status\">\r\n {{ success() }}\r\n </div>\r\n }\r\n</div>", styles: [":host{display:inline-flex;flex-direction:column}.checkbox-container{display:flex;flex-direction:column;gap:var(--form-field-gap)}.checkbox-label{display:inline-flex;align-items:center;gap:var(--form-label-gap);cursor:pointer;-webkit-user-select:none;user-select:none;color:var(--text-color)}.checkbox-label.label-left{flex-direction:row-reverse;justify-content:space-between}.checkbox-input{position:absolute;opacity:0;width:0;height:0}.checkbox-control{display:flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:.125rem solid var(--surface-border);border-radius:var(--border-radius);background:var(--surface-card);transition:all var(--animation-duration-fast) var(--animation-easing-default);color:#fff;position:relative;flex-shrink:0}:host.checkbox-checked .checkbox-control,:host.checkbox-indeterminate .checkbox-control{background:var(--primary-color);border-color:var(--primary-color)}.checkbox-input:focus-visible+.checkbox-control{outline:.125rem solid var(--focus-outline-color);outline-offset:.125rem}.checkbox-control i{font-size:.875rem;line-height:1}:host.checkbox-small .checkbox-control{width:1rem;height:1rem}:host.checkbox-large .checkbox-control{width:1.5rem;height:1.5rem}:host.checkbox-error .checkbox-control{border-color:var(--danger-color)}:host.checkbox-error.checkbox-checked .checkbox-control{background:var(--danger-color)}:host.checkbox-success .checkbox-control{border-color:var(--success-color)}:host.checkbox-success.checkbox-checked .checkbox-control{background:var(--success-color)}:host.checkbox-disabled{opacity:var(--opacity-disabled);cursor:not-allowed}:host.checkbox-disabled .checkbox-label{cursor:not-allowed}.checkbox-message{font-size:.875rem}.checkbox-message.error{color:var(--danger-color)}.checkbox-message.success{color:var(--success-color)}\n"] }]
1318
- }], ctorParameters: () => [], propDecorators: { checkboxInput: [{ type: i0.ViewChild, args: ['checkboxInput', { isSignal: true }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }, { type: i0.Output, args: ["indeterminateChange"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], success: [{ type: i0.Input, args: [{ isSignal: true, alias: "success", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], labelPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPosition", required: false }] }] } });
1326
+ }, template: "<div class=\"checkbox-container\">\r\n <label class=\"checkbox-label\" [class.label-left]=\"labelPosition() === 'left'\">\r\n <input\r\n #checkboxInput\r\n type=\"checkbox\"\r\n class=\"checkbox-input\"\r\n [checked]=\"checked()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required()\"\r\n [attr.aria-label]=\"computedAriaLabel()\"\r\n [attr.aria-checked]=\"ariaChecked()\"\r\n [attr.aria-required]=\"required() || null\"\r\n [attr.aria-invalid]=\"error() ? 'true' : null\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\"\r\n (change)=\"toggle()\"\r\n (keydown)=\"handleKeydown($event)\"\r\n />\r\n \r\n <span class=\"checkbox-control\">\r\n @if (checked() && !indeterminate()) {\r\n <i class=\"ph ph-check\" aria-hidden=\"true\"></i>\r\n }\r\n @if (indeterminate()) {\r\n <i class=\"ph ph-minus\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n\r\n <span class=\"checkbox-text\" #labelSlot>\r\n @if (label()) {\r\n {{ label() }}\r\n } @else {\r\n <ng-content></ng-content>\r\n }\r\n </span>\r\n </label>\r\n\r\n @if (error()) {\r\n <div [id]=\"uniqueId + '-error'\" class=\"checkbox-message error\" role=\"alert\">\r\n {{ error() }}\r\n </div>\r\n } @else if (success()) {\r\n <div [id]=\"uniqueId + '-success'\" class=\"checkbox-message success\" role=\"status\">\r\n {{ success() }}\r\n </div>\r\n }\r\n</div>", styles: [":host{display:inline-flex;flex-direction:column}.checkbox-container{display:flex;flex-direction:column;gap:var(--form-field-gap)}.checkbox-label{display:inline-flex;align-items:center;gap:var(--form-label-gap);cursor:pointer;-webkit-user-select:none;user-select:none;color:var(--text-color)}.checkbox-label.label-left{flex-direction:row-reverse;justify-content:space-between}.checkbox-input{position:absolute;opacity:0;width:0;height:0}.checkbox-control{display:flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:.125rem solid var(--surface-border);border-radius:var(--border-radius);background:var(--surface-card);transition:all var(--animation-duration-fast) var(--animation-easing-default);color:#fff;position:relative;flex-shrink:0}:host.checkbox-checked .checkbox-control,:host.checkbox-indeterminate .checkbox-control{background:var(--primary-color);border-color:var(--primary-color)}.checkbox-input:focus-visible+.checkbox-control{outline:.125rem solid var(--focus-outline-color);outline-offset:.125rem}.checkbox-control i{font-size:.875rem;line-height:1}:host.checkbox-small .checkbox-control{width:1rem;height:1rem}:host.checkbox-large .checkbox-control{width:1.5rem;height:1.5rem}:host.checkbox-error .checkbox-control{border-color:var(--danger-color)}:host.checkbox-error.checkbox-checked .checkbox-control{background:var(--danger-color)}:host.checkbox-success .checkbox-control{border-color:var(--success-color)}:host.checkbox-success.checkbox-checked .checkbox-control{background:var(--success-color)}:host.checkbox-disabled{opacity:var(--opacity-disabled);cursor:not-allowed}:host.checkbox-disabled .checkbox-label{cursor:not-allowed}.checkbox-message{font-size:.875rem}.checkbox-message.error{color:var(--danger-color)}.checkbox-message.success{color:var(--success-color)}\n"] }]
1327
+ }], propDecorators: { checkboxInput: [{ type: i0.ViewChild, args: ['checkboxInput', { isSignal: true }] }], labelSlot: [{ type: i0.ViewChild, args: ['labelSlot', { isSignal: true }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }, { type: i0.Output, args: ["indeterminateChange"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], success: [{ type: i0.Input, args: [{ isSignal: true, alias: "success", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], labelPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPosition", required: false }] }] } });
1319
1328
 
1320
1329
  class PshCollapseComponent {
1321
1330
  constructor() {
@@ -1325,14 +1334,18 @@ class PshCollapseComponent {
1325
1334
  ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1326
1335
  this.variant = input('default', { ...(ngDevMode ? { debugName: "variant" } : /* istanbul ignore next */ {}), transform: (value) => {
1327
1336
  if (!['default', 'outline'].includes(value)) {
1328
- console.warn(`[psh-collapse] Invalid variant "${value}", falling back to "default"`);
1337
+ if (isDevMode()) {
1338
+ console.warn(`[psh-collapse] Invalid variant "${value}", falling back to "default"`);
1339
+ }
1329
1340
  return 'default';
1330
1341
  }
1331
1342
  return value;
1332
1343
  } });
1333
1344
  this.size = input('medium', { ...(ngDevMode ? { debugName: "size" } : /* istanbul ignore next */ {}), transform: (value) => {
1334
1345
  if (!['small', 'medium', 'large'].includes(value)) {
1335
- console.warn(`[psh-collapse] Invalid size "${value}", falling back to "medium"`);
1346
+ if (isDevMode()) {
1347
+ console.warn(`[psh-collapse] Invalid size "${value}", falling back to "medium"`);
1348
+ }
1336
1349
  return 'medium';
1337
1350
  }
1338
1351
  return value;
@@ -3505,16 +3518,31 @@ class PshPaginationComponent {
3505
3518
  this.itemsPerPageInput = input(10, { ...(ngDevMode ? { debugName: "itemsPerPageInput" } : /* istanbul ignore next */ {}), alias: 'itemsPerPage' });
3506
3519
  this.itemsPerPage = linkedSignal(this.itemsPerPageInput, /* @ts-ignore */
3507
3520
  ...(ngDevMode ? [{ debugName: "itemsPerPage" }] : /* istanbul ignore next */ []));
3521
+ /**
3522
+ * Number of pages used for rendering and navigation: never below 1, and never
3523
+ * written back to the `totalPages` input. `totalPages` is derived data owned by
3524
+ * the caller (`Math.ceil(total / pageSize)`, or a server-side page count), so the
3525
+ * component reads it and clamps its own view of it instead of correcting it.
3526
+ */
3527
+ this.effectiveTotalPages = computed(() => {
3528
+ const total = this.totalPages();
3529
+ return Number.isInteger(total) && total >= 1 ? total : 1;
3530
+ }, /* @ts-ignore */
3531
+ ...(ngDevMode ? [{ debugName: "effectiveTotalPages" }] : /* istanbul ignore next */ []));
3508
3532
  this.size = input(this.config.size ?? 'medium', { ...(ngDevMode ? { debugName: "size" } : /* istanbul ignore next */ {}), transform: (value) => {
3509
3533
  if (!['small', 'medium', 'large'].includes(value)) {
3510
- console.warn(`[psh-pagination] Invalid size "${value}", falling back to "medium"`);
3534
+ if (isDevMode()) {
3535
+ console.warn(`[psh-pagination] Invalid size "${value}", falling back to "medium"`);
3536
+ }
3511
3537
  return 'medium';
3512
3538
  }
3513
3539
  return value;
3514
3540
  } });
3515
3541
  this.variant = input(this.config.variant ?? 'default', { ...(ngDevMode ? { debugName: "variant" } : /* istanbul ignore next */ {}), transform: (value) => {
3516
3542
  if (!['default', 'outline'].includes(value)) {
3517
- console.warn(`[psh-pagination] Invalid variant "${value}", falling back to "default"`);
3543
+ if (isDevMode()) {
3544
+ console.warn(`[psh-pagination] Invalid variant "${value}", falling back to "default"`);
3545
+ }
3518
3546
  return 'default';
3519
3547
  }
3520
3548
  return value;
@@ -3559,7 +3587,7 @@ class PshPaginationComponent {
3559
3587
  const pages = [];
3560
3588
  const halfVisible = Math.floor(this.maxVisiblePages() / 2);
3561
3589
  let start = Math.max(1, this.currentPage() - halfVisible);
3562
- const end = Math.min(this.totalPages(), start + this.maxVisiblePages() - 1);
3590
+ const end = Math.min(this.effectiveTotalPages(), start + this.maxVisiblePages() - 1);
3563
3591
  if (end - start + 1 < this.maxVisiblePages()) {
3564
3592
  start = Math.max(1, end - this.maxVisiblePages() + 1);
3565
3593
  }
@@ -3571,60 +3599,70 @@ class PshPaginationComponent {
3571
3599
  ...(ngDevMode ? [{ debugName: "pages" }] : /* istanbul ignore next */ []));
3572
3600
  this.state = computed(() => this.getState(), /* @ts-ignore */
3573
3601
  ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
3574
- this.canGoNext = computed(() => this.currentPage() < this.totalPages(), /* @ts-ignore */
3602
+ this.canGoNext = computed(() => this.currentPage() < this.effectiveTotalPages(), /* @ts-ignore */
3575
3603
  ...(ngDevMode ? [{ debugName: "canGoNext" }] : /* istanbul ignore next */ []));
3576
3604
  this.canGoPrevious = computed(() => this.currentPage() > 1, /* @ts-ignore */
3577
3605
  ...(ngDevMode ? [{ debugName: "canGoPrevious" }] : /* istanbul ignore next */ []));
3578
3606
  this.isFirstPage = computed(() => this.currentPage() === 1, /* @ts-ignore */
3579
3607
  ...(ngDevMode ? [{ debugName: "isFirstPage" }] : /* istanbul ignore next */ []));
3580
- this.isLastPage = computed(() => this.currentPage() === this.totalPages(), /* @ts-ignore */
3608
+ this.isLastPage = computed(() => this.currentPage() === this.effectiveTotalPages(), /* @ts-ignore */
3581
3609
  ...(ngDevMode ? [{ debugName: "isLastPage" }] : /* istanbul ignore next */ []));
3582
3610
  this.currentPageAnnouncement = computed(() => {
3583
- return `Page ${this.currentPage()} sur ${this.totalPages()}`;
3611
+ return `Page ${this.currentPage()} sur ${this.effectiveTotalPages()}`;
3584
3612
  }, /* @ts-ignore */
3585
3613
  ...(ngDevMode ? [{ debugName: "currentPageAnnouncement" }] : /* istanbul ignore next */ []));
3586
- effect(() => {
3587
- const total = this.totalPages();
3588
- if (total < 1 || !Number.isFinite(total)) {
3589
- console.warn(`[psh-pagination] Invalid totalPages "${total}", setting to 1`);
3590
- this.totalPages.set(1);
3591
- }
3592
- });
3614
+ if (isDevMode()) {
3615
+ effect(() => {
3616
+ // `0` is a legitimate state (empty list, or a server-side page count that is
3617
+ // not loaded yet): stay silent. Only report what betrays a caller bug.
3618
+ const total = this.totalPages();
3619
+ if (total !== 0 && (!Number.isInteger(total) || total < 1)) {
3620
+ console.warn(`[psh-pagination] Invalid totalPages "${total}", using 1 page`);
3621
+ }
3622
+ });
3623
+ effect(() => {
3624
+ const maxVisible = this.maxVisiblePages();
3625
+ if (maxVisible < 1 || !Number.isFinite(maxVisible)) {
3626
+ console.warn(`[psh-pagination] Invalid maxVisiblePages "${maxVisible}", must be >= 1`);
3627
+ }
3628
+ });
3629
+ }
3630
+ // `currentPage` IS owned by the component (it navigates), so correcting it with
3631
+ // a write-back is the intended behaviour — clamped on the derived page count.
3593
3632
  effect(() => {
3594
3633
  const current = this.currentPage();
3595
- const total = this.totalPages();
3634
+ const total = this.effectiveTotalPages();
3596
3635
  if (current < 1 || !Number.isFinite(current)) {
3597
- console.warn(`[psh-pagination] Invalid currentPage "${current}", setting to 1`);
3636
+ if (isDevMode()) {
3637
+ console.warn(`[psh-pagination] Invalid currentPage "${current}", setting to 1`);
3638
+ }
3598
3639
  this.currentPage.set(1);
3599
3640
  }
3600
- else if (current > total && total >= 1) {
3601
- console.warn(`[psh-pagination] currentPage "${current}" exceeds totalPages "${total}", setting to ${total}`);
3641
+ else if (current > total) {
3642
+ if (isDevMode()) {
3643
+ console.warn(`[psh-pagination] currentPage "${current}" exceeds totalPages "${total}", setting to ${total}`);
3644
+ }
3602
3645
  this.currentPage.set(total);
3603
3646
  }
3604
3647
  });
3605
- effect(() => {
3606
- const maxVisible = this.maxVisiblePages();
3607
- if (maxVisible < 1 || !Number.isFinite(maxVisible)) {
3608
- console.warn(`[psh-pagination] Invalid maxVisiblePages "${maxVisible}", must be >= 1`);
3609
- }
3610
- });
3611
3648
  }
3612
3649
  getState() {
3613
3650
  if (this.currentPage() === 1)
3614
3651
  return 'first';
3615
- if (this.currentPage() === this.totalPages())
3652
+ if (this.currentPage() === this.effectiveTotalPages())
3616
3653
  return 'last';
3617
3654
  return 'default';
3618
3655
  }
3619
3656
  goToPage(page) {
3620
- if (page !== this.currentPage() && page >= 1 && page <= this.totalPages()) {
3657
+ const total = this.effectiveTotalPages();
3658
+ if (page !== this.currentPage() && page >= 1 && page <= total) {
3621
3659
  this.currentPage.set(page);
3622
3660
  this.pageChange.emit(page);
3623
3661
  }
3624
- else if (page < 1 || page > this.totalPages()) {
3662
+ else if (page < 1 || page > total) {
3625
3663
  this.navigationError.emit({
3626
3664
  action: 'goToPage',
3627
- reason: `Page ${page} is out of bounds (1-${this.totalPages()})`,
3665
+ reason: `Page ${page} is out of bounds (1-${total})`,
3628
3666
  });
3629
3667
  }
3630
3668
  }
@@ -3632,7 +3670,7 @@ class PshPaginationComponent {
3632
3670
  this.goToPage(1);
3633
3671
  }
3634
3672
  goToLastPage() {
3635
- this.goToPage(this.totalPages());
3673
+ this.goToPage(this.effectiveTotalPages());
3636
3674
  }
3637
3675
  goToNextPage() {
3638
3676
  if (this.canGoNext()) {
@@ -3865,11 +3903,13 @@ class PshRadioComponent {
3865
3903
  // so aria-describedby references whichever message is actually rendered.
3866
3904
  this.ariaDescribedBy = computed(() => this.errorMessageId() ?? this.successMessageId(), /* @ts-ignore */
3867
3905
  ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
3868
- effect(() => {
3869
- if (!this.label() && !this.ariaLabel() && !this.hasProjectedContent()) {
3870
- console.warn('[psh-radio] No accessible label provided. Please use label input, ariaLabel input, or projected content.');
3871
- }
3872
- }, { allowSignalWrites: false });
3906
+ if (isDevMode()) {
3907
+ effect(() => {
3908
+ if (!this.label() && !this.ariaLabel() && !this.hasProjectedContent()) {
3909
+ console.warn('[psh-radio] No accessible label provided. Please use label input, ariaLabel input, or projected content.');
3910
+ }
3911
+ });
3912
+ }
3873
3913
  }
3874
3914
  getState() {
3875
3915
  if (this.disabled())
@@ -3880,6 +3920,11 @@ class PshRadioComponent {
3880
3920
  return 'success';
3881
3921
  return this.checked() ? 'checked' : 'unchecked';
3882
3922
  }
3923
+ /**
3924
+ * Declares whether a label is projected (`<psh-radio>Label</psh-radio>`). Not detected
3925
+ * automatically yet: until a consumer calls this, a projected label still triggers the
3926
+ * dev-only accessibility warning and `aria-label="Radio"` overrides the visible text.
3927
+ */
3883
3928
  updateProjectedContent(hasContent) {
3884
3929
  this.hasProjectedContent.set(hasContent);
3885
3930
  }