@jjlmoya/utils-babies 1.13.0 → 1.15.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 (46) hide show
  1. package/package.json +2 -2
  2. package/src/category/index.ts +3 -0
  3. package/src/entries.ts +4 -1
  4. package/src/index.ts +1 -0
  5. package/src/pages/[locale]/[slug].astro +31 -39
  6. package/src/tests/locale_completeness.test.ts +2 -2
  7. package/src/tests/shared-test-helpers.ts +56 -0
  8. package/src/tests/tool_exports.test.ts +34 -0
  9. package/src/tests/tool_validation.test.ts +2 -2
  10. package/src/tool/baby-budget-planner/baby-budget-planner.css +405 -0
  11. package/src/tool/baby-budget-planner/bibliography.astro +14 -0
  12. package/src/tool/baby-budget-planner/bibliography.ts +14 -0
  13. package/src/tool/baby-budget-planner/component.astro +406 -0
  14. package/src/tool/baby-budget-planner/entry.ts +77 -0
  15. package/src/tool/baby-budget-planner/i18n/de.ts +182 -0
  16. package/src/tool/baby-budget-planner/i18n/en.ts +184 -0
  17. package/src/tool/baby-budget-planner/i18n/es.ts +183 -0
  18. package/src/tool/baby-budget-planner/i18n/fr.ts +183 -0
  19. package/src/tool/baby-budget-planner/i18n/id.ts +183 -0
  20. package/src/tool/baby-budget-planner/i18n/it.ts +183 -0
  21. package/src/tool/baby-budget-planner/i18n/ja.ts +183 -0
  22. package/src/tool/baby-budget-planner/i18n/ko.ts +183 -0
  23. package/src/tool/baby-budget-planner/i18n/nl.ts +183 -0
  24. package/src/tool/baby-budget-planner/i18n/pl.ts +183 -0
  25. package/src/tool/baby-budget-planner/i18n/pt.ts +183 -0
  26. package/src/tool/baby-budget-planner/i18n/ru.ts +183 -0
  27. package/src/tool/baby-budget-planner/i18n/sv.ts +183 -0
  28. package/src/tool/baby-budget-planner/i18n/tr.ts +183 -0
  29. package/src/tool/baby-budget-planner/i18n/zh.ts +183 -0
  30. package/src/tool/baby-budget-planner/index.ts +10 -0
  31. package/src/tool/baby-budget-planner/logic.ts +68 -0
  32. package/src/tool/baby-budget-planner/seo.astro +15 -0
  33. package/src/tool/baby-feeding-calculator/baby-feeding-calculator.css +40 -40
  34. package/src/tool/baby-feeding-calculator/bibliography.astro +9 -6
  35. package/src/tool/baby-feeding-calculator/seo.astro +2 -45
  36. package/src/tool/baby-percentile-calculator/baby-weight-height-percentile.css +43 -43
  37. package/src/tool/baby-percentile-calculator/seo.astro +7 -28
  38. package/src/tool/baby-size-converter/baby-size-converter.css +42 -42
  39. package/src/tool/baby-size-converter/seo.astro +7 -28
  40. package/src/tool/fertile-days-estimator/fertile-days-calculator.css +23 -23
  41. package/src/tool/fertile-days-estimator/seo.astro +7 -28
  42. package/src/tool/pregnancy-calculator/pregnancy-weeks-calculator.css +37 -37
  43. package/src/tool/pregnancy-calculator/seo.astro +7 -28
  44. package/src/tool/vaccination-calendar/baby-vaccination-calendar-spain.css +16 -16
  45. package/src/tool/vaccination-calendar/seo.astro +7 -28
  46. package/src/tools.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-babies",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -22,7 +22,7 @@
22
22
  "build": "astro build",
23
23
  "preview": "astro preview",
24
24
  "astro": "astro",
25
- "lint": "eslint src/ --max-warnings 0 && stylelint \"src/**/*.{css,astro}\"",
25
+ "lint": "eslint src/ --max-warnings 0 && stylelint \"src/**/*.css\" \"src/tool/**/*.astro\" \"src/layouts/**/*.astro\" \"src/components/**/*.astro\"",
26
26
  "check": "astro check",
27
27
  "type-check": "astro check",
28
28
  "test": "vitest run",
@@ -6,10 +6,13 @@ import { fertileDaysEstimator } from '../tool/fertile-days-estimator/entry';
6
6
  import { babyPercentileCalculator } from '../tool/baby-percentile-calculator/entry';
