@c-code/c-code-fw 1.3.0 → 2.1.0

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 (28) hide show
  1. package/README.md +118 -104
  2. package/fesm2022/c-code-c-code-fw-ui.mjs +622 -141
  3. package/fesm2022/c-code-c-code-fw-ui.mjs.map +1 -1
  4. package/package.json +1 -1
  5. package/ui/lib/components/button/button.component.d.ts +24 -0
  6. package/ui/lib/components/category-menu/category-menu.component.d.ts +12 -3
  7. package/ui/lib/components/faq/faq.component.d.ts +11 -4
  8. package/ui/lib/components/faq-item/faq-item.component.d.ts +21 -0
  9. package/ui/lib/components/gallery/gallery.component.d.ts +7 -3
  10. package/ui/lib/components/info-item/info-item.component.d.ts +10 -2
  11. package/ui/lib/components/lightbox/lightbox.component.d.ts +18 -3
  12. package/ui/lib/components/notice/notice.component.d.ts +17 -0
  13. package/ui/lib/components/page-banner/page-banner.component.d.ts +9 -3
  14. package/ui/lib/components/plan-card/plan-card.component.d.ts +29 -6
  15. package/ui/lib/components/plan-catalog/plan-catalog.component.d.ts +43 -19
  16. package/ui/lib/components/plan-details/plan-details.component.d.ts +26 -11
  17. package/ui/lib/components/plan-filter-form/plan-filter-form.component.d.ts +15 -5
  18. package/ui/lib/components/promo-modal/promo-modal.component.d.ts +39 -7
  19. package/ui/lib/components/search-box/search-box.component.d.ts +6 -1
  20. package/ui/lib/components/section-heading/section-heading.component.d.ts +34 -0
  21. package/ui/lib/components/social-links/social-links.component.d.ts +10 -6
  22. package/ui/lib/components/whatsapp-button/whatsapp-button.component.d.ts +12 -2
  23. package/ui/lib/models/promo.models.d.ts +25 -0
  24. package/ui/lib/utils/promo.utils.d.ts +18 -0
  25. package/ui/lib/utils/text.utils.d.ts +10 -0
  26. package/ui/public-api.d.ts +6 -0
  27. package/ui/theme/component-base.css +136 -0
  28. package/ui/theme/tokens.css +111 -45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-code/c-code-fw",
3
- "version": "1.3.0",
3
+ "version": "2.1.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^19.2.0",
6
6
  "@angular/core": "^19.2.0",
