@jjlmoya/utils-home 1.15.0 → 1.17.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 (140) hide show
  1. package/package.json +5 -3
  2. package/scripts/postinstall.mjs +27 -0
  3. package/src/pages/[locale]/[slug].astro +28 -12
  4. package/src/tests/locale_completeness.test.ts +2 -20
  5. package/src/tests/shared-test-helpers.ts +56 -0
  6. package/src/tests/tool_exports.test.ts +34 -0
  7. package/src/tool/dewPointCalculator/bibliography.ts +10 -0
  8. package/src/tool/dewPointCalculator/component.astro +0 -232
  9. package/src/tool/dewPointCalculator/dew-point-calculator.css +230 -0
  10. package/src/tool/dewPointCalculator/i18n/de.ts +2 -12
  11. package/src/tool/dewPointCalculator/i18n/en.ts +2 -12
  12. package/src/tool/dewPointCalculator/i18n/es.ts +2 -12
  13. package/src/tool/dewPointCalculator/i18n/fr.ts +2 -12
  14. package/src/tool/dewPointCalculator/i18n/id.ts +2 -12
  15. package/src/tool/dewPointCalculator/i18n/it.ts +2 -12
  16. package/src/tool/dewPointCalculator/i18n/ja.ts +2 -12
  17. package/src/tool/dewPointCalculator/i18n/ko.ts +2 -12
  18. package/src/tool/dewPointCalculator/i18n/nl.ts +2 -12
  19. package/src/tool/dewPointCalculator/i18n/pl.ts +2 -12
  20. package/src/tool/dewPointCalculator/i18n/pt.ts +2 -12
  21. package/src/tool/dewPointCalculator/i18n/ru.ts +2 -12
  22. package/src/tool/dewPointCalculator/i18n/sv.ts +2 -12
  23. package/src/tool/dewPointCalculator/i18n/tr.ts +2 -12
  24. package/src/tool/dewPointCalculator/i18n/zh.ts +2 -12
  25. package/src/tool/dewPointCalculator/seo.astro +2 -1
  26. package/src/tool/heatingComparator/bibliography.ts +14 -0
  27. package/src/tool/heatingComparator/component.astro +0 -269
  28. package/src/tool/heatingComparator/heating-consumption-comparator.css +267 -0
  29. package/src/tool/heatingComparator/i18n/de.ts +2 -16
  30. package/src/tool/heatingComparator/i18n/en.ts +2 -12
  31. package/src/tool/heatingComparator/i18n/es.ts +2 -16
  32. package/src/tool/heatingComparator/i18n/fr.ts +2 -12
  33. package/src/tool/heatingComparator/i18n/id.ts +2 -16
  34. package/src/tool/heatingComparator/i18n/it.ts +2 -16
  35. package/src/tool/heatingComparator/i18n/ja.ts +296 -310
  36. package/src/tool/heatingComparator/i18n/ko.ts +296 -306
  37. package/src/tool/heatingComparator/i18n/nl.ts +2 -16
  38. package/src/tool/heatingComparator/i18n/pl.ts +2 -16
  39. package/src/tool/heatingComparator/i18n/pt.ts +2 -16
  40. package/src/tool/heatingComparator/i18n/ru.ts +2 -12
  41. package/src/tool/heatingComparator/i18n/sv.ts +2 -16
  42. package/src/tool/heatingComparator/i18n/tr.ts +2 -16
  43. package/src/tool/heatingComparator/i18n/zh.ts +296 -306
  44. package/src/tool/heatingComparator/seo.astro +3 -3
  45. package/src/tool/ledSavingCalculator/bibliography.ts +14 -0
  46. package/src/tool/ledSavingCalculator/component.astro +0 -305
  47. package/src/tool/ledSavingCalculator/i18n/de.ts +2 -12
  48. package/src/tool/ledSavingCalculator/i18n/en.ts +2 -16
  49. package/src/tool/ledSavingCalculator/i18n/es.ts +2 -16
  50. package/src/tool/ledSavingCalculator/i18n/fr.ts +2 -16
  51. package/src/tool/ledSavingCalculator/i18n/id.ts +2 -12
  52. package/src/tool/ledSavingCalculator/i18n/it.ts +2 -12
  53. package/src/tool/ledSavingCalculator/i18n/ja.ts +2 -12
  54. package/src/tool/ledSavingCalculator/i18n/ko.ts +2 -12
  55. package/src/tool/ledSavingCalculator/i18n/nl.ts +2 -12
  56. package/src/tool/ledSavingCalculator/i18n/pl.ts +2 -12
  57. package/src/tool/ledSavingCalculator/i18n/pt.ts +2 -12
  58. package/src/tool/ledSavingCalculator/i18n/ru.ts +2 -12
  59. package/src/tool/ledSavingCalculator/i18n/sv.ts +2 -12
  60. package/src/tool/ledSavingCalculator/i18n/tr.ts +2 -12
  61. package/src/tool/ledSavingCalculator/i18n/zh.ts +2 -12
  62. package/src/tool/ledSavingCalculator/led-saving-calculator.css +303 -0
  63. package/src/tool/ledSavingCalculator/seo.astro +2 -1
  64. package/src/tool/projectorCalculator/bibliography.ts +5 -0
  65. package/src/tool/projectorCalculator/component.astro +0 -359
  66. package/src/tool/projectorCalculator/i18n/de.ts +2 -6
  67. package/src/tool/projectorCalculator/i18n/en.ts +2 -7
  68. package/src/tool/projectorCalculator/i18n/es.ts +2 -7
  69. package/src/tool/projectorCalculator/i18n/fr.ts +2 -7
  70. package/src/tool/projectorCalculator/i18n/id.ts +2 -7
  71. package/src/tool/projectorCalculator/i18n/it.ts +2 -6
  72. package/src/tool/projectorCalculator/i18n/ja.ts +175 -179
  73. package/src/tool/projectorCalculator/i18n/ko.ts +175 -179
  74. package/src/tool/projectorCalculator/i18n/nl.ts +2 -6
  75. package/src/tool/projectorCalculator/i18n/pl.ts +2 -6
  76. package/src/tool/projectorCalculator/i18n/pt.ts +2 -6
  77. package/src/tool/projectorCalculator/i18n/ru.ts +2 -6
  78. package/src/tool/projectorCalculator/i18n/sv.ts +2 -6
  79. package/src/tool/projectorCalculator/i18n/tr.ts +2 -6
  80. package/src/tool/projectorCalculator/i18n/zh.ts +175 -179
  81. package/src/tool/projectorCalculator/projector-throw-calculator.css +357 -0
  82. package/src/tool/projectorCalculator/seo.astro +2 -1
  83. package/src/tool/qrGenerator/bibliography.ts +14 -0
  84. package/src/tool/qrGenerator/component.astro +0 -266
  85. package/src/tool/qrGenerator/i18n/de.ts +192 -202
  86. package/src/tool/qrGenerator/i18n/en.ts +2 -16
  87. package/src/tool/qrGenerator/i18n/es.ts +2 -16
  88. package/src/tool/qrGenerator/i18n/fr.ts +2 -16
  89. package/src/tool/qrGenerator/i18n/id.ts +146 -150
  90. package/src/tool/qrGenerator/i18n/it.ts +169 -173
  91. package/src/tool/qrGenerator/i18n/ja.ts +146 -150
  92. package/src/tool/qrGenerator/i18n/ko.ts +146 -150
  93. package/src/tool/qrGenerator/i18n/nl.ts +146 -150
  94. package/src/tool/qrGenerator/i18n/pl.ts +146 -150
  95. package/src/tool/qrGenerator/i18n/pt.ts +146 -150
  96. package/src/tool/qrGenerator/i18n/ru.ts +146 -150
  97. package/src/tool/qrGenerator/i18n/sv.ts +146 -150
  98. package/src/tool/qrGenerator/i18n/tr.ts +146 -150
  99. package/src/tool/qrGenerator/i18n/zh.ts +146 -150
  100. package/src/tool/qrGenerator/qr-generator.css +264 -0
  101. package/src/tool/qrGenerator/seo.astro +2 -1
  102. package/src/tool/solarCalculator/bibliography.ts +5 -0
  103. package/src/tool/solarCalculator/component.astro +0 -298
  104. package/src/tool/solarCalculator/i18n/de.ts +141 -145
  105. package/src/tool/solarCalculator/i18n/en.ts +2 -7
  106. package/src/tool/solarCalculator/i18n/es.ts +2 -7
  107. package/src/tool/solarCalculator/i18n/fr.ts +2 -7
  108. package/src/tool/solarCalculator/i18n/id.ts +2 -6
  109. package/src/tool/solarCalculator/i18n/it.ts +2 -6
  110. package/src/tool/solarCalculator/i18n/ja.ts +121 -125
  111. package/src/tool/solarCalculator/i18n/ko.ts +116 -120
  112. package/src/tool/solarCalculator/i18n/nl.ts +2 -5
  113. package/src/tool/solarCalculator/i18n/pl.ts +2 -6
  114. package/src/tool/solarCalculator/i18n/pt.ts +2 -6
  115. package/src/tool/solarCalculator/i18n/ru.ts +2 -5
  116. package/src/tool/solarCalculator/i18n/sv.ts +2 -5
  117. package/src/tool/solarCalculator/i18n/tr.ts +2 -5
  118. package/src/tool/solarCalculator/i18n/zh.ts +116 -120
  119. package/src/tool/solarCalculator/seo.astro +2 -1
  120. package/src/tool/solarCalculator/solar-panel-calculator.css +296 -0
  121. package/src/tool/tariffComparator/bibliography.ts +7 -0
  122. package/src/tool/tariffComparator/component.astro +0 -337
  123. package/src/tool/tariffComparator/electricity-tariff-comparator.css +335 -0
  124. package/src/tool/tariffComparator/i18n/de.ts +129 -132
  125. package/src/tool/tariffComparator/i18n/en.ts +2 -9
  126. package/src/tool/tariffComparator/i18n/es.ts +2 -9
  127. package/src/tool/tariffComparator/i18n/fr.ts +2 -9
  128. package/src/tool/tariffComparator/i18n/id.ts +2 -5
  129. package/src/tool/tariffComparator/i18n/it.ts +2 -5
  130. package/src/tool/tariffComparator/i18n/ja.ts +129 -132
  131. package/src/tool/tariffComparator/i18n/ko.ts +129 -132
  132. package/src/tool/tariffComparator/i18n/nl.ts +2 -5
  133. package/src/tool/tariffComparator/i18n/pl.ts +2 -5
  134. package/src/tool/tariffComparator/i18n/pt.ts +2 -5
  135. package/src/tool/tariffComparator/i18n/ru.ts +2 -5
  136. package/src/tool/tariffComparator/i18n/sv.ts +2 -5
  137. package/src/tool/tariffComparator/i18n/tr.ts +2 -5
  138. package/src/tool/tariffComparator/i18n/zh.ts +129 -132
  139. package/src/tool/tariffComparator/seo.astro +2 -1
  140. package/src/types.ts +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-home",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -10,7 +10,8 @@