7
7
  import { pregnancyCalculator } from '../tool/pregnancy-calculator/entry';
8
8
 
9
+ import { babyBudgetPlanner } from '../tool/baby-budget-planner/entry';
10
+
9
11
  export const babiesCategory: BabiesCategoryEntry = {
10
12
  icon: 'mdi:baby-carriage',
11
13
  tools: [
12
14
  babyFeedingCalculator,
15
+ babyBudgetPlanner,
13
16
  babySizeConverter,
14
17
  vaccinationCalendar,
15
18
  fertileDaysEstimator,
package/src/entries.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { babyFeedingCalculator } from './tool/baby-feeding-calculator/entry';
2
+ export { babyBudgetPlanner } from './tool/baby-budget-planner/entry';
2
3
  export type { BabyFeedingCalculatorUI, BabyFeedingCalculatorLocaleContent } from './tool/baby-feeding-calculator/entry';
4
+ export type { BabyBudgetPlannerUI, BabyBudgetPlannerLocaleContent } from './tool/baby-budget-planner/entry';
3
5
  export { babyPercentileCalculator } from './tool/baby-percentile-calculator/entry';
4
6
  export type { BabyPercentileCalculatorUI, BabyPercentileCalculatorLocaleContent } from './tool/baby-percentile-calculator/entry';
5
7
  export { babySizeConverter } from './tool/baby-size-converter/entry';
@@ -12,9 +14,10 @@ export { vaccinationCalendar } from './tool/vaccination-calendar/entry';
12
14
  export type { VaccinationCalendarUI, VaccinationCalendarLocaleContent } from './tool/vaccination-calendar/entry';
13
15
  export { babiesCategory } from './category';
14
16
  import { babyFeedingCalculator } from './tool/baby-feeding-calculator/entry';
17
+ import { babyBudgetPlanner } from './tool/baby-budget-planner/entry';
15
18
  import { babyPercentileCalculator } from './tool/baby-percentile-calculator/entry';
16
19
  import { babySizeConverter } from './tool/baby-size-converter/entry';
17
20
  import { fertileDaysEstimator } from './tool/fertile-days-estimator/entry';
18
21
  import { pregnancyCalculator } from './tool/pregnancy-calculator/entry';
19
22
  import { vaccinationCalendar } from './tool/vaccination-calendar/entry';
20
- export const ALL_ENTRIES = [babyFeedingCalculator, babyPercentileCalculator, babySizeConverter, fertileDaysEstimator, pregnancyCalculator, vaccinationCalendar];
23
+ export const ALL_ENTRIES = [babyFeedingCalculator, babyBudgetPlanner, babyPercentileCalculator, babySizeConverter, fertileDaysEstimator, pregnancyCalculator, vaccinationCalendar];
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { babiesCategory } from './category';
2
2
 
3
3
  export * from './tool/baby-feeding-calculator';
4
+ export * from './tool/baby-budget-planner';
4
5
  export * from './tool/baby-size-converter';
5
6
  export * from './tool/baby-percentile-calculator';
6
7
  export * from './tool/fertile-days-estimator';
@@ -16,37 +16,19 @@ export async function getStaticPaths() {
16
16
 
17
17
  for (const { entry, Component: lazyComp } of ALL_TOOLS) {
18
18
  const { default: Component } = await lazyComp();
19
- const localeEntries = Object.entries(entry.i18n) as [
20
- KnownLocale,
21
- () => Promise<ToolLocaleContent>,
22
- ][];
23
- const localeContents = await Promise.all(
24
- localeEntries.map(async ([locale, loader]) => ({
25
- locale,
26
- content: await loader(),
27
- })),
28
- );
29
-
30
- const localeUrls = Object.fromEntries(
31
- localeContents.map(({ locale, content }) => [
32
- locale,
33
- `/${locale}/${content.slug}`,
34
- ]),
35
- ) as Partial<Record<KnownLocale, string>>;
19
+ const localeEntries = Object.entries(entry.i18n) as [KnownLocale, () => Promise<ToolLocaleContent>][];
20
+ const localeContents = await Promise.all(localeEntries.map(async ([locale, loader]) => ({ locale, content: await loader() })));
21
+ const localeUrls = Object.fromEntries(localeContents.map(({ locale, content }) => [locale, `/${locale}/${content.slug}`])) as Partial<Record<KnownLocale, string>>;
22
+ const englishSlug = entry.i18n.en ? (await entry.i18n.en()).slug : entry.id;
36
23
 
37
24
  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
- );
46
- paths.push({
47
- params: { locale, slug: content.slug },
48
- props: { Component, locale, content, localeUrls, allToolsNav },
49
- });
25
+ const allToolsNav = (await Promise.all(ALL_TOOLS.map(async ({ entry: n }) => {
26
+ const l = n.i18n[locale] ?? n.i18n.en;
27
+ if (!l) return null;
28
+ const c = await l();
29
+ return { id: n.id, title: c.title, href: `/${locale}/${c.slug}`, isActive: n.id === entry.id };
30
+ }))).filter(Boolean) as NavItem[];
31
+ paths.push({ params: { locale, slug: content.slug }, props: { Component, locale, content, localeUrls, allToolsNav, englishSlug } });
50
32
  }
51
33
  }
52
34
 
@@ -66,11 +48,22 @@ interface Props {
66
48
  content: ToolLocaleContent;
67
49
  localeUrls: Partial<Record<KnownLocale, string>>;
68
50
  allToolsNav: NavItem[];
51
+ englishSlug: string;
69
52
  }
70
53
 
71
- const { Component, locale, content, localeUrls, allToolsNav } = Astro.props;
54
+ const { Component, locale, content, localeUrls, allToolsNav, englishSlug } =
55
+ Astro.props;
72
56
 
73
- const seoContent: UtilitySEOContent = { locale, sections: content.seo };
57
+ const cssFiles = import.meta.glob("../" + "../tool/**" + "/*.css", {
58
+ query: "?raw",
59
+ import: "default",
60
+ });
61
+ const cssKey = Object.keys(cssFiles).find((k) =>
62
+ k.endsWith(`/${englishSlug}.css`),
63
+ );
64
+ const toolCss = cssKey ? ((await cssFiles[cssKey]()) as string) : "";
65
+
66
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
74
67
 
75
68
  const words = content.title.split(" ");
76
69
  const titleHighlight = words[0] || "";
@@ -83,14 +76,11 @@ const titleBase = words.slice(1).join(" ") || "";
83
76
  localeUrls={localeUrls}
84
77
  hasSidebar={true}
85
78
  >
86
- <PreviewNavSidebar
87
- slot="sidebar"
88
- categoryTitle="Tools"
89
- tools={allToolsNav}
90
- />
79
+ <PreviewNavSidebar slot="sidebar" categoryTitle="Tools" tools={allToolsNav} />
91
80
  <Fragment slot="head">
81
+ {toolCss && <style set:html={toolCss} />}
92
82
  {
93
- content.schemas.map((schema: unknown) => (
83
+ (content.schemas ?? []).map((schema) => (
94
84
  <script
95
85
  is:inline
96
86
  type="application/ld+json"
@@ -116,11 +106,11 @@ const titleBase = words.slice(1).join(" ") || "";
116
106
  </section>
117
107
 
118
108
  <section class="section-faq">
119
- <FAQSection items={content.faq} title={content.ui['faqTitle'] ?? ''} inLanguage={locale} />
109
+ <FAQSection items={content.faq} inLanguage={locale} />
120
110
  </section>
121
111
 
122
112
  <section class="section-bibliography">
123
- <Bibliography links={content.bibliography} title={content.ui['bibliographyTitle'] ?? ''} />
113
+ <Bibliography links={content.bibliography} />
124
114
  </section>
125
115
  </div>
126
116
  </PreviewLayout>
@@ -131,11 +121,13 @@ const titleBase = words.slice(1).join(" ") || "";
131
121
  flex-direction: column;
132
122
  gap: 2rem;
133
123
  }
124
+
134
125
  .section-tool {
135
126
  max-width: 1200px;
136
127
  margin: 0 auto;
137
128
  width: 100%;
138
129
  }
130
+
139
131
  .section-seo,
140
132
  .section-faq,
141
133
  .section-bibliography {
@@ -35,8 +35,8 @@ describe('Locale Completeness Validation', () => {
35
35
  });
36
36
  });
37
37
 
38
- it('all 6 tools registered', () => {
39
- expect(ALL_TOOLS.length).toBe(6);
38
+ it('all 7 tools registered', () => {
39
+ expect(ALL_TOOLS.length).toBe(7);
40
40
  });
41
41
  });
42
42
 
@@ -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
+ });
@@ -4,8 +4,8 @@ import { babiesCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 6 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(6);
7
+ it('should have 7 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(7);
9
9
  });
10
10
 
11
11
  it('babiesCategory should be defined', () => {
@@ -0,0 +1,405 @@
1
+ .bbp-card {
2
+ --bbp-primary: #f43f5e;
3
+ --bbp-primary-soft: rgba(244, 63, 94, 0.05);
4
+ --bbp-primary-border: rgba(244, 63, 94, 0.1);
5
+ --bbp-bg-card: #fff;
6
+ --bbp-bg-left: #f8fafc;
7
+ --bbp-bg-item: #fff;
8
+ --bbp-text-main: #0f172a;
9
+ --bbp-text-muted: #64748b;
10
+ --bbp-border: #e2e8f0;
11
+ --bbp-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05);
12
+
13
+ display: flex;
14
+ flex-direction: column;
15
+ background: var(--bbp-bg-card);
16
+ border-radius: 2rem;
17
+ overflow: hidden;
18
+ border: 1px solid var(--bbp-border);
19
+ box-shadow: var(--bbp-shadow);
20
+ color: var(--bbp-text-main);
21
+ }
22
+
23
+ .theme-dark .bbp-card {
24
+ --bbp-bg-card: #020617;
25
+ --bbp-bg-left: #0f172a;
26
+ --bbp-bg-item: #1e293b;
27
+ --bbp-text-main: #f8fafc;
28
+ --bbp-text-muted: #94a3b8;
29
+ --bbp-border: #334155;
30
+ --bbp-primary-soft: rgba(244, 63, 94, 0.15);
31
+ --bbp-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
32
+ }
33
+
34
+ .bbp-main {
35
+ display: grid;
36
+ grid-template-columns: 1.2fr 0.8fr;
37
+ min-height: 600px;
38
+ }
39
+
40
+ .bbp-left {
41
+ background: var(--bbp-bg-left);
42
+ padding: 40px;
43
+ border-right: 1px solid var(--bbp-border);
44
+ }
45
+
46
+ .bbp-right {
47
+ padding: 40px;
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: 48px;
51
+ }
52
+
53
+ .bbp-right-col {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 32px;
57
+ }
58
+
59
+ @media (max-width: 1024px) {
60
+ .bbp-main {
61
+ grid-template-columns: 1fr;
62
+ }
63
+ .bbp-left {
64
+ border-right: none;
65
+ border-bottom: 1px solid var(--bbp-border);
66
+ }
67
+ }
68
+
69
+ .bbp-section-marker {
70
+ display: block;
71
+ font-size: 0.7rem;
72
+ font-weight: 800;
73
+ text-transform: uppercase;
74
+ letter-spacing: 0.1em;
75
+ color: var(--bbp-primary);
76
+ margin-bottom: 20px;
77
+ }
78
+
79
+ .bbp-group {
80
+ margin-bottom: 24px;
81
+ }
82
+
83
+ .bbp-label {
84
+ display: block;
85
+ font-size: 0.85rem;
86
+ font-weight: 600;
87
+ margin-bottom: 12px;
88
+ }
89
+
90
+ .bbp-options-grid {
91
+ display: grid;
92
+ grid-template-columns: repeat(3, 1fr);
93
+ gap: 8px;
94
+ }
95
+
96
+ .bbp-choice-btn {
97
+ background: var(--bbp-bg-item);
98
+ border: 1px solid var(--bbp-border);
99
+ padding: 12px 8px;
100
+ border-radius: 12px;
101
+ font-size: 0.8rem;
102
+ font-weight: 600;
103
+ cursor: pointer;
104
+ transition: all 0.2s ease;
105
+ color: var(--bbp-text-main);
106
+ }
107
+
108
+ .bbp-choice-btn:hover {
109
+ border-color: var(--bbp-primary);
110
+ }
111
+
112
+ .bbp-choice-btn.active {
113
+ background: var(--bbp-primary);
114
+ color: white;
115
+ border-color: var(--bbp-primary);
116
+ box-shadow: 0 4px 12px rgba(244, 63, 94, 0.2);
117
+ }
118
+
119
+ .bbp-cta-btn {
120
+ width: 100%;
121
+ background: var(--bbp-primary);
122
+ color: white;
123
+ border: none;
124
+ padding: 18px;
125
+ border-radius: 16px;
126
+ font-weight: 800;
127
+ font-size: 1rem;
128
+ cursor: pointer;
129
+ transition: transform 0.2s ease;
130
+ margin-top: 10px;
131
+ }
132
+
133
+ .bbp-cta-btn:hover { transform: translateY(-2px); }
134
+
135
+ .bbp-item-row {
136
+ display: flex;
137
+ justify-content: space-between;
138
+ align-items: center;
139
+ padding: 16px;
140
+ background: var(--bbp-bg-item);
141
+ border-radius: 16px;
142
+ margin-bottom: 12px;
143
+ border: 1px solid var(--bbp-border);
144
+ }
145
+
146
+ .bbp-item-meta { flex: 1; }
147
+
148
+ .bbp-item-name {
149
+ display: block;
150
+ font-weight: 700;
151
+ font-size: 0.95rem;
152
+ margin-bottom: 4px;
153
+ }
154
+
155
+ .bbp-toggles {
156
+ display: flex;
157
+ gap: 12px;
158
+ }
159
+
160
+ .bbp-toggle-pill {
161
+ font-size: 0.7rem;
162
+ font-weight: 700;
163
+ color: var(--bbp-text-muted);
164
+ cursor: pointer;
165
+ padding: 2px 0;
166
+ border-bottom: 2px solid transparent;
167
+ }
168
+
169
+ .bbp-toggle-pill.sh.checked {
170
+ color: #10b981;
171
+ border-bottom-color: #10b981;
172
+ }
173
+
174
+ .bbp-toggle-pill.wl.checked {
175
+ color: #06b6d4;
176
+ border-bottom-color: #06b6d4;
177
+ }
178
+
179
+ .bbp-price-area {
180
+ text-align: right;
181
+ min-width: 100px;
182
+ }
183
+
184
+ .bbp-price-bubble {
185
+ font-weight: 800;
186
+ font-size: 1.1rem;
187
+ cursor: pointer;
188
+ padding: 4px 8px;
189
+ border-radius: 8px;
190
+ }
191
+
192
+ .bbp-price-bubble:hover { background: var(--bbp-primary-soft); }
193
+
194
+ .bbp-price-bubble.is-zero {
195
+ color: #10b981;
196
+ }
197
+
198
+ .bbp-input-fancy {
199
+ width: 100%;
200
+ padding: 12px;
201
+ border-radius: 12px;
202
+ border: 2px solid var(--bbp-border);
203
+ background: var(--bbp-bg-card);
204
+ color: var(--bbp-text-main);
205
+ font-weight: 700;
206
+ }
207
+
208
+ .bbp-input-fancy:focus {
209
+ outline: none;
210
+ border-color: var(--bbp-primary);
211
+ }
212
+
213
+ .bbp-total-box {
214
+ background: var(--bbp-primary-soft);
215
+ padding: 1.5rem;
216
+ border-radius: 1.25rem;
217
+ text-align: center;
218
+ border: 1px solid var(--bbp-primary-border);
219
+ }
220
+
221
+ .bbp-import-promo {
222
+ flex: 1;
223
+ display: flex;
224
+ flex-direction: column;
225
+ align-items: center;
226
+ justify-content: center;
227
+ gap: 1.5rem;
228
+ padding: 2rem;
229
+ background: var(--bbp-bg-item);
230
+ border: 2px dashed var(--bbp-border);
231
+ border-radius: 1.5rem;
232
+ min-height: 300px;
233
+ }
234
+
235
+ .bbp-promo-icon {
236
+ width: 64px;
237
+ height: 64px;
238
+ color: var(--bbp-primary);
239
+ opacity: 0.5;
240
+ }
241
+
242
+ .bbp-tool-btn.primary-btn {
243
+ background: var(--bbp-primary);
244
+ color: white;
245
+ padding: 1rem 2rem;
246
+ font-size: 1rem;
247
+ }
248
+
249
+ .bbp-total-val {
250
+ font-size: 2.5rem;
251
+ font-weight: 900;
252
+ color: var(--bbp-primary);
253
+ margin: 10px 0;
254
+ }
255
+
256
+ .bbp-stat-label {
257
+ font-size: 0.75rem;
258
+ font-weight: 700;
259
+ color: var(--bbp-text-muted);
260
+ text-transform: uppercase;
261
+ }
262
+
263
+ .bbp-progress-rail {
264
+ height: 8px;
265
+ background: var(--bbp-border);
266
+ border-radius: 4px;
267
+ overflow: hidden;
268
+ margin: 15px 0;
269
+ }
270
+
271
+ .bbp-progress-thumb {
272
+ height: 100%;
273
+ background: var(--bbp-primary);
274
+ transition: width 0.3s ease;
275
+ }
276
+
277
+ .bbp-curve-wrapper {
278
+ display: flex;
279
+ flex-direction: column;
280
+ gap: 8px;
281
+ }
282
+
283
+ .bbp-curve-chart {
284
+ height: 140px;
285
+ display: flex;
286
+ align-items: flex-end;
287
+ gap: 2px;
288
+ padding-top: 20px;
289
+ }
290
+
291
+ .bbp-bar-wrap {
292
+ flex: 1;
293
+ display: flex;
294
+ flex-direction: column;
295
+ align-items: center;
296
+ justify-content: flex-end;
297
+ height: 100%;
298
+ gap: 4px;
299
+ }
300
+
301
+ .bbp-bar-val {
302
+ font-size: 0.6rem;
303
+ font-weight: 800;
304
+ color: var(--bbp-primary);
305
+ transition: opacity 0.2s ease;
306
+ }
307
+
308
+ .bbp-bar {
309
+ width: 100%;
310
+ background: var(--bbp-primary);
311
+ border-radius: 4px 4px 0 0;
312
+ min-height: 2px;
313
+ transition: all 0.2s ease;
314
+ }
315
+
316
+ .bbp-curve-labels {
317
+ display: flex;
318
+ gap: 2px;
319
+ }
320
+
321
+ .bbp-bar-lbl {
322
+ flex: 1;
323
+ text-align: center;
324
+ font-size: 0.55rem;
325
+ font-weight: 800;
326
+ color: var(--bbp-text-muted);
327
+ text-transform: uppercase;
328
+ }
329
+
330
+ .bbp-ghost-item {
331
+ padding: 12px 16px;
332
+ background: rgba(245, 158, 11, 0.05);
333
+ border-radius: 12px;
334
+ font-size: 0.85rem;
335
+ font-weight: 700;
336
+ border: 1px solid rgba(245, 158, 11, 0.2);
337
+ color: #b45309;
338
+ }
339
+
340
+ .theme-dark .bbp-ghost-item {
341
+ background: rgba(245, 158, 11, 0.1);
342
+ color: #fcd34d;
343
+ border-color: rgba(245, 158, 11, 0.3);
344
+ }
345
+
346
+ .bbp-panel {
347
+ background: var(--bbp-bg-item);
348
+ border: 1px solid var(--bbp-border);
349
+ border-radius: 1.5rem;
350
+ padding: 24px;
351
+ }
352
+
353
+
354
+ .bbp-ghost-list {
355
+ display: flex;
356
+ flex-direction: column;
357
+ gap: 8px;
358
+ }
359
+
360
+ .bbp-diaper-grid {
361
+ display: grid;
362
+ grid-template-columns: 1fr 1fr;
363
+ gap: 12px;
364
+ }
365
+
366
+ .bbp-diaper-result {
367
+ font-weight: 800;
368
+ font-size: 0.9rem;
369
+ color: var(--bbp-primary);
370
+ margin-top: 8px;
371
+ text-align: center;
372
+ }
373
+
374
+ .bbp-action-grid {
375
+ display: grid;
376
+ grid-template-columns: 1fr 1fr;
377
+ gap: 12px;
378
+ margin-top: auto;
379
+ }
380
+
381
+ .bbp-tool-btn {
382
+ background: var(--bbp-bg-item);
383
+ border: 1px solid var(--bbp-border);
384
+ padding: 14px;
385
+ border-radius: 14px;
386
+ font-weight: 700;
387
+ font-size: 0.85rem;
388
+ cursor: pointer;
389
+ color: var(--bbp-text-main);
390
+ }
391
+
392
+ #bbp-reset-btn {
393
+ border-color: rgba(244, 63, 94, 0.2);
394
+ color: #f43f5e;
395
+ }
396
+
397
+ #bbp-reset-btn[data-confirm='true'] {
398
+ background: #f43f5e;
399
+ color: white;
400
+ border-color: #f43f5e;
401
+ }
402
+
403
+ .bbp-hidden {
404
+ display: none;
405
+ }