@jjlmoya/utils-alcohol 1.12.0 → 1.14.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.
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
1
  {
2
- "name": "@jjlmoya/utils-alcohol",
3
- "version": "1.12.0",
4
- "type": "module",
5
- "main": "./src/index.ts",
6
- "types": "./src/index.ts",
7
- "exports": {
8
- ".": "./src/index.ts",
9
- "./data": "./src/data.ts"
10
- },
11
- "files": [
12
- "src"
13
- ],
14
- "publishConfig": {
15
- "access": "public"
16
- },
17
- "scripts": {
18
- "dev": "astro dev",
19
- "start": "astro dev",
20
- "build": "astro build",
21
- "preview": "astro preview",
22
- "astro": "astro",
23
- "lint": "eslint src/ --max-warnings 0 && stylelint \"src/**/*.{css,astro}\"",
24
- "check": "astro check",
25
- "type-check": "astro check",
26
- "test": "vitest run",
27
- "preversion": "npm run lint && npm run test",
28
- "postversion": "git push && git push --tags",
29
- "patch": "npm version patch",
30
- "minor": "npm version minor",
31
- "major": "npm version major"
32
- },
33
- "lint-staged": {
34
- "*.{ts,tsx,astro}": [
35
- "eslint --fix"
36
- ]
37
- },
38
- "dependencies": {
39
- "@iconify-json/mdi": "^1.2.3",
40
- "@jjlmoya/utils-shared": "^1.1.0",
41
- "astro": "^6.1.2",
42
- "astro-icon": "^1.1.0"
43
- },
44
- "devDependencies": {
45
- "@astrojs/check": "^0.9.8",
46
- "eslint": "^9.39.4",
47
- "eslint-plugin-astro": "^1.6.0",
48
- "eslint-plugin-no-comments": "^1.1.10",
49
- "husky": "^9.1.7",
50
- "lint-staged": "^16.4.0",
51
- "postcss-html": "^1.8.1",
52
- "schema-dts": "^1.1.2",
53
- "stylelint": "^17.6.0",
54
- "stylelint-config-standard": "^40.0.0",
55
- "stylelint-declaration-strict-value": "^1.11.1",
56
- "typescript": "^5.4.0",
57
- "typescript-eslint": "^8.58.0",
58
- "vitest": "^4.1.2"
59
- }
2
+ "name": "@jjlmoya/utils-alcohol",
3
+ "version": "1.14.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./data": "./src/data.ts"
10
+ },
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "scripts": {
18
+ "dev": "astro dev",
19
+ "start": "astro dev",
20
+ "build": "astro build",
21
+ "preview": "astro preview",
22
+ "astro": "astro",
23
+ "lint": "eslint src/ --max-warnings 0 && stylelint \"src/**/*.{css,astro}\"",
24
+ "check": "astro check",
25
+ "type-check": "astro check",
26
+ "test": "vitest run",
27
+ "preversion": "npm run lint && npm run test",
28
+ "postversion": "git push && git push --tags",
29
+ "patch": "npm version patch",
30
+ "minor": "npm version minor",
31
+ "major": "npm version major"
32
+ },
33
+ "lint-staged": {
34
+ "*.{ts,tsx,astro}": [
35
+ "eslint --fix"
36
+ ]
37
+ },
38
+ "dependencies": {
39
+ "@iconify-json/mdi": "^1.2.3",
40
+ "@jjlmoya/utils-shared": "1.2.0",
41
+ "astro": "^6.1.2",
42
+ "astro-icon": "^1.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@astrojs/check": "^0.9.8",
46
+ "eslint": "^9.39.4",
47
+ "eslint-plugin-astro": "^1.6.0",
48
+ "eslint-plugin-no-comments": "^1.1.10",
49
+ "husky": "^9.1.7",
50
+ "lint-staged": "^16.4.0",
51
+ "postcss-html": "^1.8.1",
52
+ "schema-dts": "^1.1.2",
53
+ "stylelint": "^17.6.0",
54
+ "stylelint-config-standard": "^40.0.0",
55
+ "stylelint-declaration-strict-value": "^1.11.1",
56
+ "typescript": "^5.4.0",
57
+ "typescript-eslint": "^8.58.0",
58
+ "vitest": "^4.1.2"
59
+ }
60
60
  }
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import type { ToolLocaleContent } from '../types';
4
+
5
+ describe('Schemas Fulfillment Validation', () => {
6
+ ALL_TOOLS.forEach((tool) => {
7
+ describe(`Tool: ${tool.entry.id}`, () => {
8
+ Object.keys(tool.entry.i18n).forEach((locale) => {
9
+ it(`Locale: ${locale} should have faqSchema, appSchema and howToSchema`, async () => {
10
+ const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
11
+ if (!loader) return;
12
+ const content = (await loader()) as ToolLocaleContent;
13
+
14
+ const schemaTypes = content.schemas.map((s: any) => s['@type']);
15
+
16
+ expect(schemaTypes, `Tool "${tool.entry.id}" locale "${locale}" is missing FAQPage schema`).toContain('FAQPage');
17
+ expect(schemaTypes, `Tool "${tool.entry.id}" locale "${locale}" is missing SoftwareApplication schema`).toContain('SoftwareApplication');
18
+ expect(schemaTypes, `Tool "${tool.entry.id}" locale "${locale}" is missing HowTo schema`).toContain('HowTo');
19
+ });
20
+ });
21
+ });
22
+ });
23
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, it } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ function getFiles(dir: string, ext: string[]): string[] {
6
+ const results: string[] = [];
7
+ if (!fs.existsSync(dir)) return results;
8
+ const list = fs.readdirSync(dir);
9
+ for (const file of list) {
10
+ const fullPath = path.join(dir, file);
11
+ const stat = fs.statSync(fullPath);
12
+ if (stat && stat.isDirectory()) {
13
+ results.push(...getFiles(fullPath, ext));
14
+ } else if (ext.some((e) => file.endsWith(e))) {
15
+ results.push(fullPath);
16
+ }
17
+ }
18
+ return results;
19
+ }
20
+
21
+ const SRC_DIR = path.join(process.cwd(), 'src');
22
+
23
+ describe('Project Titles - Separator Validation', () => {
24
+ const files = [
25
+ ...getFiles(path.join(SRC_DIR, 'tool'), ['.ts']),
26
+ ...getFiles(path.join(SRC_DIR, 'category'), ['.ts']),
27
+ ].filter(f => f.includes('i18n'));
28
+
29
+ it.each(files)('Verify that titles in %s do not contain | or -', (filePath) => {
30
+ const content = fs.readFileSync(filePath, 'utf-8');
31
+ const relativePath = path.relative(process.cwd(), filePath);
32
+
33
+ const titlePatterns = [
34
+ /const\s+title\s*=\s*['"]([^'"]+)['"]/g,
35
+ /title\s*:\s*['"]([^'"]+)['"]/g,
36
+ ];
37
+
38
+ const findings: string[] = [];
39
+
40
+ for (const pattern of titlePatterns) {
41
+ let match;
42
+ while ((match = pattern.exec(content)) !== null) {
43
+ const title = match[1];
44
+ if (title.includes('|') || title.includes('-')) {
45
+ findings.push(title);
46
+ }
47
+ }
48
+ }
49
+
50
+ if (findings.length > 0) {
51
+ const list = findings.map((f) => ` - "${f}"`).join('\n');
52
+ throw new Error(`Forbidden separators (| or -) found in titles in ${relativePath}:\n${list}`);
53
+ }
54
+ });
55
+ });
@@ -438,6 +438,68 @@ const { ui } = Astro.props;
438
438
  border-radius: 10px;
439
439
  }