10
10
  "./entries": "./src/entries.ts"
11
11
  },
12
12
  "files": [
13
- "src"
13
+ "src",
14
+ "scripts"
14
15
  ],
15
16
  "publishConfig": {
16
17
  "access": "public"
@@ -29,7 +30,8 @@
29
30
  "postversion": "git push && git push --tags",
30
31
  "patch": "npm version patch",
31
32
  "minor": "npm version minor",
32
- "major": "npm version major"
33
+ "major": "npm version major",
34
+ "postinstall": "node scripts/postinstall.mjs"
33
35
  },
34
36
  "lint-staged": {
35
37
  "*.{ts,tsx,astro}": [
@@ -0,0 +1,27 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const libDir = dirname(fileURLToPath(import.meta.url));
6
+ const toolsDir = join(libDir, '../src/tool');
7
+
8
+ const inNodeModules = libDir.includes('node_modules');
9
+ if (!inNodeModules) process.exit(0);
10
+
11
+ const projectRoot = join(libDir, '../../../..');
12
+ const categoryKey = JSON.parse(readFileSync(join(libDir, '../package.json'), 'utf8')).name.replace('@jjlmoya/utils-', '');
13
+ const destDir = join(projectRoot, `public/styles/lib/${categoryKey}`);
14
+
15
+ mkdirSync(destDir, { recursive: true });
16
+
17
+ const tools = readdirSync(toolsDir, { withFileTypes: true }).filter(d => d.isDirectory());
18
+ for (const tool of tools) {
19
+ const toolDir = join(toolsDir, tool.name);
20
+ let files;
21
+ try { files = readdirSync(toolDir).filter(f => f.endsWith('.css')); }
22
+ catch { continue; }
23
+ for (const file of files) {
24
+ writeFileSync(join(destDir, file), readFileSync(join(toolDir, file)));
25
+ console.log(`[@jjlmoya/utils-${categoryKey}] copied ${file}`);
26
+ }
27
+ }
@@ -34,18 +34,28 @@ export async function getStaticPaths() {
34
34
  ]),
35
35
  ) as Partial<Record<KnownLocale, string>>;
