Vipooshanb commited on
Commit
a0372d5
·
1 Parent(s): e67c6c0

Create Sample Kandan

Browse files
HR System/index.html CHANGED
@@ -2,7 +2,6 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>hr-system</title>
8
  </head>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>hr-system</title>
7
  </head>
HR System/src/App.css DELETED
@@ -1,184 +0,0 @@
1
- .counter {
2
- font-size: 16px;
3
- padding: 5px 10px;
4
- border-radius: 5px;
5
- color: var(--accent);
6
- background: var(--accent-bg);
7
- border: 2px solid transparent;
8
- transition: border-color 0.3s;
9
- margin-bottom: 24px;
10
-
11
- &:hover {
12
- border-color: var(--accent-border);
13
- }
14
- &:focus-visible {
15
- outline: 2px solid var(--accent);
16
- outline-offset: 2px;
17
- }
18
- }
19
-
20
- .hero {
21
- position: relative;
22
-
23
- .base,
24
- .framework,
25
- .vite {
26
- inset-inline: 0;
27
- margin: 0 auto;
28
- }
29
-
30
- .base {
31
- width: 170px;
32
- position: relative;
33
- z-index: 0;
34
- }
35
-
36
- .framework,
37
- .vite {
38
- position: absolute;
39
- }
40
-
41
- .framework {
42
- z-index: 1;
43
- top: 34px;
44
- height: 28px;
45
- transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
46
- scale(1.4);
47
- }
48
-
49
- .vite {
50
- z-index: 0;
51
- top: 107px;
52
- height: 26px;
53
- width: auto;
54
- transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
55
- scale(0.8);
56
- }
57
- }
58
-
59
- #center {
60
- display: flex;
61
- flex-direction: column;
62
- gap: 25px;
63
- place-content: center;
64
- place-items: center;
65
- flex-grow: 1;
66
-
67
- @media (max-width: 1024px) {
68
- padding: 32px 20px 24px;
69
- gap: 18px;
70
- }
71
- }
72
-
73
- #next-steps {
74
- display: flex;
75
- border-top: 1px solid var(--border);
76
- text-align: left;
77
-
78
- & > div {
79
- flex: 1 1 0;
80
- padding: 32px;
81
- @media (max-width: 1024px) {
82
- padding: 24px 20px;
83
- }
84
- }
85
-
86
- .icon {
87
- margin-bottom: 16px;
88
- width: 22px;
89
- height: 22px;
90
- }
91
-
92
- @media (max-width: 1024px) {
93
- flex-direction: column;
94
- text-align: center;
95
- }
96
- }
97
-
98
- #docs {
99
- border-right: 1px solid var(--border);
100
-
101
- @media (max-width: 1024px) {
102
- border-right: none;
103
- border-bottom: 1px solid var(--border);
104
- }
105
- }
106
-
107
- #next-steps ul {
108
- list-style: none;
109
- padding: 0;
110
- display: flex;
111
- gap: 8px;
112
- margin: 32px 0 0;
113
-
114
- .logo {
115
- height: 18px;
116
- }
117
-
118
- a {
119
- color: var(--text-h);
120
- font-size: 16px;
121
- border-radius: 6px;
122
- background: var(--social-bg);
123
- display: flex;
124
- padding: 6px 12px;
125
- align-items: center;
126
- gap: 8px;
127
- text-decoration: none;
128
- transition: box-shadow 0.3s;
129
-
130
- &:hover {
131
- box-shadow: var(--shadow);
132
- }
133
- .button-icon {
134
- height: 18px;
135
- width: 18px;
136
- }
137
- }
138
-
139
- @media (max-width: 1024px) {
140
- margin-top: 20px;
141
- flex-wrap: wrap;
142
- justify-content: center;
143
-
144
- li {
145
- flex: 1 1 calc(50% - 8px);
146
- }
147
-
148
- a {
149
- width: 100%;
150
- justify-content: center;
151
- box-sizing: border-box;
152
- }
153
- }
154
- }
155
-
156
- #spacer {
157
- height: 88px;
158
- border-top: 1px solid var(--border);
159
- @media (max-width: 1024px) {
160
- height: 48px;
161
- }
162
- }
163
-
164
- .ticks {
165
- position: relative;
166
- width: 100%;
167
-
168
- &::before,
169
- &::after {
170
- content: '';
171
- position: absolute;
172
- top: -4.5px;
173
- border: 5px solid transparent;
174
- }
175
-
176
- &::before {
177
- left: 0;
178
- border-left-color: var(--border);
179
- }
180
- &::after {
181
- right: 0;
182
- border-right-color: var(--border);
183
- }
184
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
HR System/src/App.jsx CHANGED
@@ -1,120 +1,272 @@
1
- import { useState } from 'react'
2
- import reactLogo from './assets/react.svg'
3
- import viteLogo from './assets/vite.svg'
4
- import heroImg from './assets/hero.png'
5
- import './App.css'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  function App() {
8
- const [count, setCount] = useState(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  return (
11
- <>
12
- <section id="center">
13
- <div className="hero">
14
- <img src={heroImg} className="base" width="170" height="179" alt="" />
15
- <img src={reactLogo} className="framework" alt="React logo" />
16
- <img src={viteLogo} className="vite" alt="Vite logo" />
17
- </div>
18
- <div>
19
- <h1>Get started</h1>
20
- <p>
21
- Edit <code>src/App.jsx</code> and save to test <code>HMR</code>
22
- </p>
23
- </div>
24
- <button
25
- className="counter"
26
- onClick={() => setCount((count) => count + 1)}
27
- >
28
- Count is {count}
29
- </button>
30
- </section>
31
-
32
- <div className="ticks"></div>
33
-
34
- <section id="next-steps">
35
- <div id="docs">
36
- <svg className="icon" role="presentation" aria-hidden="true">
37
- <use href="/icons.svg#documentation-icon"></use>
38
- </svg>
39
- <h2>Documentation</h2>
40
- <p>Your questions, answered</p>
41
- <ul>
42
- <li>
43
- <a href="https://vite.dev/" target="_blank">
44
- <img className="logo" src={viteLogo} alt="" />
45
- Explore Vite
46
- </a>
47
- </li>
48
- <li>
49
- <a href="https://react.dev/" target="_blank">
50
- <img className="button-icon" src={reactLogo} alt="" />
51
- Learn more
52
- </a>
53
- </li>
54
- </ul>
55
- </div>
56
- <div id="social">
57
- <svg className="icon" role="presentation" aria-hidden="true">
58
- <use href="/icons.svg#social-icon"></use>
59
- </svg>
60
- <h2>Connect with us</h2>
61
- <p>Join the Vite community</p>
62
- <ul>
63
- <li>
64
- <a href="https://github.com/vitejs/vite" target="_blank">
65
- <svg
66
- className="button-icon"
67
- role="presentation"
68
- aria-hidden="true"
69
- >
70
- <use href="/icons.svg#github-icon"></use>
71
- </svg>
72
- GitHub
73
- </a>
74
- </li>
75
- <li>
76
- <a href="https://chat.vite.dev/" target="_blank">
77
- <svg
78
- className="button-icon"
79
- role="presentation"
80
- aria-hidden="true"
81
- >
82
- <use href="/icons.svg#discord-icon"></use>
83
- </svg>
84
- Discord
85
- </a>
86
- </li>
87
- <li>
88
- <a href="https://x.com/vite_js" target="_blank">
89
- <svg
90
- className="button-icon"
91
- role="presentation"
92
- aria-hidden="true"
93
- >
94
- <use href="/icons.svg#x-icon"></use>
95
- </svg>
96
- X.com
97
- </a>
98
- </li>
99
- <li>
100
- <a href="https://bsky.app/profile/vite.dev" target="_blank">
101
- <svg
102
- className="button-icon"
103
- role="presentation"
104
- aria-hidden="true"
105
- >
106
- <use href="/icons.svg#bluesky-icon"></use>
107
- </svg>
108
- Bluesky
109
- </a>
110
- </li>
111
- </ul>
112
- </div>
113
- </section>
114
-
115
- <div className="ticks"></div>
116
- <section id="spacer"></section>
117
- </>
118
  )
119
  }
120
 
 
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import { dummyCandidates, stageOrder } from './data/dummyCandidates'
3
+ import { getCandidates } from './services/candidateApi'
4
+ import Sidebar from './components/Sidebar'
5
+ import Header from './components/Header'
6
+ import FilterBar from './components/FilterBar'
7
+ import KanbanBoard from './components/KanbanBoard'
8
+ import CandidateModal from './components/CandidateModal'
9
+ import TopActions from './components/TopActions'
10
+ import CandidateDirectory from './components/CandidateDirectory'
11
+ import CalendarView from './components/CalendarView'
12
+ import SettingsPanel from './components/SettingsPanel'
13
+
14
+ const initialTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
15
+
16
+ const viewCopy = {
17
+ Dashboard: {
18
+ description: 'Track the live hiring pipeline.',
19
+ statLabel: 'Visible',
20
+ },
21
+ Candidates: {
22
+ description: 'Search and review every applied candidate.',
23
+ statLabel: 'Matched',
24
+ },
25
+ Calendar: {
26
+ description: 'See the hiring month at a glance.',
27
+ statLabel: 'Month',
28
+ },
29
+ Settings: {
30
+ description: 'Adjust the application appearance.',
31
+ statLabel: 'Theme',
32
+ },
33
+ }
34
 
35
  function App() {
36
+ const [candidates, setCandidates] = useState(dummyCandidates)
37
+ const [selectedCandidate, setSelectedCandidate] = useState(null)
38
+ const [isModalOpen, setIsModalOpen] = useState(false)
39
+ const [activeView, setActiveView] = useState('Dashboard')
40
+ const [searchTerm, setSearchTerm] = useState('')
41
+ const [selectedStage, setSelectedStage] = useState('All')
42
+ const [scoreFilter, setScoreFilter] = useState('All')
43
+ const [candidateSearchTerm, setCandidateSearchTerm] = useState('')
44
+ const [candidateStageFilter, setCandidateStageFilter] = useState('All')
45
+ const [candidatePositionFilter, setCandidatePositionFilter] = useState('All')
46
+ const [theme, setTheme] = useState(initialTheme)
47
+ const [loading, setLoading] = useState(true)
48
+ const [error, setError] = useState('')
49
+
50
+ useEffect(() => {
51
+ document.documentElement.dataset.theme = theme
52
+ window.localStorage.setItem('dashboard-theme', theme)
53
+ }, [theme])
54
+
55
+ useEffect(() => {
56
+ const storedTheme = window.localStorage.getItem('dashboard-theme')
57
+ if (storedTheme === 'light' || storedTheme === 'dark') {
58
+ setTheme(storedTheme)
59
+ }
60
+ }, [])
61
+
62
+ useEffect(() => {
63
+ let active = true
64
+
65
+ async function loadCandidates() {
66
+ try {
67
+ setLoading(true)
68
+ const data = await getCandidates()
69
+ if (active) {
70
+ setCandidates(data)
71
+ setError('')
72
+ }
73
+ } catch (loadError) {
74
+ if (active) {
75
+ setError(loadError instanceof Error ? loadError.message : 'Failed to load candidates')
76
+ }
77
+ } finally {
78
+ if (active) {
79
+ setLoading(false)
80
+ }
81
+ }
82
+ }
83
+
84
+ loadCandidates()
85
+
86
+ return () => {
87
+ active = false
88
+ }
89
+ }, [])
90
+
91
+ const filteredCandidates = useMemo(() => {
92
+ return candidates.filter((candidate) => {
93
+ const matchesSearch = candidate.name.toLowerCase().includes(searchTerm.toLowerCase())
94
+ const matchesStage = selectedStage === 'All' ? true : candidate.stage === selectedStage
95
+
96
+ const matchesScore =
97
+ scoreFilter === 'All'
98
+ ? true
99
+ : scoreFilter === '4+'
100
+ ? candidate.overallScore >= 4
101
+ : scoreFilter === '3-4'
102
+ ? candidate.overallScore >= 3 && candidate.overallScore < 4
103
+ : candidate.overallScore > 0 && candidate.overallScore < 3
104
+
105
+ return matchesSearch && matchesStage && matchesScore
106
+ })
107
+ }, [candidates, searchTerm, selectedStage, scoreFilter])
108
+
109
+ const candidatePositions = useMemo(() => {
110
+ return Array.from(
111
+ new Set(
112
+ candidates
113
+ .map((candidate) => candidate.appliedPosition ?? candidate.role)
114
+ .filter(Boolean),
115
+ ),
116
+ ).sort((left, right) => left.localeCompare(right))
117
+ }, [candidates])
118
+
119
+ const filteredDirectoryCandidates = useMemo(() => {
120
+ return candidates.filter((candidate) => {
121
+ const appliedPosition = candidate.appliedPosition ?? candidate.role
122
+ const searchValue = candidateSearchTerm.toLowerCase()
123
+ const matchesSearch =
124
+ candidate.name.toLowerCase().includes(searchValue) ||
125
+ candidate.email.toLowerCase().includes(searchValue) ||
126
+ candidate.phone.toLowerCase().includes(searchValue) ||
127
+ appliedPosition.toLowerCase().includes(searchValue)
128
+ const matchesStage = candidateStageFilter === 'All' ? true : candidate.stage === candidateStageFilter
129
+ const matchesPosition =
130
+ candidatePositionFilter === 'All' ? true : appliedPosition === candidatePositionFilter
131
+
132
+ return matchesSearch && matchesStage && matchesPosition
133
+ })
134
+ }, [candidates, candidateSearchTerm, candidateStageFilter, candidatePositionFilter])
135
+
136
+ const currentViewCopy = viewCopy[activeView]
137
+ const activeStatValue =
138
+ activeView === 'Dashboard'
139
+ ? `${filteredCandidates.length} candidates`
140
+ : activeView === 'Candidates'
141
+ ? `${filteredDirectoryCandidates.length} profiles`
142
+ : activeView === 'Calendar'
143
+ ? new Date().toLocaleString('en-US', { month: 'long', year: 'numeric' })
144
+ : theme === 'dark'
145
+ ? 'Dark mode'
146
+ : 'Light mode'
147
+
148
+ const handleOpenCandidate = (candidate) => {
149
+ setSelectedCandidate(candidate)
150
+ setIsModalOpen(true)
151
+ }
152
+
153
+ const handleSaveCandidate = (updatedCandidate) => {
154
+ setCandidates((currentCandidates) =>
155
+ currentCandidates.map((candidate) =>
156
+ candidate.id === updatedCandidate.id ? updatedCandidate : candidate,
157
+ ),
158
+ )
159
+ setSelectedCandidate(updatedCandidate)
160
+ setIsModalOpen(true)
161
+ }
162
+
163
+ const handleCreateCandidate = () => {
164
+ const nextCandidate = {
165
+ id: `cand-${String(candidates.length + 1).padStart(3, '0')}`,
166
+ name: 'New Candidate',
167
+ role: 'Research and Development Officer',
168
+ appliedPosition: 'Research and Development Officer',
169
+ stage: 'Applied',
170
+ applicationDate: new Date().toISOString().slice(0, 10),
171
+ overallScore: 0,
172
+ referred: false,
173
+ assessmentAdded: false,
174
+ email: 'new.candidate@example.com',
175
+ phone: '+94 70 000 0000',
176
+ location: 'Colombo, Sri Lanka',
177
+ notes: 'New placeholder candidate for frontend-only workflow.',
178
+ }
179
+
180
+ setCandidates((currentCandidates) => [nextCandidate, ...currentCandidates])
181
+ handleOpenCandidate(nextCandidate)
182
+ }
183
+
184
+ const handleResetFilters = () => {
185
+ setSearchTerm('')
186
+ setSelectedStage('All')
187
+ setScoreFilter('All')
188
+ }
189
+
190
+ const handleResetCandidateFilters = () => {
191
+ setCandidateSearchTerm('')
192
+ setCandidateStageFilter('All')
193
+ setCandidatePositionFilter('All')
194
+ }
195
 
196
  return (
197
+ <div className="app-shell">
198
+ <Sidebar activeView={activeView} onSelectView={setActiveView} />
199
+
200
+ <main className="workspace">
201
+ <Header
202
+ title={activeView}
203
+ description={currentViewCopy.description}
204
+ statLabel={currentViewCopy.statLabel}
205
+ statValue={activeStatValue}
206
+ />
207
+
208
+ {activeView === 'Dashboard' ? (
209
+ <>
210
+ <div className="workspace__toolbar">
211
+ <FilterBar
212
+ searchTerm={searchTerm}
213
+ onSearchTermChange={setSearchTerm}
214
+ stageFilter={selectedStage}
215
+ onStageFilterChange={setSelectedStage}
216
+ scoreFilter={scoreFilter}
217
+ onScoreFilterChange={setScoreFilter}
218
+ stages={stageOrder}
219
+ onResetFilters={handleResetFilters}
220
+ />
221
+ <TopActions selectedCount={filteredCandidates.length} onCreateCandidate={handleCreateCandidate} />
222
+ </div>
223
+
224
+ {error ? <div className="banner banner--error">{error}</div> : null}
225
+ {loading ? <div className="banner">Loading candidates from mock data...</div> : null}
226
+
227
+ <KanbanBoard
228
+ candidates={candidates}
229
+ searchTerm={searchTerm}
230
+ selectedStage={selectedStage}
231
+ scoreFilter={scoreFilter}
232
+ onSelectCandidate={handleOpenCandidate}
233
+ />
234
+ </>
235
+ ) : null}
236
+
237
+ {activeView === 'Candidates' ? (
238
+ <CandidateDirectory
239
+ candidates={filteredDirectoryCandidates}
240
+ searchTerm={candidateSearchTerm}
241
+ onSearchTermChange={setCandidateSearchTerm}
242
+ stageFilter={candidateStageFilter}
243
+ onStageFilterChange={setCandidateStageFilter}
244
+ positionFilter={candidatePositionFilter}
245
+ onPositionFilterChange={setCandidatePositionFilter}
246
+ positions={candidatePositions}
247
+ onResetFilters={handleResetCandidateFilters}
248
+ onSelectCandidate={handleOpenCandidate}
249
+ />
250
+ ) : null}
251
+
252
+ {activeView === 'Calendar' ? <CalendarView /> : null}
253
+
254
+ {activeView === 'Settings' ? (
255
+ <SettingsPanel
256
+ theme={theme}
257
+ onToggleTheme={() => setTheme((currentTheme) => (currentTheme === 'dark' ? 'light' : 'dark'))}
258
+ />
259
+ ) : null}
260
+ </main>
261
+
262
+ <CandidateModal
263
+ candidate={selectedCandidate}
264
+ isOpen={isModalOpen}
265
+ onClose={() => setIsModalOpen(false)}
266
+ onSave={handleSaveCandidate}
267
+ stageOrder={stageOrder}
268
+ />
269
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  )
271
  }
272
 
HR System/src/components/CalendarView.jsx ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function CalendarView() {
2
+ const today = new Date()
3
+ const year = today.getFullYear()
4
+ const month = today.getMonth()
5
+ const monthLabel = today.toLocaleString('en-US', { month: 'long', year: 'numeric' })
6
+ const firstDay = new Date(year, month, 1)
7
+ const lastDay = new Date(year, month + 1, 0)
8
+ const leadingDays = firstDay.getDay()
9
+ const totalDays = lastDay.getDate()
10
+ const previousMonthTotal = new Date(year, month, 0).getDate()
11
+
12
+ const days = Array.from({ length: 42 }, (_, index) => {
13
+ const dayNumber = index - leadingDays + 1
14
+ const isPreviousMonth = dayNumber < 1
15
+ const isCurrentMonth = dayNumber >= 1 && dayNumber <= totalDays
16
+ const isNextMonth = dayNumber > totalDays
17
+ const displayDay = isPreviousMonth
18
+ ? previousMonthTotal + dayNumber
19
+ : isNextMonth
20
+ ? dayNumber - totalDays
21
+ : dayNumber
22
+
23
+ const date = isCurrentMonth ? new Date(year, month, dayNumber) : null
24
+ const isToday =
25
+ date &&
26
+ date.getDate() === today.getDate() &&
27
+ date.getMonth() === today.getMonth() &&
28
+ date.getFullYear() === today.getFullYear()
29
+
30
+ return {
31
+ displayDay,
32
+ isCurrentMonth,
33
+ isToday,
34
+ }
35
+ })
36
+
37
+ return (
38
+ <section className="calendar-view" aria-label="Monthly calendar">
39
+ <div className="calendar-view__header">
40
+ <div>
41
+ <p className="calendar-view__eyebrow">Calendar</p>
42
+ <h2>{monthLabel}</h2>
43
+ <p>Static month view for dates only. No day cells are clickable.</p>
44
+ </div>
45
+ <div className="calendar-view__badge">
46
+ <strong>{totalDays}</strong>
47
+ <span>days in month</span>
48
+ </div>
49
+ </div>
50
+
51
+ <div className="calendar-grid" role="presentation">
52
+ {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((label) => (
53
+ <div key={label} className="calendar-grid__weekday">
54
+ {label}
55
+ </div>
56
+ ))}
57
+
58
+ {days.map((day, index) => (
59
+ <div
60
+ key={`${day.displayDay}-${index}`}
61
+ className={`calendar-grid__day${day.isCurrentMonth ? '' : ' is-muted'}${day.isToday ? ' is-today' : ''}`}
62
+ >
63
+ <span>{day.displayDay}</span>
64
+ </div>
65
+ ))}
66
+ </div>
67
+ </section>
68
+ )
69
+ }
70
+
71
+ export default CalendarView
HR System/src/components/CandidateCard.jsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function CandidateCard({ candidate, onClick }) {
2
+ const scoreLabel = candidate.overallScore > 0 ? `${candidate.overallScore.toFixed(1)} Overall` : 'No score yet'
3
+
4
+ return (
5
+ <button type="button" className="candidate-card" onClick={onClick}>
6
+ <div className="candidate-card__top">
7
+ <div className={`avatar avatar--${candidate.id.slice(-1)}`} aria-hidden="true">
8
+ {candidate.name
9
+ .split(' ')
10
+ .map((part) => part.charAt(0))
11
+ .slice(0, 2)
12
+ .join('')}
13
+ </div>
14
+ <div className="candidate-card__heading">
15
+ <strong>{candidate.name}</strong>
16
+ <span>{candidate.applicationDate}</span>
17
+ </div>
18
+ </div>
19
+
20
+ <div className="candidate-card__body">
21
+ <span className="pill pill--score">★ {scoreLabel}</span>
22
+ {candidate.referred ? <span className="pill pill--muted">Referred</span> : null}
23
+ </div>
24
+
25
+ <div className="candidate-card__footer">
26
+ {candidate.assessmentAdded ? (
27
+ <span className="candidate-card__action">Assessment added</span>
28
+ ) : (
29
+ <span className="candidate-card__action candidate-card__action--accent">Add assessment</span>
30
+ )}
31
+ <span className="candidate-card__chevron" aria-hidden="true">
32
+
33
+ </span>
34
+ </div>
35
+ </button>
36
+ )
37
+ }
38
+
39
+ export default CandidateCard
HR System/src/components/CandidateDirectory.jsx ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import EmptyState from './EmptyState'
2
+ import { stageOrder } from '../data/dummyCandidates'
3
+
4
+ function CandidateDirectory({
5
+ candidates,
6
+ searchTerm,
7
+ onSearchTermChange,
8
+ stageFilter,
9
+ onStageFilterChange,
10
+ positionFilter,
11
+ onPositionFilterChange,
12
+ positions,
13
+ onResetFilters,
14
+ onSelectCandidate,
15
+ }) {
16
+ return (
17
+ <section className="directory" aria-label="Candidate directory">
18
+ <div className="directory__header">
19
+ <div>
20
+ <p className="directory__eyebrow">Candidate directory</p>
21
+ <h2>Applied candidates</h2>
22
+ <p>Search, filter, and review every applicant in one place.</p>
23
+ </div>
24
+ <div className="directory__count">
25
+ <strong>{candidates.length}</strong>
26
+ <span>matches</span>
27
+ </div>
28
+ </div>
29
+
30
+ <div className="directory__filters">
31
+ <label className="field field--search">
32
+ <span className="sr-only">Search candidates</span>
33
+ <input
34
+ type="search"
35
+ placeholder="Search candidates, email, phone, or position"
36
+ value={searchTerm}
37
+ onChange={(event) => onSearchTermChange(event.target.value)}
38
+ />
39
+ </label>
40
+
41
+ <label className="field">
42
+ <span className="field__label">Stage</span>
43
+ <select value={stageFilter} onChange={(event) => onStageFilterChange(event.target.value)}>
44
+ <option value="All">All stages</option>
45
+ {stageOrder.map((stage) => (
46
+ <option key={stage} value={stage}>
47
+ {stage}
48
+ </option>
49
+ ))}
50
+ </select>
51
+ </label>
52
+
53
+ <label className="field">
54
+ <span className="field__label">Applied position</span>
55
+ <select value={positionFilter} onChange={(event) => onPositionFilterChange(event.target.value)}>
56
+ <option value="All">All positions</option>
57
+ {positions.map((position) => (
58
+ <option key={position} value={position}>
59
+ {position}
60
+ </option>
61
+ ))}
62
+ </select>
63
+ </label>
64
+
65
+ <button type="button" className="ghost-button" onClick={onResetFilters}>
66
+ Reset filters
67
+ </button>
68
+ </div>
69
+
70
+ {candidates.length ? (
71
+ <div className="directory__list">
72
+ <div className="directory__list-head" aria-hidden="true">
73
+ <span>Candidate</span>
74
+ <span>Contact</span>
75
+ <span>Applied position</span>
76
+ <span>Stage</span>
77
+ </div>
78
+
79
+ {candidates.map((candidate) => {
80
+ const appliedPosition = candidate.appliedPosition ?? candidate.role
81
+ const initials = candidate.name
82
+ .split(' ')
83
+ .map((part) => part.charAt(0))
84
+ .slice(0, 2)
85
+ .join('')
86
+
87
+ return (
88
+ <button
89
+ key={candidate.id}
90
+ type="button"
91
+ className="candidate-row"
92
+ onClick={() => onSelectCandidate(candidate)}
93
+ >
94
+ <div className="candidate-row__identity">
95
+ <div className={`avatar avatar--${candidate.id.slice(-1)}`} aria-hidden="true">
96
+ {initials}
97
+ </div>
98
+ <div className="candidate-row__nameblock">
99
+ <strong>{candidate.name}</strong>
100
+ <span>{candidate.applicationDate}</span>
101
+ </div>
102
+ </div>
103
+
104
+ <div className="candidate-row__contact">
105
+ <span>{candidate.phone}</span>
106
+ <span>{candidate.email}</span>
107
+ </div>
108
+
109
+ <div className="candidate-row__position">
110
+ <span>{appliedPosition}</span>
111
+ </div>
112
+
113
+ <div className="candidate-row__stage">
114
+ <span className="pill pill--score">{candidate.stage}</span>
115
+ </div>
116
+ </button>
117
+ )
118
+ })}
119
+ </div>
120
+ ) : (
121
+ <EmptyState
122
+ title="No matching candidates"
123
+ description="Try widening the search, stage, or applied position filters."
124
+ />
125
+ )}
126
+ </section>
127
+ )
128
+ }
129
+
130
+ export default CandidateDirectory
HR System/src/components/CandidateModal.jsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function CandidateModal({ candidate, isOpen, onClose, onSave, stageOrder }) {
2
+ if (!isOpen || !candidate) {
3
+ return null
4
+ }
5
+
6
+ return (
7
+ <div className="modal-backdrop" role="presentation" onClick={onClose}>
8
+ <div
9
+ className="modal"
10
+ role="dialog"
11
+ aria-modal="true"
12
+ aria-labelledby="candidate-modal-title"
13
+ onClick={(event) => event.stopPropagation()}
14
+ >
15
+ <div className="modal__header">
16
+ <div>
17
+ <p className="modal__eyebrow">Candidate details</p>
18
+ <h2 id="candidate-modal-title">{candidate.name}</h2>
19
+ </div>
20
+ <button type="button" className="ghost-button ghost-button--inline" onClick={onClose}>
21
+ Close
22
+ </button>
23
+ </div>
24
+
25
+ <div className="modal__content">
26
+ <section className="modal__card">
27
+ <div className={`avatar avatar--${candidate.id.slice(-1)} avatar--large`} aria-hidden="true">
28
+ {candidate.name
29
+ .split(' ')
30
+ .map((part) => part.charAt(0))
31
+ .slice(0, 2)
32
+ .join('')}
33
+ </div>
34
+ <div className="modal__facts">
35
+ <p><strong>Applied Position:</strong> {candidate.appliedPosition ?? candidate.role}</p>
36
+ <p><strong>Email:</strong> {candidate.email}</p>
37
+ <p><strong>Phone:</strong> {candidate.phone}</p>
38
+ <p><strong>Location:</strong> {candidate.location}</p>
39
+ </div>
40
+ </section>
41
+
42
+ <section className="modal__card">
43
+ <label className="field field--stacked">
44
+ <span className="field__label">Stage</span>
45
+ <select
46
+ value={candidate.stage}
47
+ onChange={(event) => onSave({ ...candidate, stage: event.target.value })}
48
+ >
49
+ {stageOrder.map((stage) => (
50
+ <option key={stage} value={stage}>
51
+ {stage}
52
+ </option>
53
+ ))}
54
+ </select>
55
+ </label>
56
+
57
+ <div className="modal__grid">
58
+ <div>
59
+ <span className="modal__stat-label">Applied</span>
60
+ <strong>{candidate.applicationDate}</strong>
61
+ </div>
62
+ <div>
63
+ <span className="modal__stat-label">Score</span>
64
+ <strong>{candidate.overallScore > 0 ? candidate.overallScore.toFixed(1) : 'N/A'}</strong>
65
+ </div>
66
+ <div>
67
+ <span className="modal__stat-label">Referral</span>
68
+ <strong>{candidate.referred ? 'Yes' : 'No'}</strong>
69
+ </div>
70
+ <div>
71
+ <span className="modal__stat-label">Assessment</span>
72
+ <strong>{candidate.assessmentAdded ? 'Added' : 'Pending'}</strong>
73
+ </div>
74
+ </div>
75
+ </section>
76
+
77
+ <section className="modal__card modal__card--full">
78
+ <span className="modal__stat-label">Notes</span>
79
+ <p>{candidate.notes}</p>
80
+ </section>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ )
85
+ }
86
+
87
+ export default CandidateModal
HR System/src/components/EmptyState.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ function EmptyState({ title, description }) {
2
+ return (
3
+ <div className="empty-state">
4
+ <strong>{title}</strong>
5
+ <p>{description}</p>
6
+ </div>
7
+ )
8
+ }
9
+
10
+ export default EmptyState
HR System/src/components/FilterBar.jsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function FilterBar({ searchTerm, onSearchTermChange, stageFilter, onStageFilterChange, scoreFilter, onScoreFilterChange, stages, onResetFilters }) {
2
+ return (
3
+ <section className="filter-bar" aria-label="Search and filters">
4
+ <label className="field field--search">
5
+ <span className="sr-only">Search candidates</span>
6
+ <input
7
+ type="search"
8
+ placeholder="Search candidates"
9
+ value={searchTerm}
10
+ onChange={(event) => onSearchTermChange(event.target.value)}
11
+ />
12
+ </label>
13
+
14
+ <label className="field">
15
+ <span className="field__label">Stage</span>
16
+ <select value={stageFilter} onChange={(event) => onStageFilterChange(event.target.value)}>
17
+ <option value="All">All stages</option>
18
+ {stages.map((stage) => (
19
+ <option key={stage} value={stage}>
20
+ {stage}
21
+ </option>
22
+ ))}
23
+ </select>
24
+ </label>
25
+
26
+ <label className="field">
27
+ <span className="field__label">Score</span>
28
+ <select value={scoreFilter} onChange={(event) => onScoreFilterChange(event.target.value)}>
29
+ <option value="All">All scores</option>
30
+ <option value="4+">4.0 and above</option>
31
+ <option value="3-4">3.0 to 3.9</option>
32
+ <option value="Below 3">Below 3.0</option>
33
+ </select>
34
+ </label>
35
+
36
+ <button type="button" className="ghost-button" onClick={onResetFilters}>
37
+ Reset filters
38
+ </button>
39
+ </section>
40
+ )
41
+ }
42
+
43
+ export default FilterBar
HR System/src/components/Header.jsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function Header({ title, description, statLabel, statValue }) {
2
+ return (
3
+ <header className="header">
4
+ <div className="header__copy">
5
+ <p className="header__eyebrow">HR workspace</p>
6
+ <h1 className="header__title">{title}</h1>
7
+ <div className="header__meta">
8
+ <span>{description}</span>
9
+ <span className="header__dot" aria-hidden="true" />
10
+ <span>
11
+ {statLabel}: {statValue}
12
+ </span>
13
+ </div>
14
+ </div>
15
+
16
+ <div className="header__actions">
17
+ <div className="header__crumb">
18
+ <span className="header__crumb-label">Active view</span>
19
+ <strong>{title}</strong>
20
+ </div>
21
+ </div>
22
+ </header>
23
+ )
24
+ }
25
+
26
+ export default Header
HR System/src/components/KanbanBoard.jsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { stageOrder } from '../data/dummyCandidates'
2
+ import StageColumn from './StageColumn'
3
+
4
+ function KanbanBoard({ candidates, searchTerm, selectedStage, scoreFilter, onSelectCandidate }) {
5
+ const matchesScoreFilter = (candidate) => {
6
+ if (scoreFilter === 'All') {
7
+ return true
8
+ }
9
+
10
+ if (scoreFilter === '4+') {
11
+ return candidate.overallScore >= 4
12
+ }
13
+
14
+ if (scoreFilter === '3-4') {
15
+ return candidate.overallScore >= 3 && candidate.overallScore < 4
16
+ }
17
+
18
+ return candidate.overallScore > 0 && candidate.overallScore < 3
19
+ }
20
+
21
+ const filteredCandidates = candidates.filter((candidate) => {
22
+ const matchesSearch = candidate.name.toLowerCase().includes(searchTerm.toLowerCase())
23
+ const matchesStage = selectedStage === 'All' ? true : candidate.stage === selectedStage
24
+ return matchesSearch && matchesStage && matchesScoreFilter(candidate)
25
+ })
26
+
27
+ return (
28
+ <section className="board-wrap" aria-label="Candidate board">
29
+ <div className="kanban-board">
30
+ {stageOrder.map((stage) => {
31
+ const stageCandidates = filteredCandidates.filter((candidate) => candidate.stage === stage)
32
+ return (
33
+ <StageColumn
34
+ key={stage}
35
+ stage={{ name: stage }}
36
+ candidates={stageCandidates}
37
+ onSelectCandidate={onSelectCandidate}
38
+ />
39
+ )
40
+ })}
41
+ </div>
42
+ </section>
43
+ )
44
+ }
45
+
46
+ export default KanbanBoard
HR System/src/components/SettingsPanel.jsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function SettingsPanel({ theme, onToggleTheme }) {
2
+ const isDark = theme === 'dark'
3
+
4
+ return (
5
+ <section className="settings-panel" aria-label="Settings">
6
+ <div className="settings-panel__header">
7
+ <div>
8
+ <p className="settings-panel__eyebrow">Settings</p>
9
+ <h2>Appearance</h2>
10
+ <p>Theme switching lives here only, as requested.</p>
11
+ </div>
12
+ </div>
13
+
14
+ <div className="settings-panel__card">
15
+ <div className="settings-panel__copy">
16
+ <span className="settings-panel__label">Light mode / Dark mode</span>
17
+ <strong>{isDark ? 'Dark mode is active' : 'Light mode is active'}</strong>
18
+ <p>
19
+ Use the toggle to switch the whole app theme. The rest of the interface does not show
20
+ any other theme control.
21
+ </p>
22
+ </div>
23
+
24
+ <button
25
+ type="button"
26
+ className={`theme-switch${isDark ? ' is-dark' : ''}`}
27
+ onClick={onToggleTheme}
28
+ aria-pressed={isDark}
29
+ aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
30
+ >
31
+ <span className="theme-switch__track" aria-hidden="true">
32
+ <span className="theme-switch__thumb" />
33
+ </span>
34
+ <span className="theme-switch__labels">
35
+ <span>Light mode</span>
36
+ <span>Dark mode</span>
37
+ </span>
38
+ </button>
39
+ </div>
40
+ </section>
41
+ )
42
+ }
43
+
44
+ export default SettingsPanel
HR System/src/components/Sidebar.jsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const navigationItems = [
2
+ {
3
+ label: 'Dashboard',
4
+ view: 'Dashboard',
5
+ icon: (
6
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
7
+ <rect x="3" y="3" width="8" height="8" rx="2" fill="currentColor" />
8
+ <rect x="13" y="3" width="8" height="5" rx="2" fill="currentColor" opacity="0.85" />
9
+ <rect x="13" y="10" width="8" height="11" rx="2" fill="currentColor" opacity="0.72" />
10
+ <rect x="3" y="13" width="8" height="8" rx="2" fill="currentColor" opacity="0.9" />
11
+ </svg>
12
+ ),
13
+ },
14
+ {
15
+ label: 'Candidates',
16
+ view: 'Candidates',
17
+ icon: (
18
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
19
+ <path
20
+ d="M16.5 12.5c1.93 0 3.5-1.57 3.5-3.5S18.43 5.5 16.5 5.5 13 7.07 13 9s1.57 3.5 3.5 3.5ZM7.5 11.5A3.5 3.5 0 1 0 7.5 4.5a3.5 3.5 0 0 0 0 7Zm9 2c-2.67 0-8 1.34-8 4v1.5h16V17.5c0-2.66-5.33-4-8-4Zm-9 0c-.92 0-2.15.17-3.25.5C2.1 14.4 1 15.39 1 16.75V20h4.5v-1.5c0-1.1.34-2.04.96-2.82.26-.32.56-.62.89-.88-.24-.17-.42-.3-.65-.3Z"
21
+ fill="currentColor"
22
+ />
23
+ </svg>
24
+ ),
25
+ },
26
+ {
27
+ label: 'Calendar',
28
+ view: 'Calendar',
29
+ icon: (
30
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
31
+ <rect x="3" y="5" width="18" height="16" rx="3" fill="none" stroke="currentColor" strokeWidth="2" />
32
+ <path d="M7 3v4M17 3v4M3 9h18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
33
+ <rect x="6" y="12" width="3" height="3" rx="0.8" fill="currentColor" />
34
+ <rect x="11" y="12" width="3" height="3" rx="0.8" fill="currentColor" opacity="0.72" />
35
+ <rect x="16" y="12" width="3" height="3" rx="0.8" fill="currentColor" opacity="0.55" />
36
+ </svg>
37
+ ),
38
+ },
39
+ {
40
+ label: 'Settings',
41
+ view: 'Settings',
42
+ icon: (
43
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
44
+ <path
45
+ d="M19.14 12.94c.04-.31.06-.63.06-.94s-.02-.63-.06-.94l2.02-1.58a.5.5 0 0 0 .12-.63l-1.91-3.3a.5.5 0 0 0-.6-.22l-2.39.96a7.13 7.13 0 0 0-1.63-.94l-.36-2.53A.5.5 0 0 0 14.4 1h-3.82a.5.5 0 0 0-.49.42l-.36 2.53c-.58.23-1.13.54-1.63.94l-2.39-.96a.5.5 0 0 0-.6.22L3.2 7.45a.5.5 0 0 0 .12.63l2.02 1.58c-.04.31-.06.63-.06.94s.02.63.06.94L3.32 13.12a.5.5 0 0 0-.12.63l1.91 3.3a.5.5 0 0 0 .6.22l2.39-.96c.5.4 1.05.71 1.63.94l.36 2.53a.5.5 0 0 0 .49.42h3.82a.5.5 0 0 0 .49-.42l.36-2.53c.58-.23 1.13-.54 1.63-.94l2.39.96a.5.5 0 0 0 .6-.22l1.91-3.3a.5.5 0 0 0-.12-.63l-2.02-1.58ZM12 15.25A3.25 3.25 0 1 1 12 8.75a3.25 3.25 0 0 1 0 6.5Z"
46
+ fill="currentColor"
47
+ />
48
+ </svg>
49
+ ),
50
+ },
51
+ ]
52
+
53
+ function Sidebar({ activeView, onSelectView }) {
54
+ return (
55
+ <aside className="sidebar" aria-label="Primary">
56
+ <div className="sidebar__brand">
57
+ <span className="sidebar__logo">HR</span>
58
+ </div>
59
+
60
+ <nav className="sidebar__nav">
61
+ {navigationItems.map((item) => (
62
+ <button
63
+ key={item.label}
64
+ type="button"
65
+ className={`sidebar__nav-item${item.view === activeView ? ' is-active' : ''}`}
66
+ aria-current={item.view === activeView ? 'page' : undefined}
67
+ aria-label={item.label}
68
+ data-label={item.label}
69
+ onClick={() => onSelectView(item.view)}
70
+ >
71
+ <span className="sidebar__nav-icon" aria-hidden="true">
72
+ {item.icon}
73
+ </span>
74
+ <span className="sidebar__nav-label">{item.label}</span>
75
+ </button>
76
+ ))}
77
+ </nav>
78
+ </aside>
79
+ )
80
+ }
81
+
82
+ export default Sidebar
HR System/src/components/StageColumn.jsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import CandidateCard from './CandidateCard'
2
+ import EmptyState from './EmptyState'
3
+
4
+ function StageColumn({ stage, candidates, onSelectCandidate }) {
5
+ return (
6
+ <section className="stage-column">
7
+ <header className="stage-column__header">
8
+ <div>
9
+ <span className="stage-column__title">{stage.name}</span>
10
+ <span className="stage-column__count">{candidates.length}</span>
11
+ </div>
12
+ <span className="stage-column__detail">Detail</span>
13
+ </header>
14
+
15
+ <div className="stage-column__content">
16
+ {candidates.length ? (
17
+ candidates.map((candidate) => (
18
+ <CandidateCard
19
+ key={candidate.id}
20
+ candidate={candidate}
21
+ onClick={() => onSelectCandidate(candidate)}
22
+ />
23
+ ))
24
+ ) : (
25
+ <EmptyState
26
+ title={`No candidates in ${stage.name}`}
27
+ description="Try adjusting the search or filter options to reveal more profiles."
28
+ />
29
+ )}
30
+ </div>
31
+ </section>
32
+ )
33
+ }
34
+
35
+ export default StageColumn
HR System/src/components/TabsNav.jsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function TabsNav({ tabs, activeTab, onSelectTab }) {
2
+ return (
3
+ <nav className="tabs-nav" aria-label="Candidate sections">
4
+ {tabs.map((tab) => (
5
+ <button
6
+ key={tab}
7
+ type="button"
8
+ className={`tabs-nav__item${tab === activeTab ? ' is-active' : ''}`}
9
+ onClick={() => onSelectTab(tab)}
10
+ >
11
+ {tab}
12
+ </button>
13
+ ))}
14
+ </nav>
15
+ )
16
+ }
17
+
18
+ export default TabsNav
HR System/src/components/TopActions.jsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function TopActions({ selectedCount, onCreateCandidate }) {
2
+ return (
3
+ <div className="top-actions">
4
+ <div className="top-actions__summary">
5
+ <span className="top-actions__label">Pipeline snapshot</span>
6
+ <strong>{selectedCount} visible candidates</strong>
7
+ </div>
8
+ <button type="button" className="primary-button" onClick={onCreateCandidate}>
9
+ Add candidate
10
+ </button>
11
+ </div>
12
+ )
13
+ }
14
+
15
+ export default TopActions
HR System/src/data/dummyCandidates.js ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const stageOrder = ['Applied', 'Screening', 'Interview', 'Test']
2
+
3
+ export const dashboardTabs = [
4
+ 'Candidates',
5
+ 'Job Info',
6
+ 'Calendar',
7
+ 'Score Card',
8
+ 'Activity',
9
+ 'Application Form',
10
+ 'Automation',
11
+ ]
12
+
13
+ export const dummyCandidates = [
14
+ {
15
+ id: 'cand-001',
16
+ name: 'Marlon Reynolds',
17
+ role: 'Research and Development Officer',
18
+ appliedPosition: 'Research and Development Officer',
19
+ stage: 'Applied',
20
+ applicationDate: '2023-10-29',
21
+ overallScore: 3.5,
22
+ referred: true,
23
+ assessmentAdded: false,
24
+ email: 'marlon.reynolds@example.com',
25
+ phone: '+94 77 210 1188',
26
+ location: 'Colombo, Sri Lanka',
27
+ notes: 'Strong research exposure and a clear interest in process improvement.',
28
+ },
29
+ {
30
+ id: 'cand-002',
31
+ name: 'Regina Hane',
32
+ role: 'Research and Development Officer',
33
+ appliedPosition: 'Research and Development Officer',
34
+ stage: 'Applied',
35
+ applicationDate: '2023-10-29',
36
+ overallScore: 2,
37
+ referred: false,
38
+ assessmentAdded: false,
39
+ email: 'regina.hane@example.com',
40
+ phone: '+94 71 110 4412',
41
+ location: 'Gampaha, Sri Lanka',
42
+ notes: 'Needs stronger case study examples, but the fundamentals are solid.',
43
+ },
44
+ {
45
+ id: 'cand-003',
46
+ name: 'Curtis Baumbach',
47
+ role: 'Research and Development Officer',
48
+ appliedPosition: 'Research and Development Officer',
49
+ stage: 'Applied',
50
+ applicationDate: '2023-10-29',
51
+ overallScore: 3,
52
+ referred: true,
53
+ assessmentAdded: false,
54
+ email: 'curtis.baumbach@example.com',
55
+ phone: '+94 70 314 6088',
56
+ location: 'Kandy, Sri Lanka',
57
+ notes: 'Good communication and reliable background in analytical work.',
58
+ },
59
+ {
60
+ id: 'cand-004',
61
+ name: 'Jaime Anderson',
62
+ role: 'Research and Development Officer',
63
+ appliedPosition: 'Research and Development Officer',
64
+ stage: 'Applied',
65
+ applicationDate: '2023-10-29',
66
+ overallScore: 0,
67
+ referred: false,
68
+ assessmentAdded: true,
69
+ email: 'jaime.anderson@example.com',
70
+ phone: '+94 76 623 3311',
71
+ location: 'Negombo, Sri Lanka',
72
+ notes: 'Assessment pending review by the hiring manager.',
73
+ },
74
+ {
75
+ id: 'cand-005',
76
+ name: 'Kristi Sipes',
77
+ role: 'Research and Development Officer',
78
+ appliedPosition: 'Research and Development Officer',
79
+ stage: 'Screening',
80
+ applicationDate: '2023-10-20',
81
+ overallScore: 3.5,
82
+ referred: false,
83
+ assessmentAdded: true,
84
+ email: 'kristi.sipes@example.com',
85
+ phone: '+94 72 702 3391',
86
+ location: 'Colombo, Sri Lanka',
87
+ notes: 'Screened successfully. Ready for a deeper interview discussion.',
88
+ },
89
+ {
90
+ id: 'cand-006',
91
+ name: 'Randy Dibbert',
92
+ role: 'Research and Development Officer',
93
+ appliedPosition: 'Research and Development Officer',
94
+ stage: 'Screening',
95
+ applicationDate: '2023-10-18',
96
+ overallScore: 3.5,
97
+ referred: false,
98
+ assessmentAdded: true,
99
+ email: 'randy.dibbert@example.com',
100
+ phone: '+94 77 324 9084',
101
+ location: 'Matara, Sri Lanka',
102
+ notes: 'Balanced profile with good technical clarity and calm communication.',
103
+ },
104
+ {
105
+ id: 'cand-007',
106
+ name: 'Jane Anderson',
107
+ role: 'Research and Development Officer',
108
+ appliedPosition: 'Research and Development Officer',
109
+ stage: 'Screening',
110
+ applicationDate: '2023-10-18',
111
+ overallScore: 0,
112
+ referred: false,
113
+ assessmentAdded: false,
114
+ email: 'jane.anderson@example.com',
115
+ phone: '+94 77 888 9010',
116
+ location: 'Kotte, Sri Lanka',
117
+ notes: 'Waiting for final screening notes before advancing.',
118
+ },
119
+ {
120
+ id: 'cand-008',
121
+ name: 'Shelia Doyle',
122
+ role: 'Research and Development Officer',
123
+ appliedPosition: 'Research and Development Officer',
124
+ stage: 'Screening',
125
+ applicationDate: '2023-10-13',
126
+ overallScore: 4.5,
127
+ referred: true,
128
+ assessmentAdded: true,
129
+ email: 'shelia.doyle@example.com',
130
+ phone: '+94 75 901 2234',
131
+ location: 'Galle, Sri Lanka',
132
+ notes: 'Strong review. Good candidate to move into interview.',
133
+ },
134
+ {
135
+ id: 'cand-009',
136
+ name: 'Cassandra Hartmann',
137
+ role: 'Research and Development Officer',
138
+ appliedPosition: 'Research and Development Officer',
139
+ stage: 'Screening',
140
+ applicationDate: '2023-10-11',
141
+ overallScore: 4,
142
+ referred: false,
143
+ assessmentAdded: true,
144
+ email: 'cassandra.hartmann@example.com',
145
+ phone: '+94 71 593 8844',
146
+ location: 'Kurunegala, Sri Lanka',
147
+ notes: 'Very organized candidate with clear domain thinking.',
148
+ },
149
+ {
150
+ id: 'cand-010',
151
+ name: 'Cameron Dickens',
152
+ role: 'Research and Development Officer',
153
+ appliedPosition: 'Research and Development Officer',
154
+ stage: 'Interview',
155
+ applicationDate: '2023-09-03',
156
+ overallScore: 4,
157
+ referred: false,
158
+ assessmentAdded: true,
159
+ email: 'cameron.dickens@example.com',
160
+ phone: '+94 77 112 4455',
161
+ location: 'Colombo, Sri Lanka',
162
+ notes: 'Interview questions were answered clearly with strong ownership.',
163
+ },
164
+ {
165
+ id: 'cand-011',
166
+ name: 'Merle Vandervort',
167
+ role: 'Research and Development Officer',
168
+ appliedPosition: 'Research and Development Officer',
169
+ stage: 'Interview',
170
+ applicationDate: '2023-09-09',
171
+ overallScore: 4,
172
+ referred: false,
173
+ assessmentAdded: true,
174
+ email: 'merle.vandervort@example.com',
175
+ phone: '+94 76 501 6721',
176
+ location: 'Nugegoda, Sri Lanka',
177
+ notes: 'Technically strong and comfortable with structured problem solving.',
178
+ },
179
+ {
180
+ id: 'cand-012',
181
+ name: 'Jasmine Wiza',
182
+ role: 'Research and Development Officer',
183
+ appliedPosition: 'Research and Development Officer',
184
+ stage: 'Interview',
185
+ applicationDate: '2023-09-10',
186
+ overallScore: 0,
187
+ referred: false,
188
+ assessmentAdded: false,
189
+ email: 'jasmine.wiza@example.com',
190
+ phone: '+94 75 302 2199',
191
+ location: 'Moratuwa, Sri Lanka',
192
+ notes: 'Interview feedback pending from the panel.',
193
+ },
194
+ {
195
+ id: 'cand-013',
196
+ name: 'Lola Kirlin',
197
+ role: 'Research and Development Officer',
198
+ appliedPosition: 'Research and Development Officer',
199
+ stage: 'Test',
200
+ applicationDate: '2023-09-03',
201
+ overallScore: 4.5,
202
+ referred: false,
203
+ assessmentAdded: true,
204
+ email: 'lola.kirlin@example.com',
205
+ phone: '+94 77 201 9080',
206
+ location: 'Battaramulla, Sri Lanka',
207
+ notes: 'Excellent test result. Candidate looks ready for a final decision.',
208
+ },
209
+ {
210
+ id: 'cand-014',
211
+ name: 'Virgil Larkin',
212
+ role: 'Research and Development Officer',
213
+ appliedPosition: 'Research and Development Officer',
214
+ stage: 'Test',
215
+ applicationDate: '2023-09-03',
216
+ overallScore: 0,
217
+ referred: false,
218
+ assessmentAdded: false,
219
+ email: 'virgil.larkin@example.com',
220
+ phone: '+94 72 801 7744',
221
+ location: 'Dehiwala, Sri Lanka',
222
+ notes: 'Test assignment has been scheduled but not yet submitted.',
223
+ },
224
+ ]
HR System/src/index.css CHANGED
@@ -1,111 +1,3 @@
1
  :root {
2
- --text: #6b6375;
3
- --text-h: #08060d;
4
- --bg: #fff;
5
- --border: #e5e4e7;
6
- --code-bg: #f4f3ec;
7
- --accent: #aa3bff;
8
- --accent-bg: rgba(170, 59, 255, 0.1);
9
- --accent-border: rgba(170, 59, 255, 0.5);
10
- --social-bg: rgba(244, 243, 236, 0.5);
11
- --shadow:
12
- rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
13
-
14
- --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
15
- --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
16
- --mono: ui-monospace, Consolas, monospace;
17
-
18
- font: 18px/145% var(--sans);
19
- letter-spacing: 0.18px;
20
- color-scheme: light dark;
21
- color: var(--text);
22
- background: var(--bg);
23
- font-synthesis: none;
24
- text-rendering: optimizeLegibility;
25
- -webkit-font-smoothing: antialiased;
26
- -moz-osx-font-smoothing: grayscale;
27
-
28
- @media (max-width: 1024px) {
29
- font-size: 16px;
30
- }
31
- }
32
-
33
- @media (prefers-color-scheme: dark) {
34
- :root {
35
- --text: #9ca3af;
36
- --text-h: #f3f4f6;
37
- --bg: #16171d;
38
- --border: #2e303a;
39
- --code-bg: #1f2028;
40
- --accent: #c084fc;
41
- --accent-bg: rgba(192, 132, 252, 0.15);
42
- --accent-border: rgba(192, 132, 252, 0.5);
43
- --social-bg: rgba(47, 48, 58, 0.5);
44
- --shadow:
45
- rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
46
- }
47
-
48
- #social .button-icon {
49
- filter: invert(1) brightness(2);
50
- }
51
- }
52
-
53
- body {
54
- margin: 0;
55
- }
56
-
57
- #root {
58
- width: 1126px;
59
- max-width: 100%;
60
- margin: 0 auto;
61
- text-align: center;
62
- border-inline: 1px solid var(--border);
63
- min-height: 100svh;
64
- display: flex;
65
- flex-direction: column;
66
- box-sizing: border-box;
67
- }
68
-
69
- h1,
70
- h2 {
71
- font-family: var(--heading);
72
- font-weight: 500;
73
- color: var(--text-h);
74
- }
75
-
76
- h1 {
77
- font-size: 56px;
78
- letter-spacing: -1.68px;
79
- margin: 32px 0;
80
- @media (max-width: 1024px) {
81
- font-size: 36px;
82
- margin: 20px 0;
83
- }
84
- }
85
- h2 {
86
- font-size: 24px;
87
- line-height: 118%;
88
- letter-spacing: -0.24px;
89
- margin: 0 0 8px;
90
- @media (max-width: 1024px) {
91
- font-size: 20px;
92
- }
93
- }
94
- p {
95
- margin: 0;
96
- }
97
-
98
- code,
99
- .counter {
100
- font-family: var(--mono);
101
- display: inline-flex;
102
- border-radius: 4px;
103
- color: var(--text-h);
104
- }
105
-
106
- code {
107
- font-size: 15px;
108
- line-height: 135%;
109
- padding: 4px 8px;
110
- background: var(--code-bg);
111
  }
 
