@c-code/c-code-fw 1.1.9 → 1.3.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 (31) hide show
  1. package/README.md +104 -0
  2. package/fesm2022/c-code-c-code-fw-ui.mjs +815 -0
  3. package/fesm2022/c-code-c-code-fw-ui.mjs.map +1 -0
  4. package/fesm2022/c-code-c-code-fw.mjs +37 -30
  5. package/fesm2022/c-code-c-code-fw.mjs.map +1 -1
  6. package/lib/screen_width-event.service.d.ts +8 -8
  7. package/package.json +9 -2
  8. package/public-api.d.ts +1 -1
  9. package/ui/index.d.ts +5 -0
  10. package/ui/lib/components/category-menu/category-menu.component.d.ts +19 -0
  11. package/ui/lib/components/faq/faq.component.d.ts +16 -0
  12. package/ui/lib/components/gallery/gallery.component.d.ts +27 -0
  13. package/ui/lib/components/info-item/info-item.component.d.ts +18 -0
  14. package/ui/lib/components/lightbox/lightbox.component.d.ts +26 -0
  15. package/ui/lib/components/page-banner/page-banner.component.d.ts +23 -0
  16. package/ui/lib/components/plan-card/plan-card.component.d.ts +23 -0
  17. package/ui/lib/components/plan-catalog/plan-catalog.component.d.ts +78 -0
  18. package/ui/lib/components/plan-details/plan-details.component.d.ts +44 -0
  19. package/ui/lib/components/plan-filter-form/plan-filter-form.component.d.ts +28 -0
  20. package/ui/lib/components/promo-modal/promo-modal.component.d.ts +24 -0
  21. package/ui/lib/components/search-box/search-box.component.d.ts +18 -0
  22. package/ui/lib/components/social-links/social-links.component.d.ts +17 -0
  23. package/ui/lib/components/whatsapp-button/whatsapp-button.component.d.ts +19 -0
  24. package/ui/lib/models/plan.models.d.ts +50 -0
  25. package/ui/lib/models/ui.models.d.ts +24 -0
  26. package/ui/lib/services/plan-catalog.service.d.ts +43 -0
  27. package/ui/lib/services/seo.service.d.ts +39 -0
  28. package/ui/lib/utils/plan.utils.d.ts +16 -0
  29. package/ui/lib/utils/text.utils.d.ts +15 -0
  30. package/ui/public-api.d.ts +20 -0
  31. package/ui/theme/tokens.css +45 -0