36
36
 
37
+ const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
38
+ const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
39
+
37
40
  for (const { locale, content } of localeContents) {
38
- const allToolsNav = await Promise.all(
39
- ALL_TOOLS.map(async ({ entry: navEntry }) => ({
40
- id: navEntry.id,
41
- title: (await navEntry.i18n[locale]!()).title,
42
- href: `/${locale}/${(await navEntry.i18n[locale]!()).slug}`,
43
- isActive: navEntry.id === entry.id,
44
- })),
45
- );
41
+ const allToolsNav = (
42
+ await Promise.all(
43
+ ALL_TOOLS.map(async ({ entry: navEntry }) => {
44
+ const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
45
+ if (!loader) return null;
46
+ const navContent = await loader();
47
+ return {
48
+ id: navEntry.id,
49
+ title: navContent.title,
50
+ href: `/${locale}/${navContent.slug}`,
51
+ isActive: navEntry.id === entry.id,
52
+ };
53
+ }),
54
+ )
55
+ ).filter(Boolean) as NavItem[];
46
56
  paths.push({
47
57
  params: { locale, slug: content.slug },
48
- props: { Component, locale, content, localeUrls, allToolsNav },
58
+ props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
49
59
  });
50
60
  }
51
61
  }
@@ -66,11 +76,16 @@ interface Props {
66
76
  content: ToolLocaleContent;
67
77
  localeUrls: Partial<Record<KnownLocale, string>>;
68
78
  allToolsNav: NavItem[];
79
+ englishSlug: string;
69
80
  }
