@jjlmoya/utils-finance 1.1.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 (133) hide show
  1. package/package.json +64 -0
  2. package/src/category/i18n/en.ts +159 -0
  3. package/src/category/i18n/es.ts +159 -0
  4. package/src/category/i18n/fr.ts +159 -0
  5. package/src/category/index.ts +24 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +32 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +39 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +23 -0
  22. package/src/tests/title_quality.test.ts +55 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/compoundInterest/bibliography.astro +14 -0
  25. package/src/tool/compoundInterest/component.astro +697 -0
  26. package/src/tool/compoundInterest/i18n/en.ts +212 -0
  27. package/src/tool/compoundInterest/i18n/es.ts +212 -0
  28. package/src/tool/compoundInterest/i18n/fr.ts +212 -0
  29. package/src/tool/compoundInterest/index.ts +34 -0
  30. package/src/tool/compoundInterest/logic.ts +40 -0
  31. package/src/tool/compoundInterest/seo.astro +14 -0
  32. package/src/tool/compoundInterest/ui.ts +19 -0
  33. package/src/tool/courtFeeCalculator/bibliography.astro +14 -0
  34. package/src/tool/courtFeeCalculator/component.astro +427 -0
  35. package/src/tool/courtFeeCalculator/i18n/en.ts +268 -0
  36. package/src/tool/courtFeeCalculator/i18n/es.ts +268 -0
  37. package/src/tool/courtFeeCalculator/i18n/fr.ts +268 -0
  38. package/src/tool/courtFeeCalculator/index.ts +33 -0
  39. package/src/tool/courtFeeCalculator/logic.ts +86 -0
  40. package/src/tool/courtFeeCalculator/seo.astro +14 -0
  41. package/src/tool/courtFeeCalculator/ui.ts +31 -0
  42. package/src/tool/debtSnowball/bibliography.astro +14 -0
  43. package/src/tool/debtSnowball/component.astro +1052 -0
  44. package/src/tool/debtSnowball/i18n/en.ts +288 -0
  45. package/src/tool/debtSnowball/i18n/es.ts +288 -0
  46. package/src/tool/debtSnowball/i18n/fr.ts +288 -0
  47. package/src/tool/debtSnowball/index.ts +37 -0
  48. package/src/tool/debtSnowball/logic.ts +156 -0
  49. package/src/tool/debtSnowball/seo.astro +14 -0
  50. package/src/tool/debtSnowball/ui.ts +49 -0
  51. package/src/tool/fireCalculator/bibliography.astro +14 -0
  52. package/src/tool/fireCalculator/component.astro +689 -0
  53. package/src/tool/fireCalculator/i18n/en.ts +261 -0
  54. package/src/tool/fireCalculator/i18n/es.ts +261 -0
  55. package/src/tool/fireCalculator/i18n/fr.ts +261 -0
  56. package/src/tool/fireCalculator/index.ts +33 -0
  57. package/src/tool/fireCalculator/logic.ts +108 -0
  58. package/src/tool/fireCalculator/seo.astro +14 -0
  59. package/src/tool/fireCalculator/ui.ts +33 -0
  60. package/src/tool/ibanBicSwiftConverter/bibliography.astro +14 -0
  61. package/src/tool/ibanBicSwiftConverter/component.astro +581 -0
  62. package/src/tool/ibanBicSwiftConverter/i18n/en.ts +167 -0
  63. package/src/tool/ibanBicSwiftConverter/i18n/es.ts +216 -0
  64. package/src/tool/ibanBicSwiftConverter/i18n/fr.ts +167 -0
  65. package/src/tool/ibanBicSwiftConverter/index.ts +33 -0
  66. package/src/tool/ibanBicSwiftConverter/logic.ts +101 -0
  67. package/src/tool/ibanBicSwiftConverter/seo.astro +14 -0
  68. package/src/tool/ibanBicSwiftConverter/ui.ts +17 -0
  69. package/src/tool/inflation/bibliography.astro +14 -0
  70. package/src/tool/inflation/component.astro +400 -0
  71. package/src/tool/inflation/i18n/en.ts +301 -0
  72. package/src/tool/inflation/i18n/es.ts +301 -0
  73. package/src/tool/inflation/i18n/fr.ts +301 -0
  74. package/src/tool/inflation/index.ts +34 -0
  75. package/src/tool/inflation/logic.ts +92 -0
  76. package/src/tool/inflation/seo.astro +14 -0
  77. package/src/tool/inflation/ui.ts +17 -0
  78. package/src/tool/lateInterest/bibliography.astro +14 -0
  79. package/src/tool/lateInterest/component.astro +655 -0
  80. package/src/tool/lateInterest/i18n/en.ts +255 -0
  81. package/src/tool/lateInterest/i18n/es.ts +224 -0
  82. package/src/tool/lateInterest/i18n/fr.ts +224 -0
  83. package/src/tool/lateInterest/index.ts +34 -0
  84. package/src/tool/lateInterest/logic.ts +89 -0
  85. package/src/tool/lateInterest/seo.astro +14 -0
  86. package/src/tool/lateInterest/ui.ts +33 -0
  87. package/src/tool/legalInterestRate/bibliography.astro +14 -0
  88. package/src/tool/legalInterestRate/component.astro +650 -0
  89. package/src/tool/legalInterestRate/i18n/en.ts +159 -0
  90. package/src/tool/legalInterestRate/i18n/es.ts +159 -0
  91. package/src/tool/legalInterestRate/i18n/fr.ts +159 -0
  92. package/src/tool/legalInterestRate/index.ts +33 -0
  93. package/src/tool/legalInterestRate/logic.ts +29 -0
  94. package/src/tool/legalInterestRate/seo.astro +14 -0
  95. package/src/tool/legalInterestRate/ui.ts +23 -0
  96. package/src/tool/lotteryOptimizer/bibliography.astro +14 -0
  97. package/src/tool/lotteryOptimizer/component.astro +863 -0
  98. package/src/tool/lotteryOptimizer/i18n/en.ts +188 -0
  99. package/src/tool/lotteryOptimizer/i18n/es.ts +281 -0
  100. package/src/tool/lotteryOptimizer/i18n/fr.ts +188 -0
  101. package/src/tool/lotteryOptimizer/index.ts +33 -0
  102. package/src/tool/lotteryOptimizer/logic.ts +133 -0
  103. package/src/tool/lotteryOptimizer/seo.astro +14 -0
  104. package/src/tool/lotteryOptimizer/ui.ts +33 -0
  105. package/src/tool/mortgage/bibliography.astro +14 -0
  106. package/src/tool/mortgage/component.astro +1030 -0
  107. package/src/tool/mortgage/i18n/en.ts +227 -0
  108. package/src/tool/mortgage/i18n/es.ts +227 -0
  109. package/src/tool/mortgage/i18n/fr.ts +227 -0
  110. package/src/tool/mortgage/index.ts +34 -0
  111. package/src/tool/mortgage/logic.ts +118 -0
  112. package/src/tool/mortgage/seo.astro +14 -0
  113. package/src/tool/mortgage/ui.ts +35 -0
  114. package/src/tool/percentageCalculator/bibliography.astro +14 -0
  115. package/src/tool/percentageCalculator/component.astro +640 -0
  116. package/src/tool/percentageCalculator/i18n/en.ts +233 -0
  117. package/src/tool/percentageCalculator/i18n/es.ts +233 -0
  118. package/src/tool/percentageCalculator/i18n/fr.ts +233 -0
  119. package/src/tool/percentageCalculator/index.ts +34 -0
  120. package/src/tool/percentageCalculator/logic.ts +48 -0
  121. package/src/tool/percentageCalculator/seo.astro +14 -0
  122. package/src/tool/percentageCalculator/ui.ts +30 -0
  123. package/src/tool/rentIncreaseCalculator/bibliography.astro +14 -0
  124. package/src/tool/rentIncreaseCalculator/component.astro +587 -0
  125. package/src/tool/rentIncreaseCalculator/i18n/en.ts +159 -0
  126. package/src/tool/rentIncreaseCalculator/i18n/es.ts +220 -0
  127. package/src/tool/rentIncreaseCalculator/i18n/fr.ts +159 -0
  128. package/src/tool/rentIncreaseCalculator/index.ts +33 -0
  129. package/src/tool/rentIncreaseCalculator/logic.ts +43 -0
  130. package/src/tool/rentIncreaseCalculator/seo.astro +14 -0
  131. package/src/tool/rentIncreaseCalculator/ui.ts +19 -0
  132. package/src/tools.ts +16 -0
  133. package/src/types.ts +74 -0