440
440
 
441
+ :global(.drink-list-row) {
442
+ display: flex;
443
+ align-items: center;
444
+ justify-content: space-between;
445
+ padding: 0.75rem;
446
+ background: #f8fafc;
447
+ border-radius: 0.75rem;
448
+ border: 1px solid #e2e8f0;
449
+ }
450
+ :global(.theme-dark) :global(.drink-list-row) {
451
+ background: #1e293b;
452
+ border-color: #334155;
453
+ }
454
+
455
+ :global(.drink-row-info) {
456
+ display: flex;
457
+ align-items: center;
458
+ gap: 0.75rem;
459
+ }
460
+
461
+ :global(.drink-row-name) {
462
+ font-weight: 700;
463
+ color: #334155;
464
+ }
465
+ :global(.theme-dark) :global(.drink-row-name) {
466
+ color: #e2e8f0;
467
+ }
468
+
469
+ :global(.drink-row-qty) {
470
+ font-size: 0.75rem;
471
+ background: #e0e7ff;
472
+ color: #4f46e5;
473
+ padding: 0.125rem 0.5rem;
474
+ border-radius: 0.25rem;
475
+ font-weight: 700;
476
+ }
477
+ :global(.theme-dark) :global(.drink-row-qty) {
478
+ background: #312e81;
479
+ color: #a5b4fc;
480
+ }
481
+
482
+ :global(.drink-row-del) {
483
+ display: flex;
484
+ align-items: center;
485
+ justify-content: center;
486
+ width: 2rem;
487
+ height: 2rem;
488
+ padding: 0;
489
+ background: none;
490
+ border: none;
491
+ color: #ef4444;
492
+ cursor: pointer;
493
+ transition: background 0.2s;
494
+ }
495
+ :global(.drink-row-del:hover) {
496
+ background: #fef2f2;
497
+ border-radius: 0.5rem;
498
+ }
499
+ :global(.theme-dark) :global(.drink-row-del:hover) {
500
+ background: rgba(239,68,68,0.1);
501
+ }
502
+
441
503
  .alc-results-panel {
442
504
  display: flex;
443
505
  flex-direction: column;
@@ -1,4 +1,4 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { AlcoholClearanceUI, AlcoholClearanceLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'alcohol-clearance-calculator';
@@ -158,6 +158,27 @@ const seo: AlcoholClearanceLocaleContent['seo'] = [
158
158
  ];
159
159
 
160
160
  const schemas: AlcoholClearanceLocaleContent['schemas'] = [
161
+ {
162
+ '@context': 'https://schema.org',
163
+ '@type': 'FAQPage',
164
+ mainEntity: faq.map((item) => ({
165
+ '@type': 'Question',
166
+ name: item.question,
167
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
168
+ })),
169
+ } as WithContext<FAQPage>,
170
+ {
171
+ '@context': 'https://schema.org',
172
+ '@type': 'HowTo',
173
+ name: title,
174
+ description: description,
175
+ step: howTo.map((step, i) => ({
176
+ '@type': 'HowToStep',
177
+ position: i + 1,
178
+ name: step.name,
179
+ text: step.text,
180
+ })),
181
+ } as WithContext<HowTo>,
161
182
  {
162
183
  '@context': 'https://schema.org',
163
184
  '@type': 'SoftwareApplication',
@@ -1,4 +1,4 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { AlcoholClearanceUI, AlcoholClearanceLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'calculadora-alcohol-resaca';
@@ -167,6 +167,27 @@ const seo: AlcoholClearanceLocaleContent['seo'] = [
167
167
  ];
168
168
 
169
169
  const schemas: AlcoholClearanceLocaleContent['schemas'] = [
170
+ {
171
+ '@context': 'https://schema.org',
172
+ '@type': 'FAQPage',
173
+ mainEntity: faq.map((item) => ({
174
+ '@type': 'Question',
175
+ name: item.question,
176
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
177
+ })),
178
+ } as WithContext<FAQPage>,
179
+ {
180
+ '@context': 'https://schema.org',
181
+ '@type': 'HowTo',
182
+ name: title,
183
+ description: description,
184
+ step: howTo.map((step, i) => ({
185
+ '@type': 'HowToStep',
186
+ position: i + 1,
187
+ name: step.name,
188
+ text: step.text,
189
+ })),
190
+ } as WithContext<HowTo>,
170
191
  {
171
192
  '@context': 'https://schema.org',
172
193
  '@type': 'SoftwareApplication',
@@ -1,4 +1,4 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { AlcoholClearanceUI, AlcoholClearanceLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'calculateur-elimination-alcohol';
@@ -149,6 +149,27 @@ const seo: AlcoholClearanceLocaleContent['seo'] = [
149
149
  ];
150
150
 
151
151
  const schemas: AlcoholClearanceLocaleContent['schemas'] = [
152
+ {
153
+ '@context': 'https://schema.org',
154
+ '@type': 'FAQPage',
155
+ mainEntity: faq.map((item) => ({
156
+ '@type': 'Question',
157
+ name: item.question,
158
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
159
+ })),
160
+ } as WithContext<FAQPage>,
161
+ {
162
+ '@context': 'https://schema.org',
163
+ '@type': 'HowTo',
164
+ name: title,
165
+ description: description,
166
+ step: howTo.map((step, i) => ({
167
+ '@type': 'HowToStep',
168
+ position: i + 1,
169
+ name: step.name,
170
+ text: step.text,
171
+ })),
172
+ } as WithContext<HowTo>,
152
173
  {
153
174
  '@context': 'https://schema.org',
154
175
  '@type': 'SoftwareApplication',
@@ -1,4 +1,4 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { BeerCoolerUI, BeerCoolerLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'beer-cooler';
@@ -135,7 +135,7 @@ const seo: BeerCoolerLocaleContent['seo'] = [
135
135
  },
136
136
  {
137
137
  type: 'tip',
138
- title: 'Pro Tip: Pre-chill the Glass',
138
+ title: 'Pro Tip: Prechill the Glass',
139
139
  html: 'The glass you serve beer into has a significant thermal mass. A warm glass can raise the temperature of a perfectly cold beer by 2-3°C instantly on contact. Place your serving glass in the freezer for 5 minutes before pouring, or rinse it with cold water. This simple step extends the window where your beer tastes exactly as intended.'
140
140
  },
141
141
  {
@@ -150,11 +150,32 @@ const seo: BeerCoolerLocaleContent['seo'] = [
150
150
  ];
151
151
 
152
152
  const schemas: BeerCoolerLocaleContent['schemas'] = [
153
+ {
154
+ '@context': 'https://schema.org',
155
+ '@type': 'FAQPage',
156
+ mainEntity: faq.map((item) => ({
157
+ '@type': 'Question',
158
+ name: item.question,
159
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
160
+ })),
161
+ } as WithContext<FAQPage>,
162
+ {
163
+ '@context': 'https://schema.org',
164
+ '@type': 'HowTo',
165
+ name: title,
166
+ description: description,
167
+ step: howTo.map((step, i) => ({
168
+ '@type': 'HowToStep',
169
+ position: i + 1,
170
+ name: step.name,
171
+ text: step.text,
172
+ })),
173
+ } as WithContext<HowTo>,
153
174
  {
154
175
  '@context': 'https://schema.org',
155
176
  '@type': 'SoftwareApplication',
156
- name: "Beer Cooling Calculator – Newton's Law of Cooling",
157
- description: 'Applied thermodynamics tool that uses Newton\'s Law of Cooling to calculate the exact time for your beer to reach the ideal serving temperature in a fridge or freezer.',
177
+ name: title,
178
+ description: description,
158
179
  applicationCategory: 'UtilityApplication',
159
180
  operatingSystem: 'Web',
160
181
  offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
@@ -1,4 +1,4 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { BeerCoolerUI, BeerCoolerLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'calculadora-enfriamiento-cerveza';
@@ -135,7 +135,7 @@ const seo: BeerCoolerLocaleContent['seo'] = [
135
135
  },
136
136
  {
137
137
  type: 'tip',
138
- title: 'Consejo Pro: Pre-enfría el Vaso',
138
+ title: 'Consejo Pro: Preenfría el Vaso',
139
139
  html: 'El vaso en el que sirves la cerveza tiene una masa térmica significativa. Un vaso caliente puede subir la temperatura de una cerveza perfectamente fría en 2-3°C al instante. Mete el vaso de servicio en el congelador 5 minutos antes de servir, o enjuágalo con agua fría. Este simple paso prolonga la ventana en que tu cerveza sabe exactamente como debe.'
140
140
  },
141
141
  {
@@ -163,11 +163,32 @@ const seo: BeerCoolerLocaleContent['seo'] = [
163
163
  ];
164
164
 
165
165
  const schemas: BeerCoolerLocaleContent['schemas'] = [
166
+ {
167
+ '@context': 'https://schema.org',
168
+ '@type': 'FAQPage',
169
+ mainEntity: faq.map((item) => ({
170
+ '@type': 'Question',
171
+ name: item.question,
172
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
173
+ })),
174
+ } as WithContext<FAQPage>,
175
+ {
176
+ '@context': 'https://schema.org',
177
+ '@type': 'HowTo',
178
+ name: title,
179
+ description: description,
180
+ step: howTo.map((step, i) => ({
181
+ '@type': 'HowToStep',
182
+ position: i + 1,
183
+ name: step.name,
184
+ text: step.text,
185
+ })),
186
+ } as WithContext<HowTo>,
166
187
  {
167
188
  '@context': 'https://schema.org',
168
189
  '@type': 'SoftwareApplication',
169
- name: 'Calculadora de Enfriamiento de Cerveza – Ley de Newton',
170
- description: 'Herramienta de termodinámica aplicada que calcula con la Ley de Enfriamiento de Newton el tiempo exacto para que tu cerveza alcance la temperatura de servicio ideal en nevera o congelador.',
190
+ name: title,
191
+ description: description,
171
192
  applicationCategory: 'UtilityApplication',
172
193
  operatingSystem: 'Web',
173
194
  offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
@@ -1,4 +1,4 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { BeerCoolerUI, BeerCoolerLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'refroidisseur-biere';
@@ -135,7 +135,7 @@ const seo: BeerCoolerLocaleContent['seo'] = [
135
135
  },
136
136
  {
137
137
  type: 'tip',
138
- title: 'Conseil Pro : Pré-refroidir le Verre',
138
+ title: 'Conseil Pro : Prérefroidir le Verre',
139
139
  html: 'Le verre dans lequel vous servez la bière possède une masse thermique significative. Un verre chaud peut augmenter la température d\'une bière parfaitement froide de 2 à 3°C instantanément au contact. Placez votre verre de service au congélateur 5 minutes avant de verser, ou rincez-le à l\'eau froide. Cette simple étape prolonge la fenêtre où votre bière a exactement le goût voulu.'
140
140
  },
141
141
  {
@@ -150,11 +150,32 @@ const seo: BeerCoolerLocaleContent['seo'] = [
150
150
  ];
151
151
 
152
152
  const schemas: BeerCoolerLocaleContent['schemas'] = [
153
+ {
154
+ '@context': 'https://schema.org',
155
+ '@type': 'FAQPage',
156
+ mainEntity: faq.map((item) => ({
157
+ '@type': 'Question',
158
+ name: item.question,
159
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
160
+ })),
161
+ } as WithContext<FAQPage>,
162
+ {
163
+ '@context': 'https://schema.org',
164
+ '@type': 'HowTo',
165
+ name: title,
166
+ description: description,
167
+ step: howTo.map((step, i) => ({
168
+ '@type': 'HowToStep',
169
+ position: i + 1,
170
+ name: step.name,
171
+ text: step.text,
172
+ })),
173
+ } as WithContext<HowTo>,
153
174
  {
154
175
  '@context': 'https://schema.org',
155
176
  '@type': 'SoftwareApplication',
156
- name: 'Calculateur de Refroidissement de Bière – Loi du Refroidissement de Newton',
157
- description: 'Outil de thermodynamique appliquée qui utilise la Loi du Refroidissement de Newton pour calculer le temps exact nécessaire à votre bière pour atteindre la température de service idéale au frigo ou au congélateur.',
177
+ name: title,
178
+ description: description,
158
179
  applicationCategory: 'UtilityApplication',
159
180
  operatingSystem: 'Web',
160
181
  offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
@@ -1,4 +1,4 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { CarbonationUI, CarbonationLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'beer-carbonation-calculator';
@@ -153,6 +153,27 @@ const seo: CarbonationLocaleContent['seo'] = [
153
153
  ];
154
154
 
155
155
  const schemas: CarbonationLocaleContent['schemas'] = [
156
+ {
157
+ '@context': 'https://schema.org',
158
+ '@type': 'FAQPage',
159
+ mainEntity: faq.map((item) => ({
160
+ '@type': 'Question',
161
+ name: item.question,
162
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
163
+ })),
164
+ } as WithContext<FAQPage>,
165
+ {
166
+ '@context': 'https://schema.org',
167
+ '@type': 'HowTo',
168
+ name: title,
169
+ description: description,
170
+ step: howTo.map((step, i) => ({
171
+ '@type': 'HowToStep',
172
+ position: i + 1,
173
+ name: step.name,
174
+ text: step.text,
175
+ })),
176
+ } as WithContext<HowTo>,
156
177
  {
157
178
  '@context': 'https://schema.org',
158
179
  '@type': 'SoftwareApplication',
@@ -1,4 +1,4 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { CarbonationUI, CarbonationLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'calculadora-carbonatacion';
@@ -157,6 +157,27 @@ const seo: CarbonationLocaleContent['seo'] = [
157
157
  ];
158
158
 
159
159
  const schemas: CarbonationLocaleContent['schemas'] = [
160
+ {
161
+ '@context': 'https://schema.org',
162
+ '@type': 'FAQPage',
163
+ mainEntity: faq.map((item) => ({
164
+ '@type': 'Question',
165
+ name: item.question,
166
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
167
+ })),
168
+ } as WithContext<FAQPage>,
169
+ {
170
+ '@context': 'https://schema.org',
171
+ '@type': 'HowTo',
172
+ name: title,
173
+ description: description,
174
+ step: howTo.map((step, i) => ({
175
+ '@type': 'HowToStep',
176
+ position: i + 1,
177
+ name: step.name,
178
+ text: step.text,
179
+ })),
180
+ } as WithContext<HowTo>,
160
181
  {
161
182
  '@context': 'https://schema.org',
162
183
  '@type': 'SoftwareApplication',
@@ -1,4 +1,4 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { CarbonationUI, CarbonationLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'calculateur-carbonatation-biere';
@@ -153,6 +153,27 @@ const seo: CarbonationLocaleContent['seo'] = [
153
153
  ];
154
154
 
155
155
  const schemas: CarbonationLocaleContent['schemas'] = [
156
+ {
157
+ '@context': 'https://schema.org',
158
+ '@type': 'FAQPage',
159
+ mainEntity: faq.map((item) => ({
160
+ '@type': 'Question',
161
+ name: item.question,
162
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
163
+ })),
164
+ } as WithContext<FAQPage>,
165
+ {
166
+ '@context': 'https://schema.org',
167
+ '@type': 'HowTo',
168
+ name: title,
169
+ description: description,
170
+ step: howTo.map((step, i) => ({
171
+ '@type': 'HowToStep',
172
+ position: i + 1,
173
+ name: step.name,
174
+ text: step.text,
175
+ })),
176
+ } as WithContext<HowTo>,
156
177
  {
157
178
  '@context': 'https://schema.org',
158
179
  '@type': 'SoftwareApplication',
@@ -1488,7 +1488,7 @@ function getIngIconName(type: string): string {
1488
1488
  padding: 0.75rem;
1489
1489
  }
1490
1490
 
1491
- .recipe-row {
1491
+ :global(.recipe-row) {
1492
1492
  background: #fff;
1493
1493
  border-radius: 1rem;
1494
1494
  padding: 1rem;
@@ -1496,34 +1496,34 @@ function getIngIconName(type: string): string {
1496
1496
  border: 1px solid #f1f5f9;
1497
1497
  }
1498
1498
 
1499
- :global(.theme-dark) .recipe-row {
1499
+ :global(.theme-dark .recipe-row) {
1500
1500
  background: #0f172a;
1501
1501
  border-color: #1e293b;
1502
1502
  }
1503
1503
 
1504
- .recipe-row-top {
1504
+ :global(.recipe-row-top) {
1505
1505
  display: flex;
1506
1506
  align-items: center;
1507
1507
  justify-content: space-between;
1508
1508
  margin-bottom: 0.75rem;
1509
1509
  }
1510
1510
 
1511
- .recipe-row-info {
1511
+ :global(.recipe-row-info) {
1512
1512
  display: flex;
1513
1513
  align-items: center;
1514
1514
  gap: 0.75rem;
1515
1515
  }
1516
1516
 
1517
- .recipe-row-name {
1517
+ :global(.recipe-row-name) {
1518
1518
  font-weight: 700;
1519
1519
  color: #334155;
1520
1520
  }
1521
1521
 
1522
- :global(.theme-dark) .recipe-row-name {
1522
+ :global(.theme-dark .recipe-row-name) {
1523
1523
  color: #e2e8f0;
1524
1524
  }
1525
1525
 
1526
- .recipe-row-type {
1526
+ :global(.recipe-row-type) {
1527
1527
  font-size: 0.625rem;
1528
1528
  background: #f1f5f9;
1529
1529
  padding: 0.125rem 0.5rem;
@@ -1533,26 +1533,26 @@ function getIngIconName(type: string): string {
1533
1533
  text-transform: uppercase;
1534
1534
  }
1535
1535
 
1536
- :global(.theme-dark) .recipe-row-type {
1536
+ :global(.theme-dark .recipe-row-type) {
1537
1537
  background: #1e293b;
1538
1538
  }
1539
1539
 
1540
- .recipe-row-del {
1540
+ :global(.recipe-row-del) {
1541
1541
  opacity: 0;
1542
1542
  transition: opacity 0.2s;
1543
1543
  }
1544
1544
 
1545
- .recipe-row:hover .recipe-row-del {
1545
+ :global(.recipe-row:hover .recipe-row-del) {
1546
1546
  opacity: 1;
1547
1547
  }
1548
1548
 
1549
- .recipe-row-controls {
1549
+ :global(.recipe-row-controls) {
1550
1550
  display: flex;
1551
1551
  align-items: center;
1552
1552
  gap: 1rem;
1553
1553
  }
1554
1554
 
1555
- .recipe-range {
1555
+ :global(.recipe-range) {
1556
1556
  flex: 1;
1557
1557
  height: 0.5rem;
1558
1558
  border-radius: 0.5rem;
@@ -1562,16 +1562,16 @@ function getIngIconName(type: string): string {
1562
1562
  background: #e2e8f0;
1563
1563
  }
1564
1564
 
1565
- :global(.theme-dark) .recipe-range {
1565
+ :global(.theme-dark .recipe-range) {
1566
1566
  background: #334155;
1567
1567
  }
1568
1568
 
1569
- .recipe-number-wrap {
1569
+ :global(.recipe-number-wrap) {
1570
1570
  position: relative;
1571
1571
  width: 5rem;
1572
1572
  }
1573
1573
 
1574
- .recipe-number {
1574
+ :global(.recipe-number) {
1575
1575
  width: 100%;
1576
1576
  background: #f8fafc;
1577
1577
  border: 1px solid #e2e8f0;
@@ -1584,7 +1584,7 @@ function getIngIconName(type: string): string {
1584
1584
  box-sizing: border-box;
1585
1585
  }
1586
1586
 
1587
- :global(.theme-dark) .recipe-number {
1587
+ :global(.theme-dark .recipe-number) {
1588
1588
  background: #1e293b;
1589
1589
  border-color: #334155;
1590
1590
  color: #e2e8f0;
@@ -1,8 +1,8 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { CocktailBalancerUI, CocktailBalancerLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'cocktail-balancer';
5
- const title = 'Cocktail Balancer - The Sour Law';
5
+ const title = 'Cocktail Balancer: The Sour Law';
6
6
  const description = 'Calculate the perfect balance between sweet and sour for your cocktails. Master the golden ratio of mixology.';
7
7
 
8
8
  const ui: CocktailBalancerUI = {
@@ -175,6 +175,27 @@ const seo: CocktailBalancerLocaleContent['seo'] = [
175
175
  ];
176
176
 
177
177
  const schemas: CocktailBalancerLocaleContent['schemas'] = [
178
+ {
179
+ '@context': 'https://schema.org',
180
+ '@type': 'FAQPage',
181
+ mainEntity: faq.map((item) => ({
182
+ '@type': 'Question',
183
+ name: item.question,
184
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
185
+ })),
186
+ } as WithContext<FAQPage>,
187
+ {
188
+ '@context': 'https://schema.org',
189
+ '@type': 'HowTo',
190
+ name: title,
191
+ description: description,
192
+ step: howTo.map((step, i) => ({
193
+ '@type': 'HowToStep',
194
+ position: i + 1,
195
+ name: step.name,
196
+ text: step.text,
197
+ })),
198
+ } as WithContext<HowTo>,
178
199
  {
179
200
  '@context': 'https://schema.org',
180
201
  '@type': 'SoftwareApplication',
@@ -1,8 +1,8 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { CocktailBalancerUI, CocktailBalancerLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'equilibrador-cocteles';
5
- const title = 'Equilibrador de Cócteles - La Ley del Sour';
5
+ const title = 'Equilibrador de Cócteles: La Ley del Sour';
6
6
  const description = 'Calcula el equilibrio perfecto entre dulce y ácido para tus cócteles. Domina la proporción áurea de la mixología.';
7
7
 
8
8
  const ui: CocktailBalancerUI = {
@@ -175,6 +175,27 @@ const seo: CocktailBalancerLocaleContent['seo'] = [
175
175
  ];
176
176
 
177
177
  const schemas: CocktailBalancerLocaleContent['schemas'] = [
178
+ {
179
+ '@context': 'https://schema.org',
180
+ '@type': 'FAQPage',
181
+ mainEntity: faq.map((item) => ({
182
+ '@type': 'Question',
183
+ name: item.question,
184
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
185
+ })),
186
+ } as WithContext<FAQPage>,
187
+ {
188
+ '@context': 'https://schema.org',
189
+ '@type': 'HowTo',
190
+ name: title,
191
+ description: description,
192
+ step: howTo.map((step, i) => ({
193
+ '@type': 'HowToStep',
194
+ position: i + 1,
195
+ name: step.name,
196
+ text: step.text,
197
+ })),
198
+ } as WithContext<HowTo>,
178
199
  {
179
200
  '@context': 'https://schema.org',
180
201
  '@type': 'SoftwareApplication',
@@ -1,8 +1,8 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { CocktailBalancerUI, CocktailBalancerLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'equilibre-cocktail';
5
- const title = 'Équilibreur de Cocktails - La Loi du Sour';
5
+ const title = 'Équilibreur de Cocktails: La Loi du Sour';
6
6
  const description = 'Calculez l\'équilibre parfait entre le sucré et l\'acide pour vos cocktails. Maîtrisez le nombre d\'or de la mixologie.';
7
7
 
8
8
  const ui: CocktailBalancerUI = {
@@ -175,6 +175,27 @@ const seo: CocktailBalancerLocaleContent['seo'] = [
175
175
  ];
176
176
 
177
177
  const schemas: CocktailBalancerLocaleContent['schemas'] = [
178
+ {
179
+ '@context': 'https://schema.org',
180
+ '@type': 'FAQPage',
181
+ mainEntity: faq.map((item) => ({
182
+ '@type': 'Question',
183
+ name: item.question,
184
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
185
+ })),
186
+ } as WithContext<FAQPage>,
187
+ {
188
+ '@context': 'https://schema.org',
189
+ '@type': 'HowTo',
190
+ name: title,
191
+ description: description,
192
+ step: howTo.map((step, i) => ({
193
+ '@type': 'HowToStep',
194
+ position: i + 1,
195
+ name: step.name,
196
+ text: step.text,
197
+ })),
198
+ } as WithContext<HowTo>,
178
199
  {
179
200
  '@context': 'https://schema.org',
180
201
  '@type': 'SoftwareApplication',
@@ -825,7 +825,7 @@ const { ui } = Astro.props;
825
825
  animation: drop-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
826
826
  }
827
827
 
828
- .visual-container {
828
+ :global(.visual-container) {
829
829
  display: flex;
830
830
  align-items: flex-end;
831
831
  justify-content: center;
@@ -835,12 +835,12 @@ const { ui } = Astro.props;
835
835
  }
836
836
 
837
837
  @media (min-width: 1024px) {
838
- .visual-container {
838
+ :global(.visual-container) {
839
839
  gap: 4rem;
840
840
  }
841
841
  }
842
842
 
843
- .keg-stack {
843
+ :global(.keg-stack) {
844
844
  display: flex;
845
845
  flex-direction: column-reverse;
846
846
  align-items: center;
@@ -850,18 +850,18 @@ const { ui } = Astro.props;
850
850
  min-width: 80px;
851
851
  }
852
852
 
853
- .keg-item {
853
+ :global(.keg-item) {
854
854
  position: relative;
855
855
  transition: transform 0.3s;
856
856
  transform-origin: bottom;
857
857
  cursor: default;
858
858
  }
859
859
 
860
- .keg-item:hover {
860
+ :global(.keg-item:hover) {
861
861
  transform: scale(1.05);
862
862
  }
863
863
 
864
- .keg-plus {
864
+ :global(.keg-plus) {
865
865
  text-align: center;
866
866
  font-weight: 900;
867
867
  font-size: 1.25rem;
@@ -891,7 +891,7 @@ const { ui } = Astro.props;
891
891
  }
892
892
  }
893
893
 
894
- .ice-pile {
894
+ :global(.ice-pile) {
895
895
  display: flex;
896
896
  flex-wrap: wrap;
897
897
  align-content: flex-end;
@@ -902,17 +902,17 @@ const { ui } = Astro.props;
902
902
  position: relative;
903
903
  }
904
904
 
905
- .ice-bag {
905
+ :global(.ice-bag) {
906
906
  transition: transform 0.3s;
907
907
  cursor: pointer;
908
908
  }
909
909
 
910
- .ice-bag:hover {
910
+ :global(.ice-bag:hover) {
911
911
  transform: scale(1.1);
912
912
  z-index: 50;
913
913
  }
914
914
 
915
- .bags-plus {
915
+ :global(.bags-plus) {
916
916
  position: absolute;
917
917
  top: -3rem;
918
918
  left: 50%;
@@ -1,4 +1,4 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { PartyKegUI, PartyKegLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'party-stock-calculator';
@@ -140,6 +140,27 @@ const seo: PartyKegLocaleContent['seo'] = [
140
140
  ];
141
141
 
142
142
  const schemas: PartyKegLocaleContent['schemas'] = [
143
+ {
144
+ '@context': 'https://schema.org',
145
+ '@type': 'FAQPage',
146
+ mainEntity: faq.map((item) => ({
147
+ '@type': 'Question',
148
+ name: item.question,
149
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
150
+ })),
151
+ } as WithContext<FAQPage>,
152
+ {
153
+ '@context': 'https://schema.org',
154
+ '@type': 'HowTo',
155
+ name: title,
156
+ description: description,
157
+ step: howTo.map((step, i) => ({
158
+ '@type': 'HowToStep',
159
+ position: i + 1,
160
+ name: step.name,
161
+ text: step.text,
162
+ })),
163
+ } as WithContext<HowTo>,
143
164
  {
144
165
  '@context': 'https://schema.org',
145
166
  '@type': 'SoftwareApplication',
@@ -1,4 +1,4 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { PartyKegUI, PartyKegLocaleContent } from '../index';
3
3
 
4
4
  const slug = 'calculadora-barriles-fiesta';
@@ -144,6 +144,27 @@ const seo: PartyKegLocaleContent['seo'] = [
144
144
  ];
145
145
 
146
146
  const schemas: PartyKegLocaleContent['schemas'] = [
147
+ {
148
+ '@context': 'https://schema.org',
149
+ '@type': 'FAQPage',
150
+ mainEntity: faq.map((item) => ({
151
+ '@type': 'Question',
152
+ name: item.question,
153
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
154
+ })),
155
+ } as WithContext<FAQPage>,
156
+ {
157
+ '@context': 'https://schema.org',
158
+ '@type': 'HowTo',
159
+ name: title,
160
+ description: description,
161
+ step: howTo.map((step, i) => ({
162
+ '@type': 'HowToStep',
163
+ position: i + 1,
164
+ name: step.name,
165
+ text: step.text,
166
+ })),
167
+ } as WithContext<HowTo>,
147
168
  {
148
169
  '@context': 'https://schema.org',
149
170
  '@type': 'SoftwareApplication',
@@ -1,7 +1,7 @@
1
- import type { WithContext, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
2
2
  import type { PartyKegUI, PartyKegLocaleContent } from '../index';
3
3
 
4
- const slug = 'calculateur-fete-fût';
4
+ const slug = 'calculateur-fete-fut';
5
5
  const title = 'Calculateur de Bière pour Fêtes : Quantité par Personne, Mariage ou Anniversaire';
6
6
  const description = 'Outil gratuit pour calculer la quantité de bière et de glace selon les invités, la durée et la température. Idéal pour les mariages, anniversaires et événements en plein air.';
7
7
 
@@ -140,6 +140,27 @@ const seo: PartyKegLocaleContent['seo'] = [
140
140
  ];
141
141
 
142
142
  const schemas: PartyKegLocaleContent['schemas'] = [
143
+ {
144
+ '@context': 'https://schema.org',
145
+ '@type': 'FAQPage',
146
+ mainEntity: faq.map((item) => ({
147
+ '@type': 'Question',
148
+ name: item.question,
149
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
150
+ })),
151
+ } as WithContext<FAQPage>,
152
+ {
153
+ '@context': 'https://schema.org',
154
+ '@type': 'HowTo',
155
+ name: title,
156
+ description: description,
157
+ step: howTo.map((step, i) => ({
158
+ '@type': 'HowToStep',
159
+ position: i + 1,
160
+ name: step.name,
161
+ text: step.text,
162
+ })),
163
+ } as WithContext<HowTo>,
143
164
  {
144
165
  '@context': 'https://schema.org',
145
166
  '@type': 'SoftwareApplication',