70
81
 
71
- const { Component, locale, content, localeUrls, allToolsNav } = Astro.props;
82
+ const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
83
+
84
+ const cssFiles = import.meta.glob("../../tool/*/*.css", { query: "?raw", import: "default" });
85
+ const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
86
+ const toolCss = cssKey ? await cssFiles[cssKey]() as string : "";
72
87
 
73
- const seoContent: UtilitySEOContent = { locale, sections: content.seo };
88
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
74
89
 
75
90
  const words = content.title.split(" ");
76
91
  const titleHighlight = words[0] || "";
@@ -89,8 +104,9 @@ const titleBase = words.slice(1).join(" ") || "";
89
104
  tools={allToolsNav}
90
105
  />
91
106
  <Fragment slot="head">
107
+ {toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
92
108
  {
93
- content.schemas.map((schema: unknown) => (
109
+ ( content.schemas ?? []).map((schema: unknown) => (
94
110
  <script
95
111
  is:inline
96
112
  type="application/ld+json"
@@ -7,28 +7,10 @@ describe('Locale Completeness Validation', () => {
7
7
  describe(`Tool: ${tool.entry.id}`, () => {
8
8
  Object.keys(tool.entry.i18n).forEach((locale) => {
9
9
  describe(`Locale: ${locale}`, () => {
10
- it('faqTitle should be defined when faq items exist', async () => {
10
+ it('should load without errors', async () => {
11
11
  const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
12
12
  const content = (await loader?.()) as ToolLocaleContent;
13
-
14
- if (content.faq.length > 0) {
15
- expect(
16
- content.faqTitle,
17
- `Tool "${tool.entry.id}" locale "${locale}" has ${content.faq.length} FAQ items but is missing faqTitle`,
18
- ).toBeTruthy();
19
- }
20
- });
21
-
22
- it('bibliographyTitle should be defined when bibliography items exist', async () => {
23
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
24
- const content = (await loader?.()) as ToolLocaleContent;
25
-
26
- if (content.bibliography.length > 0) {
27
- expect(
28
- content.bibliographyTitle,
29
- `Tool "${tool.entry.id}" locale "${locale}" has ${content.bibliography.length} bibliography items but is missing bibliographyTitle`,
30
- ).toBeTruthy();
31
- }
13
+ expect(content).toBeDefined();
32
14
  });
33
15
  });
34
16
  });
@@ -0,0 +1,56 @@
1
+ import type { ToolDefinition } from '../types';
2
+
3
+ export interface ToolExportValidationResult {
4
+ passed: boolean;
5
+ failures: string[];
6
+ }
7
+
8
+ function validateComponentType(
9
+ toolId: string,
10
+ componentName: string,
11
+ component: unknown,
12
+ failures: string[],
13
+ ): void {
14
+ if (typeof component !== 'function') {
15
+ failures.push(`${toolId}: ${componentName} is not a function (${typeof component})`);
16
+ }
17
+ }
18
+
19
+ async function validateComponentExecution(
20
+ toolId: string,
21
+ componentName: string,
22
+ fn: () => Promise<unknown>,
23
+ failures: string[],
24
+ ): Promise<void> {
25
+ try {
26
+ const result = await fn();
27
+ if (!result || typeof result !== 'object') {
28
+ failures.push(`${toolId}: ${componentName} import returned invalid result`);
29
+ }
30
+ } catch (error) {
31
+ failures.push(`${toolId}: ${componentName} execution error - ${error instanceof Error ? error.message : 'unknown'}`);
32
+ }
33
+ }
34
+
35
+ export async function validateToolExports(tools: ToolDefinition[]): Promise<ToolExportValidationResult> {
36
+ const failures: string[] = [];
37
+
38
+ for (const tool of tools) {
39
+ validateComponentType(tool.entry.id, 'Component', tool.Component, failures);
40
+ validateComponentType(tool.entry.id, 'SEOComponent', tool.SEOComponent, failures);
41
+ validateComponentType(tool.entry.id, 'BibliographyComponent', tool.BibliographyComponent, failures);
42
+
43
+ const componentFn = tool.Component as () => Promise<unknown>;
44
+ const seoFn = tool.SEOComponent as () => Promise<unknown>;
45
+ const bibFn = tool.BibliographyComponent as () => Promise<unknown>;
46
+
47
+ await validateComponentExecution(tool.entry.id, 'Component', componentFn, failures);
48
+ await validateComponentExecution(tool.entry.id, 'SEOComponent', seoFn, failures);
49
+ await validateComponentExecution(tool.entry.id, 'BibliographyComponent', bibFn, failures);
50
+ }
51
+
52
+ return {
53
+ passed: failures.length === 0,
54
+ failures,
55
+ };
56
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import { validateToolExports } from './shared-test-helpers';
4
+
5
+ describe('Tool Exports Pattern Validation', () => {
6
+ describe('Component Exports Format', () => {
7
+ ALL_TOOLS.forEach((tool) => {
8
+ it(`${tool.entry.id}: Component should be a lazy-loaded function`, () => {
9
+ expect(typeof tool.Component).toBe('function');
10
+ expect(tool.Component).toBeInstanceOf(Function);
11
+ });
12
+
13
+ it(`${tool.entry.id}: SEOComponent should be a lazy-loaded function`, () => {
14
+ expect(typeof tool.SEOComponent).toBe('function');
15
+ expect(tool.SEOComponent).toBeInstanceOf(Function);
16
+ });
17
+
18
+ it(`${tool.entry.id}: BibliographyComponent should be a lazy-loaded function`, () => {
19
+ expect(typeof tool.BibliographyComponent).toBe('function');
20
+ expect(tool.BibliographyComponent).toBeInstanceOf(Function);
21
+ });
22
+ });
23
+ });
24
+
25
+ describe('Dynamic Import Validation', () => {
26
+ it('all tools must have functional dynamic imports', async () => {
27
+ const result = await validateToolExports(ALL_TOOLS);
28
+ if (!result.passed) {
29
+ throw new Error(`Tool export validation failed:\n${result.failures.join('\n')}`);
30
+ }
31
+ expect(result.passed).toBe(true);
32
+ });
33
+ });
34
+ });
@@ -0,0 +1,10 @@
1
+ export const bibliography = [
2
+ {
3
+ name: 'Magnus Approximation of the Dew-Point — Meteorological Applications (2011)',
4
+ url: 'https://es.scribd.com/document/331352069/dew-point',
5
+ },
6
+ {
7
+ name: 'Guide to Meteorological Instruments and Methods of Observation — WMO (2021)',
8
+ url: 'https://community.wmo.int/site/knowledge-hub/programmes-and-initiatives/instruments-and-methods-of-observation-programme-imop/guide-instruments-and-methods-of-observation-wmo-no-8',
9
+ },
10
+ ];
@@ -209,235 +209,3 @@ const fillW = Math.abs(tempX - dewX);
209
209
  init();
210
210
  </script>
211
211
 
212
- <style>
213
- .dew-wrapper {
214
- --dew-p: #8b5cf6;
215
-
216
- width: 100%;
217
- padding: 1rem 0;
218
- }
219
-
220
- .dew-card {
221
- background: var(--bg-surface);
222
- width: calc(100% - 24px);
223
- max-width: 900px;
224
- margin: 0 auto;
225
- border-radius: 24px;
226
- overflow: hidden;
227
- display: flex;
228
- flex-direction: column;
229
- border: 1px solid var(--border-color);
230
- color: var(--text-main);
231
- }
232
-
233
- @media (min-width: 768px) {
234
- .dew-card {
235
- flex-direction: row;
236
- min-height: 500px;
237
- }
238
- }
239
-
240
- .dew-left {
241
- flex: 0 0 auto;
242
- width: 100%;
243
- padding: 32px;
244
- border-bottom: 1px solid var(--border-color);
245
- display: flex;
246
- flex-direction: column;
247
- gap: 32px;
248
- }
249
-
250
- @media (min-width: 768px) {
251
- .dew-left {
252
- width: 340px;
253
- border-bottom: none;
254
- border-right: 1px solid var(--border-color);
255
- }
256
- }
257
-
258
- .dew-right {
259
- flex: 1;
260
- background: var(--bg-page);
261
- display: flex;
262
- flex-direction: column;
263
- align-items: center;
264
- justify-content: center;
265
- gap: 24px;
266
- padding: 32px;
267
- min-height: 360px;
268
- }
269
-
270
- .dew-field {
271
- display: flex;
272
- flex-direction: column;
273
- gap: 12px;
274
- }
275
-
276
- .dew-field-top {
277
- display: flex;
278
- justify-content: space-between;
279
- align-items: flex-end;
280
- }
281
-
282
- .dew-label {
283
- font-size: 0.6875rem;
284
- font-weight: 700;
285
- text-transform: uppercase;
286
- letter-spacing: 0.1em;
287
- color: var(--dew-p);
288
- }
289
-
290
- .dew-val-display {
291
- font-size: 2rem;
292
- font-weight: 900;
293
- color: var(--text-main);
294
- line-height: 1;
295
- }
296
-
297
- .dew-unit {
298
- font-size: 1rem;
299
- font-weight: 400;
300
- color: var(--text-muted);
301
- }
302
-
303
- .dew-slider {
304
- width: 100%;
305
- height: 6px;
306
- accent-color: var(--dew-p);
307
- cursor: pointer;
308
- border-radius: 9999px;
309
- }
310
-
311
- .dew-steps {
312
- display: flex;
313
- gap: 8px;
314
- }
315
-
316
- .dew-step {
317
- flex: 1;
318
- padding: 8px;
319
- border-radius: 10px;
320
- border: 1px solid var(--border-color);
321
- background: var(--bg-surface);
322
- color: var(--text-main);
323
- font-size: 1.25rem;
324
- font-weight: 700;
325
- cursor: pointer;
326
- transition: all 0.15s;
327
- }
328
-
329
- .dew-step:hover {
330
- border-color: var(--dew-p);
331
- color: var(--dew-p);
332
- }
333
-
334
- .dew-result-section {
335
- text-align: center;
336
- }
337
-
338
- .dew-result-label {
339
- font-size: 0.6875rem;
340
- font-weight: 700;
341
- text-transform: uppercase;
342
- letter-spacing: 0.15em;
343
- color: var(--text-muted);
344
- margin: 0 0 8px;
345
- }
346
-
347
- .dew-result-value {
348
- font-size: 5rem;
349
- font-weight: 900;
350
- color: #06b6d4;
351
- line-height: 1;
352
- margin: 0;
353
- }
354
-
355
- .dew-result-unit {
356
- font-size: 2.5rem;
357
- font-weight: 300;
358
- color: var(--text-muted);
359
- }
360
-
361
- .dew-risk-badge {
362
- display: flex;
363
- align-items: center;
364
- gap: 10px;
365
- padding: 12px 18px;
366
- border-radius: 12px;
367
- width: 100%;
368
- max-width: 320px;
369
- transition: background 0.3s;
370
- }
371
-
372
- .dew-risk-dot {
373
- width: 10px;
374
- height: 10px;
375
- border-radius: 50%;
376
- flex-shrink: 0;
377
- }
378
-
379
- .dew-risk-text {
380
- display: flex;
381
- flex-direction: column;
382
- gap: 2px;
383
- }
384
-
385
- .dew-risk-name {
386
- font-size: 0.875rem;
387
- font-weight: 700;
388
- }
389
-
390
- .dew-risk-desc {
391
- font-size: 0.6875rem;
392
- opacity: 0.8;
393
- }
394
-
395
- .dew-risk-low {
396
- background: rgba(34, 197, 94, 0.1);
397
- border: 1px solid rgba(34, 197, 94, 0.2);
398
- color: #4ade80;
399
- }
400
-
401
- .dew-risk-low .dew-risk-dot {
402
- background: #22c55e;
403
- }
404
-
405
- .dew-risk-medium {
406
- background: rgba(245, 158, 11, 0.1);
407
- border: 1px solid rgba(245, 158, 11, 0.2);
408
- color: #fbbf24;
409
- }
410
-
411
- .dew-risk-medium .dew-risk-dot {
412
- background: #f59e0b;
413
- }
414
-
415
- .dew-risk-high {
416
- background: rgba(249, 115, 22, 0.1);
417
- border: 1px solid rgba(249, 115, 22, 0.2);
418
- color: #fb923c;
419
- }
420
-
421
- .dew-risk-high .dew-risk-dot {
422
- background: #f97316;
423
- }
424
-
425
- .dew-risk-extreme {
426
- background: rgba(239, 68, 68, 0.1);
427
- border: 1px solid rgba(239, 68, 68, 0.2);
428
- color: #f87171;
429
- }
430
-
431
- .dew-risk-extreme .dew-risk-dot {
432
- background: #ef4444;
433
- }
434
-
435
- .dew-gauge {
436
- width: 100%;
437
- max-width: 320px;
438
- }
439
-
440
- .dew-gauge-track {
441
- fill: var(--bg-muted);
442
- }
443
- </style>