@@ -0,0 +1,24 @@
1
+ import * as i0 from "@angular/core";
2
+ export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'whatsapp' | 'inverse';
3
+ export type ButtonSize = 'sm' | 'md' | 'lg';
4
+ /**
5
+ * Shared button look for links and buttons. Every component in the library uses it,
6
+ * so all calls to action share size, focus and colors.
7
+ *
8
+ * ```html
9
+ * <a ccButton [href]="whatsapp" target="_blank">Reservar</a>
10
+ * <button ccButton variant="secondary" size="sm" type="button">Limpiar</button>
11
+ * ```
12
+ *
13
+ * Tokens: --cc-btn-bg, --cc-btn-color, --cc-btn-hover-bg, --cc-btn-hover-color,
14
+ * --cc-btn-border, --cc-btn-radius, --cc-btn-padding-x, --cc-btn-font-weight,
15
+ * --cc-btn-letter-spacing, --cc-btn-text-transform.
16
+ */
17
+ export declare class ButtonComponent {
18
+ readonly variant: import("@angular/core").InputSignal<ButtonVariant>;
19
+ readonly size: import("@angular/core").InputSignal<ButtonSize>;
20
+ /** Full width. */
21
+ readonly block: import("@angular/core").InputSignalWithTransform<boolean, unknown>;
22
+ static ɵfac: i0.ɵɵFactoryDeclaration<ButtonComponent, never>;
23
+ static ɵcmp: i0.ɵɵComponentDeclaration<ButtonComponent, "a[ccButton], button[ccButton]", never, { "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "block": { "alias": "block"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
24
+ }
@@ -1,19 +1,28 @@
1
1
  import { CategoryOption } from '../../models/plan.models';
2
2
  import * as i0 from "@angular/core";
3
3
  /**
4
- * Vertical category list with a single selected option.
5
- * Replaces the `appElementActive` markup the sites used for plan categories.
4
+ * Category selector with a single selected option. Each option is a toggle button
5
+ * (`aria-pressed`), so it works with Tab and Enter/Space.
6
+ *
7
+ * `layout`:
8
+ * - `list`: vertical list, for sidebars.
9
+ * - `chips`: one scrollable row of pills, for small screens.
10
+ * - `auto` (default): chips below 1024px, list from 1024px.
6
11
  *
7
12
  * ```html
8
13
  * <cc-category-menu [options]="categories" [(selected)]="category" />
9
14
  * ```
15
+ *
16
+ * Tokens: --cc-category-color, --cc-category-hover-bg, --cc-category-active-bg,
17
+ * --cc-category-active-color, --cc-category-chip-border.
10
18
  */
11
19
  export declare class CategoryMenuComponent<T = unknown> {
12
20
  readonly options: import("@angular/core").InputSignal<CategoryOption<T>[]>;
13
21
  /** Selected value. Use `[(selected)]` to bind both ways. */
14
22
  readonly selected: import("@angular/core").ModelSignal<T | null>;
15
23
  readonly title: import("@angular/core").InputSignal<string>;
24
+ readonly layout: import("@angular/core").InputSignal<"auto" | "list" | "chips">;
16
25
  select(value: T): void;
17
26
  static ɵfac: i0.ɵɵFactoryDeclaration<CategoryMenuComponent<any>, never>;
18
- static ɵcmp: i0.ɵɵComponentDeclaration<CategoryMenuComponent<any>, "cc-category-menu", never, { "options": { "alias": "options"; "required": true; "isSignal": true; }; "selected": { "alias": "selected"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; }, { "selected": "selectedChange"; }, never, never, true, never>;
27
+ static ɵcmp: i0.ɵɵComponentDeclaration<CategoryMenuComponent<any>, "cc-category-menu", never, { "options": { "alias": "options"; "required": true; "isSignal": true; }; "selected": { "alias": "selected"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "layout": { "alias": "layout"; "required": false; "isSignal": true; }; }, { "selected": "selectedChange"; }, never, never, true, never>;
19
28
  }
@@ -1,16 +1,23 @@
1
1
  import { FaqItem } from '../../models/ui.models';
2
2
  import * as i0 from "@angular/core";
3
3
  /**
4
- * Frequently asked questions as native `<details>` elements, so the answers
5
- * stay in the prerendered HTML for search engines.
4
+ * Frequently asked questions as native `<details>` elements, so the answers stay in
5
+ * the prerendered HTML for search engines. Pass plain `items`, or project
6
+ * `<cc-faq-item>` elements when an answer needs links.
6
7
  *
7
8
  * ```html
8
- * <cc-faq title="PREGUNTAS FRECUENTES" [items]="faq" />
9
+ * <cc-faq title="Preguntas frecuentes" [items]="faq" />
10
+ * <cc-faq title="Preguntas frecuentes">
11
+ * <cc-faq-item question="¿Qué planes tienen?">Míralos en <a routerLink="/planes">planes</a>.</cc-faq-item>
12
+ * </cc-faq>
9
13
  * ```
14
+ *
15
+ * Tokens: --cc-faq-title-color, --cc-faq-gap, plus the ones of `cc-faq-item`.
10
16
  */
11
17
  export declare class FaqComponent {
12
18
  readonly items: import("@angular/core").InputSignal<FaqItem[]>;
13
19
  readonly title: import("@angular/core").InputSignal<string>;
20
+ readonly headingLevel: import("@angular/core").InputSignal<"h2" | "h3">;
14
21
  static ɵfac: i0.ɵɵFactoryDeclaration<FaqComponent, never>;
15
- static ɵcmp: i0.ɵɵComponentDeclaration<FaqComponent, "cc-faq", never, { "items": { "alias": "items"; "required": true; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
22
+ static ɵcmp: i0.ɵɵComponentDeclaration<FaqComponent, "cc-faq", never, { "items": { "alias": "items"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "headingLevel": { "alias": "headingLevel"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
16
23
  }
@@ -0,0 +1,21 @@
1
+ import * as i0 from "@angular/core";
2
+ /**
3
+ * One question of `cc-faq`. The answer is projected, so it can contain links.
4
+ *
5
+ * ```html
6
+ * <cc-faq title="Preguntas frecuentes">
7
+ * <cc-faq-item question="¿Qué planes tienen?">
8
+ * Míralos todos en <a routerLink="/planes">planes</a>.
9
+ * </cc-faq-item>
10
+ * </cc-faq>
11
+ * ```
12
+ *
13
+ * Tokens: --cc-faq-bg, --cc-faq-question-color, --cc-faq-answer-color.
14
+ */
15
+ export declare class FaqItemComponent {
16
+ readonly question: import("@angular/core").InputSignal<string>;
17
+ /** Starts expanded. */
18
+ readonly open: import("@angular/core").InputSignal<boolean>;
19
+ static ɵfac: i0.ɵɵFactoryDeclaration<FaqItemComponent, never>;
20
+ static ɵcmp: i0.ɵɵComponentDeclaration<FaqItemComponent, "cc-faq-item", never, { "question": { "alias": "question"; "required": true; "isSignal": true; }; "open": { "alias": "open"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
21
+ }
@@ -1,21 +1,25 @@
1
1
  import { GalleryImage } from '../../models/ui.models';
2
2
  import * as i0 from "@angular/core";
3
3
  /**
4
- * Thumbnail grid that opens a lightbox on click.
4
+ * Grid of square thumbnails that opens a lightbox on click.
5
5
  *
6
6
  * ```html
7
- * <cc-gallery [images]="images" />
7
+ * <cc-gallery [images]="images" [columns]="4" />
8
8
  * ```
9
9
  *
10
10
  * `numberedImages()` builds the list for folders named 1.jpeg, 2.jpeg, …
11
+ *
12
+ * Tokens: --cc-gallery-gap, --cc-gallery-radius, --cc-gallery-bg.
11
13
  */
12
14
  export declare class GalleryComponent {
13
15
  readonly images: import("@angular/core").InputSignal<GalleryImage[]>;
16
+ /** Columns from 1024px. Small screens use 2, tablets 3. */
17
+ readonly columns: import("@angular/core").InputSignal<number>;
14
18
  readonly backgroundImage: import("@angular/core").InputSignal<string>;
15
19
  readonly openLabel: import("@angular/core").InputSignal<string>;
16
20
  protected readonly open: import("@angular/core").WritableSignal<number | null>;
17
21
  static ɵfac: i0.ɵɵFactoryDeclaration<GalleryComponent, never>;
18
- static ɵcmp: i0.ɵɵComponentDeclaration<GalleryComponent, "cc-gallery", never, { "images": { "alias": "images"; "required": true; "isSignal": true; }; "backgroundImage": { "alias": "backgroundImage"; "required": false; "isSignal": true; }; "openLabel": { "alias": "openLabel"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
22
+ static ɵcmp: i0.ɵɵComponentDeclaration<GalleryComponent, "cc-gallery", never, { "images": { "alias": "images"; "required": true; "isSignal": true; }; "columns": { "alias": "columns"; "required": false; "isSignal": true; }; "backgroundImage": { "alias": "backgroundImage"; "required": false; "isSignal": true; }; "openLabel": { "alias": "openLabel"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
19
23
  }
20
24
  /**
21
25
  * Builds a gallery list for numbered files.
@@ -5,14 +5,22 @@ import * as i0 from "@angular/core";
5
5
  *
6
6
  * ```html
7
7
  * <cc-info-item title="Pagos" text="Aceptamos Nequi, Daviplata y efectivo." />
8
- * <cc-info-item title="Reservaciones" [text]="texto">
8
+ * <cc-info-item title="Bonos Spa" [text]="texto" headingId="bonos">
9
9
  * <ul><li>…</li></ul>
10
10
  * </cc-info-item>
11
11
  * ```
12
+ *
13
+ * Tokens: --cc-info-title-color, --cc-info-text-color, --cc-info-text-align, --cc-info-max-width.
12
14
  */
13
15
  export declare class InfoItemComponent {
14
16
  readonly title: import("@angular/core").InputSignal<string>;
15
17
  readonly text: import("@angular/core").InputSignal<string>;
18
+ /** `accent` colors the title with the accent text color, `neutral` with the heading color. */
19
+ readonly tone: import("@angular/core").InputSignal<"accent" | "neutral">;
20
+ readonly size: import("@angular/core").InputSignal<"sm" | "md">;
21
+ readonly headingLevel: import("@angular/core").InputSignal<"h2" | "h3">;
22
+ /** Id of the heading, for anchors from a table of contents. */
23
+ readonly headingId: import("@angular/core").InputSignal<string | null>;
16
24
  static ɵfac: i0.ɵɵFactoryDeclaration<InfoItemComponent, never>;
17
- static ɵcmp: i0.ɵɵComponentDeclaration<InfoItemComponent, "cc-info-item", never, { "title": { "alias": "title"; "required": true; "isSignal": true; }; "text": { "alias": "text"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
25
+ static ɵcmp: i0.ɵɵComponentDeclaration<InfoItemComponent, "cc-info-item", never, { "title": { "alias": "title"; "required": true; "isSignal": true; }; "text": { "alias": "text"; "required": false; "isSignal": true; }; "tone": { "alias": "tone"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "headingLevel": { "alias": "headingLevel"; "required": false; "isSignal": true; }; "headingId": { "alias": "headingId"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
18
26
  }
@@ -2,11 +2,15 @@ import { GalleryImage } from '../../models/ui.models';
2
2
  import * as i0 from "@angular/core";
3
3
  /**
4
4
  * Full-screen image viewer. It is open while `index` is not null.
5
- * Arrow keys move between images and Escape closes it.
5
+ * Arrow keys and swipe move between images; Escape and the backdrop close it.
6
+ * Focus moves into the viewer on open, stays inside while open, and returns to the
7
+ * element that opened it on close.
6
8
  *
7
9
  * ```html
8
10
  * <cc-lightbox [images]="images" [(index)]="openIndex" />
9
11
  * ```
12
+ *
13
+ * Tokens: --cc-lightbox-backdrop, --cc-lightbox-control-bg, --cc-lightbox-color.
10
14
  */
11
15
  export declare class LightboxComponent {
12
16
  readonly images: import("@angular/core").InputSignal<GalleryImage[]>;
@@ -15,12 +19,23 @@ export declare class LightboxComponent {
15
19
  readonly closeLabel: import("@angular/core").InputSignal<string>;
16
20
  readonly prevLabel: import("@angular/core").InputSignal<string>;
17
21
  readonly nextLabel: import("@angular/core").InputSignal<string>;
22
+ /** Text of the position counter, for example "3 de 18". */
23
+ readonly counterSeparator: import("@angular/core").InputSignal<string>;
18
24
  protected readonly current: import("@angular/core").Signal<GalleryImage | null>;
19
- private document;
25
+ /** Changes only on open/close, not when moving between images. */
26
+ private readonly isOpen;
27
+ private readonly document;
28
+ private readonly dialog;
29
+ private readonly closeButton;
30
+ private touchStartX;
20
31
  constructor();
21
32
  close(): void;
22
33
  next(): void;
23
34
  prev(): void;
35
+ /** Keeps Tab and Shift+Tab inside the viewer. */
36
+ protected trapFocus(event: KeyboardEvent): void;
37
+ protected onTouchStart(event: TouchEvent): void;
38
+ protected onTouchEnd(event: TouchEvent): void;
24
39
  static ɵfac: i0.ɵɵFactoryDeclaration<LightboxComponent, never>;
25
- static ɵcmp: i0.ɵɵComponentDeclaration<LightboxComponent, "cc-lightbox", never, { "images": { "alias": "images"; "required": true; "isSignal": true; }; "index": { "alias": "index"; "required": false; "isSignal": true; }; "closeLabel": { "alias": "closeLabel"; "required": false; "isSignal": true; }; "prevLabel": { "alias": "prevLabel"; "required": false; "isSignal": true; }; "nextLabel": { "alias": "nextLabel"; "required": false; "isSignal": true; }; }, { "index": "indexChange"; }, never, never, true, never>;
40
+ static ɵcmp: i0.ɵɵComponentDeclaration<LightboxComponent, "cc-lightbox", never, { "images": { "alias": "images"; "required": true; "isSignal": true; }; "index": { "alias": "index"; "required": false; "isSignal": true; }; "closeLabel": { "alias": "closeLabel"; "required": false; "isSignal": true; }; "prevLabel": { "alias": "prevLabel"; "required": false; "isSignal": true; }; "nextLabel": { "alias": "nextLabel"; "required": false; "isSignal": true; }; "counterSeparator": { "alias": "counterSeparator"; "required": false; "isSignal": true; }; }, { "index": "indexChange"; }, never, never, true, never>;
26
41
  }
@@ -0,0 +1,17 @@
1
+ import * as i0 from "@angular/core";
2
+ /**
3
+ * Highlighted note for important conditions, warnings or tips. The content is projected.
4
+ *
5
+ * ```html
6
+ * <cc-notice tone="warning" title="Importante">Su bono tiene vigencia de 30 días.</cc-notice>
7
+ * ```
8
+ *
9
+ * Tokens: --cc-notice-bg, --cc-notice-border, --cc-notice-color.
10
+ */
11
+ export declare class NoticeComponent {
12
+ /** `info` uses the brand colors, `warning` a red accent, `success` the primary color. */
13
+ readonly tone: import("@angular/core").InputSignal<"info" | "warning" | "success">;
14
+ readonly title: import("@angular/core").InputSignal<string>;
15
+ static ɵfac: i0.ɵɵFactoryDeclaration<NoticeComponent, never>;
16
+ static ɵcmp: i0.ɵɵComponentDeclaration<NoticeComponent, "cc-notice", never, { "tone": { "alias": "tone"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
17
+ }
@@ -6,18 +6,24 @@ import * as i0 from "@angular/core";
6
6
  *
7
7
  * ```html
8
8
  * <cc-page-banner title="Reserva tu plan - Pareja" />
9
- * <cc-page-banner [title]="plan.name" backLink="/planes" iconSrc="assets/icons/hoja.png" variant="plain" />
9
+ * <cc-page-banner [title]="plan.name" backLink="/planes" iconSrc="assets/icons/hoja.png" size="lg" italic />
10
10
  * ```
11
+ *
12
+ * Tokens: --cc-banner-bg, --cc-banner-color, --cc-banner-padding.
11
13
  */
12
14
  export declare class PageBannerComponent {
13
15
  readonly title: import("@angular/core").InputSignal<string>;
14
- /** `band` uses the surface color; `plain` uses the page background. */
16
+ readonly subtitle: import("@angular/core").InputSignal<string>;
17
+ /** `band` draws a surface-colored strip; `plain` has no background. */
15
18
  readonly variant: import("@angular/core").InputSignal<"band" | "plain">;
19
+ readonly size: import("@angular/core").InputSignal<"sm" | "md" | "lg">;
20
+ readonly italic: import("@angular/core").InputSignalWithTransform<boolean, unknown>;
21
+ readonly align: import("@angular/core").InputSignal<"center" | "start">;
16
22
  readonly iconSrc: import("@angular/core").InputSignal<string>;
17
23
  /** Shows a back arrow that links here. */
18
24
  readonly backLink: import("@angular/core").InputSignal<LinkTarget | null>;
19
25
  readonly backIconSrc: import("@angular/core").InputSignal<string>;
20
26
  readonly backLabel: import("@angular/core").InputSignal<string>;
21
27
  static ɵfac: i0.ɵɵFactoryDeclaration<PageBannerComponent, never>;
22
- static ɵcmp: i0.ɵɵComponentDeclaration<PageBannerComponent, "cc-page-banner", never, { "title": { "alias": "title"; "required": true; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "iconSrc": { "alias": "iconSrc"; "required": false; "isSignal": true; }; "backLink": { "alias": "backLink"; "required": false; "isSignal": true; }; "backIconSrc": { "alias": "backIconSrc"; "required": false; "isSignal": true; }; "backLabel": { "alias": "backLabel"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
28
+ static ɵcmp: i0.ɵɵComponentDeclaration<PageBannerComponent, "cc-page-banner", never, { "title": { "alias": "title"; "required": true; "isSignal": true; }; "subtitle": { "alias": "subtitle"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "italic": { "alias": "italic"; "required": false; "isSignal": true; }; "align": { "alias": "align"; "required": false; "isSignal": true; }; "iconSrc": { "alias": "iconSrc"; "required": false; "isSignal": true; }; "backLink": { "alias": "backLink"; "required": false; "isSignal": true; }; "backIconSrc": { "alias": "backIconSrc"; "required": false; "isSignal": true; }; "backLabel": { "alias": "backLabel"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
23
29
  }
@@ -1,23 +1,46 @@
1
1
  import { LinkTarget } from '../../models/ui.models';
2
+ import { PriceFormat } from '../../utils/text.utils';
3
+ import { ButtonVariant } from '../button/button.component';
2
4
  import * as i0 from "@angular/core";
3
5
  /**
4
- * Plan card: image, name and a button.
5
- * With `link` the button navigates with the router; without it, it emits `ctaClick`.
6
+ * Plan card: photo, name, details, price and a call to action.
7
+ * With `link`, the whole card is clickable and navigates with the router; without it,
8
+ * the button emits `ctaClick`. With `skeleton`, it renders a loading placeholder.
6
9
  *
7
10
  * ```html
8
- * <cc-plan-card [name]="plan.name" [imageSrc]="plan.imgPath" [link]="['/planes', slug]" />
11
+ * <cc-plan-card [name]="plan.name" [imageSrc]="plan.imgPath" [price]="plan.price"
12
+ * meta="2 h · 2 personas" [link]="['/planes', slug]" />
9
13
  * ```
14
+ *
15
+ * Tokens: --cc-plan-card-bg, --cc-plan-card-color, --cc-plan-card-border,
16
+ * --cc-plan-card-radius, --cc-plan-card-shadow, --cc-plan-card-image-ratio,
17
+ * --cc-plan-card-price-color.
10
18
  */
11
19
  export declare class PlanCardComponent {
12
20
  readonly name: import("@angular/core").InputSignal<string>;
13
21
  readonly imageSrc: import("@angular/core").InputSignal<string>;
14
22
  /** Defaults to `name`. */
15
23
  readonly imageAlt: import("@angular/core").InputSignal<string>;
16
- /** Optional line under the name, for example the formatted price. */
17
- readonly subtitle: import("@angular/core").InputSignal<string>;
24
+ /** Short line under the name, for example "2 h 20 min · 2 personas". */
25
+ readonly meta: import("@angular/core").InputSignal<string>;
26
+ /** Price to show. Leave it null to hide it. */
27
+ readonly price: import("@angular/core").InputSignal<number | null>;
28
+ /** Text before the price, for example "Desde". */
29
+ readonly priceLabel: import("@angular/core").InputSignal<string>;
30
+ readonly priceFormat: import("@angular/core").InputSignal<PriceFormat>;
18
31
  readonly link: import("@angular/core").InputSignal<LinkTarget | null>;
32
+ /** Query params of `link`, for example `{ categoria: 'pareja' }`. */
33
+ readonly queryParams: import("@angular/core").InputSignal<Record<string, string> | null>;
19
34
  readonly ctaLabel: import("@angular/core").InputSignal<string>;
35
+ readonly ctaVariant: import("@angular/core").InputSignal<ButtonVariant>;
36
+ /** `filled` uses the surface color, `outlined` a border on white, `plain` no box. */
37
+ readonly appearance: import("@angular/core").InputSignal<"plain" | "filled" | "outlined">;
38
+ /** Heading level of the name inside the page outline. */
39
+ readonly headingLevel: import("@angular/core").InputSignal<"h2" | "h3">;
40
+ /** Renders a loading placeholder instead of the content. */
41
+ readonly skeleton: import("@angular/core").InputSignal<boolean>;
20
42
  readonly ctaClick: import("@angular/core").OutputEmitterRef<void>;
43
+ protected readonly formattedPrice: import("@angular/core").Signal<string>;
21
44
  static ɵfac: i0.ɵɵFactoryDeclaration<PlanCardComponent, never>;
22
- static ɵcmp: i0.ɵɵComponentDeclaration<PlanCardComponent, "cc-plan-card", never, { "name": { "alias": "name"; "required": true; "isSignal": true; }; "imageSrc": { "alias": "imageSrc"; "required": true; "isSignal": true; }; "imageAlt": { "alias": "imageAlt"; "required": false; "isSignal": true; }; "subtitle": { "alias": "subtitle"; "required": false; "isSignal": true; }; "link": { "alias": "link"; "required": false; "isSignal": true; }; "ctaLabel": { "alias": "ctaLabel"; "required": false; "isSignal": true; }; }, { "ctaClick": "ctaClick"; }, never, never, true, never>;
45
+ static ɵcmp: i0.ɵɵComponentDeclaration<PlanCardComponent, "cc-plan-card", never, { "name": { "alias": "name"; "required": false; "isSignal": true; }; "imageSrc": { "alias": "imageSrc"; "required": false; "isSignal": true; }; "imageAlt": { "alias": "imageAlt"; "required": false; "isSignal": true; }; "meta": { "alias": "meta"; "required": false; "isSignal": true; }; "price": { "alias": "price"; "required": false; "isSignal": true; }; "priceLabel": { "alias": "priceLabel"; "required": false; "isSignal": true; }; "priceFormat": { "alias": "priceFormat"; "required": false; "isSignal": true; }; "link": { "alias": "link"; "required": false; "isSignal": true; }; "queryParams": { "alias": "queryParams"; "required": false; "isSignal": true; }; "ctaLabel": { "alias": "ctaLabel"; "required": false; "isSignal": true; }; "ctaVariant": { "alias": "ctaVariant"; "required": false; "isSignal": true; }; "appearance": { "alias": "appearance"; "required": false; "isSignal": true; }; "headingLevel": { "alias": "headingLevel"; "required": false; "isSignal": true; }; "skeleton": { "alias": "skeleton"; "required": false; "isSignal": true; }; }, { "ctaClick": "ctaClick"; }, never, never, true, never>;
23
46
  }
@@ -1,6 +1,8 @@
1
1
  import { TemplateRef } from '@angular/core';
2
2
  import { AdditionalService, CategoryOption, Plan, PlanCategory, PlanFilter, PriceRange } from '../../models/plan.models';
3
3
  import { LinkTarget } from '../../models/ui.models';
4
+ import { PriceFormat } from '../../utils/text.utils';
5
+ import { ButtonVariant } from '../button/button.component';
4
6
  import * as i0 from "@angular/core";
5
7
  /** Context of a custom card template: `<ng-template ccPlanCard let-plan let-link="link">`. */
6
8
  export interface PlanCardContext {
@@ -13,25 +15,30 @@ export declare class PlanCardTemplateDirective {
13
15
  static ɵfac: i0.ɵɵFactoryDeclaration<PlanCardTemplateDirective, never>;
14
16
  static ɵdir: i0.ɵɵDirectiveDeclaration<PlanCardTemplateDirective, "ng-template[ccPlanCard]", never, {}, {}, never, never, true, never>;
15
17
  }
18
+ /** Default detail line of a card: "2 horas 20 minutos · 2 personas". */
19
+ export declare function defaultPlanMeta(plan: Plan): string;
16
20
  /**
17
21
  * Full plan list page: title, name search, category menu, filter and card grid.
18
22
  * It only needs the plans; the rest has Spanish defaults you can override.
19
23
  *
20
- * Selecting a category shows its plans. Searching by name or applying the filter
21
- * looks across every category, as the sites already did.
24
+ * - `plans` set to `null` means "loading" and shows placeholder cards.
25
+ * - `[(category)]` is two-way, so a page can keep it in the URL (`?categoria=pareja`).
26
+ * - Selecting a category shows its plans. Searching by name or filtering looks across
27
+ * every category; clearing the search goes back to the selected category.
22
28
  *
23
29
  * ```html
24
- * <cc-plan-catalog
25
- * [plans]="plans()"
26
- * [services]="services()"
27
- * [priceRanges]="ranges()"
28
- * imageAltSuffix=" - Laurel Spa Medellín"
29
- * />
30
+ * <cc-plan-catalog [plans]="plans()" [services]="services()" [priceRanges]="ranges()" [(category)]="category" />
30
31
  * ```
32
+ *
33
+ * Tokens: --cc-catalog-panel-bg, --cc-catalog-sidebar-width, --cc-catalog-column-min,
34
+ * --cc-catalog-gap.
31
35
  */
32
36
  export declare class PlanCatalogComponent {
33
- readonly plans: import("@angular/core").InputSignal<Plan[]>;
37
+ /** The plans, or `null` while they load. */
38
+ readonly plans: import("@angular/core").InputSignal<Plan[] | null>;
34
39
  readonly categories: import("@angular/core").InputSignal<CategoryOption<number>[]>;
40
+ /** Selected category. Use `[(category)]` to bind it; `null` means `initialCategory`. */
41
+ readonly category: import("@angular/core").ModelSignal<number | null>;
35
42
  readonly initialCategory: import("@angular/core").InputSignal<number>;
36
43
  /** Shows how many plans each category has, counted from `plans`. */
37
44
  readonly showCounts: import("@angular/core").InputSignal<boolean>;
@@ -40,30 +47,43 @@ export declare class PlanCatalogComponent {
40
47
  readonly priceRanges: import("@angular/core").InputSignal<PriceRange[]>;
41
48
  /** Where each card links to. Defaults to `/planes/<slug>`. */
42
49
  readonly detailsLink: import("@angular/core").InputSignal<(plan: Plan) => LinkTarget>;
50
+ readonly showPrice: import("@angular/core").InputSignal<boolean>;
51
+ readonly priceLabel: import("@angular/core").InputSignal<string>;
52
+ readonly priceFormat: import("@angular/core").InputSignal<PriceFormat>;
53
+ /** Detail line of each card. Defaults to duration and number of people. */
54
+ readonly planMeta: import("@angular/core").InputSignal<(plan: Plan) => string>;
55
+ readonly cardAppearance: import("@angular/core").InputSignal<"plain" | "filled" | "outlined">;
56
+ readonly ctaLabel: import("@angular/core").InputSignal<string>;
57
+ readonly ctaVariant: import("@angular/core").InputSignal<ButtonVariant>;
58
+ /** Added after the plan name in each image's alt text. */
59
+ readonly imageAltSuffix: import("@angular/core").InputSignal<string>;
60
+ readonly skeletonCount: import("@angular/core").InputSignal<number>;
43
61
  readonly showBanner: import("@angular/core").InputSignal<boolean>;
44
62
  readonly showSearch: import("@angular/core").InputSignal<boolean>;
45
63
  readonly titlePrefix: import("@angular/core").InputSignal<string>;
46
64
  readonly resultsTitle: import("@angular/core").InputSignal<string>;
47
65
  readonly categoriesTitle: import("@angular/core").InputSignal<string>;
66
+ readonly categoriesLayout: import("@angular/core").InputSignal<"auto" | "list" | "chips">;
48
67
  readonly searchPlaceholder: import("@angular/core").InputSignal<string>;
49
68
  readonly searchIconSrc: import("@angular/core").InputSignal<string>;
50
69
  readonly filterTitle: import("@angular/core").InputSignal<string>;
51
70
  readonly filterIconSrc: import("@angular/core").InputSignal<string>;
52
- readonly ctaLabel: import("@angular/core").InputSignal<string>;
53
- /** Added after the plan name in each image's alt text. */
54
- readonly imageAltSuffix: import("@angular/core").InputSignal<string>;
55
71
  readonly emptyMessage: import("@angular/core").InputSignal<string>;
56
- readonly categoryChange: import("@angular/core").OutputEmitterRef<number>;
72
+ readonly emptyActionLabel: import("@angular/core").InputSignal<string>;
73
+ /** Optional contact link shown in the empty state, for example WhatsApp. */
74
+ readonly contactHref: import("@angular/core").InputSignal<string>;
75
+ readonly contactLabel: import("@angular/core").InputSignal<string>;
57
76
  protected readonly cardTemplate: import("@angular/core").Signal<TemplateRef<any> | undefined>;
58
- protected readonly category: import("@angular/core").WritableSignal<number | null>;
77
+ private readonly filterForm;
59
78
  protected readonly query: import("@angular/core").WritableSignal<string>;
60
79
  protected readonly filter: import("@angular/core").WritableSignal<PlanFilter | null>;
61
80
  protected readonly filterOpen: import("@angular/core").WritableSignal<boolean>;
62
81
  /** Set when the user searched or filtered; results then cover every category. */
63
82
  protected readonly searching: import("@angular/core").WritableSignal<boolean>;
64
- /** Category that was open before searching, to return to it when the search is cleared. */
65
- private lastCategory;
66
- constructor();
83
+ protected readonly loading: import("@angular/core").Signal<boolean>;
84
+ protected readonly skeletons: import("@angular/core").Signal<number[]>;
85
+ /** Category shown in the menu: none while searching. */
86
+ protected readonly activeCategory: import("@angular/core").Signal<number | null>;
67
87
  protected readonly categoryOptions: import("@angular/core").Signal<CategoryOption<number>[]>;
68
88
  protected readonly hasFilter: import("@angular/core").Signal<boolean>;
69
89
  protected readonly visiblePlans: import("@angular/core").Signal<Plan[]>;
@@ -71,8 +91,12 @@ export declare class PlanCatalogComponent {
71
91
  protected selectCategory(value: PlanCategory | number | null): void;
72
92
  protected onSearch(query: string): void;
73
93
  protected onFilter(filter: PlanFilter): void;
94
+ /** Clears search and filter and shows the selected category again. */
95
+ protected showAll(): void;
74
96
  protected linkFor(plan: Plan): LinkTarget;
75
- private startSearch;
97
+ protected metaFor(plan: Plan): string;
98
+ private clearSearch;
99
+ private updateSearching;
76
100
  static ɵfac: i0.ɵɵFactoryDeclaration<PlanCatalogComponent, never>;
77
- static ɵcmp: i0.ɵɵComponentDeclaration<PlanCatalogComponent, "cc-plan-catalog", never, { "plans": { "alias": "plans"; "required": true; "isSignal": true; }; "categories": { "alias": "categories"; "required": false; "isSignal": true; }; "initialCategory": { "alias": "initialCategory"; "required": false; "isSignal": true; }; "showCounts": { "alias": "showCounts"; "required": false; "isSignal": true; }; "services": { "alias": "services"; "required": false; "isSignal": true; }; "priceRanges": { "alias": "priceRanges"; "required": false; "isSignal": true; }; "detailsLink": { "alias": "detailsLink"; "required": false; "isSignal": true; }; "showBanner": { "alias": "showBanner"; "required": false; "isSignal": true; }; "showSearch": { "alias": "showSearch"; "required": false; "isSignal": true; }; "titlePrefix": { "alias": "titlePrefix"; "required": false; "isSignal": true; }; "resultsTitle": { "alias": "resultsTitle"; "required": false; "isSignal": true; }; "categoriesTitle": { "alias": "categoriesTitle"; "required": false; "isSignal": true; }; "searchPlaceholder": { "alias": "searchPlaceholder"; "required": false; "isSignal": true; }; "searchIconSrc": { "alias": "searchIconSrc"; "required": false; "isSignal": true; }; "filterTitle": { "alias": "filterTitle"; "required": false; "isSignal": true; }; "filterIconSrc": { "alias": "filterIconSrc"; "required": false; "isSignal": true; }; "ctaLabel": { "alias": "ctaLabel"; "required": false; "isSignal": true; }; "imageAltSuffix": { "alias": "imageAltSuffix"; "required": false; "isSignal": true; }; "emptyMessage": { "alias": "emptyMessage"; "required": false; "isSignal": true; }; }, { "categoryChange": "categoryChange"; }, ["cardTemplate"], never, true, never>;
101
+ static ɵcmp: i0.ɵɵComponentDeclaration<PlanCatalogComponent, "cc-plan-catalog", never, { "plans": { "alias": "plans"; "required": true; "isSignal": true; }; "categories": { "alias": "categories"; "required": false; "isSignal": true; }; "category": { "alias": "category"; "required": false; "isSignal": true; }; "initialCategory": { "alias": "initialCategory"; "required": false; "isSignal": true; }; "showCounts": { "alias": "showCounts"; "required": false; "isSignal": true; }; "services": { "alias": "services"; "required": false; "isSignal": true; }; "priceRanges": { "alias": "priceRanges"; "required": false; "isSignal": true; }; "detailsLink": { "alias": "detailsLink"; "required": false; "isSignal": true; }; "showPrice": { "alias": "showPrice"; "required": false; "isSignal": true; }; "priceLabel": { "alias": "priceLabel"; "required": false; "isSignal": true; }; "priceFormat": { "alias": "priceFormat"; "required": false; "isSignal": true; }; "planMeta": { "alias": "planMeta"; "required": false; "isSignal": true; }; "cardAppearance": { "alias": "cardAppearance"; "required": false; "isSignal": true; }; "ctaLabel": { "alias": "ctaLabel"; "required": false; "isSignal": true; }; "ctaVariant": { "alias": "ctaVariant"; "required": false; "isSignal": true; }; "imageAltSuffix": { "alias": "imageAltSuffix"; "required": false; "isSignal": true; }; "skeletonCount": { "alias": "skeletonCount"; "required": false; "isSignal": true; }; "showBanner": { "alias": "showBanner"; "required": false; "isSignal": true; }; "showSearch": { "alias": "showSearch"; "required": false; "isSignal": true; }; "titlePrefix": { "alias": "titlePrefix"; "required": false; "isSignal": true; }; "resultsTitle": { "alias": "resultsTitle"; "required": false; "isSignal": true; }; "categoriesTitle": { "alias": "categoriesTitle"; "required": false; "isSignal": true; }; "categoriesLayout": { "alias": "categoriesLayout"; "required": false; "isSignal": true; }; "searchPlaceholder": { "alias": "searchPlaceholder"; "required": false; "isSignal": true; }; "searchIconSrc": { "alias": "searchIconSrc"; "required": false; "isSignal": true; }; "filterTitle": { "alias": "filterTitle"; "required": false; "isSignal": true; }; "filterIconSrc": { "alias": "filterIconSrc"; "required": false; "isSignal": true; }; "emptyMessage": { "alias": "emptyMessage"; "required": false; "isSignal": true; }; "emptyActionLabel": { "alias": "emptyActionLabel"; "required": false; "isSignal": true; }; "contactHref": { "alias": "contactHref"; "required": false; "isSignal": true; }; "contactLabel": { "alias": "contactLabel"; "required": false; "isSignal": true; }; }, { "category": "categoryChange"; }, ["cardTemplate"], never, true, never>;
78
102
  }
@@ -1,20 +1,32 @@
1
1
  import { Plan } from '../../models/plan.models';
2
2
  import { LinkTarget } from '../../models/ui.models';
3
+ import { PriceFormat } from '../../utils/text.utils';
4
+ import { ButtonVariant } from '../button/button.component';
3
5
  import * as i0 from "@angular/core";
4
6
  /**
5
- * Plan detail view: title with a back link, photo, price, description, duration,
6
- * number of people, included services, and a gift box with the booking button.
7
- * It only renders; the page loads the plan and handles SEO.
7
+ * Plan detail view: title with a back link, photo, price with an early booking button,
8
+ * description, duration, number of people, included services and a gift box.
9
+ * On screens below 1024px a bar with the price and the booking button stays fixed at
10
+ * the bottom while the plan is on screen. It only renders; the page loads the plan and
11
+ * handles SEO.
8
12
  *
9
13
  * ```html
10
- * <cc-plan-details [plan]="plan" [bookingUrl]="whatsappLink" imageAltSuffix=" en Laurel Spa Medellín" />
14
+ * <cc-plan-details [plan]="plan" [bookingUrl]="bookingFor" imageAltSuffix=" en Laurel Spa" />
11
15
  * ```
16
+ *
17
+ * Tokens: --cc-details-panel-bg, --cc-details-price-bg, --cc-details-price-color,
18
+ * --cc-details-gift-border, --cc-details-bar-bg, --cc-details-image-ratio.
12
19
  */
13
20
  export declare class PlanDetailsComponent {
14
21
  readonly plan: import("@angular/core").InputSignal<Plan>;
15
- /** Link of the booking button, usually WhatsApp. */
16
- readonly bookingUrl: import("@angular/core").InputSignal<string>;
22
+ /** Booking link, usually WhatsApp. A function receives the plan, to mention it in the message. */
23
+ readonly bookingUrl: import("@angular/core").InputSignal<string | ((plan: Plan) => string)>;
17
24
  readonly bookingLabel: import("@angular/core").InputSignal<string>;
25
+ /** Shorter label for the bottom bar on small screens. */
26
+ readonly barBookingLabel: import("@angular/core").InputSignal<string>;
27
+ readonly bookingVariant: import("@angular/core").InputSignal<ButtonVariant>;
28
+ /** Keeps a bar with price and booking button at the bottom on small screens. */
29
+ readonly stickyBar: import("@angular/core").InputSignal<boolean>;
18
30
  readonly showBanner: import("@angular/core").InputSignal<boolean>;
19
31
  readonly backLink: import("@angular/core").InputSignal<LinkTarget | null>;
20
32
  readonly backLabel: import("@angular/core").InputSignal<string>;
@@ -22,13 +34,14 @@ export declare class PlanDetailsComponent {
22
34
  readonly titleIconSrc: import("@angular/core").InputSignal<string>;
23
35
  /** Added after the plan name in the photo's alt text. */
24
36
  readonly imageAltSuffix: import("@angular/core").InputSignal<string>;
25
- /** Arguments of Angular's `currency` pipe. */
26
- readonly currencyCode: import("@angular/core").InputSignal<string>;
27
- readonly currencyDisplay: import("@angular/core").InputSignal<string>;
28
- readonly currencyDigits: import("@angular/core").InputSignal<string>;
37
+ readonly priceFormat: import("@angular/core").InputSignal<PriceFormat>;
38
+ /** Small text under the price. Defaults to "Precio por plan". */
39
+ readonly priceNote: import("@angular/core").InputSignal<string>;
29
40
  readonly durationIconSrc: import("@angular/core").InputSignal<string>;
30
41
  readonly personIconSrc: import("@angular/core").InputSignal<string>;
31
42
  readonly peopleIconSrc: import("@angular/core").InputSignal<string>;
43
+ readonly durationLabel: import("@angular/core").InputSignal<string>;
44
+ readonly peopleCaption: import("@angular/core").InputSignal<string>;
32
45
  readonly personLabel: import("@angular/core").InputSignal<string>;
33
46
  readonly peopleLabel: import("@angular/core").InputSignal<string>;
34
47
  readonly includesLabel: import("@angular/core").InputSignal<string>;
@@ -39,6 +52,8 @@ export declare class PlanDetailsComponent {
39
52
  readonly perksImageAlt: import("@angular/core").InputSignal<string>;
40
53
  protected readonly people: import("@angular/core").Signal<number>;
41
54
  protected readonly services: import("@angular/core").Signal<import("@c-code/c-code-fw/ui").AdditionalService[]>;
55
+ protected readonly price: import("@angular/core").Signal<string>;
56
+ protected readonly booking: import("@angular/core").Signal<string>;
42
57
  static ɵfac: i0.ɵɵFactoryDeclaration<PlanDetailsComponent, never>;
43
- static ɵcmp: i0.ɵɵComponentDeclaration<PlanDetailsComponent, "cc-plan-details", never, { "plan": { "alias": "plan"; "required": true; "isSignal": true; }; "bookingUrl": { "alias": "bookingUrl"; "required": true; "isSignal": true; }; "bookingLabel": { "alias": "bookingLabel"; "required": false; "isSignal": true; }; "showBanner": { "alias": "showBanner"; "required": false; "isSignal": true; }; "backLink": { "alias": "backLink"; "required": false; "isSignal": true; }; "backLabel": { "alias": "backLabel"; "required": false; "isSignal": true; }; "backIconSrc": { "alias": "backIconSrc"; "required": false; "isSignal": true; }; "titleIconSrc": { "alias": "titleIconSrc"; "required": false; "isSignal": true; }; "imageAltSuffix": { "alias": "imageAltSuffix"; "required": false; "isSignal": true; }; "currencyCode": { "alias": "currencyCode"; "required": false; "isSignal": true; }; "currencyDisplay": { "alias": "currencyDisplay"; "required": false; "isSignal": true; }; "currencyDigits": { "alias": "currencyDigits"; "required": false; "isSignal": true; }; "durationIconSrc": { "alias": "durationIconSrc"; "required": false; "isSignal": true; }; "personIconSrc": { "alias": "personIconSrc"; "required": false; "isSignal": true; }; "peopleIconSrc": { "alias": "peopleIconSrc"; "required": false; "isSignal": true; }; "personLabel": { "alias": "personLabel"; "required": false; "isSignal": true; }; "peopleLabel": { "alias": "peopleLabel"; "required": false; "isSignal": true; }; "includesLabel": { "alias": "includesLabel"; "required": false; "isSignal": true; }; "perksTitle": { "alias": "perksTitle"; "required": false; "isSignal": true; }; "perks": { "alias": "perks"; "required": false; "isSignal": true; }; "perksImageSrc": { "alias": "perksImageSrc"; "required": false; "isSignal": true; }; "perksImageAlt": { "alias": "perksImageAlt"; "required": false; "isSignal": true; }; }, {}, never, ["[ccDetailsMedia]", "*"], true, never>;
58
+ static ɵcmp: i0.ɵɵComponentDeclaration<PlanDetailsComponent, "cc-plan-details", never, { "plan": { "alias": "plan"; "required": true; "isSignal": true; }; "bookingUrl": { "alias": "bookingUrl"; "required": true; "isSignal": true; }; "bookingLabel": { "alias": "bookingLabel"; "required": false; "isSignal": true; }; "barBookingLabel": { "alias": "barBookingLabel"; "required": false; "isSignal": true; }; "bookingVariant": { "alias": "bookingVariant"; "required": false; "isSignal": true; }; "stickyBar": { "alias": "stickyBar"; "required": false; "isSignal": true; }; "showBanner": { "alias": "showBanner"; "required": false; "isSignal": true; }; "backLink": { "alias": "backLink"; "required": false; "isSignal": true; }; "backLabel": { "alias": "backLabel"; "required": false; "isSignal": true; }; "backIconSrc": { "alias": "backIconSrc"; "required": false; "isSignal": true; }; "titleIconSrc": { "alias": "titleIconSrc"; "required": false; "isSignal": true; }; "imageAltSuffix": { "alias": "imageAltSuffix"; "required": false; "isSignal": true; }; "priceFormat": { "alias": "priceFormat"; "required": false; "isSignal": true; }; "priceNote": { "alias": "priceNote"; "required": false; "isSignal": true; }; "durationIconSrc": { "alias": "durationIconSrc"; "required": false; "isSignal": true; }; "personIconSrc": { "alias": "personIconSrc"; "required": false; "isSignal": true; }; "peopleIconSrc": { "alias": "peopleIconSrc"; "required": false; "isSignal": true; }; "durationLabel": { "alias": "durationLabel"; "required": false; "isSignal": true; }; "peopleCaption": { "alias": "peopleCaption"; "required": false; "isSignal": true; }; "personLabel": { "alias": "personLabel"; "required": false; "isSignal": true; }; "peopleLabel": { "alias": "peopleLabel"; "required": false; "isSignal": true; }; "includesLabel": { "alias": "includesLabel"; "required": false; "isSignal": true; }; "perksTitle": { "alias": "perksTitle"; "required": false; "isSignal": true; }; "perks": { "alias": "perks"; "required": false; "isSignal": true; }; "perksImageSrc": { "alias": "perksImageSrc"; "required": false; "isSignal": true; }; "perksImageAlt": { "alias": "perksImageAlt"; "required": false; "isSignal": true; }; }, {}, never, ["[ccDetailsMedia]", "*"], true, never>;
44
59
  }
@@ -4,25 +4,35 @@ import * as i0 from "@angular/core";
4
4
  * Filter by included service and price range. The options come in as inputs,
5
5
  * so the form does not load data by itself.
6
6
  *
7
+ * `mode`:
8
+ * - `instant` (default): emits `filterChange` as soon as an option changes.
9
+ * - `submit`: emits when the user presses the submit button.
10
+ *
7
11
  * ```html
8
12
  * <cc-plan-filter-form [services]="services" [priceRanges]="ranges" (filterChange)="apply($event)" />
9
13
  * ```
14
+ *
15
+ * Tokens: --cc-filter-field-bg, --cc-filter-field-border, --cc-filter-label-color.
10
16
  */
11
17
  export declare class PlanFilterFormComponent {
12
18
  readonly services: import("@angular/core").InputSignal<AdditionalService[]>;
13
19
  readonly priceRanges: import("@angular/core").InputSignal<PriceRange[]>;
20
+ readonly mode: import("@angular/core").InputSignal<"instant" | "submit">;
14
21
  readonly serviceLabel: import("@angular/core").InputSignal<string>;
15
22
  readonly priceLabel: import("@angular/core").InputSignal<string>;
16
23
  readonly anyLabel: import("@angular/core").InputSignal<string>;
17
24
  readonly submitLabel: import("@angular/core").InputSignal<string>;
18
25
  readonly resetLabel: import("@angular/core").InputSignal<string>;
19
- /** Emits on submit, and with an empty filter on reset. */
26
+ /** Emits the current filter, and an empty filter on reset. */
20
27
  readonly filterChange: import("@angular/core").OutputEmitterRef<PlanFilter>;
21
- protected serviceIndex: import("@angular/core").WritableSignal<number>;
22
- protected rangeIndex: import("@angular/core").WritableSignal<number>;
23
- protected toIndex(event: Event): number;
28
+ protected readonly serviceIndex: import("@angular/core").WritableSignal<number>;
29
+ protected readonly rangeIndex: import("@angular/core").WritableSignal<number>;
30
+ protected readonly hasSelection: import("@angular/core").Signal<boolean>;
31
+ protected onServiceChange(event: Event): void;
32
+ protected onRangeChange(event: Event): void;
24
33
  submit(): void;
25
34
  reset(): void;
35
+ private toIndex;
26
36
  static ɵfac: i0.ɵɵFactoryDeclaration<PlanFilterFormComponent, never>;
27
- static ɵcmp: i0.ɵɵComponentDeclaration<PlanFilterFormComponent, "cc-plan-filter-form", never, { "services": { "alias": "services"; "required": false; "isSignal": true; }; "priceRanges": { "alias": "priceRanges"; "required": false; "isSignal": true; }; "serviceLabel": { "alias": "serviceLabel"; "required": false; "isSignal": true; }; "priceLabel": { "alias": "priceLabel"; "required": false; "isSignal": true; }; "anyLabel": { "alias": "anyLabel"; "required": false; "isSignal": true; }; "submitLabel": { "alias": "submitLabel"; "required": false; "isSignal": true; }; "resetLabel": { "alias": "resetLabel"; "required": false; "isSignal": true; }; }, { "filterChange": "filterChange"; }, never, never, true, never>;
37
+ static ɵcmp: i0.ɵɵComponentDeclaration<PlanFilterFormComponent, "cc-plan-filter-form", never, { "services": { "alias": "services"; "required": false; "isSignal": true; }; "priceRanges": { "alias": "priceRanges"; "required": false; "isSignal": true; }; "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "serviceLabel": { "alias": "serviceLabel"; "required": false; "isSignal": true; }; "priceLabel": { "alias": "priceLabel"; "required": false; "isSignal": true; }; "anyLabel": { "alias": "anyLabel"; "required": false; "isSignal": true; }; "submitLabel": { "alias": "submitLabel"; "required": false; "isSignal": true; }; "resetLabel": { "alias": "resetLabel"; "required": false; "isSignal": true; }; }, { "filterChange": "filterChange"; }, never, never, true, never>;
28
38
  }