@@ -0,0 +1,26 @@
1
+ import { GalleryImage } from '../../models/ui.models';
2
+ import * as i0 from "@angular/core";
3
+ /**
4
+ * Full-screen image viewer. It is open while `index` is not null.
5
+ * Arrow keys move between images and Escape closes it.
6
+ *
7
+ * ```html
8
+ * <cc-lightbox [images]="images" [(index)]="openIndex" />
9
+ * ```
10
+ */
11
+ export declare class LightboxComponent {
12
+ readonly images: import("@angular/core").InputSignal<GalleryImage[]>;
13
+ /** Index of the open image, or null when closed. Use `[(index)]`. */
14
+ readonly index: import("@angular/core").ModelSignal<number | null>;
15
+ readonly closeLabel: import("@angular/core").InputSignal<string>;
16
+ readonly prevLabel: import("@angular/core").InputSignal<string>;
17
+ readonly nextLabel: import("@angular/core").InputSignal<string>;
18
+ protected readonly current: import("@angular/core").Signal<GalleryImage | null>;
19
+ private document;
20
+ constructor();
21
+ close(): void;
22
+ next(): void;
23
+ prev(): void;
24
+ 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>;
26
+ }
@@ -0,0 +1,23 @@
1
+ import { LinkTarget } from '../../models/ui.models';
2
+ import * as i0 from "@angular/core";
3
+ /**
4
+ * Title strip at the top of a page, with an optional back link and icon.
5
+ * Renders the page's `<h1>`.
6
+ *
7
+ * ```html
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" />
10
+ * ```
11
+ */
12
+ export declare class PageBannerComponent {
13
+ readonly title: import("@angular/core").InputSignal<string>;
14
+ /** `band` uses the surface color; `plain` uses the page background. */
15
+ readonly variant: import("@angular/core").InputSignal<"band" | "plain">;
16
+ readonly iconSrc: import("@angular/core").InputSignal<string>;
17
+ /** Shows a back arrow that links here. */
18
+ readonly backLink: import("@angular/core").InputSignal<LinkTarget | null>;
19
+ readonly backIconSrc: import("@angular/core").InputSignal<string>;
20
+ readonly backLabel: import("@angular/core").InputSignal<string>;
21
+ 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>;
23
+ }
@@ -0,0 +1,23 @@
1
+ import { LinkTarget } from '../../models/ui.models';
2
+ import * as i0 from "@angular/core";
3
+ /**
4
+ * Plan card: image, name and a button.
5
+ * With `link` the button navigates with the router; without it, it emits `ctaClick`.
6
+ *
7
+ * ```html
8
+ * <cc-plan-card [name]="plan.name" [imageSrc]="plan.imgPath" [link]="['/planes', slug]" />
9
+ * ```
10
+ */
11
+ export declare class PlanCardComponent {
12
+ readonly name: import("@angular/core").InputSignal<string>;
13
+ readonly imageSrc: import("@angular/core").InputSignal<string>;
14
+ /** Defaults to `name`. */
15
+ 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>;
18
+ readonly link: import("@angular/core").InputSignal<LinkTarget | null>;
19
+ readonly ctaLabel: import("@angular/core").InputSignal<string>;
20
+ readonly ctaClick: import("@angular/core").OutputEmitterRef<void>;
21
+ 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>;
23
+ }
@@ -0,0 +1,78 @@
1
+ import { TemplateRef } from '@angular/core';
2
+ import { AdditionalService, CategoryOption, Plan, PlanCategory, PlanFilter, PriceRange } from '../../models/plan.models';
3
+ import { LinkTarget } from '../../models/ui.models';
4
+ import * as i0 from "@angular/core";
5
+ /** Context of a custom card template: `<ng-template ccPlanCard let-plan let-link="link">`. */
6
+ export interface PlanCardContext {
7
+ $implicit: Plan;
8
+ link: LinkTarget;
9
+ }
10
+ /** Marks an `<ng-template>` inside `cc-plan-catalog` as the card to render for each plan. */
11
+ export declare class PlanCardTemplateDirective {
12
+ static ngTemplateContextGuard(_dir: PlanCardTemplateDirective, ctx: unknown): ctx is PlanCardContext;
13
+ static ɵfac: i0.ɵɵFactoryDeclaration<PlanCardTemplateDirective, never>;
14
+ static ɵdir: i0.ɵɵDirectiveDeclaration<PlanCardTemplateDirective, "ng-template[ccPlanCard]", never, {}, {}, never, never, true, never>;
15
+ }
16
+ /**
17
+ * Full plan list page: title, name search, category menu, filter and card grid.
18
+ * It only needs the plans; the rest has Spanish defaults you can override.
19
+ *
20
+ * Selecting a category shows its plans. Searching by name or applying the filter
21
+ * looks across every category, as the sites already did.
22
+ *
23
+ * ```html
24
+ * <cc-plan-catalog
25
+ * [plans]="plans()"
26
+ * [services]="services()"
27
+ * [priceRanges]="ranges()"
28
+ * imageAltSuffix=" - Laurel Spa Medellín"
29
+ * />
30
+ * ```
31
+ */
32
+ export declare class PlanCatalogComponent {
33
+ readonly plans: import("@angular/core").InputSignal<Plan[]>;
34
+ readonly categories: import("@angular/core").InputSignal<CategoryOption<number>[]>;
35
+ readonly initialCategory: import("@angular/core").InputSignal<number>;
36
+ /** Shows how many plans each category has, counted from `plans`. */
37
+ readonly showCounts: import("@angular/core").InputSignal<boolean>;
38
+ /** Options for the filter form. The filter is hidden when both are empty. */
39
+ readonly services: import("@angular/core").InputSignal<AdditionalService[]>;
40
+ readonly priceRanges: import("@angular/core").InputSignal<PriceRange[]>;
41
+ /** Where each card links to. Defaults to `/planes/<slug>`. */
42
+ readonly detailsLink: import("@angular/core").InputSignal<(plan: Plan) => LinkTarget>;
43
+ readonly showBanner: import("@angular/core").InputSignal<boolean>;
44
+ readonly showSearch: import("@angular/core").InputSignal<boolean>;
45
+ readonly titlePrefix: import("@angular/core").InputSignal<string>;
46
+ readonly resultsTitle: import("@angular/core").InputSignal<string>;
47
+ readonly categoriesTitle: import("@angular/core").InputSignal<string>;
48
+ readonly searchPlaceholder: import("@angular/core").InputSignal<string>;
49
+ readonly searchIconSrc: import("@angular/core").InputSignal<string>;
50
+ readonly filterTitle: import("@angular/core").InputSignal<string>;
51
+ 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
+ readonly emptyMessage: import("@angular/core").InputSignal<string>;
56
+ readonly categoryChange: import("@angular/core").OutputEmitterRef<number>;
57
+ protected readonly cardTemplate: import("@angular/core").Signal<TemplateRef<any> | undefined>;
58
+ protected readonly category: import("@angular/core").WritableSignal<number | null>;
59
+ protected readonly query: import("@angular/core").WritableSignal<string>;
60
+ protected readonly filter: import("@angular/core").WritableSignal<PlanFilter | null>;
61
+ protected readonly filterOpen: import("@angular/core").WritableSignal<boolean>;
62
+ /** Set when the user searched or filtered; results then cover every category. */
63
+ 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();
67
+ protected readonly categoryOptions: import("@angular/core").Signal<CategoryOption<number>[]>;
68
+ protected readonly hasFilter: import("@angular/core").Signal<boolean>;
69
+ protected readonly visiblePlans: import("@angular/core").Signal<Plan[]>;
70
+ protected readonly title: import("@angular/core").Signal<string>;
71
+ protected selectCategory(value: PlanCategory | number | null): void;
72
+ protected onSearch(query: string): void;
73
+ protected onFilter(filter: PlanFilter): void;
74
+ protected linkFor(plan: Plan): LinkTarget;
75
+ private startSearch;
76
+ 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>;
78
+ }
@@ -0,0 +1,44 @@
1
+ import { Plan } from '../../models/plan.models';
2
+ import { LinkTarget } from '../../models/ui.models';
3
+ import * as i0 from "@angular/core";
4
+ /**
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.
8
+ *
9
+ * ```html
10
+ * <cc-plan-details [plan]="plan" [bookingUrl]="whatsappLink" imageAltSuffix=" en Laurel Spa Medellín" />
11
+ * ```
12
+ */
13
+ export declare class PlanDetailsComponent {
14
+ readonly plan: import("@angular/core").InputSignal<Plan>;
15
+ /** Link of the booking button, usually WhatsApp. */
16
+ readonly bookingUrl: import("@angular/core").InputSignal<string>;
17
+ readonly bookingLabel: import("@angular/core").InputSignal<string>;
18
+ readonly showBanner: import("@angular/core").InputSignal<boolean>;
19
+ readonly backLink: import("@angular/core").InputSignal<LinkTarget | null>;
20
+ readonly backLabel: import("@angular/core").InputSignal<string>;
21
+ readonly backIconSrc: import("@angular/core").InputSignal<string>;
22
+ readonly titleIconSrc: import("@angular/core").InputSignal<string>;
23
+ /** Added after the plan name in the photo's alt text. */
24
+ 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>;
29
+ readonly durationIconSrc: import("@angular/core").InputSignal<string>;
30
+ readonly personIconSrc: import("@angular/core").InputSignal<string>;
31
+ readonly peopleIconSrc: import("@angular/core").InputSignal<string>;
32
+ readonly personLabel: import("@angular/core").InputSignal<string>;
33
+ readonly peopleLabel: import("@angular/core").InputSignal<string>;
34
+ readonly includesLabel: import("@angular/core").InputSignal<string>;
35
+ /** Gift box. It is hidden when `perks` is empty. */
36
+ readonly perksTitle: import("@angular/core").InputSignal<string>;
37
+ readonly perks: import("@angular/core").InputSignal<string[]>;
38
+ readonly perksImageSrc: import("@angular/core").InputSignal<string>;
39
+ readonly perksImageAlt: import("@angular/core").InputSignal<string>;
40
+ protected readonly people: import("@angular/core").Signal<number>;
41
+ protected readonly services: import("@angular/core").Signal<import("@c-code/c-code-fw/ui").AdditionalService[]>;
42
+ 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>;
44
+ }
@@ -0,0 +1,28 @@
1
+ import { AdditionalService, PlanFilter, PriceRange } from '../../models/plan.models';
2
+ import * as i0 from "@angular/core";
3
+ /**
4
+ * Filter by included service and price range. The options come in as inputs,
5
+ * so the form does not load data by itself.
6
+ *
7
+ * ```html
8
+ * <cc-plan-filter-form [services]="services" [priceRanges]="ranges" (filterChange)="apply($event)" />
9
+ * ```
10
+ */
11
+ export declare class PlanFilterFormComponent {
12
+ readonly services: import("@angular/core").InputSignal<AdditionalService[]>;
13
+ readonly priceRanges: import("@angular/core").InputSignal<PriceRange[]>;
14
+ readonly serviceLabel: import("@angular/core").InputSignal<string>;
15
+ readonly priceLabel: import("@angular/core").InputSignal<string>;
16
+ readonly anyLabel: import("@angular/core").InputSignal<string>;
17
+ readonly submitLabel: import("@angular/core").InputSignal<string>;
18
+ readonly resetLabel: import("@angular/core").InputSignal<string>;
19
+ /** Emits on submit, and with an empty filter on reset. */
20
+ 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;
24
+ submit(): void;
25
+ reset(): void;
26
+ 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>;
28
+ }
@@ -0,0 +1,24 @@
1
+ import * as i0 from "@angular/core";
2
+ /**
3
+ * Modal over the page, for example a promotion shown when the site opens.
4
+ * Shows `imageSrc`, or the projected content when there is no image.
5
+ * Closes with the ✕ button, a backdrop click or the Escape key.
6
+ *
7
+ * ```html
8
+ * <cc-promo-modal [(open)]="showPromo" imageSrc="assets/images/pop.jpeg" imageAlt="Promoción" />
9
+ * ```
10
+ */
11
+ export declare class PromoModalComponent {
12
+ /** Whether the modal is visible. Use `[(open)]` to bind both ways. */
13
+ readonly open: import("@angular/core").ModelSignal<boolean>;
14
+ readonly imageSrc: import("@angular/core").InputSignal<string>;
15
+ readonly imageAlt: import("@angular/core").InputSignal<string>;
16
+ /** Makes the image a link, for example to WhatsApp. */
17
+ readonly link: import("@angular/core").InputSignal<string>;
18
+ readonly closeOnBackdrop: import("@angular/core").InputSignal<boolean>;
19
+ readonly closeLabel: import("@angular/core").InputSignal<string>;
20
+ readonly closed: import("@angular/core").OutputEmitterRef<void>;
21
+ close(): void;
22
+ static ɵfac: i0.ɵɵFactoryDeclaration<PromoModalComponent, never>;
23
+ static ɵcmp: i0.ɵɵComponentDeclaration<PromoModalComponent, "cc-promo-modal", never, { "open": { "alias": "open"; "required": false; "isSignal": true; }; "imageSrc": { "alias": "imageSrc"; "required": false; "isSignal": true; }; "imageAlt": { "alias": "imageAlt"; "required": false; "isSignal": true; }; "link": { "alias": "link"; "required": false; "isSignal": true; }; "closeOnBackdrop": { "alias": "closeOnBackdrop"; "required": false; "isSignal": true; }; "closeLabel": { "alias": "closeLabel"; "required": false; "isSignal": true; }; }, { "open": "openChange"; "closed": "closed"; }, never, ["*"], true, never>;
24
+ }
@@ -0,0 +1,18 @@
1
+ import * as i0 from "@angular/core";
2
+ /**
3
+ * Text field with a search button. Emits `search` on Enter or on the button.
4
+ *
5
+ * ```html
6
+ * <cc-search-box placeholder="Buscar plan" [(value)]="query" (search)="onSearch($event)" />
7
+ * ```
8
+ */
9
+ export declare class SearchBoxComponent {
10
+ readonly value: import("@angular/core").ModelSignal<string>;
11
+ readonly placeholder: import("@angular/core").InputSignal<string>;
12
+ readonly buttonLabel: import("@angular/core").InputSignal<string>;
13
+ readonly iconSrc: import("@angular/core").InputSignal<string>;
14
+ readonly search: import("@angular/core").OutputEmitterRef<string>;
15
+ onInput(event: Event): void;
16
+ static ɵfac: i0.ɵɵFactoryDeclaration<SearchBoxComponent, never>;
17
+ static ɵcmp: i0.ɵɵComponentDeclaration<SearchBoxComponent, "cc-search-box", never, { "value": { "alias": "value"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "buttonLabel": { "alias": "buttonLabel"; "required": false; "isSignal": true; }; "iconSrc": { "alias": "iconSrc"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "search": "search"; }, never, never, true, never>;
18
+ }
@@ -0,0 +1,17 @@
1
+ import { SocialLink } from '../../models/ui.models';
2
+ import * as i0 from "@angular/core";
3
+ /**
4
+ * Row of social network icons that open in a new tab.
5
+ *
6
+ * ```html
7
+ * <cc-social-links [links]="socials" size="1.25rem" />
8
+ * ```
9
+ */
10
+ export declare class SocialLinksComponent {
11
+ readonly links: import("@angular/core").InputSignal<SocialLink[]>;
12
+ /** Icon width, any CSS length. */
13
+ readonly size: import("@angular/core").InputSignal<string>;
14
+ readonly gap: import("@angular/core").InputSignal<string>;
15
+ static ɵfac: i0.ɵɵFactoryDeclaration<SocialLinksComponent, never>;
16
+ static ɵcmp: i0.ɵɵComponentDeclaration<SocialLinksComponent, "cc-social-links", never, { "links": { "alias": "links"; "required": true; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "gap": { "alias": "gap"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
17
+ }
@@ -0,0 +1,19 @@
1
+ import * as i0 from "@angular/core";
2
+ /**
3
+ * Floating WhatsApp button, fixed to a corner of the screen.
4
+ *
5
+ * ```html
6
+ * <cc-whatsapp-button [href]="whatsappLink" />
7
+ * <cc-whatsapp-button [href]="whatsappLink" iconSrc="assets/icons/whatsapp.png" position="left" />
8
+ * ```
9
+ */
10
+ export declare class WhatsappButtonComponent {
11
+ /** WhatsApp link. Build it with `whatsappUrl(phone, message)`. */
12
+ readonly href: import("@angular/core").InputSignal<string>;
13
+ /** Custom icon. Without it, the WhatsApp logo is drawn as SVG. */
14
+ readonly iconSrc: import("@angular/core").InputSignal<string>;
15
+ readonly label: import("@angular/core").InputSignal<string>;
16
+ readonly position: import("@angular/core").InputSignal<"right" | "left">;
17
+ static ɵfac: i0.ɵɵFactoryDeclaration<WhatsappButtonComponent, never>;
18
+ static ɵcmp: i0.ɵɵComponentDeclaration<WhatsappButtonComponent, "cc-whatsapp-button", never, { "href": { "alias": "href"; "required": true; "isSignal": true; }; "iconSrc": { "alias": "iconSrc"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "position": { "alias": "position"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
19
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Plan category. The numeric values match the `category` field stored in the sites' `plans.json`.
3
+ */
4
+ export declare enum PlanCategory {
5
+ Individual = 0,
6
+ Couple = 1,
7
+ Group = 2,
8
+ None = 3
9
+ }
10
+ /** Add-on service that a plan includes (an entry in `additionals.json`). */
11
+ export interface AdditionalService {
12
+ id: number;
13
+ iconPath: string;
14
+ name: string;
15
+ }
16
+ /** A spa plan (an entry in `plans.json`). */
17
+ export interface Plan {
18
+ id: number;
19
+ name: string;
20
+ description: string;
21
+ duration: string;
22
+ price: number;
23
+ /** Number of people the plan is for. The spelling matches the existing JSON files. */
24
+ cuantity: number;
25
+ imgPath: string;
26
+ category: PlanCategory | number;
27
+ additionalServicesId: number[];
28
+ /** Filled in by `PlanCatalogService` by joining `additionalServicesId` with `additionals.json`. */
29
+ additionalServices?: AdditionalService[];
30
+ }
31
+ /** Price range option for the filter form (an entry in `priceRanges.json`). */
32
+ export interface PriceRange {
33
+ description: string;
34
+ min: number;
35
+ max: number;
36
+ }
37
+ /** Filter produced by `cc-plan-filter-form`. Empty fields are not applied. */
38
+ export interface PlanFilter {
39
+ service?: AdditionalService | null;
40
+ priceRange?: PriceRange | null;
41
+ }
42
+ /** Entry in `cc-category-menu`. */
43
+ export interface CategoryOption<T = PlanCategory | number> {
44
+ value: T;
45
+ label: string;
46
+ /** Number shown next to the label. Leave it empty to hide it. */
47
+ count?: number;
48
+ }
49
+ /** Default Spanish labels for the plan categories. */
50
+ export declare const DEFAULT_PLAN_CATEGORIES: CategoryOption[];
@@ -0,0 +1,24 @@
1
+ /** Image for `cc-gallery` and `cc-lightbox`. */
2
+ export interface GalleryImage {
3
+ src: string;
4
+ /** Smaller version for the grid. Defaults to `src`. */
5
+ thumb?: string;
6
+ /** Alternative text. Defaults to `caption`. */
7
+ alt?: string;
8
+ /** Text shown under the image in the lightbox. */
9
+ caption?: string;
10
+ }
11
+ /** Link for `cc-social-links`. */
12
+ export interface SocialLink {
13
+ href: string;
14
+ iconSrc: string;
15
+ /** Accessible name, for example "Instagram de Laurel Spa". */
16
+ label: string;
17
+ }
18
+ /** Question for `cc-faq`. */
19
+ export interface FaqItem {
20
+ question: string;
21
+ answer: string;
22
+ }
23
+ /** Router link accepted by the components: a path string or a commands array. */
24
+ export type LinkTarget = string | any[];
@@ -0,0 +1,43 @@
1
+ import { EnvironmentProviders, InjectionToken } from '@angular/core';
2
+ import { Observable } from 'rxjs';
3
+ import { AdditionalService, Plan, PlanCategory, PlanFilter, PriceRange } from '../models/plan.models';
4
+ import * as i0 from "@angular/core";
5
+ /** Where `PlanCatalogService` loads the JSON files from. */
6
+ export interface PlanCatalogConfig {
7
+ plansUrl: string;
8
+ additionalsUrl: string;
9
+ /** Leave it empty if the site has no `priceRanges.json`; `getPriceRanges()` then returns `[]`. */
10
+ priceRangesUrl?: string | null;
11
+ }
12
+ export declare const DEFAULT_PLAN_CATALOG_CONFIG: PlanCatalogConfig;
13
+ export declare const PLAN_CATALOG_CONFIG: InjectionToken<PlanCatalogConfig>;
14
+ /**
15
+ * Overrides the JSON locations. Without it, the service reads `assets/data/*.json`.
16
+ *
17
+ * ```ts
18
+ * providers: [provideHttpClient(withFetch()), providePlanCatalog({ priceRangesUrl: null })]
19
+ * ```
20
+ */
21
+ export declare function providePlanCatalog(config?: Partial<PlanCatalogConfig>): EnvironmentProviders;
22
+ /**
23
+ * Reads plans, add-on services and price ranges from static JSON.
24
+ * Each file is requested once and cached; filtering happens in memory.
25
+ * Needs `provideHttpClient()` in the app.
26
+ */
27
+ export declare class PlanCatalogService {
28
+ private http;
29
+ private config;
30
+ private additionals$;
31
+ private plans$;
32
+ private priceRanges$;
33
+ /** Every plan, with `additionalServices` filled in. */
34
+ getPlans(): Observable<Plan[]>;
35
+ getAdditionalServices(): Observable<AdditionalService[]>;
36
+ getPriceRanges(): Observable<PriceRange[]>;
37
+ getPlansByCategory(category: PlanCategory | number): Observable<Plan[]>;
38
+ getPlansByFilter(filter: PlanFilter): Observable<Plan[]>;
39
+ getPlansByName(name: string): Observable<Plan[]>;
40
+ getPlanBySlug(slug: string): Observable<Plan | undefined>;
41
+ static ɵfac: i0.ɵɵFactoryDeclaration<PlanCatalogService, never>;
42
+ static ɵprov: i0.ɵɵInjectableDeclaration<PlanCatalogService>;
43
+ }
@@ -0,0 +1,39 @@
1
+ import { EnvironmentProviders, InjectionToken } from '@angular/core';
2
+ import * as i0 from "@angular/core";
3
+ export interface SeoConfig {
4
+ /** Production domain without a trailing slash: "https://www.laurelspamedellin.com". */
5
+ siteUrl: string;
6
+ /** Image used when a page does not set one. Relative paths are resolved against `siteUrl`. */
7
+ defaultImage?: string;
8
+ }
9
+ export interface SeoData {
10
+ title: string;
11
+ description: string;
12
+ /** Page path, for example "/planes". */
13
+ path: string;
14
+ /** Image for the share preview. Defaults to `SeoConfig.defaultImage`. */
15
+ image?: string;
16
+ }
17
+ export declare const SEO_CONFIG: InjectionToken<SeoConfig>;
18
+ /** Registers the site data `SeoService` needs. */
19
+ export declare function provideSeo(config: SeoConfig): EnvironmentProviders;
20
+ /**
21
+ * Sets the title, description, canonical URL, Open Graph and Twitter tags, and JSON-LD blocks.
22
+ * Works during SSR and prerendering. Requires `provideSeo()`.
23
+ */
24
+ export declare class SeoService {
25
+ private title;
26
+ private meta;
27
+ private document;
28
+ private config;
29
+ get siteUrl(): string;
30
+ /** Turns a site path or relative asset into an absolute URL. Absolute URLs are returned unchanged. */
31
+ absoluteUrl(pathOrUrl: string): string;
32
+ update({ title, description, path, image }: SeoData): void;
33
+ /** Adds or replaces a `<script type="application/ld+json">` with the given id. */
34
+ setJsonLd(id: string, data: object): void;
35
+ removeJsonLd(id: string): void;
36
+ private setCanonical;
37
+ static ɵfac: i0.ɵɵFactoryDeclaration<SeoService, never>;
38
+ static ɵprov: i0.ɵɵInjectableDeclaration<SeoService>;
39
+ }
@@ -0,0 +1,16 @@
1
+ import { AdditionalService, CategoryOption, Plan, PlanCategory, PlanFilter } from '../models/plan.models';
2
+ /** Fills `plan.additionalServices` from `additionalServicesId`. Unknown ids are skipped. */
3
+ export declare function joinAdditionalServices(plans: Plan[], additionals: AdditionalService[]): Plan[];
4
+ /** URL slug of a plan, derived from its name. */
5
+ export declare function planSlug(plan: Plan | string): string;
6
+ export declare function findPlanBySlug(plans: Plan[], slug: string): Plan | undefined;
7
+ export declare function filterPlansByCategory(plans: Plan[], category: PlanCategory | number): Plan[];
8
+ /** Case- and accent-insensitive search on the plan name. An empty query returns every plan. */
9
+ export declare function searchPlansByName(plans: Plan[], query: string): Plan[];
10
+ /**
11
+ * Applies a `PlanFilter`. Price uses `min < price <= max`, the same rule the sites had,
12
+ * except that `min = 0` also includes free plans.
13
+ */
14
+ export declare function filterPlans(plans: Plan[], filter: PlanFilter): Plan[];
15
+ /** Returns the options with `count` set to the number of plans in each category. */
16
+ export declare function withCategoryCounts<T extends PlanCategory | number>(options: CategoryOption<T>[], plans: Plan[]): CategoryOption<T>[];
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Turns a text into a URL-safe slug: "PLAN FLOR DE LOTTO 3" -> "plan-flor-de-lotto-3".
3
+ * Accents are removed, so "Jacuzzi Mágico" -> "jacuzzi-magico".
4
+ */
5
+ export declare function slugify(text: string): string;
6
+ /** "PLAN GARDENIA" -> "Plan Gardenia". */
7
+ export declare function titleCase(text: string): string;
8
+ /** Cuts a text at the last whole word that fits in `max` characters and adds "…". */
9
+ export declare function truncateText(text: string, max?: number): string;
10
+ /**
11
+ * Builds a WhatsApp chat link.
12
+ * @param phone Number with country code, with or without "+" and spaces: "57 310 494 8884".
13
+ * @param message Pre-filled message, optional.
14
+ */
15
+ export declare function whatsappUrl(phone: string, message?: string): string;
@@ -0,0 +1,20 @@
1
+ export * from './lib/models/plan.models';
2
+ export * from './lib/models/ui.models';
3
+ export * from './lib/utils/text.utils';
4
+ export * from './lib/utils/plan.utils';
5
+ export * from './lib/services/plan-catalog.service';
6
+ export * from './lib/services/seo.service';
7
+ export * from './lib/components/whatsapp-button/whatsapp-button.component';
8
+ export * from './lib/components/promo-modal/promo-modal.component';
9
+ export * from './lib/components/page-banner/page-banner.component';
10
+ export * from './lib/components/info-item/info-item.component';
11
+ export * from './lib/components/faq/faq.component';
12
+ export * from './lib/components/social-links/social-links.component';
13
+ export * from './lib/components/plan-card/plan-card.component';
14
+ export * from './lib/components/category-menu/category-menu.component';
15
+ export * from './lib/components/search-box/search-box.component';
16
+ export * from './lib/components/plan-filter-form/plan-filter-form.component';
17
+ export * from './lib/components/plan-catalog/plan-catalog.component';
18
+ export * from './lib/components/plan-details/plan-details.component';
19
+ export * from './lib/components/lightbox/lightbox.component';
20
+ export * from './lib/components/gallery/gallery.component';
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Design tokens for @c-code/c-code-fw/ui.
3
+ *
4
+ * The components already fall back to these values, so this file is optional.
5
+ * To apply a site's palette, set the variables in its global styles:
6
+ *
7
+ * :root {
8
+ * --cc-primary: #4C6B4A;
9
+ * --cc-secondary: #CEAB5D;
10
+ * }
11
+ *
12
+ * The overrides work on any element, not just :root, so a section can use its own palette.
13
+ *
14
+ * ROLES (optional). By default each role follows a palette color. Set a role only
15
+ * when a site needs it to differ from its palette:
16
+ *
17
+ * --cc-surface cards, sidebar, detail panel (default --cc-secondary-light)
18
+ * --cc-text text inside components (default --cc-primary)
19
+ * --cc-heading titles and the price badge (default --cc-primary)
20
+ * --cc-accent buttons and highlighted text (default --cc-secondary)
21
+ * --cc-accent-hover button hover (default --cc-secondary-dark)
22
+ * --cc-on-accent text on buttons and badges (default --cc-bg)
23
+ *
24
+ * Each component also has its own variables (for example --cc-plan-card-bg).
25
+ * They are listed in the README.
26
+ */
27
+ :root {
28
+ /* Palette */
29
+ --cc-primary: #4c6b4a;
30
+ --cc-primary-light: #a0ba9e;
31
+ --cc-primary-dark: #022b04;
32
+ --cc-secondary: #ceab5d;
33
+ --cc-secondary-light: #f8f4ee;
34
+ --cc-secondary-dark: #9a7521;
35
+ --cc-bg: #f0ffef;
36
+
37
+ /* Typography */
38
+ --cc-font-body: inherit;
39
+ --cc-font-heading: inherit;
40
+
41
+ /* Shape */
42
+ --cc-radius: 0;
43
+ --cc-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
44
+ --cc-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
45
+ }