feat(ui): replace emojis with Lucide icons and update website UI

This commit is contained in:
sankalp committed 2026-03-18 23:10:27 +05:45
1 parent 74f79e7d78
commit 20a61a0ee0
16 files changed
+851 -327

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
{
"hash": "37b4fb89",
"configHash": "4edb8c93",
"lockfileHash": "e3b0c442",
"browserHash": "e33dcc6e",
"optimized": {},
"chunks": {}
}
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+10
View File
@@ -12,6 +12,7 @@
"@ant-design/icons": "^6.1.0",
"antd": "^6.3.2",
"lodash": "^4.17.23",
"lucide-react": "^0.577.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
@@ -5283,6 +5284,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.577.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz",
"integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+1
View File
@@ -23,6 +23,7 @@
"@ant-design/icons": "^6.1.0",
"antd": "^6.3.2",
"lodash": "^4.17.23",
"lucide-react": "^0.577.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
+46 -41
View File
@@ -30,53 +30,43 @@ export default function App() {
let formCol = '100%'
if (!isMobile) {
formCol = isDesktop ? '460px' : '360px'
formCol = isDesktop ? '460px' : '380px'
}
const headerHeight = isMobile ? 68 : 76
const headerHeight = isMobile ? 56 : 76
return (
<div
style={{
minHeight: '100vh',
width: '100%',
background: 'linear-gradient(135deg, #f8faff 0%, #eef2ff 50%, #f0fdf4 100%)',
fontFamily: "'Georgia', serif",
background: '#f1f5f9',
fontFamily: 'system-ui, -apple-system, sans-serif',
boxSizing: 'border-box',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}}
>
<AppHeader />
{isMobile ? (
/* Mobile view */
<div style={{ display: 'flex', flexDirection: 'column' }}>
<FormPanel
monthly={monthly}
maritalStatus={maritalStatus}
onMonthlyChange={setMonthly}
onMaritalStatusChange={setMaritalStatus}
onSSFChange={(c, d) => {
setSSFContribution(c)
setSSFDeduction(d)
}}
onBonusChange={setYearlyBonus}
/>
<ResultsPanel
result={result}
monthly={monthly}
maritalStatus={maritalStatus}
ssfContribution={ssfContribution}
ssfDeduction={ssfDeduction}
/>
</div>
) : (
/* Tablet / Desktop view */
<div
style={{
flex: 1,
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
overflow: 'hidden',
}}
>
{/* Left Sidebar / Top Mobile */}
<div
style={{
display: 'grid',
gridTemplateColumns: `${formCol} 1fr`,
minHeight: `calc(100vh - ${headerHeight}px)`,
width: '100%',
boxSizing: 'border-box',
width: isMobile ? '100%' : formCol,
background: '#fff',
height: isMobile ? 'auto' : `calc(100vh - ${headerHeight}px)`,
overflowY: 'auto',
borderRight: isMobile ? 'none' : '1px solid #e2e8f0',
boxShadow: isMobile ? 'none' : '0 10px 15px -3px rgba(0, 0, 0, 0.05)',
zIndex: 10,
}}
>
<FormPanel
@@ -90,15 +80,30 @@ export default function App() {
}}
onBonusChange={setYearlyBonus}
/>
<ResultsPanel
result={result}
monthly={monthly}
maritalStatus={maritalStatus}
ssfContribution={ssfContribution}
ssfDeduction={ssfDeduction}
/>
</div>
)}
{/* Main Content Area */}
<div
style={{
flex: 1,
height: isMobile ? 'auto' : `calc(100vh - ${headerHeight}px)`,
overflowY: 'auto',
background: result ? '#f8fafc' : '#f1f5f9',
display: 'flex',
flexDirection: 'column',
}}
>
<div style={{ maxWidth: 900, width: '100%', margin: '0 auto' }}>
<ResultsPanel
result={result}
monthly={monthly}
maritalStatus={maritalStatus}
ssfContribution={ssfContribution}
ssfDeduction={ssfDeduction}
/>
</div>
</div>
</div>
</div>
)
}
+61 -18
View File
@@ -1,4 +1,4 @@
import { GithubOutlined } from '@ant-design/icons'
import { Github, Calculator } from 'lucide-react'
import { useBreakpoints } from '../utils'
export function AppHeader() {
@@ -9,31 +9,45 @@ export function AppHeader() {
style={{
width: '100%',
background: '#1e3a5f',
padding: isMobile ? '14px 16px' : '18px 40px',
padding: isMobile ? '12px 16px' : '16px 40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
boxSizing: 'border-box',
borderBottom: '1px solid rgba(255,255,255,0.05)',
position: 'sticky',
top: 0,
zIndex: 1000,
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
<div
style={{
width: 4,
height: isMobile ? 28 : 36,
background: 'linear-gradient(180deg, #60a5fa, #34d399)',
borderRadius: 2,
width: isMobile ? 32 : 44,
height: isMobile ? 32 : 44,
background: 'linear-gradient(45deg, #1e293b, #334155)',
borderRadius: 12,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
border: '1px solid rgba(255,255,255,0.1)',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
}}
/>
>
<Calculator size={isMobile ? 18 : 24} color="#38bdf8" strokeWidth={2} />
</div>
<div>
<div
style={{
fontSize: 9,
letterSpacing: '2px',
color: '#94a3b8',
letterSpacing: '1.5px',
color: '#64748b',
textTransform: 'uppercase',
fontWeight: 700,
fontFamily: 'sans-serif',
marginBottom: 1,
}}
>
FY 2081/82 · Income Tax Act 2058
@@ -41,24 +55,53 @@ export function AppHeader() {
<div
style={{
fontSize: isMobile ? 16 : 22,
fontWeight: 700,
color: '#f1f5f9',
fontFamily: 'Georgia, serif',
lineHeight: 1.2,
fontWeight: 800,
color: '#f8fafc',
fontFamily: 'system-ui, -apple-system, sans-serif',
lineHeight: 1.1,
}}
>
Nepal Income Tax Calculator
Nepal Tax Calculator
</div>
</div>
</div>
<div style={{ fontSize: isMobile ? 26 : 32, flexShrink: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<a
href="https://github.com/opensource-nepal/tax-calculator"
target="_blank"
style={{ color: 'white' }}
rel="noreferrer"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#94a3b8',
width: isMobile ? 34 : 40,
height: isMobile ? 34 : 40,
borderRadius: 10,
border: '1px solid rgba(255,255,255,0.1)',
background: 'rgba(255,255,255,0.05)',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
flexShrink: 0,
textDecoration: 'none',
}}
onMouseEnter={e => {
;(e.currentTarget as HTMLElement).style.color = '#fff'
;(e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.1)'
;(e.currentTarget as HTMLElement).style.borderColor =
'rgba(255,255,255,0.2)'
;(e.currentTarget as HTMLElement).style.transform = 'translateY(-1px)'
}}
onMouseLeave={e => {
;(e.currentTarget as HTMLElement).style.color = '#94a3b8'
;(e.currentTarget as HTMLElement).style.background =
'rgba(255,255,255,0.05)'
;(e.currentTarget as HTMLElement).style.borderColor =
'rgba(255,255,255,0.1)'
;(e.currentTarget as HTMLElement).style.transform = 'translateY(0)'
}}
>
<GithubOutlined />
<Github size={isMobile ? 18 : 22} strokeWidth={2} />
</a>
</div>
</div>
+129 -36
View File
@@ -1,10 +1,11 @@
import { Collapse, InputNumber, Radio, Space } from 'antd'
import type { CollapseProps, RadioChangeEvent } from 'antd'
import { Collapse, InputNumber, Space } from 'antd'
import type { CollapseProps } from 'antd'
import type { MaritalStatus } from '../types'
import SSFContribution from './SSFContribution'
import YearlyBonus from './YearlyBonus'
import { SlabReference } from './SlabReference'
import { useBreakpoints } from '../utils'
import { User, Users, ChevronDown } from 'lucide-react'
interface FormPanelProps {
monthly: number | null
@@ -28,7 +29,21 @@ export function FormPanel({
const advanceOptionsItems: CollapseProps['items'] = [
{
key: '1',
label: 'Advance Options',
label: (
<span
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
fontFamily: 'sans-serif',
fontSize: 13,
fontWeight: 600,
color: '#374151',
}}
>
Additional Deductions
</span>
),
children: (
<>
<div style={{ marginTop: 8 }}>
@@ -42,23 +57,49 @@ export function FormPanel({
},
]
const statusOptions: {
val: MaritalStatus
label: string
sub: string
Icon: typeof User
}[] = [
{ val: 'individual', label: 'Individual', sub: 'Single filer', Icon: User },
{ val: 'couple', label: 'Couple', sub: 'Joint filer', Icon: Users },
]
return (
<div
style={{
background: '#fff',
borderRight: isDesktop ? '1px solid #e5e7eb' : 'none',
borderBottom: !isDesktop ? '1px solid #e5e7eb' : 'none',
padding: isMobile ? '24px 16px' : '36px 28px',
padding: isMobile ? '20px 16px' : '32px 28px',
display: 'flex',
flexDirection: 'column',
gap: 28,
gap: 24,
boxSizing: 'border-box',
}}
>
{/* Salary Input */}
<div>
<span className="form-label">Monthly Salary</span>
<span className="form-label">Monthly Gross Salary</span>
<Space.Compact style={{ width: '100%' }}>
<Space.Addon>NPR</Space.Addon>
<Space.Addon
style={{
background: '#f3f4f6',
borderRight: '1px solid #d1d5db',
color: '#374151',
fontWeight: 600,
fontSize: 13,
fontFamily: 'sans-serif',
minWidth: 52,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
NPR
</Space.Addon>
<InputNumber
size="large"
@@ -72,56 +113,108 @@ export function FormPanel({
onChange={val => onMonthlyChange(val)}
/>
</Space.Compact>
<div
style={{
marginTop: 5,
fontSize: 11,
color: '#9ca3af',
fontFamily: 'sans-serif',
}}
>
Enter your total monthly income before any deductions
</div>
</div>
{/* Divider */}
<div style={{ height: 1, background: '#f3f4f6' }} />
{/* Filing Status */}
<div>
<Radio.Group
value={maritalStatus}
onChange={(e: RadioChangeEvent) =>
onMaritalStatusChange(e.target.value as MaritalStatus)
}
style={{ width: '100%', fontFamily: 'sans-serif' }}
>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{(['individual', 'couple'] as MaritalStatus[]).map(val => (
<Radio.Button
<span className="form-label" style={{ marginBottom: 10, display: 'block' }}>
Filing Status
</span>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
{statusOptions.map(({ val, label, sub, Icon }) => {
const isActive = maritalStatus === val
return (
<button
key={val}
value={val}
onClick={() => onMaritalStatusChange(val)}
style={{
width: '100%',
textAlign: 'center',
height: 68,
height: 76,
borderRadius: 12,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
border:
maritalStatus === val ? '2px solid #2563eb' : '1px solid #e5e7eb',
background: maritalStatus === val ? '#eff6ff' : '#fafafa',
fontSize: 13,
// color: maritalStatus === val ? "#2563eb" : "#374151",
lineHeight: 1,
border: isActive ? '2px solid #2563eb' : '1.5px solid #e5e7eb',
background: isActive ? '#eff6ff' : '#fafafa',
cursor: 'pointer',
fontFamily: 'sans-serif',
transition: 'all 0.15s ease',
outline: 'none',
boxShadow: isActive ? '0 0 0 3px rgba(37,99,235,0.1)' : 'none',
}}
>
<div style={{ fontSize: 20, paddingBottom: 8 }}>
{val === 'individual' ? '👤' : '👫'}
</div>
<div>{val === 'individual' ? 'Individual' : 'Couple'}</div>
</Radio.Button>
))}
</div>
</Radio.Group>
<Icon
size={18}
strokeWidth={2}
color={isActive ? '#2563eb' : '#6b7280'}
/>
<span
style={{
fontSize: 13,
fontWeight: isActive ? 700 : 500,
color: isActive ? '#2563eb' : '#374151',
lineHeight: 1.2,
}}
>
{label}
</span>
<span
style={{
fontSize: 10,
color: isActive ? '#93c5fd' : '#9ca3af',
fontWeight: 400,
}}
>
{sub}
</span>
</button>
)
})}
</div>
</div>
{/* Divider */}
<div style={{ height: 1, background: '#f3f4f6' }} />
{/* Advanced Options */}
<Collapse
ghost
styles={{ header: { padding: 0 }, body: { padding: 0 } }}
expandIcon={({ isActive }) => (
<ChevronDown
size={14}
color="#6b7280"
strokeWidth={2}
style={{
transform: isActive ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease',
}}
/>
)}
styles={{ header: { padding: 0 }, body: { padding: '12px 0 0 0' } }}
items={advanceOptionsItems}
/>
{!isMobile && <SlabReference maritalStatus={maritalStatus} />}
{!isMobile && (
<>
<div style={{ height: 1, background: '#f3f4f6' }} />
<SlabReference maritalStatus={maritalStatus} />
</>
)}
</div>
)
}
+59 -16
View File
@@ -1,6 +1,5 @@
import { Card } from 'antd'
import type { KpiCard } from '../types'
import { useBreakpoints } from '../utils'
interface KpiCardsProps {
@@ -15,7 +14,7 @@ export default function KpiCards({ cards }: Readonly<KpiCardsProps>) {
style={{
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)',
gap: 12,
gap: 10,
}}
>
{cards.map(card => (
@@ -25,35 +24,53 @@ export default function KpiCards({ cards }: Readonly<KpiCardsProps>) {
borderRadius: 14,
border: `1px solid ${card.border}`,
background: card.bg,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)',
boxShadow: '0 1px 8px rgba(0,0,0,0.04)',
}}
styles={{
body: { padding: isMobile ? '14px 16px' : '18px 20px 14px' },
body: { padding: isMobile ? '12px 14px' : '14px 16px' },
}}
>
{isMobile ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ fontSize: 28 }}>{card.icon}</div>
<div style={{ flex: 1 }}>
<div
style={{
width: 38,
height: 38,
borderRadius: 9,
background: 'rgba(255,255,255,0.75)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
border: `1px solid ${card.border}`,
}}
>
{card.icon}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 10,
fontWeight: 700,
color: '#6b7280',
textTransform: 'uppercase',
letterSpacing: '1px',
letterSpacing: '0.8px',
fontFamily: 'sans-serif',
marginBottom: 2,
}}
>
{card.label}
</div>
<div
style={{
fontSize: 18,
fontSize: 17,
fontWeight: 800,
color: card.accent,
fontFamily: 'monospace',
lineHeight: 1.3,
lineHeight: 1.2,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{card.value}
@@ -61,10 +78,13 @@ export default function KpiCards({ cards }: Readonly<KpiCardsProps>) {
</div>
<div
style={{
fontSize: 11,
fontSize: 10,
color: '#9ca3af',
fontFamily: 'sans-serif',
textAlign: 'right',
flexShrink: 0,
maxWidth: 80,
lineHeight: 1.4,
}}
>
{card.sub}
@@ -72,14 +92,36 @@ export default function KpiCards({ cards }: Readonly<KpiCardsProps>) {
</div>
) : (
<>
<div style={{ fontSize: 22, marginBottom: 6 }}>{card.icon}</div>
<div
style={{
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'space-between',
marginBottom: 10,
}}
>
<div
style={{
width: 34,
height: 34,
borderRadius: 9,
background: 'rgba(255,255,255,0.75)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: `1px solid ${card.border}`,
}}
>
{card.icon}
</div>
</div>
<div
style={{
fontSize: 10,
fontWeight: 700,
color: '#6b7280',
textTransform: 'uppercase',
letterSpacing: '1px',
letterSpacing: '0.8px',
fontFamily: 'sans-serif',
marginBottom: 4,
}}
@@ -88,21 +130,22 @@ export default function KpiCards({ cards }: Readonly<KpiCardsProps>) {
</div>
<div
style={{
fontSize: 18,
fontSize: 20,
fontWeight: 800,
color: card.accent,
fontFamily: 'monospace',
lineHeight: 1.2,
marginBottom: 4,
lineHeight: 1.1,
marginBottom: 6,
}}
>
{card.value}
</div>
<div
style={{
fontSize: 11,
fontSize: 10,
color: '#9ca3af',
fontFamily: 'sans-serif',
lineHeight: 1.4,
}}
>
{card.sub}
+115 -53
View File
@@ -1,48 +1,17 @@
import { Card, Table, Typography } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { Card } from 'antd'
import { fmtRaw, fmt, useBreakpoints } from '../utils'
import type { MaritalStatus, TaxResult } from '../types'
const { Title } = Typography
import { Receipt, TrendingDown } from 'lucide-react'
interface SalaryRow {
key: string
title: string
amount: string
bold?: boolean
isDeduction?: boolean
isTotal?: boolean
}
const columnStyle = {
fontSize: 12,
fontFamily: 'monospace',
}
const columns: ColumnsType<SalaryRow> = [
{
title: 'Title',
dataIndex: 'title',
key: 'title',
render: (text: string, record: SalaryRow) =>
record.bold ? (
<strong style={columnStyle}>{text}</strong>
) : (
<span style={columnStyle}>{text}</span>
),
},
{
title: 'NPR',
dataIndex: 'amount',
key: 'amount',
align: 'right',
render: (text: string, record: SalaryRow) =>
record.bold ? (
<strong style={columnStyle}>{text}</strong>
) : (
<span style={columnStyle}>{text}</span>
),
},
]
const getRows = (
result: TaxResult,
monthly: number,
@@ -52,38 +21,46 @@ const getRows = (
if (!ssfContribution || !ssfDeduction) {
return [
{ key: '1', title: 'Monthly Salary', amount: fmt(monthly) },
{ key: '2', title: 'TDS', amount: fmtRaw(result.monthlyTax) },
{ key: '2', title: 'TDS', amount: fmtRaw(result.monthlyTax), isDeduction: true },
{
key: '3',
title: 'Net Income',
amount: fmt(result.netMonthly),
bold: true,
isTotal: true,
},
]
}
return [
{ key: '1', title: 'Monthly Salary', amount: fmt(monthly) },
{ key: '2', title: 'SSF Contribution', amount: fmtRaw(ssfContribution) },
{ key: '2', title: 'SSF Contribution (Employer)', amount: fmtRaw(ssfContribution) },
{
key: '3',
title: 'Gross Income',
amount: fmtRaw(result.grossMonthly),
bold: true,
},
{ key: '4', title: 'SSF Deduction', amount: fmtRaw(ssfDeduction) },
{ key: '5', title: 'TDS', amount: fmtRaw(result.monthlyTax) },
{
key: '4',
title: 'SSF Deduction',
amount: fmtRaw(ssfDeduction),
isDeduction: true,
},
{ key: '5', title: 'TDS', amount: fmtRaw(result.monthlyTax), isDeduction: true },
{
key: '6',
title: 'Total Deduction',
amount: fmtRaw(result.monthlyDeduction),
bold: true,
isDeduction: true,
},
{
key: '7',
title: 'Net Income',
amount: fmt(result.netMonthly),
bold: true,
isTotal: true,
},
]
}
@@ -103,6 +80,27 @@ export default function MonthlyCalculation({
ssfDeduction,
}: Readonly<MonthlyCalculationProps>) {
const { isMobile } = useBreakpoints()
const rows = getRows(result, monthly, ssfContribution, ssfDeduction)
const getTitleColor = (isNet: boolean, isDeduction: boolean) => {
if (isNet) {
return '#15803d'
}
if (isDeduction) {
return '#6b7280'
}
return '#374151'
}
const getAmountColor = (isNet: boolean, isDeduction: boolean) => {
if (isNet) {
return '#15803d'
}
if (isDeduction) {
return '#c2410c'
}
return '#1e3a5f'
}
return (
<Card
@@ -112,24 +110,88 @@ export default function MonthlyCalculation({
boxShadow: '0 2px 16px rgba(30,58,95,0.06)',
background: '#fff',
}}
styles={{ body: { padding: isMobile ? '16px 12px' : '24px 24px' } }}
styles={{ body: { padding: isMobile ? '16px 12px' : '20px 24px' } }}
>
{/* Header */}
<div style={{ marginBottom: 14 }}>
<Title
level={5}
style={{ margin: 0, color: '#1e3a5f', fontFamily: 'Georgia, serif' }}
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
<div
style={{
width: 30,
height: 30,
borderRadius: 8,
background: '#f0fdf4',
border: '1px solid #bbf7d0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
Salary Calculation
</Title>
<Receipt size={15} color="#15803d" strokeWidth={2} />
</div>
<span
style={{
color: '#1e3a5f',
fontFamily: 'Georgia, serif',
fontWeight: 600,
fontSize: 15,
}}
>
Monthly Salary Breakdown
</span>
</div>
{/* Custom row list — avoids heavy ant table for just a few rows */}
<div style={{ display: 'flex', flexDirection: 'column' }}>
{rows.map((row, idx) => {
const isLast = idx === rows.length - 1
const isNet = Boolean(row.isTotal)
const isDeduction = Boolean(row.isDeduction)
return (
<div
key={row.key}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: isNet ? '12px 12px' : '9px 12px',
borderRadius: isNet ? 10 : 0,
borderBottom: !isLast && !isNet ? '1px solid #f3f4f6' : 'none',
marginTop: isNet ? 8 : 0,
background: isNet ? '#f0fdf4' : 'transparent',
border: isNet ? '1px solid #bbf7d0' : undefined,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{isDeduction && !isNet && (
<TrendingDown size={12} color="#c2410c" strokeWidth={2} />
)}
<span
style={{
fontSize: 13,
fontFamily: 'sans-serif',
color: getTitleColor(isNet, isDeduction),
fontWeight: row.bold ? 700 : 400,
}}
>
{row.title}
</span>
</div>
<span
style={{
fontSize: isNet ? 15 : 13,
fontFamily: 'monospace',
color: getAmountColor(isNet, isDeduction),
fontWeight: row.bold ? 700 : 400,
}}
>
{isDeduction && !isNet ? '- ' : ''}
{row.amount}
</span>
</div>
)
})}
</div>
<Table<SalaryRow>
columns={columns}
dataSource={getRows(result, monthly, ssfContribution, ssfDeduction)}
pagination={false}
size="small"
showHeader={false}
/>
</Card>
)
}
+101 -36
View File
@@ -1,4 +1,4 @@
import { CalculatorOutlined } from '@ant-design/icons'
import { BarChart3, CalendarDays, Wallet, ArrowRight } from 'lucide-react'
import type { TaxResult, MaritalStatus, KpiCard } from '../types'
import { fmt, useBreakpoints } from '../utils'
import KpiCards from './KpiCards'
@@ -10,11 +10,11 @@ function buildKpiCards(result: TaxResult): KpiCard[] {
{
label: 'Annual TDS',
value: fmt(result.totalTax),
sub: `Taxable Income: ${fmt(result.annual)}`,
sub: `Taxable: ${fmt(result.annual)}`,
accent: '#dc2626',
bg: '#fff5f5',
border: '#fecaca',
icon: '📊',
icon: <BarChart3 size={18} strokeWidth={1.8} color="#dc2626" />,
},
{
label: 'Monthly TDS',
@@ -23,7 +23,7 @@ function buildKpiCards(result: TaxResult): KpiCard[] {
accent: '#c2410c',
bg: '#fff7ed',
border: '#fed7aa',
icon: '📅',
icon: <CalendarDays size={18} strokeWidth={1.8} color="#c2410c" />,
},
{
label: 'Monthly Take-Home',
@@ -32,7 +32,7 @@ function buildKpiCards(result: TaxResult): KpiCard[] {
accent: '#15803d',
bg: '#f0fdf4',
border: '#bbf7d0',
icon: '💰',
icon: <Wallet size={18} strokeWidth={1.8} color="#15803d" />,
},
]
}
@@ -43,38 +43,97 @@ function EmptyState() {
return (
<div
style={{
minHeight: isMobile ? 200 : 400,
minHeight: isMobile ? 200 : '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
gap: 24,
padding: 32,
}}
>
<div style={{ fontSize: isMobile ? 48 : 64, color: '#d1d5db' }}>
<CalculatorOutlined />
</div>
{/* Steps guide */}
<div
style={{
fontSize: 16,
fontWeight: 600,
color: '#d1d5db',
display: 'flex',
flexDirection: 'column',
gap: 12,
maxWidth: 280,
width: '100%',
}}
>
{[
{ step: '1', text: 'Enter your monthly salary' },
{ step: '2', text: 'Choose your filing status' },
{ step: '3', text: 'View your tax breakdown' },
].map((item, i, arr) => (
<div
key={item.step}
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
width: '100%',
padding: '10px 14px',
borderRadius: 10,
background: '#f8fafc',
border: '1px solid #e5e7eb',
}}
>
<div
style={{
width: 26,
height: 26,
borderRadius: '50%',
background: '#1e3a5f',
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 11,
fontWeight: 700,
fontFamily: 'sans-serif',
flexShrink: 0,
}}
>
{item.step}
</div>
<span
style={{
fontSize: 13,
color: '#374151',
fontFamily: 'sans-serif',
fontWeight: 500,
}}
>
{item.text}
</span>
</div>
{i < arr.length - 1 && (
<div
style={{ width: 1, height: 8, background: '#d1d5db', margin: '2px 0' }}
/>
)}
</div>
))}
</div>
{/* Arrow hint */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
fontSize: 12,
color: '#9ca3af',
fontFamily: 'sans-serif',
}}
>
Results will appear here
</div>
<div
style={{
fontSize: 13,
color: '#d1d5db',
maxWidth: 260,
textAlign: 'center',
fontFamily: 'sans-serif',
}}
>
Enter your monthly salary {isMobile ? 'above' : 'on the left'} to compute your
TDS
<ArrowRight size={14} color="#9ca3af" />
Results appear here instantly
</div>
</div>
)
@@ -106,16 +165,22 @@ export function ResultsPanel({
}}
>
{result && monthly ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<KpiCards cards={buildKpiCards(result)} />
<MonthlyCalculation
result={result}
monthly={monthly}
maritalStatus={maritalStatus}
ssfContribution={ssfContribution}
ssfDeduction={ssfDeduction}
/>
<TdsBreakdownTable result={result} maritalStatus={maritalStatus} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div className="result-animate">
<KpiCards cards={buildKpiCards(result)} />
</div>
<div className="result-animate">
<MonthlyCalculation
result={result}
monthly={monthly}
maritalStatus={maritalStatus}
ssfContribution={ssfContribution}
ssfDeduction={ssfDeduction}
/>
</div>
<div className="result-animate">
<TdsBreakdownTable result={result} maritalStatus={maritalStatus} />
</div>
</div>
) : (
<EmptyState />
+126 -76
View File
@@ -6,7 +6,7 @@ const contributionStyle = {
fontSize: 12,
color: '#888d94',
fontFamily: 'sans-serif',
textTransform: 'uppercase',
marginTop: 2,
}
interface SSFContributionProps {
@@ -64,88 +64,138 @@ export default function SSFContribution({
})
return (
<>
<div style={{ padding: '4px 0' }}>
<Checkbox
style={{ fontWeight: 500, color: '#374151', fontFamily: 'sans-serif' }}
onChange={e => {
setSSFEnabled(e.target.checked)
handleChange()
}}
>
SSF (Social Security Fund)
Social Security Fund (SSF)
</Checkbox>
<Row gutter={8} style={{ marginTop: 8 }}>
<Col span={12}>
<span className="form-label">Basic Salary</span>
<Space.Compact>
<InputNumber
style={{ width: '100%' }}
value={basicSalaryPer}
disabled={!ssfEnabled}
max={100}
min={0}
onChange={v => {
setBasicSalaryPer(v)
handleChange()
}}
/>
<Space.Addon>%</Space.Addon>
</Space.Compact>
<div style={contributionStyle}>
{ssfEnabled && monthlySalary && basicSalaryPer
? fmt((monthlySalary * basicSalaryPer) / 100)
: 'NPR N/A'}
{ssfEnabled && (
<div
className="result-animate"
style={{
marginTop: 12,
padding: 12,
background: '#f8fafc',
borderRadius: 8,
border: '1px solid #e2e8f0',
}}
>
<Row gutter={[12, 12]}>
<Col span={24}>
<span className="form-label" style={{ fontSize: 10, color: '#94a3b8' }}>
Basic Salary % of Gross
</span>
<Space.Compact style={{ width: '100%' }}>
<InputNumber
style={{ width: '100%', fontSize: 13 }}
value={basicSalaryPer}
disabled={!ssfEnabled}
max={100}
min={0}
onChange={v => {
setBasicSalaryPer(v)
handleChange()
}}
/>
<Space.Addon>%</Space.Addon>
</Space.Compact>
<div style={contributionStyle}>
Amount:{' '}
<span style={{ color: '#64748b', fontWeight: 600 }}>
{monthlySalary && basicSalaryPer
? fmt((monthlySalary * basicSalaryPer) / 100)
: 'NPR 0'}
</span>
</div>
</Col>
<Col span={12}>
<span className="form-label" style={{ fontSize: 10, color: '#94a3b8' }}>
Employee Contribution
</span>
<Space.Compact style={{ width: '100%' }}>
<InputNumber
style={{ width: '100%', fontSize: 13 }}
value={employeeContributionPer}
disabled={!ssfEnabled}
max={100}
min={0}
onChange={v => {
setEmployeeContributionPer(v)
handleChange()
}}
/>
<Space.Addon>%</Space.Addon>
</Space.Compact>
<div style={contributionStyle}>
<span style={{ color: '#64748b', fontWeight: 600 }}>
{monthlySalary && basicSalaryPer && employeeContributionPer
? fmt(
(monthlySalary * basicSalaryPer * employeeContributionPer) /
10000
)
: 'N/A'}
</span>
</div>
</Col>
<Col span={12}>
<span className="form-label" style={{ fontSize: 10, color: '#94a3b8' }}>
Employer Contribution
</span>
<Space.Compact style={{ width: '100%' }}>
<InputNumber
style={{ width: '100%', fontSize: 13 }}
value={employerContributionPer}
disabled={!ssfEnabled}
max={100}
min={0}
onChange={v => {
setEmployerContributionPer(v)
handleChange()
}}
/>
<Space.Addon>%</Space.Addon>
</Space.Compact>
<div style={contributionStyle}>
<span style={{ color: '#64748b', fontWeight: 600 }}>
{monthlySalary && basicSalaryPer && employerContributionPer
? fmt(
(monthlySalary * basicSalaryPer * employerContributionPer) /
10000
)
: 'N/A'}
</span>
</div>
</Col>
</Row>
<div
style={{
marginTop: 12,
paddingTop: 10,
borderTop: '1px solid #e2e8f0',
fontSize: 12,
fontWeight: 600,
color: '#1e3a5f',
fontFamily: 'sans-serif',
display: 'flex',
justifyContent: 'space-between',
}}
>
<span>Total Monthly SSF</span>
<span style={{ fontFamily: 'monospace' }}>
{fmt(getMonthlySSF()?.deduction ?? 0)}
</span>
</div>
</Col>
</Row>
<Row gutter={8} style={{ marginTop: 8, marginBottom: 8 }}>
<Col span={12}>
<span className="form-label">Employee Contribution</span>
<Space.Compact>
<InputNumber
style={{ width: '100%' }}
value={employeeContributionPer}
disabled={!ssfEnabled}
max={100}
min={0}
onChange={v => {
setEmployeeContributionPer(v)
handleChange()
}}
/>
<Space.Addon>%</Space.Addon>
</Space.Compact>
<div style={contributionStyle}>
{ssfEnabled && monthlySalary && basicSalaryPer && employeeContributionPer
? fmt((monthlySalary * basicSalaryPer * employeeContributionPer) / 10000)
: 'NPR N/A'}
</div>
</Col>
<Col span={12}>
<span className="form-label">Employer Contribution</span>
<Space.Compact>
<InputNumber
style={{ width: '100%' }}
value={employerContributionPer}
disabled={!ssfEnabled}
max={100}
min={0}
onChange={v => {
setEmployerContributionPer(v)
handleChange()
}}
/>
<Space.Addon>%</Space.Addon>
</Space.Compact>
<div style={contributionStyle}>
{ssfEnabled && monthlySalary && basicSalaryPer && employerContributionPer
? fmt((monthlySalary * basicSalaryPer * employerContributionPer) / 10000)
: 'NPR N/A'}
</div>
</Col>
</Row>
<div style={{ ...contributionStyle, fontWeight: 'bold' }}>
Monthly SSF: {fmt(getMonthlySSF()?.deduction)}
</div>
</>
</div>
)}
</div>
)
}
+50 -20
View File
@@ -1,6 +1,7 @@
import { Typography } from 'antd'
import type { MaritalStatus } from '../types'
import { TAX_SLABS_INDIVIDUAL, TAX_SLABS_COUPLE, TAG_COLORS } from '../constants'
import { BookOpen } from 'lucide-react'
const { Text } = Typography
@@ -12,22 +13,31 @@ export function SlabReference({ maritalStatus }: Readonly<SlabReferenceProps>) {
const slabs = maritalStatus === 'couple' ? TAX_SLABS_COUPLE : TAX_SLABS_INDIVIDUAL
return (
<div>
<div style={{ padding: '8px 0' }}>
<div
style={{
fontSize: 11,
fontWeight: 700,
color: '#6b7280',
letterSpacing: '1px',
textTransform: 'uppercase',
fontFamily: 'sans-serif',
marginBottom: 10,
display: 'flex',
alignItems: 'center',
gap: 6,
marginBottom: 12,
}}
>
Tax Slabs Reference
<BookOpen size={13} color="#64748b" strokeWidth={2} />
<span
style={{
fontSize: 11,
fontWeight: 700,
color: '#64748b',
letterSpacing: '1px',
textTransform: 'uppercase',
fontFamily: 'sans-serif',
}}
>
Tax Slabs Reference
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{slabs.map(slab => {
const c = TAG_COLORS[slab.tag]
return (
@@ -37,17 +47,27 @@ export function SlabReference({ maritalStatus }: Readonly<SlabReferenceProps>) {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '7px 10px',
borderRadius: 8,
background: c.bg,
border: `1px solid ${c.border}`,
padding: '6px 12px',
borderRadius: 6,
background: '#fff',
border: `1px solid #f1f5f9`,
transition: 'all 0.15s ease',
}}
onMouseEnter={e => {
;(e.currentTarget as HTMLElement).style.borderColor = c.border
;(e.currentTarget as HTMLElement).style.background = c.bg
}}
onMouseLeave={e => {
;(e.currentTarget as HTMLElement).style.borderColor = '#f1f5f9'
;(e.currentTarget as HTMLElement).style.background = '#fff'
}}
>
<span
style={{
fontSize: 12,
color: '#374151',
color: '#475569',
fontFamily: 'sans-serif',
fontWeight: 500,
}}
>
{slab.label}
@@ -58,6 +78,10 @@ export function SlabReference({ maritalStatus }: Readonly<SlabReferenceProps>) {
fontWeight: 700,
color: c.color,
fontFamily: 'monospace',
background: c.bg,
padding: '1px 6px',
borderRadius: 4,
border: `1px solid ${c.border}`,
}}
>
{(slab.rate * 100).toFixed(0)}%
@@ -70,8 +94,8 @@ export function SlabReference({ maritalStatus }: Readonly<SlabReferenceProps>) {
<div
style={{
marginTop: 16,
padding: '10px 12px',
background: '#fffbeb',
padding: '10px 14px',
background: '#fef3c7',
borderRadius: 8,
border: '1px solid #fde68a',
}}
@@ -81,11 +105,17 @@ export function SlabReference({ maritalStatus }: Readonly<SlabReferenceProps>) {
fontSize: 10,
color: '#92400e',
fontFamily: 'sans-serif',
lineHeight: 1.6,
lineHeight: 1.5,
display: 'block',
}}
>
* 36% = 30% base + 20% surcharge. ** 39% = 30% base + 30% surcharge. SST =
Social Security Tax @ 1%.
<strong style={{ opacity: 0.8 }}>Notes:</strong>
<br />
SST = Social Security Tax @ 1% (applied on first slab).
<br />
* 36% = 30% base + 20% surcharge.
<br />
** 39% = 30% base + 30% surcharge.
</Text>
</div>
</div>
+26 -6
View File
@@ -9,8 +9,9 @@ import type {
} from '../types'
import { TAG_COLORS } from '../constants'
import { fmt, useBreakpoints } from '../utils'
import { Layers } from 'lucide-react'
const { Title, Text } = Typography
const { Text } = Typography
interface TdsBreakdownTableProps {
result: TaxResult
@@ -161,13 +162,32 @@ export function TdsBreakdownTable({ result }: Readonly<TdsBreakdownTableProps>)
styles={{ body: { padding: isMobile ? '16px 12px' : '24px 24px' } }}
>
{/* Header */}
<div style={{ marginBottom: 14 }}>
<Title
level={5}
style={{ margin: 0, color: '#1e3a5f', fontFamily: 'Georgia, serif' }}
<div style={{ marginBottom: 14, display: 'flex', alignItems: 'center', gap: 8 }}>
<div
style={{
width: 30,
height: 30,
borderRadius: 8,
background: '#f0f4ff',
border: '1px solid #c7d2fe',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Layers size={15} color="#4f46e5" strokeWidth={2} />
</div>
<span
style={{
margin: 0,
color: '#1e3a5f',
fontFamily: 'Georgia, serif',
fontWeight: 600,
fontSize: 15,
}}
>
TDS Breakdown
</Title>
</span>
</div>
{/* Table */}
+56 -20
View File
@@ -22,34 +22,70 @@ export default function YearlyBonus({ onBonusChange }: Readonly<YearlyBonusProps
})
return (
<>
<div style={{ padding: '4px 0' }}>
<Checkbox
style={{ fontWeight: 500, color: '#374151', fontFamily: 'sans-serif' }}
onChange={e => {
setBonusEnabled(e.target.checked)
handleChange()
}}
>
Yearly Bonus
Include Yearly Bonus
</Checkbox>
<Space.Compact style={{ width: '100%', marginTop: 8 }}>
<Space.Addon>NPR</Space.Addon>
<InputNumber
size="medium"
style={{ width: '100%' }}
placeholder=""
formatter={v => (v ? Math.round(v).toLocaleString('en-IN') : '')}
parser={v => Number((v ?? '').replaceAll(',', ''))}
min={0}
max={10_000_000}
value={bonus}
onChange={val => {
setBonus(val)
handleChange()
{bonusEnabled && (
<div
className="result-animate"
style={{
marginTop: 12,
padding: 12,
background: '#f8fafc',
borderRadius: 8,
border: '1px solid #e2e8f0',
}}
disabled={!bonusEnabled}
/>
</Space.Compact>
</>
>
<span className="form-label" style={{ fontSize: 10, color: '#94a3b8' }}>
Annual Bonus Amount
</span>
<Space.Compact style={{ width: '100%' }}>
<Space.Addon
style={{
background: '#fff',
borderRight: '1px solid #d1d5db',
fontSize: 11,
color: '#64748b',
}}
>
NPR
</Space.Addon>
<InputNumber
size="middle"
style={{ width: '100%', fontSize: 13 }}
placeholder="e.g. 50,000"
formatter={v => (v ? Math.round(v).toLocaleString('en-IN') : '')}
parser={v => Number((v ?? '').replaceAll(',', ''))}
min={0}
max={10_000_000}
value={bonus}
onChange={val => {
setBonus(val)
handleChange()
}}
disabled={!bonusEnabled}
/>
</Space.Compact>
<div
style={{
marginTop: 6,
fontSize: 11,
color: '#94a3b8',
fontFamily: 'sans-serif',
}}
>
This will be added to your annual taxable income.
</div>
</div>
)}
</div>
)
}
+58 -4
View File
@@ -3,7 +3,7 @@
line-height: 1.5;
font-weight: 400;
background-color: #242424;
background-color: #f8fafc;
font-synthesis: none;
text-rendering: optimizeLegibility;
@@ -69,7 +69,7 @@ button:focus-visible {
}
}
hr{
hr {
height: 0px;
border: none;
border-top: 1px solid #eee;
@@ -77,11 +77,11 @@ hr{
width: 100%;
}
.amount-text{
.amount-text {
font-family: monospace;
}
.form-label{
.form-label {
display: block;
margin-bottom: 4px;
font-size: 11px;
@@ -91,3 +91,57 @@ hr{
letter-spacing: 1px;
text-transform: uppercase;
}
/* Hide number input spinner arrows */
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type='number'] {
-moz-appearance: textfield;
appearance: textfield;
}
/* Hide ant design InputNumber handlers globally */
.ant-input-number-handler-wrap {
display: none !important;
}
/* Smooth result card fade-in */
@keyframes fadeSlideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.result-animate {
animation: fadeSlideIn 0.25s ease both;
}
/* Stagger children */
.result-animate:nth-child(1) { animation-delay: 0ms; }
.result-animate:nth-child(2) { animation-delay: 60ms; }
.result-animate:nth-child(3) { animation-delay: 120ms; }
/* Smooth table row highlight */
.ant-table-tbody > tr:hover > td {
background: #f0f4ff !important;
transition: background 0.15s ease;
}
/* Better focus rings for inputs */
.ant-input-number:focus-within {
border-color: #2563eb !important;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1) !important;
}
/* Better collapse transition */
.ant-collapse-content {
transition: all 0.2s ease !important;
}
+2 -1
View File
@@ -1,4 +1,5 @@
// ── Domain Types ───────────────────────────────────────────────────────────
import type { ReactNode } from 'react'
export type MaritalStatus = 'individual' | 'couple'
@@ -55,7 +56,7 @@ export interface KpiCard {
accent: string
bg: string
border: string
icon: string
icon: ReactNode
}
// ── Breakpoint helpers ─────────────────────────────────────────────────────