refactor: enhance AnnualBudget component layout with improved dark mode support, streamline table structure, and update CSS for better visual consistency

This commit is contained in:
Jennie Robinson Faber 2025-09-10 15:24:18 +01:00
parent 24e8b7a3a8
commit f073f91569
14 changed files with 1440 additions and 922 deletions

View file

@ -2,114 +2,164 @@
<div class="space-y-8">
<!-- Annual Budget Overview -->
<div class="space-y-4">
<h2 class="text-2xl font-bold">Annual Budget Overview</h2>
<div class="relative">
<div class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<div
class="relative border border-black dark:border-neutral-600 bg-white dark:bg-neutral-950">
<table
class="w-full border-collapse text-sm bg-white dark:bg-neutral-950">
<thead>
<tr class="bg-neutral-100 dark:bg-neutral-950">
<th
class="border-r-1 border-black dark:border-neutral-400 px-4 py-3 text-left font-bold text-black dark:text-white">
Category
</th>
<th
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-3 text-right font-bold text-black dark:text-white">
Planned
</th>
<th
class="px-4 py-3 text-right font-bold text-black dark:text-white">
%
</th>
</tr>
</thead>
<tbody>
<!-- Revenue Section -->
<tr class="bg-black text-white dark:bg-white dark:text-black">
<td
class="px-4 py-2 font-bold text-white dark:text-black"
colspan="3">
REVENUE
</td>
</tr>
<div class="border border-black bg-white">
<table class="w-full border-collapse text-sm">
<thead>
<tr class="border-b-2 border-black bg-neutral-100">
<th class="border-r-1 border-black px-4 py-3 text-left font-bold">
Category
</th>
<th
class="border-r border-neutral-400 px-4 py-3 text-right font-bold">
Planned
</th>
<th class="px-4 py-3 text-right font-bold">%</th>
</tr>
</thead>
<tbody>
<!-- Revenue Section -->
<tr class="bg-black text-white">
<td class="px-4 py-2 font-bold" colspan="3">REVENUE</td>
</tr>
<!-- Revenue Categories -->
<tr
v-for="(category, index) in revenueCategories"
:key="`rev-${index}`"
class="border-t border-neutral-200 dark:border-neutral-700 text-black dark:text-white"
v-show="category.planned > 0">
<td
class="border-r-1 border-black dark:border-neutral-400 px-4 py-2 text-black dark:text-white">
{{ category.name }}
</td>
<td
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-2 text-right text-black dark:text-white font-mono">
{{ formatCurrency(category.planned) }}
</td>
<td
class="px-4 py-2 text-right text-black dark:text-white font-mono">
{{ category.percentage }}%
</td>
</tr>
<!-- Revenue Categories -->
<tr
v-for="(category, index) in revenueCategories"
:key="`rev-${index}`"
class="border-t border-neutral-200"
v-show="category.planned > 0">
<td class="border-r-1 border-black px-4 py-2">
{{ category.name }}
</td>
<td class="border-r border-neutral-400 px-4 py-2 text-right">
{{ formatCurrency(category.planned) }}
</td>
<td class="px-4 py-2 text-right">{{ category.percentage }}%</td>
</tr>
<!-- Total Revenue -->
<tr
class="border-t-2 border-black dark:border-neutral-400 font-semibold bg-neutral-50 dark:bg-neutral-800">
<td
class="border-r-1 border-black dark:border-neutral-400 px-4 py-2 text-black dark:text-white">
Total Revenue
</td>
<td
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-2 text-right text-black dark:text-white font-mono">
{{ formatCurrency(totalRevenuePlanned) }}
</td>
<td
class="px-4 py-2 text-right text-black dark:text-white font-mono">
100%
</td>
</tr>
<!-- Total Revenue -->
<tr class="border-t-2 border-black font-semibold bg-neutral-50">
<td class="border-r-1 border-black px-4 py-2">Total Revenue</td>
<td class="border-r border-neutral-400 px-4 py-2 text-right">
{{ formatCurrency(totalRevenuePlanned) }}
</td>
<td class="px-4 py-2 text-right">100%</td>
</tr>
<!-- Revenue Diversification Guidance -->
<tr class="bg-neutral-50 dark:bg-neutral-800">
<td
colspan="3"
class="border-t border-neutral-300 dark:border-neutral-700 px-4 py-3 text-black dark:text-white">
<div class="text-sm">
<p class="font-medium mb-2 text-black dark:text-white">
{{ diversificationGuidance }}
</p>
<p
class="text-neutral-600 dark:text-neutral-100 mb-2"
v-if="suggestedCategories.length > 0">
Consider developing: {{ suggestedCategories.join(", ") }}
</p>
<p class="text-xs text-black dark:text-white">
<NuxtLink
to="/help#revenue-diversification"
class="text-white dark:text-neutral-100 hover:text-white dark:hover:text-white underline">
Learn how to develop these revenue streams
</NuxtLink>
</p>
</div>
</td>
</tr>
<!-- Revenue Diversification Guidance -->
<tr :class="guidanceBackgroundClass">
<td colspan="3" class="border-t border-neutral-300 px-4 py-3">
<div class="text-sm">
<p class="font-medium mb-2">{{ diversificationGuidance }}</p>
<p
class="text-neutral-600 mb-2"
v-if="suggestedCategories.length > 0">
Consider developing: {{ suggestedCategories.join(", ") }}
</p>
<p class="text-xs">
<NuxtLink
to="/help#revenue-diversification"
class="text-blue-600 hover:text-blue-800 underline">
Learn how to develop these revenue streams
</NuxtLink>
</p>
</div>
</td>
</tr>
<!-- Expenses Section -->
<tr class="bg-black text-white dark:bg-white dark:text-black">
<td
class="px-4 py-2 font-bold text-white dark:text-black"
colspan="3">
EXPENSES
</td>
</tr>
<!-- Expenses Section -->
<tr class="bg-black text-white">
<td class="px-4 py-2 font-bold" colspan="3">EXPENSES</td>
</tr>
<!-- Expense Categories -->
<tr
v-for="(category, index) in expenseCategories"
:key="`exp-${index}`"
class="text-black dark:text-white"
v-show="category.planned > 0">
<td
class="border-r-1 border-black dark:border-neutral-400 px-4 py-2 text-black dark:text-white">
{{ category.name }}
</td>
<td
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-2 text-right text-black dark:text-white font-mono">
{{ formatCurrency(category.planned) }}
</td>
<td
class="px-4 py-2 text-right text-black dark:text-white font-mono">
{{ category.percentage }}%
</td>
</tr>
<!-- Expense Categories -->
<tr
v-for="(category, index) in expenseCategories"
:key="`exp-${index}`"
class="border-t border-neutral-200"
v-show="category.planned > 0">
<td class="border-r-1 border-black px-4 py-2">
{{ category.name }}
</td>
<td class="border-r border-neutral-400 px-4 py-2 text-right">
{{ formatCurrency(category.planned) }}
</td>
<td class="px-4 py-2 text-right">{{ category.percentage }}%</td>
</tr>
<!-- Total Expenses -->
<tr class="font-semibold bg-neutral-50 dark:bg-neutral-800">
<td
class="border-r-1 border-black dark:border-neutral-400 px-4 py-2 text-black dark:text-white">
Total Expenses
</td>
<td
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-2 text-right text-black dark:text-white font-mono">
{{ formatCurrency(totalExpensesPlanned) }}
</td>
<td
class="px-4 py-2 text-right text-black dark:text-white font-mono">
100%
</td>
</tr>
<!-- Total Expenses -->
<tr class="border-t-2 border-black font-semibold bg-neutral-50">
<td class="border-r-1 border-black px-4 py-2">Total Expenses</td>
<td class="border-r border-neutral-400 px-4 py-2 text-right">
{{ formatCurrency(totalExpensesPlanned) }}
</td>
<td class="px-4 py-2 text-right">100%</td>
</tr>
<!-- Net Total -->
<tr
class="border-t-2 border-black font-bold text-lg"
:class="netTotalClass">
<td class="border-r-1 border-black px-4 py-3">NET TOTAL</td>
<td class="border-r border-neutral-400 px-4 py-3 text-right">
{{ formatCurrency(netTotal) }}
</td>
<td class="px-4 py-3 text-right">-</td>
</tr>
</tbody>
</table>
<!-- Net Total -->
<tr
class="border-t-2 border-black dark:border-neutral-400 font-bold text-lg"
:class="netTotalClass">
<td
class="border-r-1 border-black dark:border-neutral-400 px-4 py-3 text-black dark:text-white">
NET TOTAL
</td>
<td
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-3 text-right text-black dark:text-white font-mono">
{{ formatCurrency(netTotal) }}
</td>
<td class="px-4 py-3 text-right text-black dark:text-white">
-
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
@ -242,9 +292,9 @@ const netTotal = computed(
);
const netTotalClass = computed(() => {
if (netTotal.value > 0) return "bg-green-50";
if (netTotal.value < 0) return "bg-red-50";
return "bg-neutral-50";
if (netTotal.value > 0) return "bg-green-50 dark:bg-green-950";
if (netTotal.value < 0) return "bg-red-50 dark:bg-red-950";
return "bg-neutral-50 dark:bg-neutral-800";
});
// Diversification guidance
@ -308,28 +358,6 @@ const diversificationGuidance = computed(() => {
return guidance;
});
const guidanceBackgroundClass = computed(() => {
const topCategory = revenueCategories.value.reduce(
(max, cat) => (cat.percentage > max.percentage ? cat : max),
{ percentage: 0 }
);
if (topCategory.percentage >= 70) {
return "bg-red-50";
} else if (topCategory.percentage >= 50) {
return "bg-red-50";
} else {
const categoriesAbove20 = revenueCategories.value.filter(
(cat) => cat.percentage >= 20
).length;
if (categoriesAbove20 >= 3) {
return "bg-green-50";
} else {
return "bg-yellow-50";
}
}
});
// Suggested categories to develop
const suggestedCategories = computed(() => {
const categoriesWithRevenue = revenueCategories.value.filter(
@ -394,10 +422,11 @@ function formatCurrency(amount: number): string {
}
function getPercentageClass(percentage: number): string {
if (percentage > 50) return "text-red-600 font-bold";
if (percentage > 35) return "text-yellow-600 font-semibold";
if (percentage > 20) return "text-black font-medium";
return "text-neutral-500";
if (percentage > 50) return "text-red-600 dark:text-red-400 font-bold";
if (percentage > 35)
return "text-yellow-600 dark:text-yellow-400 font-semibold";
if (percentage > 20) return "text-black dark:text-white font-medium";
return "text-neutral-500 dark:text-neutral-400";
}
// Initialize

View file

@ -0,0 +1,208 @@
<template>
<UModal
v-model:open="isOpen"
title="Budget Settings"
description="Configure payroll taxes and cash flow settings"
:dismissible="true">
<template #body>
<div class="space-y-8">
<!-- Payroll Tax Settings -->
<div>
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">
Payroll Tax Percentage
</h3>
<p class="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
Set the percentage added to base payroll for taxes, benefits, and other oncosts.
</p>
<div class="space-y-4">
<UFormField label="Tax Percentage">
<UInput
v-model="newOncostPct"
type="number"
placeholder="Enter percentage"
min="0"
max="100"
step="0.1"
size="lg"
class="text-lg">
<template #trailing>
<span class="text-neutral-500 text-lg">%</span>
</template>
</UInput>
</UFormField>
<!-- Quick selection buttons -->
<div class="flex flex-wrap gap-2">
<span class="text-sm text-neutral-600 dark:text-neutral-400 mr-2">Quick select:</span>
<UButton
v-for="range in commonOncostRanges"
:key="range.value"
@click="newOncostPct = range.value"
size="xs"
variant="outline"
:ui="{ base: 'hover:bg-neutral-100 dark:hover:bg-neutral-800' }">
{{ range.value }}%
</UButton>
</div>
<!-- Preview -->
<div class="p-3 bg-neutral-100 dark:bg-neutral-800 rounded">
<p class="text-sm text-neutral-700 dark:text-neutral-300">
<strong>Example:</strong> 100 base payroll + {{ newOncostPct }}% tax =
<strong>{{ Math.round(100 * (1 + newOncostPct / 100)) }} total cost</strong>
</p>
</div>
</div>
</div>
<!-- Minimum Cash Threshold Settings -->
<div>
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">
Minimum Cash Threshold
</h3>
<p class="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
Set the minimum cash balance that your co-op should never go below. The budget will automatically reduce payroll allocations if needed to maintain this threshold.
</p>
<div class="space-y-4">
<UFormField label="Minimum Cash Balance">
<UInput
v-model="newMinCashThreshold"
type="number"
placeholder="Enter minimum amount"
min="0"
step="100"
size="lg"
class="text-lg">
<template #leading>
<span class="text-neutral-500 text-lg">{{ getCurrencySymbol(coopStore.currency) }}</span>
</template>
</UInput>
</UFormField>
<!-- Quick selection buttons -->
<div class="flex flex-wrap gap-2">
<span class="text-sm text-neutral-600 dark:text-neutral-400 mr-2">Quick select:</span>
<UButton
v-for="amount in commonCashThresholds"
:key="amount"
@click="newMinCashThreshold = amount"
size="xs"
variant="outline"
:ui="{ base: 'hover:bg-neutral-100 dark:hover:bg-neutral-800' }">
{{ getCurrencySymbol(coopStore.currency) }}{{ formatAmount(amount) }}
</UButton>
</div>
<!-- Preview -->
<div class="p-3 bg-neutral-100 dark:bg-neutral-800 rounded">
<p class="text-sm text-neutral-700 dark:text-neutral-300">
<strong>Safety buffer:</strong> Your co-op will maintain at least
<strong>{{ getCurrencySymbol(coopStore.currency) }}{{ formatAmount(newMinCashThreshold) }}</strong>
in cash at all times.
</p>
</div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-between items-center w-full">
<UButton
@click="resetToDefaults"
variant="ghost"
color="neutral"
:ui="{ base: 'hover:bg-neutral-100 dark:hover:bg-neutral-800' }">
Reset to Defaults
</UButton>
<div class="flex gap-2">
<UButton
@click="isOpen = false"
variant="outline"
color="neutral">
Cancel
</UButton>
<UButton
@click="saveSettings"
:disabled="!isValidSettings"
color="primary">
Save Settings
</UButton>
</div>
</div>
</template>
</UModal>
</template>
<script setup lang="ts">
import { getCurrencySymbol } from "~/utils/currency";
// Props
const isOpen = defineModel<boolean>("open", { default: false });
// Emit events for parent component
const emit = defineEmits<{
"settings-updated": [];
}>();
// Stores
const coopStore = useCoopBuilderStore();
// Reactive form data
const newOncostPct = ref(coopStore.payrollOncostPct || 25);
const newMinCashThreshold = ref(coopStore.minCashThreshold || 0);
// Common ranges for quick selection
const commonOncostRanges = [
{ value: 0 },
{ value: 15 },
{ value: 20 },
{ value: 25 },
{ value: 30 },
{ value: 35 }
];
const commonCashThresholds = [0, 1000, 2500, 5000, 7500, 10000];
// Computed properties
const isValidSettings = computed(() =>
newOncostPct.value >= 0 &&
newOncostPct.value <= 100 &&
newMinCashThreshold.value >= 0
);
// Utility function to format amounts
function formatAmount(amount: number): string {
return new Intl.NumberFormat().format(amount);
}
// Reset form when modal opens
watch(() => isOpen.value, (open) => {
if (open) {
newOncostPct.value = coopStore.payrollOncostPct || 25;
newMinCashThreshold.value = coopStore.minCashThreshold || 0;
}
});
function resetToDefaults() {
newOncostPct.value = 25;
newMinCashThreshold.value = 0;
}
function saveSettings() {
// Update payroll oncost percentage
coopStore.payrollOncostPct = newOncostPct.value;
// Update minimum cash threshold
coopStore.setMinCashThreshold(newMinCashThreshold.value);
// Close modal
isOpen.value = false;
// Emit event to parent to refresh calculations
emit("settings-updated");
}
</script>

View file

@ -1,323 +0,0 @@
<template>
<UModal
v-model:open="isOpen"
title="Payroll Oncost Settings"
description="Configure payroll taxes and benefits percentage"
:dismissible="true">
<template #body>
<div class="space-y-6">
<!-- Explanation -->
<div class="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
<div class="flex items-start">
<UIcon
name="i-heroicons-information-circle"
class="h-5 w-5 text-blue-400 mt-0.5 mr-3 flex-shrink-0" />
<div class="text-sm">
<p class="text-blue-800 dark:text-blue-200 font-medium mb-2">
What are payroll oncosts?
</p>
<p class="text-blue-700 dark:text-blue-300">
Payroll oncosts cover taxes, benefits, and other
employee-related expenses beyond base wages. This typically
includes employer payroll taxes, worker's compensation, benefits
contributions, and other statutory requirements.
</p>
</div>
</div>
</div>
<!-- Current Settings Display -->
<div class="bg-neutral-50 dark:bg-neutral-800 p-4 rounded-lg">
<h4 class="font-medium text-neutral-900 dark:text-white mb-3">
Current Impact
</h4>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<div class="text-neutral-600 dark:text-neutral-400">
Base Payroll
</div>
<div class="font-medium">
{{ formatCurrency(basePayroll) }}/month
</div>
</div>
<div>
<div class="text-neutral-600 dark:text-neutral-400">
Oncosts ({{ currentOncostPct }}%)
</div>
<div class="font-medium">
{{ formatCurrency(currentOncostAmount) }}/month
</div>
</div>
</div>
<div
class="mt-3 pt-3 border-t border-neutral-200 dark:border-neutral-700">
<div class="text-neutral-600 dark:text-neutral-400 text-sm">
Total Payroll Cost
</div>
<div class="font-semibold text-lg">
{{ formatCurrency(totalPayrollCost) }}/month
</div>
</div>
</div>
<!-- Percentage Input -->
<div class="space-y-3">
<label
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
Oncost Percentage
</label>
<div class="flex items-center space-x-3">
<div class="flex-1">
<UInput
v-model.number="newOncostPct"
type="number"
min="0"
max="100"
step="1"
placeholder="25"
class="text-center" />
</div>
<span class="text-sm text-neutral-500">%</span>
</div>
<!-- Slider for easier adjustment -->
<div class="space-y-2">
<input
v-model.number="newOncostPct"
type="range"
min="0"
max="50"
step="1"
class="w-full h-2 bg-neutral-200 rounded-lg appearance-none cursor-pointer dark:bg-neutral-700 slider" />
<div class="flex justify-between text-xs text-neutral-500">
<span>0%</span>
<span>25%</span>
<span>50%</span>
</div>
</div>
</div>
<!-- Preview of New Settings -->
<div
v-if="newOncostPct !== currentOncostPct"
class="bg-green-50 dark:bg-green-900/20 p-4 rounded-lg">
<h4 class="font-medium text-green-800 dark:text-green-200 mb-3">
Preview Changes
</h4>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<div class="text-green-700 dark:text-green-300">
New Oncosts ({{ newOncostPct }}%)
</div>
<div class="font-medium">
{{ formatCurrency(newOncostAmount) }}/month
</div>
</div>
<div>
<div class="text-green-700 dark:text-green-300">
New Total Cost
</div>
<div class="font-medium">
{{ formatCurrency(newTotalCost) }}/month
</div>
</div>
</div>
<div class="mt-2 text-xs">
<span class="text-green-700 dark:text-green-300">
{{ newTotalCost > totalPayrollCost ? "Increase" : "Decrease" }} of
{{
formatCurrency(Math.abs(newTotalCost - totalPayrollCost))
}}/month
</span>
</div>
</div>
<!-- Common Oncost Ranges -->
<div class="space-y-2">
<label
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
Common Ranges
</label>
<div class="flex flex-wrap gap-2">
<UButton
v-for="preset in commonRanges"
:key="preset.value"
size="xs"
color="neutral"
variant="outline"
@click="newOncostPct = preset.value">
{{ preset.label }}
</UButton>
</div>
</div>
</div>
</template>
<template #footer="{ close }">
<div class="flex justify-end gap-3">
<UButton color="neutral" variant="ghost" @click="handleCancel">
Cancel
</UButton>
<UButton
color="primary"
@click="handleSave"
:disabled="!isValidPercentage">
Update Oncost Percentage
</UButton>
</div>
</template>
</UModal>
</template>
<script setup lang="ts">
import { allocatePayroll as allocatePayrollImpl } from "~/types/members";
interface Props {
open: boolean;
}
interface Emits {
(e: "update:open", value: boolean): void;
(e: "save", percentage: number): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
// Modal state
const isOpen = computed({
get: () => props.open,
set: (value) => emit("update:open", value),
});
// Get current payroll data
const coopStore = useCoopBuilderStore();
const currentOncostPct = computed(() => coopStore.payrollOncostPct || 0);
// Calculate current payroll values using the same logic as the budget store
const { allocatePayroll } = useCoopBuilder();
const basePayroll = computed(() => {
// Calculate base payroll the same way the budget store does
const totalHours = coopStore.members.reduce(
(sum, m) =>
sum + (m.hoursPerMonth || (m.hoursPerWeek ? m.hoursPerWeek * 4.33 : 0)),
0
);
const hourlyWage = coopStore.equalHourlyWage || 0;
const basePayrollBudget = totalHours * hourlyWage;
if (basePayrollBudget > 0 && coopStore.members.length > 0) {
// Use policy-driven allocation to get actual member pay amounts
const payPolicy = {
relationship: coopStore.policy.relationship,
roleBands: coopStore.policy.roleBands,
};
// Convert members to the format expected by allocatePayroll
const membersForAllocation = coopStore.members.map((m) => ({
...m,
displayName: m.name,
monthlyPayPlanned: m.monthlyPayPlanned || 0,
minMonthlyNeeds: m.minMonthlyNeeds || 0,
hoursPerMonth:
m.hoursPerMonth || (m.hoursPerWeek ? m.hoursPerWeek * 4.33 : 0),
}));
// Use the imported allocatePayroll function
const allocatedMembers = allocatePayrollImpl(
membersForAllocation,
payPolicy,
basePayrollBudget
);
// Sum the allocated amounts for total payroll
return allocatedMembers.reduce(
(sum, m) => sum + (m.monthlyPayPlanned || 0),
0
);
}
return 0;
});
const currentOncostAmount = computed(
() => basePayroll.value * (currentOncostPct.value / 100)
);
const totalPayrollCost = computed(
() => basePayroll.value + currentOncostAmount.value
);
// New percentage input
const newOncostPct = ref(currentOncostPct.value);
// Computed values for preview
const newOncostAmount = computed(
() => basePayroll.value * (newOncostPct.value / 100)
);
const newTotalCost = computed(() => basePayroll.value + newOncostAmount.value);
const isValidPercentage = computed(
() => newOncostPct.value >= 0 && newOncostPct.value <= 100
);
// Common oncost ranges
const commonRanges = [
{ label: "0% (No oncosts)", value: 0 },
{ label: "15% (Basic)", value: 15 },
{ label: "25% (Standard)", value: 25 },
{ label: "35% (Comprehensive)", value: 35 },
];
// Reset to current value when modal opens
watch(isOpen, (open) => {
if (open) {
newOncostPct.value = currentOncostPct.value;
}
});
// Handlers
function handleCancel() {
newOncostPct.value = currentOncostPct.value;
isOpen.value = false;
}
function handleSave() {
if (isValidPercentage.value) {
emit("save", newOncostPct.value);
isOpen.value = false;
}
}
// Currency formatting
function formatCurrency(amount: number): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
}
</script>
<style scoped>
.slider::-webkit-slider-thumb {
appearance: none;
height: 20px;
width: 20px;
border-radius: 50%;
background: #3b82f6;
cursor: pointer;
}
.slider::-moz-range-thumb {
height: 20px;
width: 20px;
border-radius: 50%;
background: #3b82f6;
cursor: pointer;
border: none;
}
</style>