@c-code/c-code-fw 1.2.6 → 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 (27) 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/package.json +9 -2
  5. package/ui/index.d.ts +5 -0
  6. package/ui/lib/components/category-menu/category-menu.component.d.ts +19 -0
  7. package/ui/lib/components/faq/faq.component.d.ts +16 -0
  8. package/ui/lib/components/gallery/gallery.component.d.ts +27 -0
  9. package/ui/lib/components/info-item/info-item.component.d.ts +18 -0
  10. package/ui/lib/components/lightbox/lightbox.component.d.ts +26 -0
  11. package/ui/lib/components/page-banner/page-banner.component.d.ts +23 -0
  12. package/ui/lib/components/plan-card/plan-card.component.d.ts +23 -0
  13. package/ui/lib/components/plan-catalog/plan-catalog.component.d.ts +78 -0
  14. package/ui/lib/components/plan-details/plan-details.component.d.ts +44 -0
  15. package/ui/lib/components/plan-filter-form/plan-filter-form.component.d.ts +28 -0
  16. package/ui/lib/components/promo-modal/promo-modal.component.d.ts +24 -0
  17. package/ui/lib/components/search-box/search-box.component.d.ts +18 -0
  18. package/ui/lib/components/social-links/social-links.component.d.ts +17 -0
  19. package/ui/lib/components/whatsapp-button/whatsapp-button.component.d.ts +19 -0
  20. package/ui/lib/models/plan.models.d.ts +50 -0
  21. package/ui/lib/models/ui.models.d.ts +24 -0
  22. package/ui/lib/services/plan-catalog.service.d.ts +43 -0
  23. package/ui/lib/services/seo.service.d.ts +39 -0
  24. package/ui/lib/utils/plan.utils.d.ts +16 -0
  25. package/ui/lib/utils/text.utils.d.ts +15 -0
  26. package/ui/public-api.d.ts +20 -0
  27. package/ui/theme/tokens.css +45 -0