1
  :root {
2
+ color-scheme: light;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  }
HR System/src/main.jsx CHANGED
@@ -1,6 +1,7 @@
1
  import { StrictMode } from 'react'
2
  import { createRoot } from 'react-dom/client'
3
  import './index.css'
 
4
  import App from './App.jsx'
5
 
6
  createRoot(document.getElementById('root')).render(
 
1
  import { StrictMode } from 'react'
2
  import { createRoot } from 'react-dom/client'
3
  import './index.css'
4
+ import './styles/global.css'
5
  import App from './App.jsx'
6
 
7
  createRoot(document.getElementById('root')).render(
HR System/src/services/candidateApi.js ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { dummyCandidates } from '../data/dummyCandidates'
2
+
3
+ const clone = (value) => JSON.parse(JSON.stringify(value))
4
+
5
+ const wait = (time = 180) =>
6
+ new Promise((resolve) => {
7
+ window.setTimeout(resolve, time)
8
+ })
9
+
10
+ export async function getCandidates() {
11
+ await wait()
12
+ return clone(dummyCandidates)
13
+ }
14
+
15
+ export async function getCandidateById(id) {
16
+ await wait(120)
17
+ return clone(dummyCandidates.find((candidate) => candidate.id === id) || null)
18
+ }
19
+
20
+ export async function createCandidate(candidate) {
21
+ await wait(120)
22
+ return {
23
+ ...clone(candidate),
24
+ id: crypto.randomUUID(),
25
+ }
26
+ }
27
+
28
+ export async function updateCandidate(id, updates) {
29
+ await wait(120)
30
+ return {
31
+ id,
32
+ ...clone(updates),
33
+ }
34
+ }
35
+
36
+ export async function deleteCandidate(id) {
37
+ await wait(120)
38
+ return { id, deleted: true }
39
+ }
40
+
41
+ export async function updateCandidateStage(id, stage) {
42
+ await wait(120)
43
+ return { id, stage }
44
+ }
HR System/src/styles/global.css ADDED
@@ -0,0 +1,1229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #f1f5f9;
3
+ --bg-soft: rgba(255, 255, 255, 0.7);
4
+ --panel: #ffffff;
5
+ --panel-strong: #ffffff;
6
+ --panel-muted: #f8fafc;
7
+ --text: #334155;
8
+ --text-strong: #0f172a;
9
+ --border: rgba(148, 163, 184, 0.24);
10
+ --shadow: 0 18px 40px rgba(15, 23, 42, 0.1);
11
+ --shadow-soft: 0 10px 24px rgba(15, 23, 42, 0.08);
12
+ --accent: #1f8a70;
13
+ --accent-strong: #16624f;
14
+ --accent-soft: rgba(31, 138, 112, 0.12);
15
+ --accent-alt: #2956f6;
16
+ --success: #2f9e5c;
17
+ --warning: #e49b11;
18
+ --danger: #dc4c64;
19
+ --radius-xl: 28px;
20
+ --radius-lg: 20px;
21
+ --radius-md: 14px;
22
+ --radius-sm: 10px;
23
+ --font-body: 'Segoe UI', Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
24
+ --font-head: 'Segoe UI', Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
25
+ }
26
+
27
+ html[data-theme='dark'] {
28
+ --bg: #0f131b;
29
+ --bg-soft: rgba(15, 19, 27, 0.74);
30
+ --panel: #171d28;
31
+ --panel-strong: #1d2432;
32
+ --panel-muted: #111723;
33
+ --text: #aeb8ca;
34
+ --text-strong: #f4f7fb;
35
+ --border: rgba(148, 163, 184, 0.16);
36
+ --shadow: 0 22px 44px rgba(0, 0, 0, 0.38);
37
+ --shadow-soft: 0 14px 28px rgba(0, 0, 0, 0.28);
38
+ --accent: #57c7a6;
39
+ --accent-strong: #2aa584;
40
+ --accent-soft: rgba(87, 199, 166, 0.14);
41
+ --accent-alt: #7b93ff;
42
+ --success: #57c7a6;
43
+ --warning: #f1b84b;
44
+ --danger: #ff788f;
45
+ }
46
+
47
+ * {
48
+ box-sizing: border-box;
49
+ }
50
+
51
+ html {
52
+ min-height: 100%;
53
+ background:
54
+ radial-gradient(circle at top left, rgba(31, 138, 112, 0.1), transparent 28%),
55
+ radial-gradient(circle at top right, rgba(41, 86, 246, 0.09), transparent 24%),
56
+ linear-gradient(180deg, var(--bg) 0%, var(--bg) 100%);
57
+ color: var(--text);
58
+ font-family: var(--font-body);
59
+ font-size: 16px;
60
+ line-height: 1.5;
61
+ font-synthesis: none;
62
+ text-rendering: optimizeLegibility;
63
+ -webkit-font-smoothing: antialiased;
64
+ -moz-osx-font-smoothing: grayscale;
65
+ }
66
+
67
+ body {
68
+ margin: 0;
69
+ min-height: 100vh;
70
+ background: transparent;
71
+ }
72
+
73
+ button,
74
+ input,
75
+ select {
76
+ font: inherit;
77
+ }
78
+
79
+ button {
80
+ cursor: pointer;
81
+ }
82
+
83
+ img {
84
+ max-width: 100%;
85
+ display: block;
86
+ }
87
+
88
+ #root {
89
+ min-height: 100vh;
90
+ }
91
+
92
+ .sr-only {
93
+ position: absolute;
94
+ width: 1px;
95
+ height: 1px;
96
+ padding: 0;
97
+ margin: -1px;
98
+ overflow: hidden;
99
+ clip: rect(0, 0, 0, 0);
100
+ white-space: nowrap;
101
+ border: 0;
102
+ }
103
+
104
+ .app-shell {
105
+ min-height: 100vh;
106
+ display: grid;
107
+ grid-template-columns: 92px minmax(0, 1fr);
108
+ gap: 18px;
109
+ padding: 14px;
110
+ }
111
+
112
+ .sidebar {
113
+ display: flex;
114
+ flex-direction: column;
115
+ align-items: center;
116
+ justify-content: space-between;
117
+ gap: 22px;
118
+ padding: 18px 10px;
119
+ border-radius: 28px;
120
+ background: var(--panel);
121
+ border: 1px solid var(--border);
122
+ box-shadow: var(--shadow-soft);
123
+ position: sticky;
124
+ top: 14px;
125
+ height: calc(100vh - 28px);
126
+ }
127
+
128
+ .sidebar__brand {
129
+ width: 100%;
130
+ display: flex;
131
+ justify-content: center;
132
+ }
133
+
134
+ .sidebar__logo {
135
+ width: 56px;
136
+ height: 56px;
137
+ border-radius: 18px;
138
+ display: grid;
139
+ place-items: center;
140
+ background: linear-gradient(135deg, rgba(31, 138, 112, 0.16), rgba(41, 86, 246, 0.18));
141
+ color: var(--text-strong);
142
+ font-weight: 800;
143
+ letter-spacing: 0.08em;
144
+ text-transform: lowercase;
145
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
146
+ }
147
+
148
+ .sidebar__nav,
149
+ .sidebar__footer {
150
+ width: 100%;
151
+ display: grid;
152
+ gap: 10px;
153
+ }
154
+
155
+ .sidebar__nav-item {
156
+ width: 100%;
157
+ min-height: 54px;
158
+ padding: 0;
159
+ border: 0;
160
+ border-radius: 18px;
161
+ background: transparent;
162
+ color: var(--text);
163
+ display: grid;
164
+ justify-items: center;
165
+ align-content: center;
166
+ gap: 6px;
167
+ transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
168
+ }
169
+
170
+ .sidebar__nav-item:hover {
171
+ background: var(--accent-soft);
172
+ color: var(--text-strong);
173
+ transform: translateY(-1px);
174
+ }
175
+
176
+ .sidebar__nav-item.is-active {
177
+ background: linear-gradient(135deg, rgba(31, 138, 112, 0.18), rgba(41, 86, 246, 0.16));
178
+ color: var(--text-strong);
179
+ }
180
+
181
+ .sidebar__nav-item--ghost {
182
+ background: var(--panel-muted);
183
+ }
184
+
185
+ .sidebar__nav-icon {
186
+ width: 18px;
187
+ height: 18px;
188
+ border-radius: 6px;
189
+ background: currentColor;
190
+ opacity: 0.7;
191
+ }
192
+
193
+ .sidebar__nav-label {
194
+ font-size: 0.7rem;
195
+ font-weight: 700;
196
+ }
197
+
198
+ .workspace {
199
+ min-width: 0;
200
+ background: var(--bg-soft);
201
+ border: 1px solid var(--border);
202
+ border-radius: 32px;
203
+ padding: 18px;
204
+ box-shadow: var(--shadow);
205
+ backdrop-filter: blur(14px);
206
+ }
207
+
208
+ .header {
209
+ display: flex;
210
+ align-items: flex-start;
211
+ justify-content: space-between;
212
+ gap: 18px;
213
+ padding: 10px 10px 18px;
214
+ }
215
+
216
+ .header__copy {
217
+ min-width: 0;
218
+ }
219
+
220
+ .header__eyebrow,
221
+ .top-actions__label,
222
+ .modal__eyebrow {
223
+ margin: 0 0 8px;
224
+ text-transform: uppercase;
225
+ letter-spacing: 0.16em;
226
+ font-size: 0.72rem;
227
+ font-weight: 700;
228
+ color: var(--accent-strong);
229
+ }
230
+
231
+ .header__title {
232
+ margin: 0;
233
+ color: var(--text-strong);
234
+ font-family: var(--font-head);
235
+ font-size: clamp(1.9rem, 3vw, 3rem);
236
+ line-height: 1.05;
237
+ letter-spacing: -0.04em;
238
+ }
239
+
240
+ .header__meta {
241
+ display: flex;
242
+ flex-wrap: wrap;
243
+ align-items: center;
244
+ gap: 10px;
245
+ margin-top: 12px;
246
+ color: var(--text);
247
+ }
248
+
249
+ .header__dot {
250
+ width: 4px;
251
+ height: 4px;
252
+ border-radius: 50%;
253
+ background: currentColor;
254
+ opacity: 0.5;
255
+ }
256
+
257
+ .header__actions {
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 12px;
261
+ flex-wrap: wrap;
262
+ justify-content: flex-end;
263
+ }
264
+
265
+ .header__crumb {
266
+ padding: 10px 14px;
267
+ border-radius: 18px;
268
+ background: var(--panel);
269
+ border: 1px solid var(--border);
270
+ display: grid;
271
+ gap: 2px;
272
+ box-shadow: var(--shadow-soft);
273
+ }
274
+
275
+ .header__crumb-label {
276
+ font-size: 0.72rem;
277
+ color: var(--text);
278
+ }
279
+
280
+ .tabs-nav {
281
+ display: flex;
282
+ gap: 8px;
283
+ overflow-x: auto;
284
+ padding: 0 10px 10px;
285
+ scrollbar-width: none;
286
+ }
287
+
288
+ .tabs-nav::-webkit-scrollbar,
289
+ .board-wrap::-webkit-scrollbar {
290
+ display: none;
291
+ }
292
+
293
+ .tabs-nav__item {
294
+ border: 0;
295
+ background: transparent;
296
+ color: var(--text);
297
+ padding: 12px 16px;
298
+ border-radius: 999px;
299
+ font-weight: 700;
300
+ white-space: nowrap;
301
+ transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
302
+ }
303
+
304
+ .tabs-nav__item:hover,
305
+ .tabs-nav__item.is-active {
306
+ background: var(--panel);
307
+ color: var(--text-strong);
308
+ box-shadow: var(--shadow-soft);
309
+ }
310
+
311
+ .workspace__toolbar {
312
+ display: flex;
313
+ gap: 14px;
314
+ align-items: stretch;
315
+ justify-content: space-between;
316
+ flex-wrap: wrap;
317
+ padding: 10px;
318
+ }
319
+
320
+ .filter-bar {
321
+ flex: 1 1 640px;
322
+ display: grid;
323
+ grid-template-columns: minmax(220px, 2fr) repeat(2, minmax(160px, 1fr)) auto;
324
+ gap: 12px;
325
+ align-items: end;
326
+ }
327
+
328
+ .field {
329
+ display: grid;
330
+ gap: 6px;
331
+ }
332
+
333
+ .field--stacked {
334
+ gap: 10px;
335
+ }
336
+
337
+ .field__label {
338
+ font-size: 0.75rem;
339
+ font-weight: 700;
340
+ color: var(--text);
341
+ }
342
+
343
+ .field input,
344
+ .field select {
345
+ width: 100%;
346
+ border: 1px solid var(--border);
347
+ border-radius: 16px;
348
+ background: var(--panel);
349
+ color: var(--text-strong);
350
+ padding: 14px 16px;
351
+ outline: none;
352
+ box-shadow: var(--shadow-soft);
353
+ transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
354
+ }
355
+
356
+ .field input:focus,
357
+ .field select:focus {
358
+ border-color: rgba(31, 138, 112, 0.48);
359
+ box-shadow: 0 0 0 4px rgba(31, 138, 112, 0.14);
360
+ }
361
+
362
+ .field--search input {
363
+ padding-left: 18px;
364
+ }
365
+
366
+ .top-actions {
367
+ min-width: 240px;
368
+ flex: 0 0 auto;
369
+ display: flex;
370
+ justify-content: space-between;
371
+ align-items: center;
372
+ gap: 14px;
373
+ padding: 14px 18px;
374
+ border-radius: 22px;
375
+ border: 1px solid var(--border);
376
+ background: var(--panel);
377
+ box-shadow: var(--shadow-soft);
378
+ }
379
+
380
+ .top-actions__summary {
381
+ display: grid;
382
+ gap: 4px;
383
+ }
384
+
385
+ .chip,
386
+ .pill {
387
+ display: inline-flex;
388
+ align-items: center;
389
+ gap: 6px;
390
+ border-radius: 999px;
391
+ padding: 6px 10px;
392
+ font-size: 0.78rem;
393
+ font-weight: 700;
394
+ width: fit-content;
395
+ }
396
+
397
+ .chip--success {
398
+ background: rgba(47, 158, 92, 0.14);
399
+ color: var(--success);
400
+ }
401
+
402
+ .pill--score {
403
+ background: rgba(41, 86, 246, 0.1);
404
+ color: var(--accent-alt);
405
+ }
406
+
407
+ .pill--muted {
408
+ background: var(--panel-muted);
409
+ color: var(--text-strong);
410
+ }
411
+
412
+ .primary-button,
413
+ .ghost-button,
414
+ .icon-button {
415
+ border: 0;
416
+ border-radius: 16px;
417
+ font-weight: 700;
418
+ transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
419
+ }
420
+
421
+ .primary-button {
422
+ padding: 14px 18px;
423
+ color: #ffffff;
424
+ background: linear-gradient(135deg, var(--accent), var(--accent-alt));
425
+ box-shadow: 0 14px 30px rgba(31, 138, 112, 0.28);
426
+ }
427
+
428
+ .primary-button:hover,
429
+ .ghost-button:hover,
430
+ .icon-button:hover {
431
+ transform: translateY(-1px);
432
+ }
433
+
434
+ .ghost-button,
435
+ .icon-button {
436
+ padding: 13px 16px;
437
+ background: var(--panel);
438
+ color: var(--text-strong);
439
+ box-shadow: var(--shadow-soft);
440
+ border: 1px solid var(--border);
441
+ }
442
+
443
+ .ghost-button--inline {
444
+ padding-inline: 14px;
445
+ }
446
+
447
+ .banner {
448
+ margin: 0 10px 16px;
449
+ padding: 14px 16px;
450
+ border-radius: 18px;
451
+ background: var(--panel);
452
+ border: 1px solid var(--border);
453
+ box-shadow: var(--shadow-soft);
454
+ }
455
+
456
+ .banner--error {
457
+ border-color: rgba(220, 76, 100, 0.35);
458
+ color: var(--danger);
459
+ }
460
+
461
+ .board-wrap {
462
+ overflow-x: auto;
463
+ padding: 8px 6px 4px;
464
+ }
465
+
466
+ .kanban-board {
467
+ display: grid;
468
+ grid-template-columns: repeat(4, minmax(240px, 1fr));
469
+ gap: 14px;
470
+ min-width: max-content;
471
+ }
472
+
473
+ .stage-column {
474
+ min-width: 0;
475
+ border-radius: 24px;
476
+ padding: 12px;
477
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.18));
478
+ border: 1px solid var(--border);
479
+ box-shadow: var(--shadow-soft);
480
+ backdrop-filter: blur(12px);
481
+ }
482
+
483
+ html[data-theme='dark'] .stage-column {
484
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02));
485
+ }
486
+
487
+ .stage-column__header {
488
+ display: flex;
489
+ align-items: center;
490
+ justify-content: space-between;
491
+ gap: 10px;
492
+ padding: 4px 4px 12px;
493
+ }
494
+
495
+ .stage-column__title {
496
+ display: inline-flex;
497
+ align-items: center;
498
+ gap: 8px;
499
+ font-weight: 800;
500
+ color: var(--text-strong);
501
+ }
502
+
503
+ .stage-column__title::before {
504
+ content: '';
505
+ width: 12px;
506
+ height: 12px;
507
+ border-radius: 999px;
508
+ background: linear-gradient(135deg, var(--accent), var(--accent-alt));
509
+ box-shadow: 0 0 0 4px rgba(31, 138, 112, 0.12);
510
+ }
511
+
512
+ .stage-column__count,
513
+ .stage-column__detail {
514
+ display: inline-flex;
515
+ align-items: center;
516
+ justify-content: center;
517
+ padding: 5px 10px;
518
+ border-radius: 999px;
519
+ background: var(--panel);
520
+ border: 1px solid var(--border);
521
+ color: var(--text-strong);
522
+ font-size: 0.78rem;
523
+ font-weight: 700;
524
+ }
525
+
526
+ .stage-column__content {
527
+ display: grid;
528
+ gap: 12px;
529
+ }
530
+
531
+ .candidate-card,
532
+ .empty-state {
533
+ width: 100%;
534
+ text-align: left;
535
+ border-radius: 20px;
536
+ border: 1px solid var(--border);
537
+ background: var(--panel-strong);
538
+ color: var(--text);
539
+ box-shadow: var(--shadow-soft);
540
+ }
541
+
542
+ .candidate-card {
543
+ padding: 14px;
544
+ display: grid;
545
+ gap: 12px;
546
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
547
+ }
548
+
549
+ .candidate-card:hover {
550
+ transform: translateY(-2px);
551
+ box-shadow: var(--shadow);
552
+ border-color: rgba(31, 138, 112, 0.26);
553
+ }
554
+
555
+ .candidate-card__top {
556
+ display: flex;
557
+ align-items: center;
558
+ gap: 12px;
559
+ }
560
+
561
+ .avatar {
562
+ width: 44px;
563
+ height: 44px;
564
+ border-radius: 16px;
565
+ display: grid;
566
+ place-items: center;
567
+ color: #ffffff;
568
+ font-weight: 800;
569
+ background: linear-gradient(135deg, var(--accent), var(--accent-alt));
570
+ flex: 0 0 auto;
571
+ }
572
+
573
+ .avatar--large {
574
+ width: 72px;
575
+ height: 72px;
576
+ border-radius: 24px;
577
+ font-size: 1.1rem;
578
+ }
579
+
580
+ .candidate-card__heading {
581
+ display: grid;
582
+ gap: 3px;
583
+ min-width: 0;
584
+ }
585
+
586
+ .candidate-card__heading strong,
587
+ .modal h2,
588
+ .modal__card strong,
589
+ .top-actions strong {
590
+ color: var(--text-strong);
591
+ }
592
+
593
+ .candidate-card__heading span,
594
+ .candidate-card__action,
595
+ .modal__stat-label,
596
+ .modal__facts p,
597
+ .header__meta,
598
+ .top-actions__summary,
599
+ .empty-state p {
600
+ color: var(--text);
601
+ }
602
+
603
+ .candidate-card__body,
604
+ .candidate-card__footer,
605
+ .modal__grid {
606
+ display: flex;
607
+ align-items: center;
608
+ justify-content: space-between;
609
+ gap: 10px;
610
+ }
611
+
612
+ .candidate-card__footer {
613
+ padding-top: 8px;
614
+ border-top: 1px solid var(--border);
615
+ }
616
+
617
+ .candidate-card__action--accent {
618
+ color: var(--accent-strong);
619
+ font-weight: 700;
620
+ }
621
+
622
+ .candidate-card__chevron {
623
+ color: var(--accent-strong);
624
+ font-size: 1.4rem;
625
+ line-height: 1;
626
+ }
627
+
628
+ .empty-state {
629
+ padding: 20px;
630
+ display: grid;
631
+ gap: 8px;
632
+ background: var(--panel-muted);
633
+ }
634
+
635
+ .modal-backdrop {
636
+ position: fixed;
637
+ inset: 0;
638
+ background: rgba(15, 23, 42, 0.46);
639
+ backdrop-filter: blur(8px);
640
+ display: grid;
641
+ place-items: center;
642
+ padding: 20px;
643
+ z-index: 40;
644
+ }
645
+
646
+ .modal {
647
+ width: min(960px, 100%);
648
+ max-height: min(88vh, 900px);
649
+ overflow: auto;
650
+ border-radius: 30px;
651
+ background: var(--panel);
652
+ border: 1px solid var(--border);
653
+ box-shadow: var(--shadow);
654
+ padding: 22px;
655
+ }
656
+
657
+ .modal__header {
658
+ display: flex;
659
+ align-items: flex-start;
660
+ justify-content: space-between;
661
+ gap: 16px;
662
+ margin-bottom: 18px;
663
+ }
664
+
665
+ .modal__header h2 {
666
+ margin: 0;
667
+ font-size: clamp(1.5rem, 2vw, 2rem);
668
+ }
669
+
670
+ .modal__content {
671
+ display: grid;
672
+ grid-template-columns: repeat(2, minmax(0, 1fr));
673
+ gap: 14px;
674
+ }
675
+
676
+ .modal__card {
677
+ border-radius: 22px;
678
+ padding: 18px;
679
+ background: var(--panel-muted);
680
+ border: 1px solid var(--border);
681
+ display: grid;
682
+ gap: 14px;
683
+ }
684
+
685
+ .modal__card--full {
686
+ grid-column: 1 / -1;
687
+ }
688
+
689
+ .modal__facts {
690
+ display: grid;
691
+ gap: 8px;
692
+ }
693
+
694
+ .modal__facts p,
695
+ .modal__card p {
696
+ margin: 0;
697
+ }
698
+
699
+ .modal__grid {
700
+ flex-wrap: wrap;
701
+ }
702
+
703
+ .modal__grid > div {
704
+ flex: 1 1 160px;
705
+ display: grid;
706
+ gap: 4px;
707
+ padding: 14px;
708
+ border-radius: 18px;
709
+ background: var(--panel);
710
+ border: 1px solid var(--border);
711
+ }
712
+
713
+ .modal__stat-label {
714
+ font-size: 0.78rem;
715
+ font-weight: 700;
716
+ text-transform: uppercase;
717
+ letter-spacing: 0.08em;
718
+ }
719
+
720
+ @media (max-width: 1180px) {
721
+ .app-shell {
722
+ grid-template-columns: 76px minmax(0, 1fr);
723
+ }
724
+
725
+ .filter-bar {
726
+ grid-template-columns: minmax(220px, 1fr) repeat(2, minmax(150px, 1fr));
727
+ }
728
+
729
+ .top-actions {
730
+ flex: 1 1 100%;
731
+ justify-content: space-between;
732
+ }
733
+ }
734
+
735
+ @media (max-width: 940px) {
736
+ .app-shell {
737
+ grid-template-columns: 1fr;
738
+ }
739
+
740
+ .sidebar {
741
+ position: static;
742
+ height: auto;
743
+ width: 100%;
744
+ flex-direction: row;
745
+ align-items: center;
746
+ justify-content: space-between;
747
+ overflow-x: auto;
748
+ }
749
+
750
+ .sidebar__nav,
751
+ .sidebar__footer {
752
+ width: auto;
753
+ display: flex;
754
+ gap: 8px;
755
+ }
756
+
757
+ .sidebar__nav-item {
758
+ min-width: 84px;
759
+ padding-inline: 12px;
760
+ }
761
+
762
+ .workspace {
763
+ border-radius: 28px;
764
+ }
765
+
766
+ .header,
767
+ .workspace__toolbar {
768
+ flex-direction: column;
769
+ }
770
+
771
+ .header__actions {
772
+ width: 100%;
773
+ justify-content: flex-start;
774
+ }
775
+
776
+ .filter-bar {
777
+ flex: 1 1 100%;
778
+ grid-template-columns: 1fr;
779
+ }
780
+
781
+ .modal__content {
782
+ grid-template-columns: 1fr;
783
+ }
784
+ }
785
+
786
+ @media (max-width: 640px) {
787
+ .app-shell {
788
+ padding: 10px;
789
+ gap: 10px;
790
+ }
791
+
792
+ .workspace {
793
+ padding: 12px;
794
+ }
795
+
796
+ .header {
797
+ padding-inline: 2px;
798
+ }
799
+
800
+ .tabs-nav,
801
+ .workspace__toolbar,
802
+ .kanban-board {
803
+ padding-inline: 0;
804
+ }
805
+
806
+ .kanban-board {
807
+ grid-template-columns: repeat(4, minmax(220px, 1fr));
808
+ }
809
+
810
+ .modal {
811
+ padding: 16px;
812
+ border-radius: 22px;
813
+ }
814
+ }
815
+
816
+ .sidebar {
817
+ width: 100%;
818
+ }
819
+
820
+ .sidebar__brand {
821
+ padding-top: 2px;
822
+ }
823
+
824
+ .sidebar__logo {
825
+ letter-spacing: 0.1em;
826
+ text-transform: uppercase;
827
+ }
828
+
829
+ .sidebar__nav-item {
830
+ position: relative;
831
+ padding: 0;
832
+ min-height: 58px;
833
+ }
834
+
835
+ .sidebar__nav-icon {
836
+ width: 22px;
837
+ height: 22px;
838
+ display: grid;
839
+ place-items: center;
840
+ color: currentColor;
841
+ }
842
+
843
+ .sidebar__nav-icon svg {
844
+ width: 100%;
845
+ height: 100%;
846
+ display: block;
847
+ }
848
+
849
+ .sidebar__nav-label {
850
+ display: none;
851
+ }
852
+
853
+ .sidebar__nav-item::after {
854
+ content: attr(data-label);
855
+ position: absolute;
856
+ left: calc(100% + 12px);
857
+ top: 50%;
858
+ transform: translateY(-50%) translateX(-4px);
859
+ opacity: 0;
860
+ pointer-events: none;
861
+ padding: 8px 10px;
862
+ border-radius: 12px;
863
+ background: var(--text-strong);
864
+ color: var(--panel);
865
+ font-size: 0.78rem;
866
+ font-weight: 700;
867
+ white-space: nowrap;
868
+ box-shadow: var(--shadow-soft);
869
+ transition: opacity 0.16s ease, transform 0.16s ease;
870
+ z-index: 2;
871
+ }
872
+
873
+ .sidebar__nav-item:hover::after,
874
+ .sidebar__nav-item:focus-visible::after {
875
+ opacity: 1;
876
+ transform: translateY(-50%) translateX(0);
877
+ }
878
+
879
+ .header__actions {
880
+ align-items: flex-start;
881
+ }
882
+
883
+ .header__crumb {
884
+ min-width: 160px;
885
+ }
886
+
887
+ .directory,
888
+ .calendar-view,
889
+ .settings-panel {
890
+ display: grid;
891
+ gap: 18px;
892
+ padding: 10px 10px 4px;
893
+ }
894
+
895
+ .directory__header,
896
+ .calendar-view__header,
897
+ .settings-panel__header {
898
+ display: flex;
899
+ align-items: flex-start;
900
+ justify-content: space-between;
901
+ gap: 16px;
902
+ }
903
+
904
+ .directory__eyebrow,
905
+ .calendar-view__eyebrow,
906
+ .settings-panel__eyebrow {
907
+ margin: 0 0 8px;
908
+ text-transform: uppercase;
909
+ letter-spacing: 0.16em;
910
+ font-size: 0.72rem;
911
+ font-weight: 700;
912
+ color: var(--accent-strong);
913
+ }
914
+
915
+ .directory h2,
916
+ .calendar-view h2,
917
+ .settings-panel h2 {
918
+ margin: 0;
919
+ color: var(--text-strong);
920
+ font-size: clamp(1.5rem, 2vw, 2.2rem);
921
+ letter-spacing: -0.03em;
922
+ }
923
+
924
+ .directory__header p,
925
+ .calendar-view__header p,
926
+ .settings-panel__header p,
927
+ .settings-panel__copy p {
928
+ margin: 8px 0 0;
929
+ color: var(--text);
930
+ }
931
+
932
+ .directory__count,
933
+ .calendar-view__badge {
934
+ min-width: 112px;
935
+ padding: 14px 16px;
936
+ border-radius: 20px;
937
+ background: var(--panel);
938
+ border: 1px solid var(--border);
939
+ box-shadow: var(--shadow-soft);
940
+ display: grid;
941
+ gap: 2px;
942
+ justify-items: center;
943
+ }
944
+
945
+ .directory__count strong,
946
+ .calendar-view__badge strong {
947
+ color: var(--text-strong);
948
+ font-size: 1.5rem;
949
+ line-height: 1;
950
+ }
951
+
952
+ .directory__count span,
953
+ .calendar-view__badge span {
954
+ color: var(--text);
955
+ font-size: 0.78rem;
956
+ font-weight: 700;
957
+ text-transform: uppercase;
958
+ letter-spacing: 0.08em;
959
+ }
960
+
961
+ .directory__filters {
962
+ display: grid;
963
+ grid-template-columns: minmax(240px, 2fr) repeat(2, minmax(160px, 1fr)) auto;
964
+ gap: 12px;
965
+ align-items: end;
966
+ }
967
+
968
+ .directory__list {
969
+ display: grid;
970
+ gap: 10px;
971
+ }
972
+
973
+ .directory__list-head,
974
+ .candidate-row {
975
+ display: grid;
976
+ grid-template-columns: minmax(220px, 1.4fr) minmax(220px, 1.1fr) minmax(180px, 1fr) minmax(120px, 0.5fr);
977
+ gap: 16px;
978
+ align-items: center;
979
+ }
980
+
981
+ .directory__list-head {
982
+ padding: 0 14px;
983
+ color: var(--text);
984
+ font-size: 0.76rem;
985
+ font-weight: 700;
986
+ text-transform: uppercase;
987
+ letter-spacing: 0.08em;
988
+ }
989
+
990
+ .candidate-row {
991
+ width: 100%;
992
+ border: 1px solid var(--border);
993
+ border-radius: 22px;
994
+ background: var(--panel);
995
+ box-shadow: var(--shadow-soft);
996
+ padding: 14px;
997
+ text-align: left;
998
+ color: var(--text);
999
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
1000
+ }
1001
+
1002
+ .candidate-row:hover {
1003
+ transform: translateY(-2px);
1004
+ box-shadow: var(--shadow);
1005
+ border-color: rgba(31, 138, 112, 0.26);
1006
+ }
1007
+
1008
+ .candidate-row__identity {
1009
+ display: flex;
1010
+ align-items: center;
1011
+ gap: 12px;
1012
+ min-width: 0;
1013
+ }
1014
+
1015
+ .candidate-row__nameblock {
1016
+ display: grid;
1017
+ gap: 3px;
1018
+ min-width: 0;
1019
+ }
1020
+
1021
+ .candidate-row__nameblock strong {
1022
+ color: var(--text-strong);
1023
+ }
1024
+
1025
+ .candidate-row__nameblock span,
1026
+ .candidate-row__contact,
1027
+ .candidate-row__position {
1028
+ color: var(--text);
1029
+ font-size: 0.92rem;
1030
+ }
1031
+
1032
+ .candidate-row__contact,
1033
+ .candidate-row__position,
1034
+ .candidate-row__stage {
1035
+ display: grid;
1036
+ gap: 4px;
1037
+ }
1038
+
1039
+ .candidate-row__contact span {
1040
+ overflow: hidden;
1041
+ text-overflow: ellipsis;
1042
+ white-space: nowrap;
1043
+ }
1044
+
1045
+ .candidate-row__stage {
1046
+ justify-items: start;
1047
+ }
1048
+
1049
+ .calendar-grid {
1050
+ display: grid;
1051
+ grid-template-columns: repeat(7, minmax(0, 1fr));
1052
+ gap: 10px;
1053
+ padding: 14px;
1054
+ border-radius: 28px;
1055
+ border: 1px solid var(--border);
1056
+ background: var(--panel-soft, var(--panel));
1057
+ box-shadow: var(--shadow-soft);
1058
+ }
1059
+
1060
+ .calendar-grid__weekday {
1061
+ padding: 10px 8px;
1062
+ text-align: center;
1063
+ color: var(--text);
1064
+ font-size: 0.76rem;
1065
+ font-weight: 700;
1066
+ text-transform: uppercase;
1067
+ letter-spacing: 0.08em;
1068
+ }
1069
+
1070
+ .calendar-grid__day {
1071
+ min-height: 88px;
1072
+ padding: 12px;
1073
+ border-radius: 20px;
1074
+ border: 1px solid var(--border);
1075
+ background: var(--panel);
1076
+ color: var(--text-strong);
1077
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
1078
+ display: flex;
1079
+ align-items: flex-start;
1080
+ justify-content: flex-start;
1081
+ }
1082
+
1083
+ .calendar-grid__day.is-muted {
1084
+ color: rgba(148, 163, 184, 0.72);
1085
+ background: var(--panel-muted);
1086
+ }
1087
+
1088
+ .calendar-grid__day.is-today {
1089
+ border-color: rgba(31, 138, 112, 0.42);
1090
+ box-shadow: 0 0 0 4px rgba(31, 138, 112, 0.1);
1091
+ }
1092
+
1093
+ .settings-panel__card {
1094
+ display: flex;
1095
+ align-items: center;
1096
+ justify-content: space-between;
1097
+ gap: 20px;
1098
+ padding: 22px;
1099
+ border-radius: 28px;
1100
+ border: 1px solid var(--border);
1101
+ background: linear-gradient(135deg, rgba(31, 138, 112, 0.1), rgba(41, 86, 246, 0.08));
1102
+ box-shadow: var(--shadow-soft);
1103
+ }
1104
+
1105
+ .settings-panel__copy {
1106
+ max-width: 640px;
1107
+ display: grid;
1108
+ gap: 8px;
1109
+ }
1110
+
1111
+ .settings-panel__label {
1112
+ display: inline-flex;
1113
+ align-items: center;
1114
+ width: fit-content;
1115
+ border-radius: 999px;
1116
+ padding: 6px 10px;
1117
+ background: var(--panel);
1118
+ color: var(--accent-strong);
1119
+ font-size: 0.76rem;
1120
+ font-weight: 700;
1121
+ letter-spacing: 0.04em;
1122
+ text-transform: uppercase;
1123
+ }
1124
+
1125
+ .theme-switch {
1126
+ min-width: 220px;
1127
+ border: 0;
1128
+ border-radius: 24px;
1129
+ padding: 16px;
1130
+ background: var(--panel);
1131
+ box-shadow: var(--shadow-soft);
1132
+ display: grid;
1133
+ gap: 14px;
1134
+ justify-items: center;
1135
+ }
1136
+
1137
+ .theme-switch__track {
1138
+ width: 100%;
1139
+ height: 56px;
1140
+ padding: 6px;
1141
+ border-radius: 999px;
1142
+ background: linear-gradient(135deg, rgba(31, 138, 112, 0.2), rgba(41, 86, 246, 0.18));
1143
+ position: relative;
1144
+ }
1145
+
1146
+ .theme-switch__thumb {
1147
+ position: absolute;
1148
+ top: 6px;
1149
+ left: 6px;
1150
+ width: 44px;
1151
+ height: 44px;
1152
+ border-radius: 50%;
1153
+ background: var(--panel);
1154
+ box-shadow: 0 10px 20px rgba(15, 23, 42, 0.16);
1155
+ transition: transform 0.2s ease;
1156
+ }
1157
+
1158
+ .theme-switch.is-dark .theme-switch__thumb {
1159
+ transform: translateX(100%);
1160
+ }
1161
+
1162
+ .theme-switch__labels {
1163
+ width: 100%;
1164
+ display: flex;
1165
+ justify-content: space-between;
1166
+ color: var(--text-strong);
1167
+ font-weight: 700;
1168
+ }
1169
+
1170
+ .theme-switch__labels span:last-child {
1171
+ color: var(--text);
1172
+ }
1173
+
1174
+ html[data-theme='dark'] .calendar-grid__day.is-muted {
1175
+ color: rgba(148, 163, 184, 0.58);
1176
+ }
1177
+
1178
+ @media (max-width: 1180px) {
1179
+ .directory__filters {
1180
+ grid-template-columns: minmax(220px, 1fr) repeat(2, minmax(150px, 1fr));
1181
+ }
1182
+
1183
+ .directory__list-head,
1184
+ .candidate-row {
1185
+ grid-template-columns: minmax(200px, 1.3fr) minmax(200px, 1fr) minmax(160px, 1fr) minmax(110px, 0.5fr);
1186
+ }
1187
+ }
1188
+
1189
+ @media (max-width: 940px) {
1190
+ .sidebar__nav-item::after {
1191
+ display: none;
1192
+ }
1193
+
1194
+ .directory__header,
1195
+ .calendar-view__header,
1196
+ .settings-panel__header,
1197
+ .settings-panel__card {
1198
+ flex-direction: column;
1199
+ }
1200
+
1201
+ .directory__filters,
1202
+ .directory__list-head,
1203
+ .candidate-row {
1204
+ grid-template-columns: 1fr;
1205
+ }
1206
+
1207
+ .directory__list-head {
1208
+ display: none;
1209
+ }
1210
+
1211
+ .calendar-grid {
1212
+ gap: 8px;
1213
+ }
1214
+
1215
+ .calendar-grid__day {
1216
+ min-height: 72px;
1217
+ }
1218
+ }
1219
+
1220
+ @media (max-width: 640px) {
1221
+ .settings-panel__card {
1222
+ padding: 18px;
1223
+ }
1224
+
1225
+ .theme-switch {
1226
+ width: 100%;
1227
+ min-width: 0;
1228
+ }
1229
+ }