@@ -0,0 +1,697 @@
1
+ ---
2
+ import type { KnownLocale } from '../../types';
3
+ import type { CompoundInterestUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: KnownLocale;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ const t = (ui ?? {}) as CompoundInterestUI;
12
+ ---
13
+
14
+
15
+ <div
16
+ class="cic-root"
17
+ data-currency-code={t.currencyCode}
18
+ data-currency-locale={t.currencyLocale}
19
+ data-currency-symbol={t.currencySymbol}
20
+ data-label-year={t.labelYear}
21
+ data-label-principal={t.labelPrincipal}
22
+ data-label-interest={t.labelInterest}
23
+ data-insight-prefix={t.insightPrefix}
24
+ data-insight-suffix={t.insightSuffix}
25
+ >
26
+ <div class="cic-header">
27
+ <div class="cic-header-left">
28
+ <div class="cic-icon-wrap">
29
+ <svg
30
+ xmlns="http://www.w3.org/2000/svg"
31
+ class="cic-icon"
32
+ viewBox="0 0 24 24"
33
+ fill="none"
34
+ stroke="currentColor"
35
+ stroke-width="2"
36
+ stroke-linecap="round"
37
+ stroke-linejoin="round"
38
+ >
39
+ <path d="M12 20V10"></path>
40
+ <path d="M18 20V4"></path>
41
+ <path d="M6 20v-4"></path>
42
+ </svg>
43
+ </div>
44
+ <span class="cic-title">{t.labelTitle}</span>
45
+ </div>
46
+ <div class="cic-realtime">
47
+ <span class="cic-dot"></span>
48
+ {t.labelRealtime}
49
+ </div>
50
+ </div>
51
+
52
+ <div class="cic-grid">
53
+ <div class="cic-panel-left">
54
+ <div class="cic-fields">
55
+ <div class="cic-field">
56
+ <label class="cic-label">{t.labelInitial}</label>
57
+ <div class="cic-input-wrap">
58
+ <span class="cic-currency">{t.currencySymbol}</span>
59
+ <input
60
+ type="number"
61
+ id="cic-initial"
62
+ value="3000"
63
+ class="cic-input cic-input-prefixed"
64
+ />
65
+ </div>
66
+ </div>
67
+
68
+ <div class="cic-field">
69
+ <label class="cic-label">{t.labelMonthly}</label>
70
+ <div class="cic-input-wrap">
71
+ <span class="cic-currency">{t.currencySymbol}</span>
72
+ <input
73
+ type="number"
74
+ id="cic-monthly"
75
+ value="300"
76
+ class="cic-input cic-input-prefixed"
77
+ />
78
+ </div>
79
+ </div>
80
+
81
+ <div class="cic-grid-2">
82
+ <div class="cic-field">
83
+ <label class="cic-label">{t.labelRate}</label>
84
+ <div class="cic-input-wrap">
85
+ <input
86
+ type="number"
87
+ id="cic-rate"
88
+ value="8"
89
+ step="0.1"
90
+ class="cic-input cic-input-suffixed"
91
+ />
92
+ <span class="cic-currency cic-currency-right">%</span>
93
+ </div>
94
+ </div>
95
+ <div class="cic-field">
96
+ <label class="cic-label">{t.labelYears}</label>
97
+ <div class="cic-input-wrap">
98
+ <input
99
+ type="number"
100
+ id="cic-years"
101
+ value="20"
102
+ class="cic-input"
103
+ />
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <div class="cic-summary">
110
+ <div class="cic-summary-row">
111
+ <div class="cic-summary-label">
112
+ <span class="cic-dot-neutral"></span>
113
+ <span class="cic-text-muted">{t.labelMyMoney}</span>
114
+ </div>
115
+ <span id="cic-total-invested" class="cic-summary-value">0</span>
116
+ </div>
117
+ <div class="cic-summary-row">
118
+ <div class="cic-summary-label">
119
+ <span class="cic-dot-accent"></span>
120
+ <span class="cic-text-accent">{t.labelProfit}</span>
121
+ </div>
122
+ <span id="cic-total-interest" class="cic-summary-value cic-summary-value-accent">+0</span>
123
+ </div>
124
+ <div class="cic-summary-total">
125
+ <span class="cic-total-label">{t.labelTotal}</span>
126
+ <span id="cic-final-amount" class="cic-total-value">0</span>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <div class="cic-panel-right">
132
+ <div class="cic-canvas-wrap">
133
+ <canvas id="cic-chart"></canvas>
134
+ </div>
135
+ <div id="cic-insight" class="cic-insight"></div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <script>
141
+ import { CompoundInterestCalculator } from './logic';
142
+ import Chart from 'chart.js/auto';
143
+
144
+ let chart: Chart | null = null;
145
+
146
+ const root = document.querySelector('.cic-root') as HTMLElement;
147
+ const ctx = document.getElementById('cic-chart') as HTMLCanvasElement;
148
+
149
+ const initialEl = document.getElementById('cic-initial') as HTMLInputElement;
150
+ const monthlyEl = document.getElementById('cic-monthly') as HTMLInputElement;
151
+ const rateEl = document.getElementById('cic-rate') as HTMLInputElement;
152
+ const yearsEl = document.getElementById('cic-years') as HTMLInputElement;
153
+
154
+ const investedOut = document.getElementById('cic-total-invested');
155
+ const interestOut = document.getElementById('cic-total-interest');
156
+ const finalOut = document.getElementById('cic-final-amount');
157
+ const insightBox = document.getElementById('cic-insight');
158
+
159
+ const currencyCode = root?.dataset.currencyCode ?? 'EUR';
160
+ const currencyLocale = root?.dataset.currencyLocale ?? 'es-ES';
161
+ const labelYear = root?.dataset.labelYear ?? 'Año';
162
+ const labelPrincipal = root?.dataset.labelPrincipal ?? 'Principal';
163
+ const labelInterest = root?.dataset.labelInterest ?? 'Intereses';
164
+ const insightPrefix = root?.dataset.insightPrefix ?? '';
165
+ const insightSuffix = root?.dataset.insightSuffix ?? '';
166
+
167
+ const formatMoney = (val: number): string =>
168
+ new Intl.NumberFormat(currencyLocale, {
169
+ style: 'currency',
170
+ currency: currencyCode,
171
+ maximumFractionDigits: 0,
172
+ })
173
+ .format(val)
174
+ .replace(/\u00A0/g, ' ');
175
+
176
+ const updateSummary = (last: { invested: number; interest: number; total: number }): void => {
177
+ if (investedOut) investedOut.innerText = formatMoney(last.invested);
178
+ if (interestOut) interestOut.innerText = formatMoney(last.interest);
179
+ if (finalOut) finalOut.innerText = formatMoney(last.total);
180
+ };
181
+
182
+ const formatTooltipLabel = (context: unknown): string => {
183
+ const ctx = context as { dataset: { label: string }; parsed: { y: number | null } };
184
+ let label = ctx.dataset.label ?? '';
185
+ if (label) label += ': ';
186
+ if (ctx.parsed.y !== null) {
187
+ label += new Intl.NumberFormat(currencyLocale, {
188
+ style: 'currency',
189
+ currency: currencyCode,
190
+ maximumFractionDigits: 0,
191
+ })
192
+ .format(ctx.parsed.y)
193
+ .replace(/\u00A0/g, ' ');
194
+ }
195
+ return label;
196
+ };
197
+
198
+ const formatAxisTick = (value: unknown): string => {
199
+ return (
200
+ new Intl.NumberFormat(currencyLocale, {
201
+ notation: 'compact',
202
+ compactDisplay: 'short',
203
+ }).format(Number(value)) +
204
+ ' ' +
205
+ (root?.dataset.currencySymbol ?? '€')
206
+ );
207
+ };
208
+
209
+ const getChartOptions = () => ({
210
+ responsive: true,
211
+ maintainAspectRatio: false,
212
+ interaction: { mode: 'index' as const, intersect: false },
213
+ plugins: {
214
+ legend: { position: 'bottom' as const },
215
+ tooltip: {
216
+ callbacks: {
217
+ label: formatTooltipLabel,
218
+ },
219
+ },
220
+ },
221
+ scales: {
222
+ y: {
223
+ stacked: true,
224
+ beginAtZero: true,
225
+ grid: { color: 'rgba(0, 0, 0, 0.05)' },
226
+ ticks: {
227
+ callback: formatAxisTick,
228
+ },
229
+ },
230
+ x: { grid: { display: false } },
231
+ },
232
+ });
233
+
234
+ const getChartDatasets = (investedData: number[], interestData: number[]) => [
235
+ {
236
+ label: labelPrincipal,
237
+ data: investedData,
238
+ borderColor: '#94a3b8',
239
+ backgroundColor: 'rgba(148, 163, 184, 0.2)',
240
+ fill: true,
241
+ tension: 0.4,
242
+ pointRadius: 0,
243
+ pointHoverRadius: 6,
244
+ },
245
+ {
246
+ label: labelInterest,
247
+ data: interestData,
248
+ borderColor: '#10b981',
249
+ backgroundColor: 'rgba(16, 185, 129, 0.2)',
250
+ fill: true,
251
+ tension: 0.4,
252
+ pointRadius: 0,
253
+ pointHoverRadius: 6,
254
+ },
255
+ ];
256
+
257
+ const createChart = (labels: string[], investedData: number[], interestData: number[]): void => {
258
+ chart = new Chart(ctx, {
259
+ type: 'line',
260
+ data: { labels, datasets: getChartDatasets(investedData, interestData) },
261
+ options: getChartOptions() as Parameters<typeof Chart>[1]['options'],
262
+ });
263
+ };
264
+
265
+ const findCrossoverYear = (data: Array<{ year: number; interest: number }>, annualContrib: number): number => {
266
+ for (let i = 1; i < data.length; i++) {
267
+ const interestYearly = data[i].interest - data[i - 1].interest;
268
+ if (interestYearly > annualContrib) {
269
+ return data[i].year;
270
+ }
271
+ }
272
+ return -1;
273
+ };
274
+
275
+ const updateInsight = (crossYear: number): void => {
276
+ if (crossYear > 0 && insightBox) {
277
+ insightBox.innerHTML = `${insightPrefix}<span class="cic-insight-year">${crossYear}</span>${insightSuffix}`;
278
+ insightBox.classList.add('cic-insight-visible');
279
+ } else if (insightBox) {
280
+ insightBox.classList.remove('cic-insight-visible');
281
+ }
282
+ };
283
+
284
+ const update = (): void => {
285
+ const input = {
286
+ initialPrincipal: Number(initialEl.value) || 0,
287
+ monthlyContribution: Number(monthlyEl.value) || 0,
288
+ annualInterestRate: Number(rateEl.value) || 0,
289
+ years: Number(yearsEl.value) || 0,
290
+ };
291
+
292
+ const data = CompoundInterestCalculator.calculate(input);
293
+ if (data.length === 0) return;
294
+
295
+ const last = data[data.length - 1];
296
+ updateSummary(last);
297
+
298
+ const labels = data.map((d) => `${labelYear} ${d.year}`);
299
+ const investedData = data.map((d) => d.invested);
300
+ const interestData = data.map((d) => d.interest);
301
+
302
+ if (chart) {
303
+ chart.data.labels = labels;
304
+ chart.data.datasets[0].data = investedData;
305
+ chart.data.datasets[1].data = interestData;
306
+ chart.update();
307
+ } else {
308
+ createChart(labels, investedData, interestData);
309
+ }
310
+
311
+ const annualContrib = input.monthlyContribution * 12;
312
+ const crossYear = findCrossoverYear(data, annualContrib);
313
+ updateInsight(crossYear);
314
+ };
315
+
316
+ [initialEl, monthlyEl, rateEl, yearsEl].forEach((el) => el?.addEventListener('input', update));
317
+
318
+ update();
319
+ </script>
320
+
321
+ <style>
322
+ .cic-root {
323
+ --cic-accent: #10b981;
324
+ --cic-accent-text: #059669;
325
+ --cic-accent-icon-bg: #d1fae5;
326
+ --cic-bg: #fff;
327
+ --cic-bg-panel: rgba(248, 250, 252, 0.3);
328
+ --cic-bg-card: #fff;
329
+ --cic-bg-header: rgba(248, 250, 252, 0.5);
330
+ --cic-border: #e2e8f0;
331
+ --cic-border-subtle: #f1f5f9;
332
+ --cic-text: #0f172a;
333
+ --cic-text-muted: #64748b;
334
+ --cic-text-subtle: #94a3b8;
335
+ --cic-input-bg: #fff;
336
+ --cic-input-border: #e2e8f0;
337
+ --cic-summary-bg: #fff;
338
+ --cic-insight-bg: #ecfdf5;
339
+ --cic-insight-text: #065f46;
340
+
341
+ background: var(--cic-bg);
342
+ border-radius: 1.5rem;
343
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
344
+ border: 1px solid var(--cic-border-subtle);
345
+ overflow: hidden;
346
+ }
347
+
348
+ .cic-header {
349
+ padding: 1.5rem;
350
+ border-bottom: 1px solid var(--cic-border-subtle);
351
+ background: var(--cic-bg-header);
352
+ display: flex;
353
+ flex-wrap: wrap;
354
+ gap: 1rem;
355
+ align-items: center;
356
+ justify-content: space-between;
357
+ }
358
+
359
+ .cic-header-left {
360
+ display: flex;
361
+ align-items: center;
362
+ gap: 0.75rem;
363
+ }
364
+
365
+ .cic-icon-wrap {
366
+ padding: 0.625rem;
367
+ background: var(--cic-accent-icon-bg);
368
+ border-radius: 0.75rem;
369
+ color: var(--cic-accent-text);
370
+ display: flex;
371
+ align-items: center;
372
+ justify-content: center;
373
+ }
374
+
375
+ .cic-icon {
376
+ width: 1.5rem;
377
+ height: 1.5rem;
378
+ }
379
+
380
+ .cic-title {
381
+ font-size: 1.25rem;
382
+ font-weight: 700;
383
+ color: var(--cic-text);
384
+ }
385
+
386
+ .cic-realtime {
387
+ display: flex;
388
+ align-items: center;
389
+ gap: 0.5rem;
390
+ font-size: 0.875rem;
391
+ color: var(--cic-text-muted);
392
+ }
393
+
394
+ .cic-dot {
395
+ width: 0.5rem;
396
+ height: 0.5rem;
397
+ border-radius: 50%;
398
+ background: var(--cic-accent);
399
+ animation: cic-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
400
+ }
401
+
402
+ @keyframes cic-pulse {
403
+ 0%, 100% { opacity: 1; }
404
+ 50% { opacity: 0.4; }
405
+ }
406
+
407
+ .cic-grid {
408
+ display: grid;
409
+ }
410
+
411
+ .cic-panel-left {
412
+ padding: 1.5rem;
413
+ display: flex;
414
+ flex-direction: column;
415
+ gap: 1.5rem;
416
+ background: var(--cic-bg-panel);
417
+ border-bottom: 1px solid var(--cic-border-subtle);
418
+ }
419
+
420
+ .cic-fields {
421
+ display: flex;
422
+ flex-direction: column;
423
+ gap: 1rem;
424
+ }
425
+
426
+ .cic-grid-2 {
427
+ display: grid;
428
+ grid-template-columns: 1fr 1fr;
429
+ gap: 1rem;
430
+ }
431
+
432
+ .cic-label {
433
+ display: block;
434
+ font-size: 0.75rem;
435
+ font-weight: 700;
436
+ color: var(--cic-text-subtle);
437
+ text-transform: uppercase;
438
+ letter-spacing: 0.1em;
439
+ margin-bottom: 0.5rem;
440
+ }
441
+
442
+ .cic-input-wrap {
443
+ position: relative;
444
+ }
445
+
446
+ .cic-currency {
447
+ position: absolute;
448
+ left: 0.5rem;
449
+ top: 50%;
450
+ transform: translateY(-50%);
451
+ color: var(--cic-text-subtle);
452
+ font-weight: 700;
453
+ pointer-events: none;
454
+ font-size: 0.75rem;
455
+ }
456
+
457
+ .cic-currency-right {
458
+ left: auto;
459
+ right: 0.5rem;
460
+ }
461
+
462
+ .cic-input {
463
+ width: 100%;
464
+ padding: 0.75rem 1rem;
465
+ background: var(--cic-input-bg);
466
+ border: 2px solid var(--cic-input-border);
467
+ border-radius: 0.75rem;
468
+ outline: none;
469
+ transition: border-color 0.2s;
470
+ color: var(--cic-text);
471
+ font-weight: 700;
472
+ font-size: 1.125rem;
473
+ box-sizing: border-box;
474
+ }
475
+
476
+ .cic-input-prefixed {
477
+ padding-left: 3.5rem;
478
+ }
479
+
480
+ .cic-input-suffixed {
481
+ padding-right: 5rem;
482
+ }
483
+
484
+ .cic-input:focus {
485
+ border-color: var(--cic-accent);
486
+ }
487
+
488
+ .cic-summary {
489
+ background: var(--cic-summary-bg);
490
+ border-radius: 1rem;
491
+ padding: 1rem;
492
+ border: 1px solid var(--cic-border);
493
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
494
+ display: flex;
495
+ flex-direction: column;
496
+ gap: 0.75rem;
497
+ }
498
+
499
+ .cic-summary-row {
500
+ display: flex;
501
+ justify-content: space-between;
502
+ align-items: center;
503
+ font-size: 0.875rem;
504
+ }
505
+
506
+ .cic-summary-label {
507
+ display: flex;
508
+ align-items: center;
509
+ gap: 0.5rem;
510
+ }
511
+
512
+ .cic-dot-neutral {
513
+ width: 0.75rem;
514
+ height: 0.75rem;
515
+ border-radius: 50%;
516
+ background: var(--cic-text-subtle);
517
+ flex-shrink: 0;
518
+ }
519
+
520
+ .cic-dot-accent {
521
+ width: 0.75rem;
522
+ height: 0.75rem;
523
+ border-radius: 50%;
524
+ background: var(--cic-accent);
525
+ flex-shrink: 0;
526
+ }
527
+
528
+ .cic-text-muted {
529
+ color: var(--cic-text-muted);
530
+ }
531
+
532
+ .cic-text-accent {
533
+ color: var(--cic-accent-text);
534
+ font-weight: 500;
535
+ }
536
+
537
+ .cic-summary-value {
538
+ font-weight: 700;
539
+ color: var(--cic-text);
540
+ }
541
+
542
+ .cic-summary-value-accent {
543
+ color: var(--cic-accent-text);
544
+ }
545
+
546
+ .cic-summary-total {
547
+ padding-top: 0.75rem;
548
+ margin-top: 0.25rem;
549
+ border-top: 1px solid var(--cic-border-subtle);
550
+ display: flex;
551
+ justify-content: space-between;
552
+ align-items: flex-end;
553
+ }
554
+
555
+ .cic-total-label {
556
+ color: var(--cic-text-subtle);
557
+ font-size: 0.75rem;
558
+ font-weight: 700;
559
+ text-transform: uppercase;
560
+ letter-spacing: 0.05em;
561
+ }
562
+
563
+ .cic-total-value {
564
+ font-size: 1.5rem;
565
+ font-weight: 900;
566
+ color: var(--cic-text);
567
+ }
568
+
569
+ .cic-panel-right {
570
+ padding: 1.5rem;
571
+ display: flex;
572
+ flex-direction: column;
573
+ min-height: 500px;
574
+ }
575
+
576
+ .cic-canvas-wrap {
577
+ position: relative;
578
+ width: 100%;
579
+ flex: 1;
580
+ }
581
+
582
+ .cic-insight {
583
+ display: none;
584
+ margin-top: 1rem;
585
+ padding: 1rem;
586
+ background: var(--cic-insight-bg);
587
+ border-radius: 0.75rem;
588
+ font-size: 0.875rem;
589
+ color: var(--cic-insight-text);
590
+ opacity: 0;
591
+ transition: opacity 0.5s;
592
+ }
593
+
594
+ @media (min-width: 768px) {
595
+ .cic-insight {
596
+ display: block;
597
+ }
598
+ }
599
+
600
+ @media (min-width: 1024px) {
601
+ .cic-grid {
602
+ grid-template-columns: 4fr 8fr;
603
+ }
604
+
605
+ .cic-panel-left {
606
+ border-bottom: none;
607
+ border-right: 1px solid var(--cic-border-subtle);
608
+ }
609
+
610
+ .cic-panel-right {
611
+ padding: 2rem;
612
+ min-height: auto;
613
+ }
614
+ }
615
+ </style>
616
+
617
+ <style is:global>
618
+ .cic-insight.cic-insight-visible {
619
+ opacity: 1;
620
+ }
621
+
622
+ .cic-insight-year {
623
+ font-weight: 700;
624
+ color: #10b981;
625
+ }
626
+
627
+ .theme-dark .cic-root {
628
+ --cic-accent-text: #34d399;
629
+ --cic-accent-icon-bg: rgba(16, 185, 129, 0.2);
630
+ --cic-bg: #1e293b;
631
+ --cic-bg-panel: rgba(15, 23, 42, 0.1);
632
+ --cic-bg-card: #1e293b;
633
+ --cic-bg-header: rgba(30, 41, 59, 0.5);
634
+ --cic-border: #334155;
635
+ --cic-border-subtle: #334155;
636
+ --cic-text: #fff;
637
+ --cic-text-muted: #94a3b8;
638
+ --cic-text-subtle: #64748b;
639
+ --cic-input-bg: #0f172a;
640
+ --cic-input-border: #334155;
641
+ --cic-summary-bg: #1e293b;
642
+ --cic-insight-bg: rgba(16, 185, 129, 0.1);
643
+ --cic-insight-text: #6ee7b7;
644
+ }
645
+
646
+ .theme-dark .cic-insight-year {
647
+ color: #fff;
648
+ }
649
+
650
+ [data-theme='dark'] .cic-root {
651
+ --cic-accent-text: #34d399;
652
+ --cic-accent-icon-bg: rgba(16, 185, 129, 0.2);
653
+ --cic-bg: #1e293b;
654
+ --cic-bg-panel: rgba(15, 23, 42, 0.1);
655
+ --cic-bg-card: #1e293b;
656
+ --cic-bg-header: rgba(30, 41, 59, 0.5);
657
+ --cic-border: #334155;
658
+ --cic-border-subtle: #334155;
659
+ --cic-text: #fff;
660
+ --cic-text-muted: #94a3b8;
661
+ --cic-text-subtle: #64748b;
662
+ --cic-input-bg: #0f172a;
663
+ --cic-input-border: #334155;
664
+ --cic-summary-bg: #1e293b;
665
+ --cic-insight-bg: rgba(16, 185, 129, 0.1);
666
+ --cic-insight-text: #6ee7b7;
667
+ }
668
+
669
+ [data-theme='dark'] .cic-insight-year {
670
+ color: #fff;
671
+ }
672
+
673
+ @media (prefers-color-scheme: dark) {
674
+ .cic-root {
675
+ --cic-accent-text: #34d399;
676
+ --cic-accent-icon-bg: rgba(16, 185, 129, 0.2);
677
+ --cic-bg: #1e293b;
678
+ --cic-bg-panel: rgba(15, 23, 42, 0.1);
679
+ --cic-bg-card: #1e293b;
680
+ --cic-bg-header: rgba(30, 41, 59, 0.5);
681
+ --cic-border: #334155;
682
+ --cic-border-subtle: #334155;
683
+ --cic-text: #fff;
684
+ --cic-text-muted: #94a3b8;
685
+ --cic-text-subtle: #64748b;
686
+ --cic-input-bg: #0f172a;
687
+ --cic-input-border: #334155;
688
+ --cic-summary-bg: #1e293b;
689
+ --cic-insight-bg: rgba(16, 185, 129, 0.1);
690
+ --cic-insight-text: #6ee7b7;
691
+ }
692
+
693
+ .cic-insight-year {
694
+ color: #fff;
695
+ }
696
+ }
697
+ </style>