@@ -0,0 +1,815 @@
1
+ import { HttpClient } from '@angular/common/http';
2
+ import * as i0 from '@angular/core';
3
+ import { InjectionToken, makeEnvironmentProviders, inject, Injectable, input, ChangeDetectionStrategy, Component, model, output, signal, Directive, contentChild, TemplateRef, effect, untracked, computed } from '@angular/core';
4
+ import { shareReplay, forkJoin, map, of } from 'rxjs';
5
+ import { DOCUMENT, NgTemplateOutlet, CurrencyPipe } from '@angular/common';
6
+ import { Title, Meta } from '@angular/platform-browser';
7
+ import { RouterLink } from '@angular/router';
8
+
9
+ /**
10
+ * Plan category. The numeric values match the `category` field stored in the sites' `plans.json`.
11
+ */
12
+ var PlanCategory;
13
+ (function (PlanCategory) {
14
+ PlanCategory[PlanCategory["Individual"] = 0] = "Individual";
15
+ PlanCategory[PlanCategory["Couple"] = 1] = "Couple";
16
+ PlanCategory[PlanCategory["Group"] = 2] = "Group";
17
+ PlanCategory[PlanCategory["None"] = 3] = "None";
18
+ })(PlanCategory || (PlanCategory = {}));
19
+ /** Default Spanish labels for the plan categories. */
20
+ const DEFAULT_PLAN_CATEGORIES = [
21
+ { value: PlanCategory.Individual, label: 'Individual' },
22
+ { value: PlanCategory.Couple, label: 'Pareja' },
23
+ { value: PlanCategory.Group, label: 'Grupal' },
24
+ ];
25
+
26
+ /**
27
+ * Turns a text into a URL-safe slug: "PLAN FLOR DE LOTTO 3" -> "plan-flor-de-lotto-3".
28
+ * Accents are removed, so "Jacuzzi Mágico" -> "jacuzzi-magico".
29
+ */
30
+ function slugify(text) {
31
+ return text
32
+ .normalize('NFD')
33
+ .replace(/[̀-ͯ]/g, '')
34
+ .toLowerCase()
35
+ .trim()
36
+ .replace(/[^a-z0-9]+/g, '-')
37
+ .replace(/^-+|-+$/g, '');
38
+ }
39
+ /** "PLAN GARDENIA" -> "Plan Gardenia". */
40
+ function titleCase(text) {
41
+ return text.toLowerCase().replace(/(^|\s)\S/g, (c) => c.toUpperCase());
42
+ }
43
+ /** Cuts a text at the last whole word that fits in `max` characters and adds "…". */
44
+ function truncateText(text, max = 160) {
45
+ if (text.length <= max)
46
+ return text;
47
+ const cut = text.lastIndexOf(' ', max - 1);
48
+ return text.slice(0, cut > 0 ? cut : max - 1) + '…';
49
+ }
50
+ /**
51
+ * Builds a WhatsApp chat link.
52
+ * @param phone Number with country code, with or without "+" and spaces: "57 310 494 8884".
53
+ * @param message Pre-filled message, optional.
54
+ */
55
+ function whatsappUrl(phone, message) {
56
+ const params = new URLSearchParams({ phone: phone.replace(/\D/g, '') });
57
+ if (message)
58
+ params.set('text', message);
59
+ params.set('type', 'phone_number');
60
+ params.set('app_absent', '0');
61
+ return `https://api.whatsapp.com/send/?${params.toString()}`;
62
+ }
63
+
64
+ /** Fills `plan.additionalServices` from `additionalServicesId`. Unknown ids are skipped. */
65
+ function joinAdditionalServices(plans, additionals) {
66
+ const byId = new Map();
67
+ // First entry wins, so a repeated id in the JSON keeps the original behavior of `Array.find`.
68
+ for (const service of additionals) {
69
+ if (!byId.has(service.id))
70
+ byId.set(service.id, service);
71
+ }
72
+ return plans.map((plan) => ({
73
+ ...plan,
74
+ additionalServices: plan.additionalServicesId
75
+ .map((id) => byId.get(id))
76
+ .filter((s) => !!s),
77
+ }));
78
+ }
79
+ /** URL slug of a plan, derived from its name. */
80
+ function planSlug(plan) {
81
+ return slugify(typeof plan === 'string' ? plan : plan.name);
82
+ }
83
+ function findPlanBySlug(plans, slug) {
84
+ return plans.find((plan) => planSlug(plan) === slug);
85
+ }
86
+ function filterPlansByCategory(plans, category) {
87
+ return plans.filter((plan) => plan.category === category);
88
+ }
89
+ /** Case- and accent-insensitive search on the plan name. An empty query returns every plan. */
90
+ function searchPlansByName(plans, query) {
91
+ const q = slugify(query);
92
+ if (!q)
93
+ return plans;
94
+ return plans.filter((plan) => planSlug(plan).includes(q));
95
+ }
96
+ /**
97
+ * Applies a `PlanFilter`. Price uses `min < price <= max`, the same rule the sites had,
98
+ * except that `min = 0` also includes free plans.
99
+ */
100
+ function filterPlans(plans, filter) {
101
+ const { service, priceRange } = filter;
102
+ return plans.filter((plan) => {
103
+ const matchesService = !service ||
104
+ (plan.additionalServices ?? []).some((s) => s.id === service.id) ||
105
+ plan.additionalServicesId.includes(service.id);
106
+ const matchesPrice = !priceRange ||
107
+ (plan.price <= priceRange.max &&
108
+ (plan.price > priceRange.min || (priceRange.min === 0 && plan.price === 0)));
109
+ return matchesService && matchesPrice;
110
+ });
111
+ }
112
+ /** Returns the options with `count` set to the number of plans in each category. */
113
+ function withCategoryCounts(options, plans) {
114
+ return options.map((option) => ({
115
+ ...option,
116
+ count: plans.filter((plan) => plan.category === option.value).length,
117
+ }));
118
+ }
119
+
120
+ const DEFAULT_PLAN_CATALOG_CONFIG = {
121
+ plansUrl: 'assets/data/plans.json',
122
+ additionalsUrl: 'assets/data/additionals.json',
123
+ priceRangesUrl: 'assets/data/priceRanges.json',
124
+ };
125
+ const PLAN_CATALOG_CONFIG = new InjectionToken('PLAN_CATALOG_CONFIG', { providedIn: 'root', factory: () => DEFAULT_PLAN_CATALOG_CONFIG });
126
+ /**
127
+ * Overrides the JSON locations. Without it, the service reads `assets/data/*.json`.
128
+ *
129
+ * ```ts
130
+ * providers: [provideHttpClient(withFetch()), providePlanCatalog({ priceRangesUrl: null })]
131
+ * ```
132
+ */
133
+ function providePlanCatalog(config = {}) {
134
+ return makeEnvironmentProviders([
135
+ {
136
+ provide: PLAN_CATALOG_CONFIG,
137
+ useValue: { ...DEFAULT_PLAN_CATALOG_CONFIG, ...config },
138
+ },
139
+ ]);
140
+ }
141
+ /**
142
+ * Reads plans, add-on services and price ranges from static JSON.
143
+ * Each file is requested once and cached; filtering happens in memory.
144
+ * Needs `provideHttpClient()` in the app.
145
+ */
146
+ class PlanCatalogService {
147
+ http = inject(HttpClient);
148
+ config = inject(PLAN_CATALOG_CONFIG);
149
+ additionals$ = this.http
150
+ .get(this.config.additionalsUrl)
151
+ .pipe(shareReplay(1));
152
+ plans$ = forkJoin({
153
+ plans: this.http.get(this.config.plansUrl),
154
+ additionals: this.additionals$,
155
+ }).pipe(map(({ plans, additionals }) => joinAdditionalServices(plans, additionals)), shareReplay(1));
156
+ priceRanges$ = (this.config.priceRangesUrl
157
+ ? this.http.get(this.config.priceRangesUrl)
158
+ : of([])).pipe(shareReplay(1));
159
+ /** Every plan, with `additionalServices` filled in. */
160
+ getPlans() {
161
+ return this.plans$;
162
+ }
163
+ getAdditionalServices() {
164
+ return this.additionals$;
165
+ }
166
+ getPriceRanges() {
167
+ return this.priceRanges$;
168
+ }
169
+ getPlansByCategory(category) {
170
+ return this.plans$.pipe(map((plans) => filterPlansByCategory(plans, category)));
171
+ }
172
+ getPlansByFilter(filter) {
173
+ return this.plans$.pipe(map((plans) => filterPlans(plans, filter)));
174
+ }
175
+ getPlansByName(name) {
176
+ return this.plans$.pipe(map((plans) => searchPlansByName(plans, name)));
177
+ }
178
+ getPlanBySlug(slug) {
179
+ return this.plans$.pipe(map((plans) => findPlanBySlug(plans, slug)));
180
+ }
181
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanCatalogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
182
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanCatalogService, providedIn: 'root' });
183
+ }
184
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanCatalogService, decorators: [{
185
+ type: Injectable,
186
+ args: [{ providedIn: 'root' }]
187
+ }] });
188
+
189
+ const SEO_CONFIG = new InjectionToken('SEO_CONFIG');
190
+ /** Registers the site data `SeoService` needs. */
191
+ function provideSeo(config) {
192
+ return makeEnvironmentProviders([
193
+ {
194
+ provide: SEO_CONFIG,
195
+ useValue: { ...config, siteUrl: config.siteUrl.replace(/\/+$/, '') },
196
+ },
197
+ ]);
198
+ }
199
+ /**
200
+ * Sets the title, description, canonical URL, Open Graph and Twitter tags, and JSON-LD blocks.
201
+ * Works during SSR and prerendering. Requires `provideSeo()`.
202
+ */
203
+ class SeoService {
204
+ title = inject(Title);
205
+ meta = inject(Meta);
206
+ document = inject(DOCUMENT);
207
+ config = inject(SEO_CONFIG);
208
+ get siteUrl() {
209
+ return this.config.siteUrl;
210
+ }
211
+ /** Turns a site path or relative asset into an absolute URL. Absolute URLs are returned unchanged. */
212
+ absoluteUrl(pathOrUrl) {
213
+ if (/^https?:\/\//.test(pathOrUrl))
214
+ return pathOrUrl;
215
+ return this.config.siteUrl + (pathOrUrl.startsWith('/') ? pathOrUrl : '/' + pathOrUrl);
216
+ }
217
+ update({ title, description, path, image }) {
218
+ const url = this.absoluteUrl(path);
219
+ const imageSrc = image ?? this.config.defaultImage;
220
+ this.title.setTitle(title);
221
+ this.meta.updateTag({ name: 'description', content: description });
222
+ this.meta.updateTag({ property: 'og:title', content: title });
223
+ this.meta.updateTag({ property: 'og:description', content: description });
224
+ this.meta.updateTag({ property: 'og:url', content: url });
225
+ this.meta.updateTag({ name: 'twitter:title', content: title });
226
+ this.meta.updateTag({ name: 'twitter:description', content: description });
227
+ if (imageSrc) {
228
+ const imageUrl = this.absoluteUrl(imageSrc);
229
+ this.meta.updateTag({ property: 'og:image', content: imageUrl });
230
+ this.meta.updateTag({ name: 'twitter:image', content: imageUrl });
231
+ }
232
+ this.setCanonical(url);
233
+ }
234
+ /** Adds or replaces a `<script type="application/ld+json">` with the given id. */
235
+ setJsonLd(id, data) {
236
+ let script = this.document.getElementById(id);
237
+ if (!script) {
238
+ script = this.document.createElement('script');
239
+ script.id = id;
240
+ script.setAttribute('type', 'application/ld+json');
241
+ this.document.head.appendChild(script);
242
+ }
243
+ script.textContent = JSON.stringify(data);
244
+ }
245
+ removeJsonLd(id) {
246
+ this.document.getElementById(id)?.remove();
247
+ }
248
+ setCanonical(url) {
249
+ let link = this.document.head.querySelector('link[rel="canonical"]');
250
+ if (!link) {
251
+ link = this.document.createElement('link');
252
+ link.setAttribute('rel', 'canonical');
253
+ this.document.head.appendChild(link);
254
+ }
255
+ link.setAttribute('href', url);
256
+ }
257
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SeoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
258
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SeoService, providedIn: 'root' });
259
+ }
260
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SeoService, decorators: [{
261
+ type: Injectable,
262
+ args: [{ providedIn: 'root' }]
263
+ }] });
264
+
265
+ /**
266
+ * Floating WhatsApp button, fixed to a corner of the screen.
267
+ *
268
+ * ```html
269
+ * <cc-whatsapp-button [href]="whatsappLink" />
270
+ * <cc-whatsapp-button [href]="whatsappLink" iconSrc="assets/icons/whatsapp.png" position="left" />
271
+ * ```
272
+ */
273
+ class WhatsappButtonComponent {
274
+ /** WhatsApp link. Build it with `whatsappUrl(phone, message)`. */
275
+ href = input.required();
276
+ /** Custom icon. Without it, the WhatsApp logo is drawn as SVG. */
277
+ iconSrc = input('');
278
+ label = input('Escríbenos por WhatsApp');
279
+ position = input('right');
280
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: WhatsappButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
281
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: WhatsappButtonComponent, isStandalone: true, selector: "cc-whatsapp-button", inputs: { href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: true, transformFunction: null }, iconSrc: { classPropertyName: "iconSrc", publicName: "iconSrc", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<a\n class=\"cc-wa\"\n [class.cc-wa--left]=\"position() === 'left'\"\n [href]=\"href()\"\n target=\"_blank\"\n rel=\"noopener\"\n [attr.aria-label]=\"label()\"\n>\n @if (iconSrc()) {\n <img [src]=\"iconSrc()\" alt=\"\" class=\"cc-wa__img\" />\n } @else {\n <svg class=\"cc-wa__svg\" viewBox=\"0 0 32 32\" aria-hidden=\"true\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" fill=\"#25D366\" />\n <path\n fill=\"#fff\"\n d=\"M23.3 8.7A10.3 10.3 0 0 0 7 21.1L5.6 26.4l5.4-1.4a10.3 10.3 0 0 0 4.9 1.3 10.3 10.3 0 0 0 7.4-17.6Zm-7.3 15.8a8.5 8.5 0 0 1-4.3-1.2l-.3-.2-3.2.8.9-3.1-.2-.3a8.5 8.5 0 1 1 7.1 4Zm4.7-6.4c-.3-.1-1.5-.7-1.8-.8-.2-.1-.4-.1-.6.1l-.8 1c-.2.2-.3.2-.6.1a7 7 0 0 1-3.5-3c-.3-.5.3-.4.7-1.4.1-.2 0-.3 0-.4l-.8-2c-.2-.5-.4-.4-.6-.4h-.5a1 1 0 0 0-.7.3 3 3 0 0 0-.9 2.2 5.2 5.2 0 0 0 1.1 2.8 11.9 11.9 0 0 0 4.6 4c1.7.7 2.4.8 3.2.7a2.8 2.8 0 0 0 1.8-1.3 2.3 2.3 0 0 0 .2-1.3c-.1-.1-.3-.2-.6-.3Z\"\n />\n </svg>\n }\n</a>\n", styles: [".cc-wa{position:fixed;z-index:var(--cc-whatsapp-z, 20);right:var(--cc-whatsapp-offset-x, 2rem);bottom:var(--cc-whatsapp-offset-y, 2.5rem);display:block;transition:transform .3s ease-in}.cc-wa--left{right:auto;left:var(--cc-whatsapp-offset-x, 2rem)}.cc-wa:hover{transform:scale(1.25)}.cc-wa__img,.cc-wa__svg{display:block;width:var(--cc-whatsapp-size, 2.5rem);height:auto;border-radius:9999px;box-shadow:var(--cc-shadow-lg, 0 10px 15px -3px rgb(0 0 0 / .1))}@media (min-width: 1024px){.cc-wa{right:var(--cc-whatsapp-offset-x-lg, 3.5rem)}.cc-wa--left{right:auto;left:var(--cc-whatsapp-offset-x-lg, 3.5rem)}.cc-wa__img,.cc-wa__svg{width:var(--cc-whatsapp-size-lg, 3.5rem)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
282
+ }
283
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: WhatsappButtonComponent, decorators: [{
284
+ type: Component,
285
+ args: [{ selector: 'cc-whatsapp-button', changeDetection: ChangeDetectionStrategy.OnPush, template: "<a\n class=\"cc-wa\"\n [class.cc-wa--left]=\"position() === 'left'\"\n [href]=\"href()\"\n target=\"_blank\"\n rel=\"noopener\"\n [attr.aria-label]=\"label()\"\n>\n @if (iconSrc()) {\n <img [src]=\"iconSrc()\" alt=\"\" class=\"cc-wa__img\" />\n } @else {\n <svg class=\"cc-wa__svg\" viewBox=\"0 0 32 32\" aria-hidden=\"true\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" fill=\"#25D366\" />\n <path\n fill=\"#fff\"\n d=\"M23.3 8.7A10.3 10.3 0 0 0 7 21.1L5.6 26.4l5.4-1.4a10.3 10.3 0 0 0 4.9 1.3 10.3 10.3 0 0 0 7.4-17.6Zm-7.3 15.8a8.5 8.5 0 0 1-4.3-1.2l-.3-.2-3.2.8.9-3.1-.2-.3a8.5 8.5 0 1 1 7.1 4Zm4.7-6.4c-.3-.1-1.5-.7-1.8-.8-.2-.1-.4-.1-.6.1l-.8 1c-.2.2-.3.2-.6.1a7 7 0 0 1-3.5-3c-.3-.5.3-.4.7-1.4.1-.2 0-.3 0-.4l-.8-2c-.2-.5-.4-.4-.6-.4h-.5a1 1 0 0 0-.7.3 3 3 0 0 0-.9 2.2 5.2 5.2 0 0 0 1.1 2.8 11.9 11.9 0 0 0 4.6 4c1.7.7 2.4.8 3.2.7a2.8 2.8 0 0 0 1.8-1.3 2.3 2.3 0 0 0 .2-1.3c-.1-.1-.3-.2-.6-.3Z\"\n />\n </svg>\n }\n</a>\n", styles: [".cc-wa{position:fixed;z-index:var(--cc-whatsapp-z, 20);right:var(--cc-whatsapp-offset-x, 2rem);bottom:var(--cc-whatsapp-offset-y, 2.5rem);display:block;transition:transform .3s ease-in}.cc-wa--left{right:auto;left:var(--cc-whatsapp-offset-x, 2rem)}.cc-wa:hover{transform:scale(1.25)}.cc-wa__img,.cc-wa__svg{display:block;width:var(--cc-whatsapp-size, 2.5rem);height:auto;border-radius:9999px;box-shadow:var(--cc-shadow-lg, 0 10px 15px -3px rgb(0 0 0 / .1))}@media (min-width: 1024px){.cc-wa{right:var(--cc-whatsapp-offset-x-lg, 3.5rem)}.cc-wa--left{right:auto;left:var(--cc-whatsapp-offset-x-lg, 3.5rem)}.cc-wa__img,.cc-wa__svg{width:var(--cc-whatsapp-size-lg, 3.5rem)}}\n"] }]
286
+ }] });
287
+
288
+ /**
289
+ * Modal over the page, for example a promotion shown when the site opens.
290
+ * Shows `imageSrc`, or the projected content when there is no image.
291
+ * Closes with the ✕ button, a backdrop click or the Escape key.
292
+ *
293
+ * ```html
294
+ * <cc-promo-modal [(open)]="showPromo" imageSrc="assets/images/pop.jpeg" imageAlt="Promoción" />
295
+ * ```
296
+ */
297
+ class PromoModalComponent {
298
+ /** Whether the modal is visible. Use `[(open)]` to bind both ways. */
299
+ open = model(true);
300
+ imageSrc = input('');
301
+ imageAlt = input('');
302
+ /** Makes the image a link, for example to WhatsApp. */
303
+ link = input('');
304
+ closeOnBackdrop = input(true);
305
+ closeLabel = input('Cerrar');
306
+ closed = output();
307
+ close() {
308
+ this.open.set(false);
309
+ this.closed.emit();
310
+ }
311
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PromoModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
312
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: PromoModalComponent, isStandalone: true, selector: "cc-promo-modal", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, imageSrc: { classPropertyName: "imageSrc", publicName: "imageSrc", isSignal: true, isRequired: false, transformFunction: null }, imageAlt: { classPropertyName: "imageAlt", publicName: "imageAlt", isSignal: true, isRequired: false, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null }, closeOnBackdrop: { classPropertyName: "closeOnBackdrop", publicName: "closeOnBackdrop", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", closed: "closed" }, host: { listeners: { "document:keydown.escape": "open() && close()" } }, ngImport: i0, template: "@if (open()) {\n <div class=\"cc-modal\" (click)=\"closeOnBackdrop() && close()\">\n <div\n class=\"cc-modal__box\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"imageAlt() || null\"\n (click)=\"$event.stopPropagation()\"\n >\n <button type=\"button\" class=\"cc-modal__close\" [attr.aria-label]=\"closeLabel()\" (click)=\"close()\">\n \u2715\n </button>\n @if (imageSrc()) {\n @if (link()) {\n <a [href]=\"link()\" target=\"_blank\" rel=\"noopener\">\n <img [src]=\"imageSrc()\" [alt]=\"imageAlt()\" class=\"cc-modal__img\" />\n </a>\n } @else {\n <img [src]=\"imageSrc()\" [alt]=\"imageAlt()\" class=\"cc-modal__img\" />\n }\n } @else {\n <ng-content />\n }\n </div>\n </div>\n}\n", styles: [".cc-modal{position:fixed;inset:0;z-index:var(--cc-modal-z, 50);display:flex;align-items:center;justify-content:center;padding:1rem;background:var(--cc-modal-backdrop, rgb(0 0 0 / .7))}.cc-modal__box{position:relative;width:fit-content;max-width:92vw}.cc-modal__img{display:block;max-width:100%;max-height:85vh;border-radius:var(--cc-modal-radius, .5rem);box-shadow:0 25px 50px -12px #00000040}.cc-modal__close{position:absolute;top:-.75rem;right:-.75rem;width:2.25rem;height:2.25rem;display:flex;align-items:center;justify-content:center;border:0;border-radius:9999px;background:#fff;color:#374151;font-size:1.125rem;line-height:1;cursor:pointer;box-shadow:var(--cc-shadow-lg, 0 10px 15px -3px rgb(0 0 0 / .1));transition:transform .15s,background-color .15s}.cc-modal__close:hover{background:#f3f4f6;transform:scale(1.1)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
313
+ }
314
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PromoModalComponent, decorators: [{
315
+ type: Component,
316
+ args: [{ selector: 'cc-promo-modal', changeDetection: ChangeDetectionStrategy.OnPush, host: { '(document:keydown.escape)': 'open() && close()' }, template: "@if (open()) {\n <div class=\"cc-modal\" (click)=\"closeOnBackdrop() && close()\">\n <div\n class=\"cc-modal__box\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"imageAlt() || null\"\n (click)=\"$event.stopPropagation()\"\n >\n <button type=\"button\" class=\"cc-modal__close\" [attr.aria-label]=\"closeLabel()\" (click)=\"close()\">\n \u2715\n </button>\n @if (imageSrc()) {\n @if (link()) {\n <a [href]=\"link()\" target=\"_blank\" rel=\"noopener\">\n <img [src]=\"imageSrc()\" [alt]=\"imageAlt()\" class=\"cc-modal__img\" />\n </a>\n } @else {\n <img [src]=\"imageSrc()\" [alt]=\"imageAlt()\" class=\"cc-modal__img\" />\n }\n } @else {\n <ng-content />\n }\n </div>\n </div>\n}\n", styles: [".cc-modal{position:fixed;inset:0;z-index:var(--cc-modal-z, 50);display:flex;align-items:center;justify-content:center;padding:1rem;background:var(--cc-modal-backdrop, rgb(0 0 0 / .7))}.cc-modal__box{position:relative;width:fit-content;max-width:92vw}.cc-modal__img{display:block;max-width:100%;max-height:85vh;border-radius:var(--cc-modal-radius, .5rem);box-shadow:0 25px 50px -12px #00000040}.cc-modal__close{position:absolute;top:-.75rem;right:-.75rem;width:2.25rem;height:2.25rem;display:flex;align-items:center;justify-content:center;border:0;border-radius:9999px;background:#fff;color:#374151;font-size:1.125rem;line-height:1;cursor:pointer;box-shadow:var(--cc-shadow-lg, 0 10px 15px -3px rgb(0 0 0 / .1));transition:transform .15s,background-color .15s}.cc-modal__close:hover{background:#f3f4f6;transform:scale(1.1)}\n"] }]
317
+ }] });
318
+
319
+ /**
320
+ * Title strip at the top of a page, with an optional back link and icon.
321
+ * Renders the page's `<h1>`.
322
+ *
323
+ * ```html
324
+ * <cc-page-banner title="Reserva tu plan - Pareja" />
325
+ * <cc-page-banner [title]="plan.name" backLink="/planes" iconSrc="assets/icons/hoja.png" variant="plain" />
326
+ * ```
327
+ */
328
+ class PageBannerComponent {
329
+ title = input.required();
330
+ /** `band` uses the surface color; `plain` uses the page background. */
331
+ variant = input('band');
332
+ iconSrc = input('');
333
+ /** Shows a back arrow that links here. */
334
+ backLink = input(null);
335
+ backIconSrc = input('');
336
+ backLabel = input('Volver');
337
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PageBannerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
338
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: PageBannerComponent, isStandalone: true, selector: "cc-page-banner", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, iconSrc: { classPropertyName: "iconSrc", publicName: "iconSrc", isSignal: true, isRequired: false, transformFunction: null }, backLink: { classPropertyName: "backLink", publicName: "backLink", isSignal: true, isRequired: false, transformFunction: null }, backIconSrc: { classPropertyName: "backIconSrc", publicName: "backIconSrc", isSignal: true, isRequired: false, transformFunction: null }, backLabel: { classPropertyName: "backLabel", publicName: "backLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"cc-banner\" [class.cc-banner--plain]=\"variant() === 'plain'\" [class.cc-banner--split]=\"!!backLink()\">\n @if (backLink()) {\n <a class=\"cc-banner__back\" [routerLink]=\"backLink()\" [attr.aria-label]=\"backLabel()\">\n @if (backIconSrc()) {\n <img [src]=\"backIconSrc()\" alt=\"\" />\n } @else {\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path d=\"M15 18l-6-6 6-6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n }\n </a>\n }\n <div class=\"cc-banner__title\">\n <h1>{{ title() }}</h1>\n @if (iconSrc()) {\n <img [src]=\"iconSrc()\" alt=\"\" class=\"cc-banner__icon\" />\n }\n </div>\n @if (backLink()) {\n <span class=\"cc-banner__spacer\"></span>\n }\n</div>\n", styles: [":host{display:block}.cc-banner{display:flex;align-items:center;justify-content:center;width:100%;margin-bottom:1.25rem;padding:.5rem 1.25rem;text-align:center;background:var(--cc-banner-bg, var(--cc-surface, var(--cc-secondary-light, #f8f4ee)))}.cc-banner--plain{margin-bottom:2.5rem;background:var(--cc-banner-bg, var(--cc-bg, #f0ffef))}.cc-banner--split{justify-content:space-between}.cc-banner__title{display:flex;gap:.5rem;align-items:center;justify-content:center}h1{margin:0;font-family:var(--cc-font-heading, inherit);font-size:var(--cc-banner-font-size, 2rem);font-weight:700;color:var(--cc-banner-color, var(--cc-heading, var(--cc-primary, #4c6b4a)))}.cc-banner__icon{width:2.5rem;height:2.5rem}.cc-banner__back{display:flex;color:var(--cc-banner-color, var(--cc-heading, var(--cc-primary, #4c6b4a)));transition:transform .3s ease-in}.cc-banner__back:hover{transform:scale(1.25)}.cc-banner__back img,.cc-banner__back svg,.cc-banner__spacer{width:2rem;height:2rem}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
339
+ }
340
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PageBannerComponent, decorators: [{
341
+ type: Component,
342
+ args: [{ selector: 'cc-page-banner', imports: [RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"cc-banner\" [class.cc-banner--plain]=\"variant() === 'plain'\" [class.cc-banner--split]=\"!!backLink()\">\n @if (backLink()) {\n <a class=\"cc-banner__back\" [routerLink]=\"backLink()\" [attr.aria-label]=\"backLabel()\">\n @if (backIconSrc()) {\n <img [src]=\"backIconSrc()\" alt=\"\" />\n } @else {\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path d=\"M15 18l-6-6 6-6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n }\n </a>\n }\n <div class=\"cc-banner__title\">\n <h1>{{ title() }}</h1>\n @if (iconSrc()) {\n <img [src]=\"iconSrc()\" alt=\"\" class=\"cc-banner__icon\" />\n }\n </div>\n @if (backLink()) {\n <span class=\"cc-banner__spacer\"></span>\n }\n</div>\n", styles: [":host{display:block}.cc-banner{display:flex;align-items:center;justify-content:center;width:100%;margin-bottom:1.25rem;padding:.5rem 1.25rem;text-align:center;background:var(--cc-banner-bg, var(--cc-surface, var(--cc-secondary-light, #f8f4ee)))}.cc-banner--plain{margin-bottom:2.5rem;background:var(--cc-banner-bg, var(--cc-bg, #f0ffef))}.cc-banner--split{justify-content:space-between}.cc-banner__title{display:flex;gap:.5rem;align-items:center;justify-content:center}h1{margin:0;font-family:var(--cc-font-heading, inherit);font-size:var(--cc-banner-font-size, 2rem);font-weight:700;color:var(--cc-banner-color, var(--cc-heading, var(--cc-primary, #4c6b4a)))}.cc-banner__icon{width:2.5rem;height:2.5rem}.cc-banner__back{display:flex;color:var(--cc-banner-color, var(--cc-heading, var(--cc-primary, #4c6b4a)));transition:transform .3s ease-in}.cc-banner__back:hover{transform:scale(1.25)}.cc-banner__back img,.cc-banner__back svg,.cc-banner__spacer{width:2rem;height:2rem}\n"] }]
343
+ }] });
344
+
345
+ /**
346
+ * Title with a paragraph, used for policy sections.
347
+ * Extra content (lists, notes) can be projected under the text.
348
+ *
349
+ * ```html
350
+ * <cc-info-item title="Pagos" text="Aceptamos Nequi, Daviplata y efectivo." />
351
+ * <cc-info-item title="Reservaciones" [text]="texto">
352
+ * <ul><li>…</li></ul>
353
+ * </cc-info-item>
354
+ * ```
355
+ */
356
+ class InfoItemComponent {
357
+ title = input.required();
358
+ text = input('');
359
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: InfoItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
360
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: InfoItemComponent, isStandalone: true, selector: "cc-info-item", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<h2 class=\"cc-info__title\">{{ title() }}</h2>\n@if (text()) {\n <p class=\"cc-info__text\">{{ text() }}</p>\n}\n<ng-content />\n", styles: [":host{display:flex;flex-direction:column;gap:.75rem;width:100%}.cc-info__title{margin:0;font-family:var(--cc-font-heading, inherit);font-size:var(--cc-info-title-size, 1.5rem);font-weight:700;color:var(--cc-info-title-color, var(--cc-accent, var(--cc-secondary, #ceab5d)))}.cc-info__text{margin:0;font-size:1rem;text-align:justify;color:var(--cc-info-text-color, var(--cc-text, var(--cc-primary, #4c6b4a)))}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
361
+ }
362
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: InfoItemComponent, decorators: [{
363
+ type: Component,
364
+ args: [{ selector: 'cc-info-item', changeDetection: ChangeDetectionStrategy.OnPush, template: "<h2 class=\"cc-info__title\">{{ title() }}</h2>\n@if (text()) {\n <p class=\"cc-info__text\">{{ text() }}</p>\n}\n<ng-content />\n", styles: [":host{display:flex;flex-direction:column;gap:.75rem;width:100%}.cc-info__title{margin:0;font-family:var(--cc-font-heading, inherit);font-size:var(--cc-info-title-size, 1.5rem);font-weight:700;color:var(--cc-info-title-color, var(--cc-accent, var(--cc-secondary, #ceab5d)))}.cc-info__text{margin:0;font-size:1rem;text-align:justify;color:var(--cc-info-text-color, var(--cc-text, var(--cc-primary, #4c6b4a)))}\n"] }]
365
+ }] });
366
+
367
+ /**
368
+ * Frequently asked questions as native `<details>` elements, so the answers
369
+ * stay in the prerendered HTML for search engines.
370
+ *
371
+ * ```html
372
+ * <cc-faq title="PREGUNTAS FRECUENTES" [items]="faq" />
373
+ * ```
374
+ */
375
+ class FaqComponent {
376
+ items = input.required();
377
+ title = input('');
378
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: FaqComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
379
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: FaqComponent, isStandalone: true, selector: "cc-faq", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (title()) {\n <h2 class=\"cc-faq__title\">{{ title() }}</h2>\n}\n@for (item of items(); track item.question) {\n <details class=\"cc-faq__item\">\n <summary>{{ item.question }}</summary>\n <p>{{ item.answer }}</p>\n </details>\n}\n", styles: [":host{display:flex;flex-direction:column;gap:1rem}.cc-faq__title{margin:0;font-family:var(--cc-font-heading, inherit);font-size:2rem;font-weight:700;color:var(--cc-faq-title-color, var(--cc-secondary-dark, #9a7521))}.cc-faq__item{padding:1rem;background:var(--cc-faq-bg, var(--cc-surface, var(--cc-secondary-light, #f8f4ee)))}summary{cursor:pointer;font-weight:600;color:var(--cc-faq-question-color, var(--cc-text, var(--cc-primary, #4c6b4a)))}p{margin:0;padding-top:.75rem;color:var(--cc-faq-answer-color, var(--cc-primary-dark, #022b04))}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
380
+ }
381
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: FaqComponent, decorators: [{
382
+ type: Component,
383
+ args: [{ selector: 'cc-faq', changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (title()) {\n <h2 class=\"cc-faq__title\">{{ title() }}</h2>\n}\n@for (item of items(); track item.question) {\n <details class=\"cc-faq__item\">\n <summary>{{ item.question }}</summary>\n <p>{{ item.answer }}</p>\n </details>\n}\n", styles: [":host{display:flex;flex-direction:column;gap:1rem}.cc-faq__title{margin:0;font-family:var(--cc-font-heading, inherit);font-size:2rem;font-weight:700;color:var(--cc-faq-title-color, var(--cc-secondary-dark, #9a7521))}.cc-faq__item{padding:1rem;background:var(--cc-faq-bg, var(--cc-surface, var(--cc-secondary-light, #f8f4ee)))}summary{cursor:pointer;font-weight:600;color:var(--cc-faq-question-color, var(--cc-text, var(--cc-primary, #4c6b4a)))}p{margin:0;padding-top:.75rem;color:var(--cc-faq-answer-color, var(--cc-primary-dark, #022b04))}\n"] }]
384
+ }] });
385
+
386
+ /**
387
+ * Row of social network icons that open in a new tab.
388
+ *
389
+ * ```html
390
+ * <cc-social-links [links]="socials" size="1.25rem" />
391
+ * ```
392
+ */
393
+ class SocialLinksComponent {
394
+ links = input.required();
395
+ /** Icon width, any CSS length. */
396
+ size = input('1rem');
397
+ gap = input('1.25rem');
398
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SocialLinksComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
399
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: SocialLinksComponent, isStandalone: true, selector: "cc-social-links", inputs: { links: { classPropertyName: "links", publicName: "links", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ul class=\"cc-social\" [style.gap]=\"gap()\">\n @for (link of links(); track link.href) {\n <li>\n <a [href]=\"link.href\" target=\"_blank\" rel=\"noopener\" [attr.aria-label]=\"link.label\">\n <img [src]=\"link.iconSrc\" alt=\"\" [style.width]=\"size()\" />\n </a>\n </li>\n }\n</ul>\n", styles: [":host{display:block}.cc-social{display:flex;align-items:center;margin:0;padding:0;list-style:none}a{display:block;transition:transform .15s ease-in}a:hover{transform:scale(1.1)}img{display:block;height:auto}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
400
+ }
401
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SocialLinksComponent, decorators: [{
402
+ type: Component,
403
+ args: [{ selector: 'cc-social-links', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ul class=\"cc-social\" [style.gap]=\"gap()\">\n @for (link of links(); track link.href) {\n <li>\n <a [href]=\"link.href\" target=\"_blank\" rel=\"noopener\" [attr.aria-label]=\"link.label\">\n <img [src]=\"link.iconSrc\" alt=\"\" [style.width]=\"size()\" />\n </a>\n </li>\n }\n</ul>\n", styles: [":host{display:block}.cc-social{display:flex;align-items:center;margin:0;padding:0;list-style:none}a{display:block;transition:transform .15s ease-in}a:hover{transform:scale(1.1)}img{display:block;height:auto}\n"] }]
404
+ }] });
405
+
406
+ /**
407
+ * Plan card: image, name and a button.
408
+ * With `link` the button navigates with the router; without it, it emits `ctaClick`.
409
+ *
410
+ * ```html
411
+ * <cc-plan-card [name]="plan.name" [imageSrc]="plan.imgPath" [link]="['/planes', slug]" />
412
+ * ```
413
+ */
414
+ class PlanCardComponent {
415
+ name = input.required();
416
+ imageSrc = input.required();
417
+ /** Defaults to `name`. */
418
+ imageAlt = input('');
419
+ /** Optional line under the name, for example the formatted price. */
420
+ subtitle = input('');
421
+ link = input(null);
422
+ ctaLabel = input('Saber más');
423
+ ctaClick = output();
424
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
425
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: PlanCardComponent, isStandalone: true, selector: "cc-plan-card", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, imageSrc: { classPropertyName: "imageSrc", publicName: "imageSrc", isSignal: true, isRequired: true, transformFunction: null }, imageAlt: { classPropertyName: "imageAlt", publicName: "imageAlt", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null }, ctaLabel: { classPropertyName: "ctaLabel", publicName: "ctaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { ctaClick: "ctaClick" }, ngImport: i0, template: "<article class=\"cc-card\">\n <img\n class=\"cc-card__img\"\n [src]=\"imageSrc()\"\n [alt]=\"imageAlt() || name()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n <h2 class=\"cc-card__name\">{{ name() }}</h2>\n @if (subtitle()) {\n <p class=\"cc-card__subtitle\">{{ subtitle() }}</p>\n }\n @if (link()) {\n <a class=\"cc-card__cta\" [routerLink]=\"link()\" (click)=\"ctaClick.emit()\">{{ ctaLabel() }}</a>\n } @else {\n <button type=\"button\" class=\"cc-card__cta\" (click)=\"ctaClick.emit()\">{{ ctaLabel() }}</button>\n }\n</article>\n", styles: [":host{display:block;width:100%;max-width:var(--cc-plan-card-width, 22rem)}.cc-card{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;justify-content:space-around;width:100%;height:var(--cc-plan-card-height, 24rem);padding:1.25rem;background:var(--cc-plan-card-bg, var(--cc-surface, var(--cc-secondary-light, #f8f4ee)));border-radius:var(--cc-radius, 0);box-shadow:var(--cc-shadow, 0 4px 6px -1px rgb(0 0 0 / .1))}@media (min-width: 1024px){:host{max-width:var(--cc-plan-card-width-lg, 15rem)}}.cc-card__img{width:13rem;max-width:100%;box-shadow:var(--cc-shadow, 0 4px 6px -1px rgb(0 0 0 / .1))}.cc-card__name{margin:0;font-size:1rem;font-weight:400;text-align:center;color:var(--cc-plan-card-color, inherit)}.cc-card__subtitle{margin:0;font-weight:600;color:var(--cc-heading, var(--cc-primary, #4c6b4a))}.cc-card__cta{padding:.5rem 1.5rem;border:0;font:inherit;text-decoration:none;color:var(--cc-on-accent, var(--cc-bg, #f0ffef));background:var(--cc-plan-card-cta-bg, var(--cc-accent, var(--cc-secondary, #ceab5d)));cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color .3s ease-in,color .3s ease-in}.cc-card__cta:hover{background:var(--cc-plan-card-cta-hover-bg, var(--cc-accent-hover, var(--cc-secondary-dark, #9a7521)))}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
426
+ }
427
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanCardComponent, decorators: [{
428
+ type: Component,
429
+ args: [{ selector: 'cc-plan-card', imports: [RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, template: "<article class=\"cc-card\">\n <img\n class=\"cc-card__img\"\n [src]=\"imageSrc()\"\n [alt]=\"imageAlt() || name()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n <h2 class=\"cc-card__name\">{{ name() }}</h2>\n @if (subtitle()) {\n <p class=\"cc-card__subtitle\">{{ subtitle() }}</p>\n }\n @if (link()) {\n <a class=\"cc-card__cta\" [routerLink]=\"link()\" (click)=\"ctaClick.emit()\">{{ ctaLabel() }}</a>\n } @else {\n <button type=\"button\" class=\"cc-card__cta\" (click)=\"ctaClick.emit()\">{{ ctaLabel() }}</button>\n }\n</article>\n", styles: [":host{display:block;width:100%;max-width:var(--cc-plan-card-width, 22rem)}.cc-card{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;justify-content:space-around;width:100%;height:var(--cc-plan-card-height, 24rem);padding:1.25rem;background:var(--cc-plan-card-bg, var(--cc-surface, var(--cc-secondary-light, #f8f4ee)));border-radius:var(--cc-radius, 0);box-shadow:var(--cc-shadow, 0 4px 6px -1px rgb(0 0 0 / .1))}@media (min-width: 1024px){:host{max-width:var(--cc-plan-card-width-lg, 15rem)}}.cc-card__img{width:13rem;max-width:100%;box-shadow:var(--cc-shadow, 0 4px 6px -1px rgb(0 0 0 / .1))}.cc-card__name{margin:0;font-size:1rem;font-weight:400;text-align:center;color:var(--cc-plan-card-color, inherit)}.cc-card__subtitle{margin:0;font-weight:600;color:var(--cc-heading, var(--cc-primary, #4c6b4a))}.cc-card__cta{padding:.5rem 1.5rem;border:0;font:inherit;text-decoration:none;color:var(--cc-on-accent, var(--cc-bg, #f0ffef));background:var(--cc-plan-card-cta-bg, var(--cc-accent, var(--cc-secondary, #ceab5d)));cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color .3s ease-in,color .3s ease-in}.cc-card__cta:hover{background:var(--cc-plan-card-cta-hover-bg, var(--cc-accent-hover, var(--cc-secondary-dark, #9a7521)))}\n"] }]
430
+ }] });
431
+
432
+ /**
433
+ * Vertical category list with a single selected option.
434
+ * Replaces the `appElementActive` markup the sites used for plan categories.
435
+ *
436
+ * ```html
437
+ * <cc-category-menu [options]="categories" [(selected)]="category" />
438
+ * ```
439
+ */
440
+ class CategoryMenuComponent {
441
+ options = input.required();
442
+ /** Selected value. Use `[(selected)]` to bind both ways. */
443
+ selected = model(null);
444
+ title = input('CATEGORÍA DE PLANES');
445
+ select(value) {
446
+ this.selected.set(value);
447
+ }
448
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CategoryMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
449
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: CategoryMenuComponent, isStandalone: true, selector: "cc-category-menu", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selectedChange" }, ngImport: i0, template: "<ul class=\"cc-cat\" role=\"listbox\" [attr.aria-label]=\"title() || null\">\n @if (title()) {\n <li class=\"cc-cat__title\" role=\"presentation\">{{ title() }}</li>\n }\n @for (option of options(); track option.value) {\n <li\n class=\"cc-cat__item\"\n role=\"option\"\n tabindex=\"0\"\n [class.cc-cat__item--active]=\"option.value === selected()\"\n [attr.aria-selected]=\"option.value === selected()\"\n (click)=\"select(option.value)\"\n (keydown.enter)=\"select(option.value)\"\n (keydown.space)=\"$event.preventDefault(); select(option.value)\"\n >\n <span>{{ option.label }}</span>\n @if (option.count !== undefined && option.count !== null) {\n <small>{{ option.count }}</small>\n }\n </li>\n }\n</ul>\n", styles: [":host{display:block}.cc-cat{width:100%;margin:0;padding:0;list-style:none}.cc-cat__title{padding:2rem .75rem;font-weight:600;color:var(--cc-category-color, var(--cc-text, var(--cc-primary, #4c6b4a)))}.cc-cat__item{display:flex;align-items:center;justify-content:space-between;height:4rem;padding:0 1.25rem;color:var(--cc-category-color, var(--cc-text, var(--cc-primary, #4c6b4a)));cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color .15s,color .15s}.cc-cat__item:hover{background:var(--cc-category-hover-bg, rgb(0 0 0 / .04))}.cc-cat__item:focus-visible{outline:2px solid var(--cc-accent, var(--cc-secondary, #ceab5d));outline-offset:-2px}.cc-cat__item--active{font-weight:700;color:var(--cc-category-active-color, var(--cc-on-accent, var(--cc-bg, #f0ffef)));background:var(--cc-category-active-bg, var(--cc-accent, var(--cc-secondary, #ceab5d)))}.cc-cat__item--active:hover{background:var(--cc-category-active-bg, var(--cc-accent, var(--cc-secondary, #ceab5d)))}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
450
+ }
451
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CategoryMenuComponent, decorators: [{
452
+ type: Component,
453
+ args: [{ selector: 'cc-category-menu', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ul class=\"cc-cat\" role=\"listbox\" [attr.aria-label]=\"title() || null\">\n @if (title()) {\n <li class=\"cc-cat__title\" role=\"presentation\">{{ title() }}</li>\n }\n @for (option of options(); track option.value) {\n <li\n class=\"cc-cat__item\"\n role=\"option\"\n tabindex=\"0\"\n [class.cc-cat__item--active]=\"option.value === selected()\"\n [attr.aria-selected]=\"option.value === selected()\"\n (click)=\"select(option.value)\"\n (keydown.enter)=\"select(option.value)\"\n (keydown.space)=\"$event.preventDefault(); select(option.value)\"\n >\n <span>{{ option.label }}</span>\n @if (option.count !== undefined && option.count !== null) {\n <small>{{ option.count }}</small>\n }\n </li>\n }\n</ul>\n", styles: [":host{display:block}.cc-cat{width:100%;margin:0;padding:0;list-style:none}.cc-cat__title{padding:2rem .75rem;font-weight:600;color:var(--cc-category-color, var(--cc-text, var(--cc-primary, #4c6b4a)))}.cc-cat__item{display:flex;align-items:center;justify-content:space-between;height:4rem;padding:0 1.25rem;color:var(--cc-category-color, var(--cc-text, var(--cc-primary, #4c6b4a)));cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color .15s,color .15s}.cc-cat__item:hover{background:var(--cc-category-hover-bg, rgb(0 0 0 / .04))}.cc-cat__item:focus-visible{outline:2px solid var(--cc-accent, var(--cc-secondary, #ceab5d));outline-offset:-2px}.cc-cat__item--active{font-weight:700;color:var(--cc-category-active-color, var(--cc-on-accent, var(--cc-bg, #f0ffef)));background:var(--cc-category-active-bg, var(--cc-accent, var(--cc-secondary, #ceab5d)))}.cc-cat__item--active:hover{background:var(--cc-category-active-bg, var(--cc-accent, var(--cc-secondary, #ceab5d)))}\n"] }]
454
+ }] });
455
+
456
+ /**
457
+ * Text field with a search button. Emits `search` on Enter or on the button.
458
+ *
459
+ * ```html
460
+ * <cc-search-box placeholder="Buscar plan" [(value)]="query" (search)="onSearch($event)" />
461
+ * ```
462
+ */
463
+ class SearchBoxComponent {
464
+ value = model('');
465
+ placeholder = input('Buscar plan');
466
+ buttonLabel = input('Buscar');
467
+ iconSrc = input('');
468
+ search = output();
469
+ onInput(event) {
470
+ this.value.set(event.target.value);
471
+ }
472
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SearchBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
473
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: SearchBoxComponent, isStandalone: true, selector: "cc-search-box", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, buttonLabel: { classPropertyName: "buttonLabel", publicName: "buttonLabel", isSignal: true, isRequired: false, transformFunction: null }, iconSrc: { classPropertyName: "iconSrc", publicName: "iconSrc", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", search: "search" }, ngImport: i0, template: "<form class=\"cc-search\" role=\"search\" (submit)=\"$event.preventDefault(); search.emit(value())\">\n <input\n type=\"search\"\n class=\"cc-search__input\"\n [placeholder]=\"placeholder()\"\n [attr.aria-label]=\"placeholder()\"\n [value]=\"value()\"\n (input)=\"onInput($event)\"\n />\n <button type=\"submit\" class=\"cc-search__btn\" [attr.aria-label]=\"buttonLabel()\">\n @if (iconSrc()) {\n <img [src]=\"iconSrc()\" alt=\"\" />\n } @else {\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <circle cx=\"11\" cy=\"11\" r=\"7\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <path d=\"m20 20-3.5-3.5\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\n </svg>\n }\n </button>\n</form>\n", styles: [":host{display:block}.cc-search{display:flex;width:100%;margin:0}.cc-search__input{flex:1;min-width:0;padding:0 .75rem;border:2px solid var(--cc-search-border, #e5e7eb);font:inherit;color:inherit;background:#fff}.cc-search__input:focus{outline:none;border-color:var(--cc-accent, var(--cc-secondary, #ceab5d))}.cc-search__btn{display:flex;align-items:center;justify-content:center;width:3rem;min-height:3rem;padding:0;border:0;color:var(--cc-on-accent, var(--cc-bg, #f0ffef));background:var(--cc-search-btn-bg, var(--cc-accent, var(--cc-secondary, #ceab5d)));cursor:pointer}.cc-search__btn:has(img){background:transparent}.cc-search__btn img{width:3rem}.cc-search__btn svg{width:1.5rem;height:1.5rem}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
474
+ }
475
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SearchBoxComponent, decorators: [{
476
+ type: Component,
477
+ args: [{ selector: 'cc-search-box', changeDetection: ChangeDetectionStrategy.OnPush, template: "<form class=\"cc-search\" role=\"search\" (submit)=\"$event.preventDefault(); search.emit(value())\">\n <input\n type=\"search\"\n class=\"cc-search__input\"\n [placeholder]=\"placeholder()\"\n [attr.aria-label]=\"placeholder()\"\n [value]=\"value()\"\n (input)=\"onInput($event)\"\n />\n <button type=\"submit\" class=\"cc-search__btn\" [attr.aria-label]=\"buttonLabel()\">\n @if (iconSrc()) {\n <img [src]=\"iconSrc()\" alt=\"\" />\n } @else {\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <circle cx=\"11\" cy=\"11\" r=\"7\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <path d=\"m20 20-3.5-3.5\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\n </svg>\n }\n </button>\n</form>\n", styles: [":host{display:block}.cc-search{display:flex;width:100%;margin:0}.cc-search__input{flex:1;min-width:0;padding:0 .75rem;border:2px solid var(--cc-search-border, #e5e7eb);font:inherit;color:inherit;background:#fff}.cc-search__input:focus{outline:none;border-color:var(--cc-accent, var(--cc-secondary, #ceab5d))}.cc-search__btn{display:flex;align-items:center;justify-content:center;width:3rem;min-height:3rem;padding:0;border:0;color:var(--cc-on-accent, var(--cc-bg, #f0ffef));background:var(--cc-search-btn-bg, var(--cc-accent, var(--cc-secondary, #ceab5d)));cursor:pointer}.cc-search__btn:has(img){background:transparent}.cc-search__btn img{width:3rem}.cc-search__btn svg{width:1.5rem;height:1.5rem}\n"] }]
478
+ }] });
479
+
480
+ /**
481
+ * Filter by included service and price range. The options come in as inputs,
482
+ * so the form does not load data by itself.
483
+ *
484
+ * ```html
485
+ * <cc-plan-filter-form [services]="services" [priceRanges]="ranges" (filterChange)="apply($event)" />
486
+ * ```
487
+ */
488
+ class PlanFilterFormComponent {
489
+ services = input([]);
490
+ priceRanges = input([]);
491
+ serviceLabel = input('Servicio incluido');
492
+ priceLabel = input('Rango de precio');
493
+ anyLabel = input('Todos');
494
+ submitLabel = input('Filtrar');
495
+ resetLabel = input('Limpiar');
496
+ /** Emits on submit, and with an empty filter on reset. */
497
+ filterChange = output();
498
+ serviceIndex = signal(-1);
499
+ rangeIndex = signal(-1);
500
+ toIndex(event) {
501
+ return Number(event.target.value);
502
+ }
503
+ submit() {
504
+ this.filterChange.emit({
505
+ service: this.services()[this.serviceIndex()] ?? null,
506
+ priceRange: this.priceRanges()[this.rangeIndex()] ?? null,
507
+ });
508
+ }
509
+ reset() {
510
+ this.serviceIndex.set(-1);
511
+ this.rangeIndex.set(-1);
512
+ this.filterChange.emit({ service: null, priceRange: null });
513
+ }
514
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanFilterFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
515
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: PlanFilterFormComponent, isStandalone: true, selector: "cc-plan-filter-form", inputs: { services: { classPropertyName: "services", publicName: "services", isSignal: true, isRequired: false, transformFunction: null }, priceRanges: { classPropertyName: "priceRanges", publicName: "priceRanges", isSignal: true, isRequired: false, transformFunction: null }, serviceLabel: { classPropertyName: "serviceLabel", publicName: "serviceLabel", isSignal: true, isRequired: false, transformFunction: null }, priceLabel: { classPropertyName: "priceLabel", publicName: "priceLabel", isSignal: true, isRequired: false, transformFunction: null }, anyLabel: { classPropertyName: "anyLabel", publicName: "anyLabel", isSignal: true, isRequired: false, transformFunction: null }, submitLabel: { classPropertyName: "submitLabel", publicName: "submitLabel", isSignal: true, isRequired: false, transformFunction: null }, resetLabel: { classPropertyName: "resetLabel", publicName: "resetLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filterChange: "filterChange" }, ngImport: i0, template: "<form class=\"cc-filter\" (submit)=\"$event.preventDefault(); submit()\">\n @if (services().length) {\n <label class=\"cc-filter__field\">\n <span>{{ serviceLabel() }}</span>\n <select (change)=\"serviceIndex.set(toIndex($event))\">\n <option value=\"-1\" [selected]=\"serviceIndex() === -1\">{{ anyLabel() }}</option>\n @for (service of services(); track $index) {\n <option [value]=\"$index\" [selected]=\"serviceIndex() === $index\">{{ service.name }}</option>\n }\n </select>\n </label>\n }\n @if (priceRanges().length) {\n <label class=\"cc-filter__field\">\n <span>{{ priceLabel() }}</span>\n <select (change)=\"rangeIndex.set(toIndex($event))\">\n <option value=\"-1\" [selected]=\"rangeIndex() === -1\">{{ anyLabel() }}</option>\n @for (range of priceRanges(); track $index) {\n <option [value]=\"$index\" [selected]=\"rangeIndex() === $index\">{{ range.description }}</option>\n }\n </select>\n </label>\n }\n <div class=\"cc-filter__actions\">\n <button type=\"submit\" class=\"cc-filter__submit\">{{ submitLabel() }}</button>\n <button type=\"button\" class=\"cc-filter__reset\" (click)=\"reset()\">{{ resetLabel() }}</button>\n </div>\n</form>\n", styles: [":host{display:block}.cc-filter{display:flex;flex-direction:column;gap:1rem;padding:1.25rem}.cc-filter__field{display:flex;flex-direction:column;gap:.375rem;font-weight:600;color:var(--cc-text, var(--cc-primary, #4c6b4a))}select{height:2.5rem;padding:0 .5rem;border:2px solid var(--cc-search-border, #e5e7eb);font:inherit;font-weight:400;color:inherit;background:#fff}.cc-filter__actions{display:flex;gap:.75rem}button{padding:.5rem 1.25rem;border:0;font:inherit;cursor:pointer;transition:background-color .15s,color .15s}.cc-filter__submit{color:var(--cc-on-accent, var(--cc-bg, #f0ffef));background:var(--cc-accent, var(--cc-secondary, #ceab5d))}.cc-filter__submit:hover{background:var(--cc-accent-hover, var(--cc-secondary-dark, #9a7521))}.cc-filter__reset{color:var(--cc-text, var(--cc-primary, #4c6b4a));background:transparent;text-decoration:underline}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
516
+ }
517
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanFilterFormComponent, decorators: [{
518
+ type: Component,
519
+ args: [{ selector: 'cc-plan-filter-form', changeDetection: ChangeDetectionStrategy.OnPush, template: "<form class=\"cc-filter\" (submit)=\"$event.preventDefault(); submit()\">\n @if (services().length) {\n <label class=\"cc-filter__field\">\n <span>{{ serviceLabel() }}</span>\n <select (change)=\"serviceIndex.set(toIndex($event))\">\n <option value=\"-1\" [selected]=\"serviceIndex() === -1\">{{ anyLabel() }}</option>\n @for (service of services(); track $index) {\n <option [value]=\"$index\" [selected]=\"serviceIndex() === $index\">{{ service.name }}</option>\n }\n </select>\n </label>\n }\n @if (priceRanges().length) {\n <label class=\"cc-filter__field\">\n <span>{{ priceLabel() }}</span>\n <select (change)=\"rangeIndex.set(toIndex($event))\">\n <option value=\"-1\" [selected]=\"rangeIndex() === -1\">{{ anyLabel() }}</option>\n @for (range of priceRanges(); track $index) {\n <option [value]=\"$index\" [selected]=\"rangeIndex() === $index\">{{ range.description }}</option>\n }\n </select>\n </label>\n }\n <div class=\"cc-filter__actions\">\n <button type=\"submit\" class=\"cc-filter__submit\">{{ submitLabel() }}</button>\n <button type=\"button\" class=\"cc-filter__reset\" (click)=\"reset()\">{{ resetLabel() }}</button>\n </div>\n</form>\n", styles: [":host{display:block}.cc-filter{display:flex;flex-direction:column;gap:1rem;padding:1.25rem}.cc-filter__field{display:flex;flex-direction:column;gap:.375rem;font-weight:600;color:var(--cc-text, var(--cc-primary, #4c6b4a))}select{height:2.5rem;padding:0 .5rem;border:2px solid var(--cc-search-border, #e5e7eb);font:inherit;font-weight:400;color:inherit;background:#fff}.cc-filter__actions{display:flex;gap:.75rem}button{padding:.5rem 1.25rem;border:0;font:inherit;cursor:pointer;transition:background-color .15s,color .15s}.cc-filter__submit{color:var(--cc-on-accent, var(--cc-bg, #f0ffef));background:var(--cc-accent, var(--cc-secondary, #ceab5d))}.cc-filter__submit:hover{background:var(--cc-accent-hover, var(--cc-secondary-dark, #9a7521))}.cc-filter__reset{color:var(--cc-text, var(--cc-primary, #4c6b4a));background:transparent;text-decoration:underline}\n"] }]
520
+ }] });
521
+
522
+ /** Marks an `<ng-template>` inside `cc-plan-catalog` as the card to render for each plan. */
523
+ class PlanCardTemplateDirective {
524
+ static ngTemplateContextGuard(_dir, ctx) {
525
+ return true;
526
+ }
527
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanCardTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
528
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: PlanCardTemplateDirective, isStandalone: true, selector: "ng-template[ccPlanCard]", ngImport: i0 });
529
+ }
530
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanCardTemplateDirective, decorators: [{
531
+ type: Directive,
532
+ args: [{ selector: 'ng-template[ccPlanCard]' }]
533
+ }] });
534
+ /**
535
+ * Full plan list page: title, name search, category menu, filter and card grid.
536
+ * It only needs the plans; the rest has Spanish defaults you can override.
537
+ *
538
+ * Selecting a category shows its plans. Searching by name or applying the filter
539
+ * looks across every category, as the sites already did.
540
+ *
541
+ * ```html
542
+ * <cc-plan-catalog
543
+ * [plans]="plans()"
544
+ * [services]="services()"
545
+ * [priceRanges]="ranges()"
546
+ * imageAltSuffix=" - Laurel Spa Medellín"
547
+ * />
548
+ * ```
549
+ */
550
+ class PlanCatalogComponent {
551
+ plans = input.required();
552
+ categories = input(DEFAULT_PLAN_CATEGORIES);
553
+ initialCategory = input(PlanCategory.Individual);
554
+ /** Shows how many plans each category has, counted from `plans`. */
555
+ showCounts = input(true);
556
+ /** Options for the filter form. The filter is hidden when both are empty. */
557
+ services = input([]);
558
+ priceRanges = input([]);
559
+ /** Where each card links to. Defaults to `/planes/<slug>`. */
560
+ detailsLink = input((plan) => ['/planes', planSlug(plan)]);
561
+ showBanner = input(true);
562
+ showSearch = input(true);
563
+ titlePrefix = input('Reserva tu plan');
564
+ resultsTitle = input('Resultados');
565
+ categoriesTitle = input('CATEGORÍA DE PLANES');
566
+ searchPlaceholder = input('Buscar plan');
567
+ searchIconSrc = input('');
568
+ filterTitle = input('Filtrar');
569
+ filterIconSrc = input('');
570
+ ctaLabel = input('Saber más');
571
+ /** Added after the plan name in each image's alt text. */
572
+ imageAltSuffix = input('');
573
+ emptyMessage = input('No encontramos planes con esos criterios.');
574
+ categoryChange = output();
575
+ cardTemplate = contentChild(PlanCardTemplateDirective, { read: TemplateRef });
576
+ category = signal(null);
577
+ query = signal('');
578
+ filter = signal(null);
579
+ filterOpen = signal(false);
580
+ /** Set when the user searched or filtered; results then cover every category. */
581
+ searching = signal(false);
582
+ /** Category that was open before searching, to return to it when the search is cleared. */
583
+ lastCategory = null;
584
+ constructor() {
585
+ // Apply `initialCategory` until the user picks one.
586
+ effect(() => {
587
+ const initial = this.initialCategory();
588
+ untracked(() => {
589
+ if (!this.searching())
590
+ this.category.set(initial);
591
+ });
592
+ });
593
+ }
594
+ categoryOptions = computed(() => this.showCounts() ? withCategoryCounts(this.categories(), this.plans()) : this.categories());
595
+ hasFilter = computed(() => this.services().length > 0 || this.priceRanges().length > 0);
596
+ visiblePlans = computed(() => {
597
+ const plans = this.plans();
598
+ if (!this.searching()) {
599
+ const category = this.category();
600
+ return category === null ? plans : filterPlansByCategory(plans, category);
601
+ }
602
+ const byName = searchPlansByName(plans, this.query());
603
+ const filter = this.filter();
604
+ return filter ? filterPlans(byName, filter) : byName;
605
+ });
606
+ title = computed(() => {
607
+ if (this.searching())
608
+ return `${this.titlePrefix()} - ${this.resultsTitle()}`;
609
+ const option = this.categories().find((c) => c.value === this.category());
610
+ return option ? `${this.titlePrefix()} - ${option.label}` : this.titlePrefix();
611
+ });
612
+ selectCategory(value) {
613
+ if (value === null)
614
+ return;
615
+ this.searching.set(false);
616
+ this.query.set('');
617
+ this.filter.set(null);
618
+ this.category.set(value);
619
+ this.categoryChange.emit(value);
620
+ }
621
+ onSearch(query) {
622
+ this.query.set(query);
623
+ if (!query.trim() && !this.filter()) {
624
+ this.selectCategory(this.lastCategory ?? this.initialCategory());
625
+ return;
626
+ }
627
+ this.startSearch();
628
+ }
629
+ onFilter(filter) {
630
+ const empty = !filter.service && !filter.priceRange;
631
+ this.filter.set(empty ? null : filter);
632
+ if (empty && !this.query().trim()) {
633
+ this.selectCategory(this.lastCategory ?? this.initialCategory());
634
+ return;
635
+ }
636
+ this.startSearch();
637
+ }
638
+ linkFor(plan) {
639
+ return this.detailsLink()(plan);
640
+ }
641
+ startSearch() {
642
+ if (!this.searching())
643
+ this.lastCategory = this.category();
644
+ this.searching.set(true);
645
+ this.category.set(null);
646
+ }
647
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanCatalogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
648
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: PlanCatalogComponent, isStandalone: true, selector: "cc-plan-catalog", inputs: { plans: { classPropertyName: "plans", publicName: "plans", isSignal: true, isRequired: true, transformFunction: null }, categories: { classPropertyName: "categories", publicName: "categories", isSignal: true, isRequired: false, transformFunction: null }, initialCategory: { classPropertyName: "initialCategory", publicName: "initialCategory", isSignal: true, isRequired: false, transformFunction: null }, showCounts: { classPropertyName: "showCounts", publicName: "showCounts", isSignal: true, isRequired: false, transformFunction: null }, services: { classPropertyName: "services", publicName: "services", isSignal: true, isRequired: false, transformFunction: null }, priceRanges: { classPropertyName: "priceRanges", publicName: "priceRanges", isSignal: true, isRequired: false, transformFunction: null }, detailsLink: { classPropertyName: "detailsLink", publicName: "detailsLink", isSignal: true, isRequired: false, transformFunction: null }, showBanner: { classPropertyName: "showBanner", publicName: "showBanner", isSignal: true, isRequired: false, transformFunction: null }, showSearch: { classPropertyName: "showSearch", publicName: "showSearch", isSignal: true, isRequired: false, transformFunction: null }, titlePrefix: { classPropertyName: "titlePrefix", publicName: "titlePrefix", isSignal: true, isRequired: false, transformFunction: null }, resultsTitle: { classPropertyName: "resultsTitle", publicName: "resultsTitle", isSignal: true, isRequired: false, transformFunction: null }, categoriesTitle: { classPropertyName: "categoriesTitle", publicName: "categoriesTitle", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, searchIconSrc: { classPropertyName: "searchIconSrc", publicName: "searchIconSrc", isSignal: true, isRequired: false, transformFunction: null }, filterTitle: { classPropertyName: "filterTitle", publicName: "filterTitle", isSignal: true, isRequired: false, transformFunction: null }, filterIconSrc: { classPropertyName: "filterIconSrc", publicName: "filterIconSrc", isSignal: true, isRequired: false, transformFunction: null }, ctaLabel: { classPropertyName: "ctaLabel", publicName: "ctaLabel", isSignal: true, isRequired: false, transformFunction: null }, imageAltSuffix: { classPropertyName: "imageAltSuffix", publicName: "imageAltSuffix", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { categoryChange: "categoryChange" }, queries: [{ propertyName: "cardTemplate", first: true, predicate: PlanCardTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "@if (showBanner()) {\n <cc-page-banner [title]=\"title()\" />\n}\n\n<div class=\"cc-catalog\">\n <aside class=\"cc-catalog__side\">\n @if (showSearch()) {\n <cc-search-box\n [placeholder]=\"searchPlaceholder()\"\n [iconSrc]=\"searchIconSrc()\"\n [(value)]=\"query\"\n (search)=\"onSearch($event)\"\n />\n }\n\n <div class=\"cc-catalog__panel\">\n <cc-category-menu\n [title]=\"categoriesTitle()\"\n [options]=\"categoryOptions()\"\n [selected]=\"category()\"\n (selectedChange)=\"selectCategory($event)\"\n />\n\n @if (hasFilter()) {\n <hr class=\"cc-catalog__rule\" />\n <button\n type=\"button\"\n class=\"cc-catalog__filter-toggle\"\n [attr.aria-expanded]=\"filterOpen()\"\n (click)=\"filterOpen.set(!filterOpen())\"\n >\n <span>{{ filterTitle() }}</span>\n @if (filterIconSrc()) {\n <img [src]=\"filterIconSrc()\" alt=\"\" />\n } @else {\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path d=\"M3 5h18l-7 8v6l-4-2v-4L3 5Z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\" />\n </svg>\n }\n </button>\n @if (filterOpen()) {\n <cc-plan-filter-form\n [services]=\"services()\"\n [priceRanges]=\"priceRanges()\"\n (filterChange)=\"onFilter($event)\"\n />\n }\n }\n </div>\n </aside>\n\n @if (visiblePlans().length) {\n <div class=\"cc-catalog__grid\">\n @for (plan of visiblePlans(); track plan.id) {\n @if (cardTemplate(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: plan, link: linkFor(plan) }\" />\n } @else {\n <cc-plan-card\n [name]=\"plan.name\"\n [imageSrc]=\"plan.imgPath\"\n [imageAlt]=\"plan.name + imageAltSuffix()\"\n [link]=\"linkFor(plan)\"\n [ctaLabel]=\"ctaLabel()\"\n />\n }\n }\n </div>\n } @else {\n <p class=\"cc-catalog__empty\">{{ emptyMessage() }}</p>\n }\n</div>\n", styles: [":host{display:block}.cc-catalog{display:flex;flex-direction:column;gap:4rem}.cc-catalog__side{display:flex;flex-direction:column;width:100%;overflow:hidden}.cc-catalog__panel{width:100%;box-sizing:border-box;padding:1.25rem;background:var(--cc-catalog-panel-bg, var(--cc-surface, var(--cc-secondary-light, #f8f4ee)))}.cc-catalog__rule{height:.1rem;margin:0 0 1.5rem;border:0;background:#d1d5db}.cc-catalog__filter-toggle{display:flex;align-items:center;justify-content:space-between;width:100%;padding:0 1.25rem;border:0;font:inherit;font-weight:700;color:var(--cc-text, var(--cc-primary, #4c6b4a));background:transparent;cursor:pointer}.cc-catalog__filter-toggle img,.cc-catalog__filter-toggle svg{width:1.75rem;height:1.75rem}.cc-catalog__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--cc-catalog-column-min, 15rem)),1fr));gap:2rem 1.5rem;width:100%;height:auto;place-items:center;align-content:start}.cc-catalog__empty{width:100%;margin:0;font-size:1.5rem;text-align:center;color:var(--cc-accent, var(--cc-secondary, #ceab5d))}@media (min-width: 1024px){.cc-catalog{flex-direction:row}.cc-catalog__side{flex:none;width:24rem}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: PageBannerComponent, selector: "cc-page-banner", inputs: ["title", "variant", "iconSrc", "backLink", "backIconSrc", "backLabel"] }, { kind: "component", type: SearchBoxComponent, selector: "cc-search-box", inputs: ["value", "placeholder", "buttonLabel", "iconSrc"], outputs: ["valueChange", "search"] }, { kind: "component", type: CategoryMenuComponent, selector: "cc-category-menu", inputs: ["options", "selected", "title"], outputs: ["selectedChange"] }, { kind: "component", type: PlanFilterFormComponent, selector: "cc-plan-filter-form", inputs: ["services", "priceRanges", "serviceLabel", "priceLabel", "anyLabel", "submitLabel", "resetLabel"], outputs: ["filterChange"] }, { kind: "component", type: PlanCardComponent, selector: "cc-plan-card", inputs: ["name", "imageSrc", "imageAlt", "subtitle", "link", "ctaLabel"], outputs: ["ctaClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
649
+ }
650
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanCatalogComponent, decorators: [{
651
+ type: Component,
652
+ args: [{ selector: 'cc-plan-catalog', imports: [
653
+ NgTemplateOutlet,
654
+ PageBannerComponent,
655
+ SearchBoxComponent,
656
+ CategoryMenuComponent,
657
+ PlanFilterFormComponent,
658
+ PlanCardComponent,
659
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (showBanner()) {\n <cc-page-banner [title]=\"title()\" />\n}\n\n<div class=\"cc-catalog\">\n <aside class=\"cc-catalog__side\">\n @if (showSearch()) {\n <cc-search-box\n [placeholder]=\"searchPlaceholder()\"\n [iconSrc]=\"searchIconSrc()\"\n [(value)]=\"query\"\n (search)=\"onSearch($event)\"\n />\n }\n\n <div class=\"cc-catalog__panel\">\n <cc-category-menu\n [title]=\"categoriesTitle()\"\n [options]=\"categoryOptions()\"\n [selected]=\"category()\"\n (selectedChange)=\"selectCategory($event)\"\n />\n\n @if (hasFilter()) {\n <hr class=\"cc-catalog__rule\" />\n <button\n type=\"button\"\n class=\"cc-catalog__filter-toggle\"\n [attr.aria-expanded]=\"filterOpen()\"\n (click)=\"filterOpen.set(!filterOpen())\"\n >\n <span>{{ filterTitle() }}</span>\n @if (filterIconSrc()) {\n <img [src]=\"filterIconSrc()\" alt=\"\" />\n } @else {\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path d=\"M3 5h18l-7 8v6l-4-2v-4L3 5Z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\" />\n </svg>\n }\n </button>\n @if (filterOpen()) {\n <cc-plan-filter-form\n [services]=\"services()\"\n [priceRanges]=\"priceRanges()\"\n (filterChange)=\"onFilter($event)\"\n />\n }\n }\n </div>\n </aside>\n\n @if (visiblePlans().length) {\n <div class=\"cc-catalog__grid\">\n @for (plan of visiblePlans(); track plan.id) {\n @if (cardTemplate(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: plan, link: linkFor(plan) }\" />\n } @else {\n <cc-plan-card\n [name]=\"plan.name\"\n [imageSrc]=\"plan.imgPath\"\n [imageAlt]=\"plan.name + imageAltSuffix()\"\n [link]=\"linkFor(plan)\"\n [ctaLabel]=\"ctaLabel()\"\n />\n }\n }\n </div>\n } @else {\n <p class=\"cc-catalog__empty\">{{ emptyMessage() }}</p>\n }\n</div>\n", styles: [":host{display:block}.cc-catalog{display:flex;flex-direction:column;gap:4rem}.cc-catalog__side{display:flex;flex-direction:column;width:100%;overflow:hidden}.cc-catalog__panel{width:100%;box-sizing:border-box;padding:1.25rem;background:var(--cc-catalog-panel-bg, var(--cc-surface, var(--cc-secondary-light, #f8f4ee)))}.cc-catalog__rule{height:.1rem;margin:0 0 1.5rem;border:0;background:#d1d5db}.cc-catalog__filter-toggle{display:flex;align-items:center;justify-content:space-between;width:100%;padding:0 1.25rem;border:0;font:inherit;font-weight:700;color:var(--cc-text, var(--cc-primary, #4c6b4a));background:transparent;cursor:pointer}.cc-catalog__filter-toggle img,.cc-catalog__filter-toggle svg{width:1.75rem;height:1.75rem}.cc-catalog__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--cc-catalog-column-min, 15rem)),1fr));gap:2rem 1.5rem;width:100%;height:auto;place-items:center;align-content:start}.cc-catalog__empty{width:100%;margin:0;font-size:1.5rem;text-align:center;color:var(--cc-accent, var(--cc-secondary, #ceab5d))}@media (min-width: 1024px){.cc-catalog{flex-direction:row}.cc-catalog__side{flex:none;width:24rem}}\n"] }]
660
+ }], ctorParameters: () => [] });
661
+
662
+ /**
663
+ * Plan detail view: title with a back link, photo, price, description, duration,
664
+ * number of people, included services, and a gift box with the booking button.
665
+ * It only renders; the page loads the plan and handles SEO.
666
+ *
667
+ * ```html
668
+ * <cc-plan-details [plan]="plan" [bookingUrl]="whatsappLink" imageAltSuffix=" en Laurel Spa Medellín" />
669
+ * ```
670
+ */
671
+ class PlanDetailsComponent {
672
+ plan = input.required();
673
+ /** Link of the booking button, usually WhatsApp. */
674
+ bookingUrl = input.required();
675
+ bookingLabel = input('¡RESERVAR AHORA!');
676
+ showBanner = input(true);
677
+ backLink = input('/planes');
678
+ backLabel = input('Volver a los planes');
679
+ backIconSrc = input('');
680
+ titleIconSrc = input('');
681
+ /** Added after the plan name in the photo's alt text. */
682
+ imageAltSuffix = input('');
683
+ /** Arguments of Angular's `currency` pipe. */
684
+ currencyCode = input('COP');
685
+ currencyDisplay = input('COP$ ');
686
+ currencyDigits = input('1.0-0');
687
+ durationIconSrc = input('');
688
+ personIconSrc = input('');
689
+ peopleIconSrc = input('');
690
+ personLabel = input('persona');
691
+ peopleLabel = input('Personas');
692
+ includesLabel = input('Incluye:');
693
+ /** Gift box. It is hidden when `perks` is empty. */
694
+ perksTitle = input('Recibe con nuestro plan');
695
+ perks = input([
696
+ 'Gift Card de invitación en PDF para regalo.',
697
+ 'Decoración según la ocasión que celebres.',
698
+ ]);
699
+ perksImageSrc = input('');
700
+ perksImageAlt = input('Gift card de regalo');
701
+ people = computed(() => Math.max(this.plan().cuantity || 1, 1));
702
+ services = computed(() => this.plan().additionalServices ?? []);
703
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanDetailsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
704
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: PlanDetailsComponent, isStandalone: true, selector: "cc-plan-details", inputs: { plan: { classPropertyName: "plan", publicName: "plan", isSignal: true, isRequired: true, transformFunction: null }, bookingUrl: { classPropertyName: "bookingUrl", publicName: "bookingUrl", isSignal: true, isRequired: true, transformFunction: null }, bookingLabel: { classPropertyName: "bookingLabel", publicName: "bookingLabel", isSignal: true, isRequired: false, transformFunction: null }, showBanner: { classPropertyName: "showBanner", publicName: "showBanner", isSignal: true, isRequired: false, transformFunction: null }, backLink: { classPropertyName: "backLink", publicName: "backLink", isSignal: true, isRequired: false, transformFunction: null }, backLabel: { classPropertyName: "backLabel", publicName: "backLabel", isSignal: true, isRequired: false, transformFunction: null }, backIconSrc: { classPropertyName: "backIconSrc", publicName: "backIconSrc", isSignal: true, isRequired: false, transformFunction: null }, titleIconSrc: { classPropertyName: "titleIconSrc", publicName: "titleIconSrc", isSignal: true, isRequired: false, transformFunction: null }, imageAltSuffix: { classPropertyName: "imageAltSuffix", publicName: "imageAltSuffix", isSignal: true, isRequired: false, transformFunction: null }, currencyCode: { classPropertyName: "currencyCode", publicName: "currencyCode", isSignal: true, isRequired: false, transformFunction: null }, currencyDisplay: { classPropertyName: "currencyDisplay", publicName: "currencyDisplay", isSignal: true, isRequired: false, transformFunction: null }, currencyDigits: { classPropertyName: "currencyDigits", publicName: "currencyDigits", isSignal: true, isRequired: false, transformFunction: null }, durationIconSrc: { classPropertyName: "durationIconSrc", publicName: "durationIconSrc", isSignal: true, isRequired: false, transformFunction: null }, personIconSrc: { classPropertyName: "personIconSrc", publicName: "personIconSrc", isSignal: true, isRequired: false, transformFunction: null }, peopleIconSrc: { classPropertyName: "peopleIconSrc", publicName: "peopleIconSrc", isSignal: true, isRequired: false, transformFunction: null }, personLabel: { classPropertyName: "personLabel", publicName: "personLabel", isSignal: true, isRequired: false, transformFunction: null }, peopleLabel: { classPropertyName: "peopleLabel", publicName: "peopleLabel", isSignal: true, isRequired: false, transformFunction: null }, includesLabel: { classPropertyName: "includesLabel", publicName: "includesLabel", isSignal: true, isRequired: false, transformFunction: null }, perksTitle: { classPropertyName: "perksTitle", publicName: "perksTitle", isSignal: true, isRequired: false, transformFunction: null }, perks: { classPropertyName: "perks", publicName: "perks", isSignal: true, isRequired: false, transformFunction: null }, perksImageSrc: { classPropertyName: "perksImageSrc", publicName: "perksImageSrc", isSignal: true, isRequired: false, transformFunction: null }, perksImageAlt: { classPropertyName: "perksImageAlt", publicName: "perksImageAlt", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (showBanner()) {\n <cc-page-banner\n variant=\"plain\"\n [title]=\"plan().name\"\n [iconSrc]=\"titleIconSrc()\"\n [backLink]=\"backLink()\"\n [backLabel]=\"backLabel()\"\n [backIconSrc]=\"backIconSrc()\"\n />\n}\n\n<div class=\"cc-details\">\n <div class=\"cc-details__media\">\n <img class=\"cc-details__photo\" [src]=\"plan().imgPath\" [alt]=\"plan().name + imageAltSuffix()\" />\n <ng-content select=\"[ccDetailsMedia]\" />\n </div>\n\n <div class=\"cc-details__panel\">\n <div class=\"cc-details__info\">\n <div class=\"cc-details__price\">\n <p>{{ plan().price | currency: currencyCode() : currencyDisplay() : currencyDigits() }}</p>\n </div>\n\n <p class=\"cc-details__description\">{{ plan().description }}</p>\n\n <div class=\"cc-details__facts\">\n <div class=\"cc-details__fact\">\n @if (durationIconSrc()) {\n <img [src]=\"durationIconSrc()\" alt=\"Duraci\u00F3n\" />\n } @else {\n <svg viewBox=\"0 0 24 24\" role=\"img\" aria-label=\"Duraci\u00F3n\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <path d=\"M12 7v5l3 2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\n </svg>\n }\n <small>{{ plan().duration }}</small>\n </div>\n\n <div class=\"cc-details__fact\">\n @if (people() > 1) {\n @if (peopleIconSrc()) {\n <img [src]=\"peopleIconSrc()\" alt=\"\" />\n } @else {\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <circle cx=\"9\" cy=\"8\" r=\"3.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <circle cx=\"17\" cy=\"9\" r=\"2.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <path d=\"M2.5 20a6.5 6.5 0 0 1 13 0M15 14.5a5 5 0 0 1 6.5 4.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\n </svg>\n }\n <small>{{ people() }} {{ peopleLabel() }}</small>\n } @else {\n @if (personIconSrc()) {\n <img [src]=\"personIconSrc()\" alt=\"\" />\n } @else {\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"8\" r=\"4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <path d=\"M4.5 20a7.5 7.5 0 0 1 15 0\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\n </svg>\n }\n <small>1 {{ personLabel() }}</small>\n }\n </div>\n </div>\n\n @if (services().length) {\n <h2 class=\"cc-details__includes\">{{ includesLabel() }}</h2>\n <ul class=\"cc-details__services\">\n @for (service of services(); track $index) {\n <li>\n <img [src]=\"service.iconPath\" alt=\"\" />\n <small>{{ service.name }}</small>\n </li>\n }\n </ul>\n }\n\n <ng-content />\n\n <div class=\"cc-details__gift\" [class.cc-details__gift--solo]=\"!perks().length\">\n @if (perks().length) {\n @if (perksImageSrc()) {\n <img class=\"cc-details__gift-img\" [src]=\"perksImageSrc()\" [alt]=\"perksImageAlt()\" loading=\"lazy\" />\n }\n <div class=\"cc-details__perks\">\n <h2>{{ perksTitle() }}</h2>\n <ul>\n @for (perk of perks(); track $index) {\n <li>{{ perk }}</li>\n }\n </ul>\n </div>\n }\n <div class=\"cc-details__cta-wrap\">\n <a class=\"cc-details__cta\" [href]=\"bookingUrl()\" target=\"_blank\" rel=\"noopener\">{{ bookingLabel() }}</a>\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;color:var(--cc-text, var(--cc-primary, #4c6b4a))}.cc-details{display:flex;flex-direction:column;gap:3rem}.cc-details__media{display:flex;flex-direction:column;gap:2.5rem}.cc-details__photo{display:block;width:100%;max-width:60rem;box-shadow:var(--cc-shadow-lg, 0 10px 15px -3px rgb(0 0 0 / .1))}.cc-details__panel{background:var(--cc-details-panel-bg, var(--cc-surface, var(--cc-secondary-light, #f8f4ee)))}.cc-details__info{position:relative;box-sizing:border-box;display:flex;flex-direction:column;gap:2.5rem;width:100%;padding:5rem 2.5rem 1.25rem}.cc-details__price{position:absolute;top:0;left:0;box-sizing:border-box;width:100%;padding:.25rem 1.5rem;white-space:nowrap;text-align:center;background:var(--cc-details-price-bg, var(--cc-heading, var(--cc-primary, #4c6b4a)));box-shadow:0 1px 2px #0000000d}.cc-details__price p{margin:0;font-family:var(--cc-font-heading, inherit);font-size:1.6rem;font-weight:700;color:var(--cc-on-accent, var(--cc-bg, #f0ffef))}.cc-details__description{margin:0}.cc-details__facts{display:flex;gap:2.5rem}.cc-details__fact{display:flex;flex-direction:column;align-items:center;gap:.25rem}.cc-details__fact img,.cc-details__fact svg{width:1.75rem;height:1.75rem}.cc-details__includes{margin:0;font-size:1rem;font-weight:600;color:var(--cc-heading, var(--cc-primary, #4c6b4a))}.cc-details__services{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem;margin:0;padding:0;list-style:none}.cc-details__services li{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;text-align:center}.cc-details__services img{width:2rem}.cc-details__gift{box-sizing:border-box;display:flex;flex-direction:column;align-items:flex-start;justify-content:space-between;gap:1.25rem;width:100%;padding:.5rem;border:2px dashed var(--cc-details-gift-border, var(--cc-secondary-light, #f8f4ee))}.cc-details__gift--solo{border:0;padding:0}.cc-details__gift-img{width:100%}.cc-details__perks{display:flex;flex-direction:column;gap:1.25rem}.cc-details__perks h2{margin:0;font-size:1rem;font-weight:600;color:var(--cc-accent, var(--cc-secondary, #ceab5d))}.cc-details__perks ul{margin:0;padding:0;list-style:none}.cc-details__cta-wrap{display:flex;align-items:flex-end;width:100%;height:100%}.cc-details__cta{padding:.75rem 2.5rem;white-space:nowrap;text-decoration:none;color:var(--cc-on-accent, var(--cc-bg, #f0ffef));background:var(--cc-accent, var(--cc-secondary, #ceab5d));box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;transition:background-color .15s ease-in}.cc-details__cta:hover{background:var(--cc-accent-hover, var(--cc-secondary-dark, #9a7521))}@media (min-width: 1024px){.cc-details{flex-direction:row;gap:7rem}.cc-details__media,.cc-details__panel{flex:1 1 0}.cc-details__info{gap:1.25rem}.cc-details__price{width:auto;min-width:30%}.cc-details__services{grid-template-columns:repeat(6,minmax(0,1fr))}.cc-details__gift{display:grid;grid-template-columns:auto 1fr;align-items:center;gap:1rem 1.5rem;padding:1.25rem}.cc-details__gift--solo{grid-template-columns:1fr;padding:0}.cc-details__gift-img{width:10rem}.cc-details__perks{gap:.5rem}.cc-details__perks ul{padding-left:1.25rem;list-style:disc}.cc-details__cta-wrap{grid-column:1 / -1;justify-content:flex-end;width:100%}}\n"], dependencies: [{ kind: "pipe", type: CurrencyPipe, name: "currency" }, { kind: "component", type: PageBannerComponent, selector: "cc-page-banner", inputs: ["title", "variant", "iconSrc", "backLink", "backIconSrc", "backLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
705
+ }
706
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PlanDetailsComponent, decorators: [{
707
+ type: Component,
708
+ args: [{ selector: 'cc-plan-details', imports: [CurrencyPipe, PageBannerComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (showBanner()) {\n <cc-page-banner\n variant=\"plain\"\n [title]=\"plan().name\"\n [iconSrc]=\"titleIconSrc()\"\n [backLink]=\"backLink()\"\n [backLabel]=\"backLabel()\"\n [backIconSrc]=\"backIconSrc()\"\n />\n}\n\n<div class=\"cc-details\">\n <div class=\"cc-details__media\">\n <img class=\"cc-details__photo\" [src]=\"plan().imgPath\" [alt]=\"plan().name + imageAltSuffix()\" />\n <ng-content select=\"[ccDetailsMedia]\" />\n </div>\n\n <div class=\"cc-details__panel\">\n <div class=\"cc-details__info\">\n <div class=\"cc-details__price\">\n <p>{{ plan().price | currency: currencyCode() : currencyDisplay() : currencyDigits() }}</p>\n </div>\n\n <p class=\"cc-details__description\">{{ plan().description }}</p>\n\n <div class=\"cc-details__facts\">\n <div class=\"cc-details__fact\">\n @if (durationIconSrc()) {\n <img [src]=\"durationIconSrc()\" alt=\"Duraci\u00F3n\" />\n } @else {\n <svg viewBox=\"0 0 24 24\" role=\"img\" aria-label=\"Duraci\u00F3n\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <path d=\"M12 7v5l3 2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\n </svg>\n }\n <small>{{ plan().duration }}</small>\n </div>\n\n <div class=\"cc-details__fact\">\n @if (people() > 1) {\n @if (peopleIconSrc()) {\n <img [src]=\"peopleIconSrc()\" alt=\"\" />\n } @else {\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <circle cx=\"9\" cy=\"8\" r=\"3.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <circle cx=\"17\" cy=\"9\" r=\"2.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <path d=\"M2.5 20a6.5 6.5 0 0 1 13 0M15 14.5a5 5 0 0 1 6.5 4.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\n </svg>\n }\n <small>{{ people() }} {{ peopleLabel() }}</small>\n } @else {\n @if (personIconSrc()) {\n <img [src]=\"personIconSrc()\" alt=\"\" />\n } @else {\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"8\" r=\"4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <path d=\"M4.5 20a7.5 7.5 0 0 1 15 0\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\n </svg>\n }\n <small>1 {{ personLabel() }}</small>\n }\n </div>\n </div>\n\n @if (services().length) {\n <h2 class=\"cc-details__includes\">{{ includesLabel() }}</h2>\n <ul class=\"cc-details__services\">\n @for (service of services(); track $index) {\n <li>\n <img [src]=\"service.iconPath\" alt=\"\" />\n <small>{{ service.name }}</small>\n </li>\n }\n </ul>\n }\n\n <ng-content />\n\n <div class=\"cc-details__gift\" [class.cc-details__gift--solo]=\"!perks().length\">\n @if (perks().length) {\n @if (perksImageSrc()) {\n <img class=\"cc-details__gift-img\" [src]=\"perksImageSrc()\" [alt]=\"perksImageAlt()\" loading=\"lazy\" />\n }\n <div class=\"cc-details__perks\">\n <h2>{{ perksTitle() }}</h2>\n <ul>\n @for (perk of perks(); track $index) {\n <li>{{ perk }}</li>\n }\n </ul>\n </div>\n }\n <div class=\"cc-details__cta-wrap\">\n <a class=\"cc-details__cta\" [href]=\"bookingUrl()\" target=\"_blank\" rel=\"noopener\">{{ bookingLabel() }}</a>\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;color:var(--cc-text, var(--cc-primary, #4c6b4a))}.cc-details{display:flex;flex-direction:column;gap:3rem}.cc-details__media{display:flex;flex-direction:column;gap:2.5rem}.cc-details__photo{display:block;width:100%;max-width:60rem;box-shadow:var(--cc-shadow-lg, 0 10px 15px -3px rgb(0 0 0 / .1))}.cc-details__panel{background:var(--cc-details-panel-bg, var(--cc-surface, var(--cc-secondary-light, #f8f4ee)))}.cc-details__info{position:relative;box-sizing:border-box;display:flex;flex-direction:column;gap:2.5rem;width:100%;padding:5rem 2.5rem 1.25rem}.cc-details__price{position:absolute;top:0;left:0;box-sizing:border-box;width:100%;padding:.25rem 1.5rem;white-space:nowrap;text-align:center;background:var(--cc-details-price-bg, var(--cc-heading, var(--cc-primary, #4c6b4a)));box-shadow:0 1px 2px #0000000d}.cc-details__price p{margin:0;font-family:var(--cc-font-heading, inherit);font-size:1.6rem;font-weight:700;color:var(--cc-on-accent, var(--cc-bg, #f0ffef))}.cc-details__description{margin:0}.cc-details__facts{display:flex;gap:2.5rem}.cc-details__fact{display:flex;flex-direction:column;align-items:center;gap:.25rem}.cc-details__fact img,.cc-details__fact svg{width:1.75rem;height:1.75rem}.cc-details__includes{margin:0;font-size:1rem;font-weight:600;color:var(--cc-heading, var(--cc-primary, #4c6b4a))}.cc-details__services{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem;margin:0;padding:0;list-style:none}.cc-details__services li{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;text-align:center}.cc-details__services img{width:2rem}.cc-details__gift{box-sizing:border-box;display:flex;flex-direction:column;align-items:flex-start;justify-content:space-between;gap:1.25rem;width:100%;padding:.5rem;border:2px dashed var(--cc-details-gift-border, var(--cc-secondary-light, #f8f4ee))}.cc-details__gift--solo{border:0;padding:0}.cc-details__gift-img{width:100%}.cc-details__perks{display:flex;flex-direction:column;gap:1.25rem}.cc-details__perks h2{margin:0;font-size:1rem;font-weight:600;color:var(--cc-accent, var(--cc-secondary, #ceab5d))}.cc-details__perks ul{margin:0;padding:0;list-style:none}.cc-details__cta-wrap{display:flex;align-items:flex-end;width:100%;height:100%}.cc-details__cta{padding:.75rem 2.5rem;white-space:nowrap;text-decoration:none;color:var(--cc-on-accent, var(--cc-bg, #f0ffef));background:var(--cc-accent, var(--cc-secondary, #ceab5d));box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;transition:background-color .15s ease-in}.cc-details__cta:hover{background:var(--cc-accent-hover, var(--cc-secondary-dark, #9a7521))}@media (min-width: 1024px){.cc-details{flex-direction:row;gap:7rem}.cc-details__media,.cc-details__panel{flex:1 1 0}.cc-details__info{gap:1.25rem}.cc-details__price{width:auto;min-width:30%}.cc-details__services{grid-template-columns:repeat(6,minmax(0,1fr))}.cc-details__gift{display:grid;grid-template-columns:auto 1fr;align-items:center;gap:1rem 1.5rem;padding:1.25rem}.cc-details__gift--solo{grid-template-columns:1fr;padding:0}.cc-details__gift-img{width:10rem}.cc-details__perks{gap:.5rem}.cc-details__perks ul{padding-left:1.25rem;list-style:disc}.cc-details__cta-wrap{grid-column:1 / -1;justify-content:flex-end;width:100%}}\n"] }]
709
+ }] });
710
+
711
+ /**
712
+ * Full-screen image viewer. It is open while `index` is not null.
713
+ * Arrow keys move between images and Escape closes it.
714
+ *
715
+ * ```html
716
+ * <cc-lightbox [images]="images" [(index)]="openIndex" />
717
+ * ```
718
+ */
719
+ class LightboxComponent {
720
+ images = input.required();
721
+ /** Index of the open image, or null when closed. Use `[(index)]`. */
722
+ index = model(null);
723
+ closeLabel = input('Cerrar');
724
+ prevLabel = input('Imagen anterior');
725
+ nextLabel = input('Imagen siguiente');
726
+ current = computed(() => {
727
+ const i = this.index();
728
+ return i === null ? null : (this.images()[i] ?? null);
729
+ });
730
+ document = inject(DOCUMENT);
731
+ constructor() {
732
+ // Stop the page behind from scrolling while the viewer is open.
733
+ effect((onCleanup) => {
734
+ if (this.current() === null)
735
+ return;
736
+ const body = this.document.body;
737
+ const previous = body.style.overflow;
738
+ body.style.overflow = 'hidden';
739
+ onCleanup(() => (body.style.overflow = previous));
740
+ });
741
+ }
742
+ close() {
743
+ if (this.index() !== null)
744
+ this.index.set(null);
745
+ }
746
+ next() {
747
+ const i = this.index();
748
+ const total = this.images().length;
749
+ if (i !== null && total)
750
+ this.index.set((i + 1) % total);
751
+ }
752
+ prev() {
753
+ const i = this.index();
754
+ const total = this.images().length;
755
+ if (i !== null && total)
756
+ this.index.set((i - 1 + total) % total);
757
+ }
758
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: LightboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
759
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: LightboxComponent, isStandalone: true, selector: "cc-lightbox", inputs: { images: { classPropertyName: "images", publicName: "images", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null }, prevLabel: { classPropertyName: "prevLabel", publicName: "prevLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { index: "indexChange" }, host: { listeners: { "document:keydown.escape": "close()", "document:keydown.arrowright": "next()", "document:keydown.arrowleft": "prev()" } }, ngImport: i0, template: "@if (current(); as image) {\n <div class=\"cc-lb\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"image.caption || image.alt || null\" (click)=\"close()\">\n <button type=\"button\" class=\"cc-lb__btn cc-lb__close\" [attr.aria-label]=\"closeLabel()\" (click)=\"close()\">\u2715</button>\n @if (images().length > 1) {\n <button type=\"button\" class=\"cc-lb__btn cc-lb__prev\" [attr.aria-label]=\"prevLabel()\" (click)=\"$event.stopPropagation(); prev()\">\u2039</button>\n }\n <img class=\"cc-lb__img\" [src]=\"image.src\" [alt]=\"image.alt ?? image.caption ?? ''\" (click)=\"$event.stopPropagation()\" />\n @if (images().length > 1) {\n <button type=\"button\" class=\"cc-lb__btn cc-lb__next\" [attr.aria-label]=\"nextLabel()\" (click)=\"$event.stopPropagation(); next()\">\u203A</button>\n }\n @if (image.caption) {\n <p class=\"cc-lb__caption\">{{ image.caption }}</p>\n }\n </div>\n}\n", styles: [".cc-lb{position:fixed;inset:0;z-index:var(--cc-lightbox-z, 50);display:flex;align-items:center;justify-content:center;background:var(--cc-lightbox-backdrop, rgb(0 0 0 / .9))}.cc-lb__img{max-width:90%;max-height:90%;border-radius:.5rem;box-shadow:var(--cc-shadow-lg, 0 10px 15px -3px rgb(0 0 0 / .1))}.cc-lb__btn{position:absolute;padding:.5rem;border:0;font-weight:700;line-height:1;color:#fff;background:transparent;cursor:pointer}.cc-lb__close{top:1.25rem;right:1.25rem;font-size:1.875rem}.cc-lb__prev,.cc-lb__next{font-size:2.5rem}.cc-lb__prev{left:1.25rem}.cc-lb__next{right:1.25rem}.cc-lb__caption{position:absolute;bottom:2.5rem;margin:0;padding:0 1rem;font-size:1.125rem;text-align:center;color:#fff}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
760
+ }
761
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: LightboxComponent, decorators: [{
762
+ type: Component,
763
+ args: [{ selector: 'cc-lightbox', changeDetection: ChangeDetectionStrategy.OnPush, host: {
764
+ '(document:keydown.escape)': 'close()',
765
+ '(document:keydown.arrowright)': 'next()',
766
+ '(document:keydown.arrowleft)': 'prev()',
767
+ }, template: "@if (current(); as image) {\n <div class=\"cc-lb\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"image.caption || image.alt || null\" (click)=\"close()\">\n <button type=\"button\" class=\"cc-lb__btn cc-lb__close\" [attr.aria-label]=\"closeLabel()\" (click)=\"close()\">\u2715</button>\n @if (images().length > 1) {\n <button type=\"button\" class=\"cc-lb__btn cc-lb__prev\" [attr.aria-label]=\"prevLabel()\" (click)=\"$event.stopPropagation(); prev()\">\u2039</button>\n }\n <img class=\"cc-lb__img\" [src]=\"image.src\" [alt]=\"image.alt ?? image.caption ?? ''\" (click)=\"$event.stopPropagation()\" />\n @if (images().length > 1) {\n <button type=\"button\" class=\"cc-lb__btn cc-lb__next\" [attr.aria-label]=\"nextLabel()\" (click)=\"$event.stopPropagation(); next()\">\u203A</button>\n }\n @if (image.caption) {\n <p class=\"cc-lb__caption\">{{ image.caption }}</p>\n }\n </div>\n}\n", styles: [".cc-lb{position:fixed;inset:0;z-index:var(--cc-lightbox-z, 50);display:flex;align-items:center;justify-content:center;background:var(--cc-lightbox-backdrop, rgb(0 0 0 / .9))}.cc-lb__img{max-width:90%;max-height:90%;border-radius:.5rem;box-shadow:var(--cc-shadow-lg, 0 10px 15px -3px rgb(0 0 0 / .1))}.cc-lb__btn{position:absolute;padding:.5rem;border:0;font-weight:700;line-height:1;color:#fff;background:transparent;cursor:pointer}.cc-lb__close{top:1.25rem;right:1.25rem;font-size:1.875rem}.cc-lb__prev,.cc-lb__next{font-size:2.5rem}.cc-lb__prev{left:1.25rem}.cc-lb__next{right:1.25rem}.cc-lb__caption{position:absolute;bottom:2.5rem;margin:0;padding:0 1rem;font-size:1.125rem;text-align:center;color:#fff}\n"] }]
768
+ }], ctorParameters: () => [] });
769
+
770
+ /**
771
+ * Thumbnail grid that opens a lightbox on click.
772
+ *
773
+ * ```html
774
+ * <cc-gallery [images]="images" />
775
+ * ```
776
+ *
777
+ * `numberedImages()` builds the list for folders named 1.jpeg, 2.jpeg, …
778
+ */
779
+ class GalleryComponent {
780
+ images = input.required();
781
+ backgroundImage = input('');
782
+ openLabel = input('Ver imagen');
783
+ open = signal(null);
784
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: GalleryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
785
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: GalleryComponent, isStandalone: true, selector: "cc-gallery", inputs: { images: { classPropertyName: "images", publicName: "images", isSignal: true, isRequired: true, transformFunction: null }, backgroundImage: { classPropertyName: "backgroundImage", publicName: "backgroundImage", isSignal: true, isRequired: false, transformFunction: null }, openLabel: { classPropertyName: "openLabel", publicName: "openLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ul class=\"cc-gallery\" [style.background-image]=\"backgroundImage() ? 'url(' + backgroundImage() + ')' : null\">\n @for (image of images(); track image.src; let i = $index) {\n <li>\n <button type=\"button\" class=\"cc-gallery__item\" [attr.aria-label]=\"openLabel() + ': ' + (image.alt ?? image.caption ?? i + 1)\" (click)=\"open.set(i)\">\n <img [src]=\"image.thumb ?? image.src\" [alt]=\"image.alt ?? image.caption ?? ''\" loading=\"lazy\" decoding=\"async\" />\n </button>\n </li>\n }\n</ul>\n<cc-lightbox [images]=\"images()\" [(index)]=\"open\" />\n", styles: [":host{display:block}.cc-gallery{box-sizing:border-box;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;place-content:center;justify-items:center;width:100%;margin:0;padding:1.25rem;list-style:none;background-size:cover;background-position:center;background-repeat:no-repeat;border-radius:.125rem;box-shadow:var(--cc-shadow, 0 4px 6px -1px rgb(0 0 0 / .1))}.cc-gallery__item{display:block;width:var(--cc-gallery-thumb-width, 7rem);padding:0;border:0;background:none;cursor:pointer}.cc-gallery__item img{display:block;width:100%;transition:transform .15s ease-in}.cc-gallery__item:hover img{transform:scale(1.03)}@media (min-width: 1024px){.cc-gallery{grid-template-columns:repeat(var(--cc-gallery-columns-lg, 6),minmax(0,1fr));gap:2.5rem;padding:2.5rem}.cc-gallery__item{width:var(--cc-gallery-thumb-width-lg, 10rem)}}\n"], dependencies: [{ kind: "component", type: LightboxComponent, selector: "cc-lightbox", inputs: ["images", "index", "closeLabel", "prevLabel", "nextLabel"], outputs: ["indexChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
786
+ }
787
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: GalleryComponent, decorators: [{
788
+ type: Component,
789
+ args: [{ selector: 'cc-gallery', imports: [LightboxComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ul class=\"cc-gallery\" [style.background-image]=\"backgroundImage() ? 'url(' + backgroundImage() + ')' : null\">\n @for (image of images(); track image.src; let i = $index) {\n <li>\n <button type=\"button\" class=\"cc-gallery__item\" [attr.aria-label]=\"openLabel() + ': ' + (image.alt ?? image.caption ?? i + 1)\" (click)=\"open.set(i)\">\n <img [src]=\"image.thumb ?? image.src\" [alt]=\"image.alt ?? image.caption ?? ''\" loading=\"lazy\" decoding=\"async\" />\n </button>\n </li>\n }\n</ul>\n<cc-lightbox [images]=\"images()\" [(index)]=\"open\" />\n", styles: [":host{display:block}.cc-gallery{box-sizing:border-box;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;place-content:center;justify-items:center;width:100%;margin:0;padding:1.25rem;list-style:none;background-size:cover;background-position:center;background-repeat:no-repeat;border-radius:.125rem;box-shadow:var(--cc-shadow, 0 4px 6px -1px rgb(0 0 0 / .1))}.cc-gallery__item{display:block;width:var(--cc-gallery-thumb-width, 7rem);padding:0;border:0;background:none;cursor:pointer}.cc-gallery__item img{display:block;width:100%;transition:transform .15s ease-in}.cc-gallery__item:hover img{transform:scale(1.03)}@media (min-width: 1024px){.cc-gallery{grid-template-columns:repeat(var(--cc-gallery-columns-lg, 6),minmax(0,1fr));gap:2.5rem;padding:2.5rem}.cc-gallery__item{width:var(--cc-gallery-thumb-width-lg, 10rem)}}\n"] }]
790
+ }] });
791
+ /**
792
+ * Builds a gallery list for numbered files.
793
+ *
794
+ * ```ts
795
+ * numberedImages(18, (i) => `assets/images/galery/${i}.jpeg`, (i) => `Instalaciones - foto ${i}`)
796
+ * ```
797
+ */
798
+ function numberedImages(count, src, caption) {
799
+ return Array.from({ length: count }, (_, i) => {
800
+ const n = i + 1;
801
+ return { src: src(n), caption: caption?.(n) };
802
+ });
803
+ }
804
+
805
+ /*
806
+ * Public API Surface of @c-code/c-code-fw/ui
807
+ */
808
+ // Models
809
+
810
+ /**
811
+ * Generated bundle index. Do not edit.
812
+ */
813
+
814
+ export { CategoryMenuComponent, DEFAULT_PLAN_CATALOG_CONFIG, DEFAULT_PLAN_CATEGORIES, FaqComponent, GalleryComponent, InfoItemComponent, LightboxComponent, PLAN_CATALOG_CONFIG, PageBannerComponent, PlanCardComponent, PlanCardTemplateDirective, PlanCatalogComponent, PlanCatalogService, PlanCategory, PlanDetailsComponent, PlanFilterFormComponent, PromoModalComponent, SEO_CONFIG, SearchBoxComponent, SeoService, SocialLinksComponent, WhatsappButtonComponent, filterPlans, filterPlansByCategory, findPlanBySlug, joinAdditionalServices, numberedImages, planSlug, providePlanCatalog, provideSeo, searchPlansByName, slugify, titleCase, truncateText, whatsappUrl, withCategoryCounts };
815
+ //# sourceMappingURL=c-code-c-code-fw-ui.mjs.map