feat(ui): replace emojis with Lucide icons and update website UI
This commit is contained in:
16 files changed
+851
-327
No files matched your search
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"hash": "37b4fb89",
|
||||
"configHash": "4edb8c93",
|
||||
"lockfileHash": "e3b0c442",
|
||||
"browserHash": "e33dcc6e",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
Generated
+10
@@ -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",
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
|
||||
@@ -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
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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
@@ -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
@@ -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 ─────────────────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user