hr_lite 0.21.2 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -0
  3. data/README.md +54 -2
  4. data/app/assets/javascripts/hr_lite/confirm.js +18 -3
  5. data/app/assets/javascripts/hr_lite/geo_punch.js +1 -1
  6. data/app/assets/javascripts/hr_lite/nav.js +17 -0
  7. data/app/assets/javascripts/hr_lite/select_filter.js +46 -0
  8. data/app/assets/stylesheets/hr_lite/hr_lite.css +223 -83
  9. data/app/controllers/hr_lite/admin/assets_controller.rb +1 -1
  10. data/app/controllers/hr_lite/admin/attendances_controller.rb +31 -0
  11. data/app/controllers/hr_lite/approvals_controller.rb +1 -12
  12. data/app/controllers/hr_lite/attendance_controller.rb +7 -0
  13. data/app/controllers/hr_lite/holidays_controller.rb +1 -0
  14. data/app/controllers/hr_lite/home_controller.rb +40 -3
  15. data/app/controllers/hr_lite/hr_requests_controller.rb +1 -1
  16. data/app/controllers/hr_lite/leave_requests_controller.rb +13 -2
  17. data/app/controllers/hr_lite/regularization_requests_controller.rb +10 -2
  18. data/app/controllers/hr_lite/tax_declarations_controller.rb +4 -0
  19. data/app/helpers/hr_lite/application_helper.rb +43 -30
  20. data/app/jobs/hr_lite/attendance_close_job.rb +6 -1
  21. data/app/models/hr_lite/approval.rb +5 -0
  22. data/app/models/hr_lite/attendance_record.rb +26 -0
  23. data/app/models/hr_lite/employee_profile.rb +1 -0
  24. data/app/models/hr_lite/hr_request.rb +2 -1
  25. data/app/models/hr_lite/office_location.rb +12 -0
  26. data/app/models/hr_lite/regularization_request.rb +10 -4
  27. data/app/services/hr_lite/attendance_puncher.rb +8 -3
  28. data/app/views/hr_lite/admin/assets/index.html.erb +3 -2
  29. data/app/views/hr_lite/admin/assets/new.html.erb +6 -2
  30. data/app/views/hr_lite/admin/attendances/index.html.erb +37 -6
  31. data/app/views/hr_lite/admin/attendances/show.html.erb +11 -7
  32. data/app/views/hr_lite/admin/attendances/week.html.erb +42 -0
  33. data/app/views/hr_lite/admin/checklists/index.html.erb +1 -1
  34. data/app/views/hr_lite/admin/comp_off_requests/index.html.erb +2 -2
  35. data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +2 -2
  36. data/app/views/hr_lite/admin/documents/index.html.erb +1 -1
  37. data/app/views/hr_lite/admin/employees/_form.html.erb +15 -7
  38. data/app/views/hr_lite/admin/employees/show.html.erb +5 -5
  39. data/app/views/hr_lite/admin/expenses/index.html.erb +2 -2
  40. data/app/views/hr_lite/admin/leave_balances/index.html.erb +3 -3
  41. data/app/views/hr_lite/admin/leave_requests/index.html.erb +2 -2
  42. data/app/views/hr_lite/admin/leave_requests/show.html.erb +4 -6
  43. data/app/views/hr_lite/admin/loans/new.html.erb +2 -1
  44. data/app/views/hr_lite/admin/overview/index.html.erb +3 -3
  45. data/app/views/hr_lite/admin/regularization_requests/index.html.erb +1 -1
  46. data/app/views/hr_lite/admin/shared/_approvals_tabs.html.erb +6 -5
  47. data/app/views/hr_lite/appraisals/show.html.erb +3 -3
  48. data/app/views/hr_lite/approvals/index.html.erb +1 -1
  49. data/app/views/hr_lite/attendance/_month_grid.html.erb +21 -26
  50. data/app/views/hr_lite/attendance/_punch_card.html.erb +29 -7
  51. data/app/views/hr_lite/attendance/show.html.erb +36 -7
  52. data/app/views/hr_lite/benefits/index.html.erb +1 -1
  53. data/app/views/hr_lite/calendar/show.html.erb +29 -22
  54. data/app/views/hr_lite/comp_off_requests/index.html.erb +3 -3
  55. data/app/views/hr_lite/comp_off_requests/new.html.erb +3 -2
  56. data/app/views/hr_lite/documents/index.html.erb +32 -30
  57. data/app/views/hr_lite/employee_profiles/show.html.erb +2 -2
  58. data/app/views/hr_lite/expenses/index.html.erb +17 -35
  59. data/app/views/hr_lite/expenses/new.html.erb +11 -6
  60. data/app/views/hr_lite/holidays/_next_holiday.html.erb +11 -8
  61. data/app/views/hr_lite/holidays/index.html.erb +6 -6
  62. data/app/views/hr_lite/home/index.html.erb +29 -19
  63. data/app/views/hr_lite/hr_requests/index.html.erb +7 -24
  64. data/app/views/hr_lite/hr_requests/new.html.erb +7 -5
  65. data/app/views/hr_lite/hr_requests/show.html.erb +4 -0
  66. data/app/views/hr_lite/kudos/index.html.erb +4 -3
  67. data/app/views/hr_lite/leave_balances/index.html.erb +14 -5
  68. data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +5 -3
  69. data/app/views/hr_lite/leave_requests/_out_list.html.erb +16 -0
  70. data/app/views/hr_lite/leave_requests/index.html.erb +8 -21
  71. data/app/views/hr_lite/leave_requests/new.html.erb +22 -9
  72. data/app/views/hr_lite/leave_requests/show.html.erb +3 -5
  73. data/app/views/hr_lite/loans/index.html.erb +18 -20
  74. data/app/views/hr_lite/policies/index.html.erb +1 -1
  75. data/app/views/hr_lite/regularization_requests/index.html.erb +2 -2
  76. data/app/views/hr_lite/regularization_requests/new.html.erb +11 -8
  77. data/app/views/hr_lite/resignations/show.html.erb +1 -1
  78. data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +9 -3
  79. data/app/views/hr_lite/salary_slips/index.html.erb +48 -44
  80. data/app/views/hr_lite/shared/_bell.html.erb +1 -1
  81. data/app/views/hr_lite/shared/_form_errors.html.erb +1 -1
  82. data/app/views/hr_lite/tax_declarations/show.html.erb +24 -19
  83. data/app/views/hr_lite/team/show.html.erb +2 -2
  84. data/app/views/layouts/hr_lite/application.html.erb +24 -8
  85. data/config/routes.rb +3 -1
  86. data/lib/hr_lite/configuration.rb +16 -2
  87. data/lib/hr_lite/notifications.rb +1 -0
  88. data/lib/hr_lite/version.rb +1 -1
  89. metadata +4 -1
@@ -3,24 +3,68 @@
3
3
  overriding the variables below (see README), never by editing this file. */
4
4
 
5
5
  :root {
6
+ /* Type. rem so OS text scaling works; nothing renders below 12px. */
6
7
  --hrl-font: -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
8
+ --hrl-font-head: var(--hrl-font);
9
+ --hrl-t-micro: .75rem;
10
+ --hrl-t-small: .875rem;
11
+ --hrl-t-body: 1rem;
12
+ --hrl-t-lead: 1.125rem;
13
+ --hrl-t-title: 1.25rem;
14
+ --hrl-t-display: 1.5rem;
15
+ --hrl-lh-tight: 1.25;
16
+ --hrl-lh-body: 1.5;
17
+ /* Colour roles. */
7
18
  --hrl-accent: #2563eb;
8
19
  --hrl-accent-ink: #ffffff;
9
20
  --hrl-ink: #111827;
10
- --hrl-muted: #6b7280;
21
+ --hrl-ink-2: #374151;
22
+ --hrl-muted: #4b5563;
11
23
  --hrl-bg: #f4f5f7;
12
24
  --hrl-card: #ffffff;
13
25
  --hrl-line: #e5e7eb;
26
+ --hrl-field: #6b7280;
14
27
  --hrl-ok: #047857;
15
28
  --hrl-ok-bg: #d1fae5;
16
29
  --hrl-warn: #b45309;
17
30
  --hrl-warn-bg: #fef3c7;
18
31
  --hrl-bad: #b91c1c;
19
32
  --hrl-bad-bg: #fee2e2;
20
- --hrl-info-bg: #dbeafe;
21
- --hrl-radius: 14px;
22
- --hrl-radius-sm: 9px;
33
+ --hrl-info-bg: #eff6ff;
34
+ --hrl-scrim: rgba(15, 23, 42, .32);
35
+ /* Spacing. */
36
+ --hrl-s-1: 4px;
37
+ --hrl-s-2: 8px;
38
+ --hrl-s-3: 12px;
39
+ --hrl-s-4: 16px;
40
+ --hrl-s-5: 20px;
41
+ --hrl-s-6: 24px;
42
+ --hrl-s-8: 32px;
43
+ --hrl-s-10: 40px;
44
+ --hrl-s-12: 48px;
45
+ --hrl-gutter: .85rem;
46
+ --hrl-target: 44px;
47
+ --hrl-gap-target: 8px;
48
+ /* Radii. */
49
+ --hrl-radius-sm: 12px;
50
+ --hrl-radius: 16px;
51
+ --hrl-radius-lg: 20px;
52
+ --hrl-radius-pill: 9999px;
53
+ /* Elevation. */
23
54
  --hrl-shadow: 0 1px 3px rgba(17, 24, 39, .07);
55
+ --hrl-shadow-md: 0 6px 18px rgba(15, 23, 42, .08);
56
+ --hrl-shadow-lg: 0 18px 40px rgba(15, 23, 42, .14);
57
+ --hrl-shadow-sheet: 0 -6px 18px rgba(15, 23, 42, .08);
58
+ /* Focus, motion, layering. */
59
+ --hrl-focus: var(--hrl-accent);
60
+ --hrl-focus-w: 2px;
61
+ --hrl-focus-offset: 2px;
62
+ --hrl-ease: cubic-bezier(.2, .6, .2, 1);
63
+ --hrl-dur-fast: 120ms;
64
+ --hrl-dur: 220ms;
65
+ --hrl-z-sticky: 20;
66
+ --hrl-z-menu: 30;
67
+ --hrl-z-sheet: 40;
24
68
  }
25
69
 
26
70
  * { box-sizing: border-box; }
@@ -29,6 +73,7 @@
29
73
  font-family: var(--hrl-font);
30
74
  color: var(--hrl-ink);
31
75
  background: var(--hrl-bg);
76
+ line-height: var(--hrl-lh-body);
32
77
  -webkit-font-smoothing: antialiased;
33
78
  overflow-x: hidden;
34
79
  }
@@ -36,6 +81,24 @@
36
81
  (.hrl-btn--primary, .hrl-nav__link, ...) can set its own link colour —
37
82
  a plain `.hrl-body a` would out-rank them and paint accent-on-accent. */
38
83
  :where(.hrl-body) a { color: var(--hrl-accent); text-decoration: none; }
84
+ /* Keyboard focus: one ring everywhere (the second selector is a date input's picker button, focused inside its shadow);
85
+ scroll padding keeps it clear of the sticky app bar and fixed tab bar. */
86
+ :where(.hrl-body) :focus-visible,
87
+ :where(.hrl-body) input:focus-within:not(:focus) { outline: var(--hrl-focus-w) solid var(--hrl-focus); outline-offset: var(--hrl-focus-offset); }
88
+ html:has(.hrl-body) { scroll-padding-top: 4rem; scroll-padding-bottom: calc(4.4rem + env(safe-area-inset-bottom)); }
89
+ .hrl-sr-only, .hrl-skip:not(:focus) {
90
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
91
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
92
+ }
93
+ .hrl-skip:focus {
94
+ position: fixed; top: var(--hrl-s-2); left: var(--hrl-s-2); z-index: var(--hrl-z-sheet);
95
+ display: inline-flex; align-items: center; min-height: var(--hrl-target); padding: 0 var(--hrl-s-4);
96
+ background: var(--hrl-card); color: var(--hrl-accent); font-weight: 600;
97
+ border-radius: var(--hrl-radius-sm); box-shadow: var(--hrl-shadow-md);
98
+ }
99
+ @media (prefers-reduced-motion: reduce) {
100
+ .hrl-body *, .hrl-body *::before, .hrl-body *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
101
+ }
39
102
  .hrl-shell { display: flex; min-height: 100vh; }
40
103
  .hrl-content { flex: 1; min-width: 0; }
41
104
  .hrl-main {
@@ -49,8 +112,8 @@
49
112
  /* Left rail (Keka-style) — hidden on phones, fixed column on >=768px */
50
113
  .hrl-side { display: none; }
51
114
  .hrl-nav__link {
52
- color: var(--hrl-muted); font-size: .88rem; padding: .5rem .6rem;
53
- border-radius: var(--hrl-radius-sm); min-height: 44px;
115
+ color: var(--hrl-muted); font-size: var(--hrl-t-small); padding: .5rem .6rem;
116
+ border-radius: var(--hrl-radius-sm); min-height: var(--hrl-target);
54
117
  display: inline-flex; align-items: center;
55
118
  }
56
119
  .hrl-nav__link--active { color: var(--hrl-accent); background: var(--hrl-info-bg); font-weight: 600; }
@@ -59,50 +122,59 @@
59
122
  .hrl-appbar--mobile {
60
123
  background: var(--hrl-card);
61
124
  border-bottom: 1px solid var(--hrl-line);
62
- padding: calc(.6rem + env(safe-area-inset-top)) .85rem .6rem;
63
- position: sticky; top: 0; z-index: 20;
125
+ padding: calc(var(--hrl-s-2) + env(safe-area-inset-top)) var(--hrl-gutter) var(--hrl-s-2);
126
+ position: sticky; top: 0; z-index: var(--hrl-z-sticky);
64
127
  display: flex; align-items: center; gap: .75rem;
65
128
  }
66
- .hrl-appbar__brand { font-weight: 700; font-size: 1.02rem; color: var(--hrl-ink); display: inline-flex; align-items: center; }
67
- .hrl-appbar__logo { height: 28px; width: auto; display: block; }
68
- .hrl-appbar__user { margin-left: auto; color: var(--hrl-muted); font-size: .82rem; white-space: nowrap; }
129
+ .hrl-appbar__brand { font-weight: 700; font-size: var(--hrl-t-body); color: var(--hrl-ink); line-height: var(--hrl-target); flex: 0 1 auto; min-width: var(--hrl-target); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
130
+ .hrl-appbar__logo { height: 28px; width: auto; vertical-align: middle; }
131
+ .hrl-appbar__user { margin-left: auto; color: var(--hrl-muted); font-size: var(--hrl-t-small); line-height: var(--hrl-target); white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
69
132
 
70
133
  /* Bottom tab bar (phone) */
71
134
  .hrl-tabbar {
72
- position: fixed; bottom: 0; left: 0; right: 0; z-index: 20;
73
- display: flex; justify-content: space-around;
135
+ position: fixed; bottom: 0; left: 0; right: 0; z-index: var(--hrl-z-sticky);
136
+ display: flex; justify-content: space-around; gap: var(--hrl-gap-target);
74
137
  background: var(--hrl-card); border-top: 1px solid var(--hrl-line);
75
138
  padding: .25rem .25rem calc(.25rem + env(safe-area-inset-bottom));
76
139
  }
77
- .hrl-tabbar .hrl-nav__link { flex: 1; justify-content: center; font-size: .78rem; }
140
+ .hrl-tabbar .hrl-nav__link { flex: 1 1 auto; min-width: var(--hrl-target); padding-inline: var(--hrl-s-1); justify-content: center; font-size: var(--hrl-t-micro); font-weight: 600; }
78
141
  /* "More" sheet — everything five tabs cannot hold. Pure <details>: the
79
142
  engine ships no Turbo or Stimulus and this must not need any. */
80
- .hrl-tabbar__more { flex: 1; display: flex; }
143
+ .hrl-tabbar__more { flex: 1 1 auto; display: flex; }
81
144
  .hrl-tabbar__more > summary {
82
145
  flex: 1; justify-content: center; list-style: none; cursor: pointer;
83
146
  }
84
147
  .hrl-tabbar__more > summary::-webkit-details-marker { display: none; }
85
- .hrl-tabbar__more[open] > summary { color: var(--hrl-accent); font-weight: 600; }
148
+ .hrl-tabbar__more[open] > summary,
149
+ .hrl-tabbar__more > summary.hrl-nav__link--active { color: var(--hrl-accent); font-weight: 600; }
86
150
  .hrl-sheet {
87
151
  position: fixed; left: 0; right: 0;
88
152
  bottom: calc(3.3rem + env(safe-area-inset-bottom));
89
- max-height: 60vh; overflow-y: auto;
90
- display: flex; flex-direction: column; gap: 2px;
153
+ max-height: calc(100vh - 8rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)); overflow-y: auto;
154
+ display: grid; grid-template-columns: 1fr 1fr; gap: 2px var(--hrl-gap-target);
91
155
  background: var(--hrl-card); border-top: 1px solid var(--hrl-line);
156
+ border-radius: var(--hrl-radius-lg) var(--hrl-radius-lg) 0 0;
92
157
  padding: .6rem .85rem;
93
- box-shadow: 0 -6px 18px rgb(0 0 0 / 8%);
158
+ box-shadow: var(--hrl-shadow-sheet);
94
159
  }
95
160
  /* Left-aligned like the desktop rail — the tab-bar rule centres its own
96
161
  links and would otherwise apply here too. */
97
162
  .hrl-sheet .hrl-nav__link {
98
163
  display: flex; justify-content: flex-start;
99
- min-height: 44px; font-size: .92rem; padding: .5rem .6rem;
164
+ min-height: var(--hrl-target); font-size: var(--hrl-t-body); padding: .5rem .6rem;
100
165
  }
101
166
  .hrl-sheet .hrl-side__group {
102
- color: var(--hrl-muted); font-size: .72rem; font-weight: 700;
167
+ color: var(--hrl-muted); font-size: var(--hrl-t-micro); font-weight: 700;
103
168
  text-transform: uppercase; letter-spacing: .06em;
104
169
  margin: .7rem 0 .15rem; padding: 0 .6rem;
105
170
  }
171
+ .hrl-sheet .hrl-side__group, .hrl-sheet .hrl-side__back { grid-column: 1 / -1; }
172
+ @media (min-width: 481px) { .hrl-sheet { grid-template-columns: 1fr; } }
173
+ /* Scrim over the page while the sheet is open; tapping it closes the sheet (nav.js). */
174
+ .hrl-tabbar__more[open]::before {
175
+ content: ""; position: fixed; inset: 0 0 calc(3.3rem + env(safe-area-inset-bottom));
176
+ background: var(--hrl-scrim); z-index: -1;
177
+ }
106
178
 
107
179
  @media (min-width: 768px) {
108
180
  .hrl-tabbar { display: none; }
@@ -116,13 +188,13 @@
116
188
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
117
189
  padding: 1rem .75rem;
118
190
  }
119
- .hrl-side__brand { display: block; padding: .25rem .5rem 1rem; }
191
+ .hrl-side__brand { display: flex; align-items: center; min-height: var(--hrl-target); padding: .25rem .5rem 1rem; }
120
192
  .hrl-side__logo { max-height: 40px; max-width: 100%; display: block; }
121
- .hrl-side__brandname { font-weight: 800; font-size: 1.1rem; color: var(--hrl-ink); }
193
+ .hrl-side__brandname { font-family: var(--hrl-font-head); font-weight: 700; font-size: var(--hrl-t-lead); color: var(--hrl-ink); }
122
194
  .hrl-side__nav { display: flex; flex-direction: column; gap: 2px; }
123
- .hrl-side__nav .hrl-nav__link { display: flex; min-height: 38px; padding: .45rem .6rem; }
195
+ .hrl-side__nav .hrl-nav__link { display: flex; padding: .45rem .6rem; }
124
196
  .hrl-side__group {
125
- margin: .9rem .5rem .25rem; font-size: .68rem; font-weight: 700;
197
+ margin: .9rem .5rem .25rem; font-size: var(--hrl-t-micro); font-weight: 700;
126
198
  letter-spacing: .08em; text-transform: uppercase; color: var(--hrl-muted);
127
199
  }
128
200
  .hrl-side__group:first-child { margin-top: 0; }
@@ -130,20 +202,27 @@
130
202
  margin-top: auto; padding: .75rem .5rem 0; border-top: 1px solid var(--hrl-line);
131
203
  display: flex; flex-direction: column; gap: .35rem;
132
204
  }
133
- .hrl-side__user { font-size: .82rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
134
- .hrl-side__back { font-size: .8rem; }
205
+ .hrl-side__user { font-size: var(--hrl-t-small); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
206
+ .hrl-side__back { display: flex; align-items: center; min-height: var(--hrl-target); font-size: var(--hrl-t-small); }
135
207
  }
136
208
 
137
209
  /* Flash */
138
- .hrl-flash { padding: .7rem .9rem; border-radius: var(--hrl-radius-sm); margin-bottom: .85rem; font-size: .9rem; }
210
+ .hrl-flash { padding: .7rem .9rem; border-radius: var(--hrl-radius-sm); margin-bottom: .85rem; font-size: var(--hrl-t-body); overflow-wrap: anywhere; }
139
211
  .hrl-flash--ok { background: var(--hrl-ok-bg); color: var(--hrl-ok); }
140
212
  .hrl-flash--warn { background: var(--hrl-warn-bg); color: var(--hrl-warn); }
141
213
 
142
214
  /* Page scaffold */
143
215
  .hrl-page__head { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin: .25rem 0 .9rem; }
144
- .hrl-page__title { font-size: 1.25rem; font-weight: 700; margin: 0; }
145
- .hrl-page__sub { color: var(--hrl-muted); font-size: .86rem; margin: .15rem 0 0; width: 100%; }
146
- .hrl-page__actions { margin-left: auto; display: flex; flex-wrap: wrap; gap: .5rem; }
216
+ .hrl-page__title { font-family: var(--hrl-font-head); font-size: var(--hrl-t-title); font-weight: 700; line-height: var(--hrl-lh-tight); margin: 0; overflow-wrap: anywhere; }
217
+ @media (min-width: 768px) { .hrl-page__title { font-size: var(--hrl-t-display); } }
218
+ .hrl-page__sub { color: var(--hrl-muted); font-size: var(--hrl-t-small); margin: .15rem 0 0; width: 100%; }
219
+ .hrl-page__actions { width: 100%; display: flex; flex-wrap: wrap; gap: .5rem; }
220
+ .hrl-page__actions .hrl-btn { flex: 1 1 auto; }
221
+ .hrl-page__actions .hrl-btn[aria-label] { flex: 0 0 auto; } /* icon-only pager arrows keep their width */
222
+ @media (min-width: 481px) {
223
+ .hrl-page__actions { width: auto; margin-left: auto; }
224
+ .hrl-page__actions .hrl-btn { flex: 0 1 auto; }
225
+ }
147
226
 
148
227
  /* Cards */
149
228
  .hrl-card {
@@ -151,123 +230,163 @@
151
230
  border-radius: var(--hrl-radius); box-shadow: var(--hrl-shadow);
152
231
  padding: .95rem; margin-bottom: .85rem;
153
232
  }
154
- .hrl-card__title { font-weight: 650; font-size: .95rem; margin: 0 0 .6rem; }
233
+ .hrl-card__title { font-family: var(--hrl-font-head); font-weight: 700; font-size: var(--hrl-t-lead); line-height: var(--hrl-lh-tight); margin: 0 0 .6rem; }
155
234
  .hrl-card--empty { text-align: center; color: var(--hrl-muted); padding: 1.8rem 1rem; }
156
- .hrl-grid { display: grid; gap: .85rem; grid-template-columns: 1fr; }
157
- @media (min-width: 700px) { .hrl-grid--2 { grid-template-columns: 1fr 1fr; } }
235
+ .hrl-card--warn { background: var(--hrl-warn-bg); border-color: transparent; }
236
+ .hrl-card > p:first-child { margin-top: 0; }
237
+ .hrl-card__link, .hrl-backlink { display: inline-flex; gap: var(--hrl-s-1); align-items: center; min-height: var(--hrl-target); min-width: var(--hrl-target); font-size: var(--hrl-t-small); font-weight: 600; }
238
+ .hrl-listrow { display: grid; grid-template-columns: 1fr auto; align-items: start; gap: var(--hrl-s-1) var(--hrl-s-3); padding: var(--hrl-s-3) 0; border-bottom: 1px solid var(--hrl-line); min-height: var(--hrl-target); color: var(--hrl-ink); overflow-wrap: anywhere; }
239
+ .hrl-listrow:last-child { border-bottom: 0; }
240
+ .hrl-listrow > :first-child { display: inline-flex; align-items: center; gap: var(--hrl-s-1); line-height: var(--hrl-lh-tight); }
241
+ /* The punch status stays in the accessibility tree (no `hidden`) so its first message is announced; empty, it takes no space. */
242
+ [data-hrl-geo-status]:empty { margin: 0; }
243
+ .hrl-grid { display: grid; gap: .85rem; grid-template-columns: 1fr; margin-bottom: .85rem; }
244
+ .hrl-grid > * { min-width: 0; }
245
+ .hrl-grid > .hrl-card { margin-bottom: 0; }
246
+ @media (min-width: 1024px) { .hrl-grid--2 { grid-template-columns: 1fr 1fr; } }
158
247
 
159
248
  /* KPI tiles */
160
- .hrl-kpis { display: grid; grid-template-columns: repeat(2, 1fr); gap: .6rem; margin-bottom: .85rem; }
161
- @media (min-width: 700px) { .hrl-kpis { grid-template-columns: repeat(4, 1fr); } }
249
+ .hrl-kpis { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; margin-bottom: .85rem; }
250
+ @media (min-width: 1100px) { .hrl-kpis { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
162
251
  .hrl-kpi { background: var(--hrl-card); border: 1px solid var(--hrl-line); border-radius: var(--hrl-radius); padding: .7rem .8rem; }
163
- .hrl-kpi__value { font-size: 1.35rem; font-weight: 700; }
164
- .hrl-kpi__label { color: var(--hrl-muted); font-size: .78rem; }
252
+ .hrl-kpi__value { font-size: var(--hrl-t-display); font-weight: 700; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
253
+ .hrl-kpi__label { color: var(--hrl-muted); font-size: var(--hrl-t-small); }
254
+ /* Rupee tiles: a lakh amount fits a half-width 320px tile without splitting its digits. */
255
+ .hrl-kpis--money .hrl-kpi__value { font-size: var(--hrl-t-lead); }
256
+ @media (min-width: 481px) { .hrl-kpis--money .hrl-kpi__value { font-size: var(--hrl-t-display); } }
165
257
 
166
258
  /* Buttons */
167
259
  .hrl-btn {
168
260
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
169
- min-height: 44px; padding: .55rem 1rem; border-radius: var(--hrl-radius-sm);
261
+ min-height: var(--hrl-target); padding: .55rem 1rem; border-radius: var(--hrl-radius-sm);
170
262
  border: 1px solid var(--hrl-line); background: var(--hrl-card); color: var(--hrl-ink);
171
- font-size: .9rem; font-weight: 600; cursor: pointer; text-decoration: none;
263
+ font-size: var(--hrl-t-body); font-weight: 600; cursor: pointer; text-decoration: none;
264
+ max-width: 100%; overflow-wrap: anywhere; text-align: center;
172
265
  }
173
266
  .hrl-btn--primary { background: var(--hrl-accent); border-color: var(--hrl-accent); color: var(--hrl-accent-ink); }
174
267
  .hrl-btn--danger { color: var(--hrl-bad); border-color: var(--hrl-bad-bg); }
175
268
  .hrl-btn--block { width: 100%; }
176
- .hrl-btn--big { min-height: 58px; font-size: 1.05rem; }
269
+ .hrl-btn--big { min-height: 58px; font-size: var(--hrl-t-lead); }
177
270
  .hrl-btn[disabled] { opacity: .55; cursor: default; }
271
+ summary.hrl-btn { list-style: none; }
272
+ summary.hrl-btn::-webkit-details-marker { display: none; } /* Safari before 18.4 draws it whatever the display */
178
273
 
179
274
  /* Badges & chips */
180
275
  .hrl-badge {
181
- display: inline-flex; align-items: center; gap: .3rem;
182
- font-size: .74rem; font-weight: 650; padding: .18rem .55rem;
183
- border-radius: 999px; background: var(--hrl-info-bg); color: var(--hrl-accent);
184
- white-space: nowrap;
276
+ display: inline-block;
277
+ font-size: var(--hrl-t-micro); font-weight: 600; line-height: var(--hrl-lh-tight); padding: .18rem .55rem;
278
+ border-radius: var(--hrl-radius-pill); background: var(--hrl-info-bg); color: var(--hrl-accent);
279
+ white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
185
280
  }
186
281
  .hrl-badge--ok { background: var(--hrl-ok-bg); color: var(--hrl-ok); }
187
282
  .hrl-badge--warn { background: var(--hrl-warn-bg); color: var(--hrl-warn); }
188
283
  .hrl-badge--bad { background: var(--hrl-bad-bg); color: var(--hrl-bad); }
189
- .hrl-badge--muted { background: var(--hrl-bg); color: var(--hrl-muted); }
190
- .hrl-mention { color: var(--hrl-accent); font-weight: 650; }
284
+ .hrl-badge--muted { background: var(--hrl-bg); color: var(--hrl-ink-2); }
285
+ /* Leave-type badge: ink text on grey; the HR-chosen colour (inline --hrl-type) is only the dot. */
286
+ .hrl-badge--type { background: var(--hrl-bg); color: var(--hrl-ink); }
287
+ .hrl-badge--type::before { content: ""; display: inline-block; width: 8px; height: 8px; margin-right: .3rem; border-radius: var(--hrl-radius-pill); background: var(--hrl-type); }
288
+ .hrl-mention { color: var(--hrl-accent); font-weight: 700; }
191
289
 
192
290
  /* Tables (stack to label/value rows on phones) */
193
291
  .hrl-table-wrap { overflow-x: auto; }
194
- .hrl-table { width: 100%; border-collapse: collapse; font-size: .88rem; background: var(--hrl-card); border-radius: var(--hrl-radius); }
292
+ .hrl-table { width: 100%; border-collapse: collapse; font-size: var(--hrl-t-small); background: var(--hrl-card); border-radius: var(--hrl-radius); }
195
293
  .hrl-table th, .hrl-table td { text-align: left; padding: .6rem .7rem; border-bottom: 1px solid var(--hrl-line); vertical-align: top; }
196
- .hrl-table th { color: var(--hrl-muted); font-size: .76rem; text-transform: uppercase; letter-spacing: .03em; }
294
+ .hrl-table th { color: var(--hrl-muted); font-size: var(--hrl-t-micro); text-transform: uppercase; letter-spacing: .03em; }
197
295
  .hrl-table td.hrl-num, .hrl-table th.hrl-num { text-align: right; font-variant-numeric: tabular-nums; }
198
296
  .hrl-table--tight th, .hrl-table--tight td { padding: .5rem .35rem; }
199
- @media (max-width: 640px) {
297
+ .hrl-table td[data-label=""] > :is(a, button) + :is(a, button, form) { margin-left: var(--hrl-s-2); }
298
+ .hrl-table td[data-label=""] > :is(a, button) + form { display: inline-block; vertical-align: top; } /* a button_to beside a link, not under it */
299
+ .hrl-table td[data-label=""] > form + form { margin-top: var(--hrl-s-2); }
300
+ .hrl-table tr:has(.hrl-btn) > td, .hrl-table tr:has(.hrl-btn) td > .hrl-badge { vertical-align: middle; } /* text lines up with a 44px button */
301
+ /* Stack on phones and on the narrow content column beside the 768-1023px rail. */
302
+ @media (max-width: 640px), (min-width: 768px) and (max-width: 1023px) {
200
303
  .hrl-table--stack thead { display: none; }
201
304
  .hrl-table--stack tr { display: block; border-bottom: 6px solid var(--hrl-bg); }
202
- .hrl-table--stack td { display: flex; justify-content: space-between; gap: .8rem; border-bottom: 1px dashed var(--hrl-line); }
203
- .hrl-table--stack td::before { content: attr(data-label); color: var(--hrl-muted); font-size: .78rem; }
305
+ .hrl-table--stack td { display: flex; flex-wrap: wrap; justify-content: space-between; gap: .8rem; border-bottom: 1px dashed var(--hrl-line); overflow-wrap: anywhere; }
306
+ .hrl-table--stack td::before { content: attr(data-label); color: var(--hrl-muted); font-size: var(--hrl-t-small); }
204
307
  .hrl-table--stack td.hrl-num { text-align: left; }
205
308
  .hrl-table--stack td.hrl-cell--void { display: none; }
309
+ .hrl-table--stack tr:last-child { border-bottom: 0; }
310
+ .hrl-table--stack td[data-label=""] { justify-content: flex-end; }
311
+ .hrl-table--stack td[data-label=""]::before { display: none; }
312
+ .hrl-table--stack td > .hrl-badge { align-self: center; } /* do not stretch beside a 44px button */
313
+ .hrl-table--stack td[data-label=""] > :is(a, button) + :is(a, button, form) { margin-left: 0; }
314
+ .hrl-table--stack td[data-label=""] > form + form { margin-top: 0; }
206
315
  }
207
316
 
208
317
  /* Forms */
209
318
  .hrl-field { margin-bottom: .8rem; }
210
- .hrl-field label { display: block; font-size: .82rem; font-weight: 600; margin-bottom: .3rem; }
211
- .hrl-field .hrl-hint { color: var(--hrl-muted); font-size: .76rem; margin-top: .25rem; }
319
+ .hrl-field label { display: block; font-size: var(--hrl-t-small); font-weight: 600; margin-bottom: .3rem; }
320
+ .hrl-hint { color: var(--hrl-muted); font-size: var(--hrl-t-small); margin-top: .25rem; }
212
321
  /* Full-width text-ish controls only — checkboxes/radios/hidden must keep
213
322
  their natural size (a bare [type] selector out-ranked .hrl-field--check
214
323
  and blew checkboxes up to 100% width). */
215
324
  .hrl-input,
216
325
  .hrl-field input[type]:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
217
326
  .hrl-field select, .hrl-field textarea {
218
- width: 100%; min-height: 44px; padding: .55rem .7rem; font: inherit;
219
- border: 1px solid var(--hrl-line); border-radius: var(--hrl-radius-sm);
327
+ width: 100%; min-height: var(--hrl-target); padding: .55rem .7rem; font: inherit; font-size: var(--hrl-t-body);
328
+ border: 1px solid var(--hrl-field); border-radius: var(--hrl-radius-sm);
220
329
  background: var(--hrl-card); color: var(--hrl-ink);
221
330
  }
222
331
  .hrl-field textarea { min-height: 90px; resize: vertical; }
223
- .hrl-field--check { display: flex; align-items: center; gap: .55rem; min-height: 44px; }
332
+ .hrl-filter { appearance: none; margin-bottom: var(--hrl-gap-target); } /* select_filter.js search box above its select */
333
+ .hrl-field--check { display: flex; align-items: center; gap: .55rem; min-height: var(--hrl-target); }
224
334
  .hrl-field--check input {
225
335
  width: 1.15rem; height: 1.15rem; min-height: 0; flex: 0 0 auto;
226
336
  accent-color: var(--hrl-accent);
227
337
  }
228
- .hrl-field--check label { margin-bottom: 0; font-weight: 600; }
229
- .hrl-errors { background: var(--hrl-bad-bg); color: var(--hrl-bad); border-radius: var(--hrl-radius-sm); padding: .7rem .9rem; margin-bottom: .85rem; font-size: .86rem; }
338
+ /* The 44px label is the tap target for the small box. */
339
+ .hrl-field--check label { margin-bottom: 0; font-weight: 600; flex: 1; min-height: var(--hrl-target); display: flex; align-items: center; }
340
+ .hrl-field--check label .hrl-hint { display: block; margin-top: 0; font-weight: 400; }
341
+ /* Rails wraps an invalid label/field in this div; contents keeps the stack, the re-pointed token reddens the border. */
342
+ .field_with_errors { display: contents; --hrl-field: var(--hrl-bad); }
343
+ .field_with_errors label { color: var(--hrl-bad); }
344
+ .hrl-errors { background: var(--hrl-bad-bg); color: var(--hrl-bad); border-radius: var(--hrl-radius-sm); padding: .7rem .9rem; margin-bottom: .85rem; font-size: var(--hrl-t-body); }
230
345
  .hrl-errors ul { margin: .3rem 0 0; padding-left: 1.1rem; }
231
346
  .hrl-form-actions { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: 1rem; }
232
347
  .hrl-form-actions .hrl-btn { flex: 1; }
233
348
  @media (min-width: 700px) { .hrl-form-actions .hrl-btn { flex: 0 0 auto; min-width: 9rem; } }
234
349
 
235
350
  /* Radio chip row (kudos badges) */
236
- .hrl-chiprow { display: flex; flex-wrap: wrap; gap: .45rem; }
351
+ .hrl-chiprow { display: flex; flex-wrap: wrap; gap: var(--hrl-gap-target); }
237
352
  .hrl-chiprow label {
238
- border: 1px solid var(--hrl-line); border-radius: 999px; padding: .4rem .8rem;
239
- font-size: .8rem; font-weight: 600; cursor: pointer; min-height: 40px; display: inline-flex; align-items: center;
353
+ border: 1px solid var(--hrl-line); border-radius: var(--hrl-radius-pill); padding: .4rem .8rem;
354
+ font-size: var(--hrl-t-small); font-weight: 600; cursor: pointer; min-height: var(--hrl-target); display: inline-flex; align-items: center;
355
+ min-width: var(--hrl-target); justify-content: center;
240
356
  }
241
357
  .hrl-chiprow input { position: absolute; opacity: 0; pointer-events: none; }
242
358
  .hrl-chiprow input:checked + span, .hrl-chiprow label:has(input:checked) { background: var(--hrl-info-bg); border-color: var(--hrl-accent); color: var(--hrl-accent); }
359
+ /* The radio is invisible, so its focus ring goes on the chip. */
360
+ .hrl-chiprow label:has(input:focus-visible) { outline: var(--hrl-focus-w) solid var(--hrl-focus); outline-offset: var(--hrl-focus-offset); }
243
361
 
244
362
  /* Feed items */
363
+ .hrl-feed { list-style: none; margin: 0; padding: 0; }
245
364
  .hrl-feed__item { display: flex; gap: .7rem; padding: .8rem 0; border-bottom: 1px solid var(--hrl-line); }
246
365
  .hrl-feed__item:last-child { border-bottom: 0; }
247
366
  .hrl-avatar {
248
- flex: 0 0 auto; width: 38px; height: 38px; border-radius: 999px;
367
+ flex: 0 0 auto; width: 38px; height: 38px; border-radius: var(--hrl-radius-pill);
249
368
  background: var(--hrl-info-bg); color: var(--hrl-accent);
250
369
  display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
251
370
  }
252
- .hrl-feed__meta { color: var(--hrl-muted); font-size: .76rem; }
371
+ .hrl-feed__meta { color: var(--hrl-muted); font-size: var(--hrl-t-small); }
253
372
  .hrl-feed__body { min-width: 0; flex: 1; overflow-wrap: anywhere; }
254
373
 
255
374
  /* Mention autocomplete menu */
256
375
  .hrl-mention-wrap { position: relative; }
257
376
  .hrl-mention-menu {
258
- position: absolute; left: 0; right: 0; z-index: 30;
377
+ position: absolute; left: 0; right: 0; z-index: var(--hrl-z-menu);
259
378
  background: var(--hrl-card); border: 1px solid var(--hrl-line);
260
379
  border-radius: var(--hrl-radius-sm); box-shadow: var(--hrl-shadow);
261
380
  max-height: 14rem; overflow-y: auto;
262
381
  }
263
382
  .hrl-mention-menu button {
264
383
  display: block; width: 100%; text-align: left; padding: .6rem .8rem;
265
- background: none; border: 0; font: inherit; cursor: pointer; min-height: 44px;
384
+ background: none; border: 0; font: inherit; cursor: pointer; min-height: var(--hrl-target);
266
385
  }
267
386
  .hrl-mention-menu button:hover, .hrl-mention-menu button.hrl-active { background: var(--hrl-bg); }
268
387
 
269
388
  /* Pagination */
270
- .hrl-pager { display: flex; align-items: center; justify-content: center; gap: .8rem; margin: 1rem 0; font-size: .88rem; }
389
+ .hrl-pager { display: flex; align-items: center; justify-content: center; gap: .8rem; margin: 1rem 0; font-size: var(--hrl-t-small); }
271
390
  .hrl-pager__info { color: var(--hrl-muted); }
272
391
 
273
392
  /* Org chart tree + reporting-line chain */
@@ -281,48 +400,69 @@
281
400
  }
282
401
  .hrl-tree__card--me { border-color: var(--hrl-accent); background: var(--hrl-info-bg); }
283
402
  .hrl-tree__name, .hrl-tree__role { min-width: 0; overflow-wrap: anywhere; }
284
- .hrl-tree__name { font-weight: 650; font-size: .9rem; }
285
- .hrl-tree__role { color: var(--hrl-muted); font-size: .8rem; }
403
+ .hrl-tree__name { font-weight: 700; font-size: var(--hrl-t-small); }
404
+ .hrl-tree__role { color: var(--hrl-muted); font-size: var(--hrl-t-small); }
286
405
  .hrl-chainrow { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
287
406
  .hrl-chainrow__step { display: inline-flex; align-items: center; gap: .4rem; min-width: 0; }
288
407
  .hrl-chainrow .hrl-badge { white-space: normal; overflow-wrap: anywhere; }
289
408
  .hrl-chainrow__arrow { color: var(--hrl-muted); }
290
409
 
291
410
  /* Definition list */
292
- .hrl-deflist { display: grid; grid-template-columns: minmax(7rem, auto) 1fr; gap: .35rem .9rem; font-size: .9rem; }
293
- .hrl-deflist dt { color: var(--hrl-muted); }
411
+ .hrl-deflist { display: grid; grid-template-columns: minmax(7rem, auto) minmax(0, 1fr); gap: .35rem .9rem; font-size: var(--hrl-t-body); }
412
+ .hrl-deflist dt { color: var(--hrl-muted); font-size: var(--hrl-t-small); }
294
413
  .hrl-deflist dd { margin: 0; overflow-wrap: anywhere; }
295
414
 
296
415
  /* Month grid (attendance) */
297
- .hrl-mgrid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; }
298
- .hrl-mgrid__head { text-align: center; color: var(--hrl-muted); font-size: .68rem; padding: .2rem 0; }
416
+ /* minmax(0, …): a bare 1fr never shrinks below its nowrap tags and pushes Sa/Su off a phone */
417
+ /* Phone: the grid alone reaches 4px from the card edge, with 2px gaps, so seven cells are 44px wide at 360px. */
418
+ .hrl-mgrid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px; }
419
+ .hrl-card > .hrl-mgrid { margin-inline: calc(var(--hrl-s-1) - .95rem); }
420
+ @media (min-width: 481px) {
421
+ .hrl-mgrid { gap: 3px; }
422
+ .hrl-card > .hrl-mgrid { margin-inline: 0; }
423
+ }
424
+ .hrl-mgrid__head { text-align: center; color: var(--hrl-muted); font-size: var(--hrl-t-micro); padding: .2rem 0; }
299
425
  .hrl-mgrid__day {
300
- min-height: 44px; border-radius: 7px; background: var(--hrl-card);
301
- border: 1px solid var(--hrl-line); padding: .2rem .3rem; font-size: .72rem;
426
+ min-height: var(--hrl-target); border-radius: var(--hrl-radius-sm); background: var(--hrl-card);
427
+ border: 1px solid var(--hrl-line); padding: .2rem calc(var(--hrl-s-1) / 2); font-size: var(--hrl-t-small);
302
428
  display: flex; flex-direction: column; gap: .1rem; position: relative;
303
429
  }
304
- .hrl-mgrid__day--muted { opacity: .45; }
305
- .hrl-mgrid__num { font-weight: 650; }
306
- .hrl-mgrid__tag { font-size: .6rem; color: var(--hrl-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
430
+ /* 2px side padding on phones leaves "Today" its 32px in a 39px cell at 320px. */
431
+ @media (min-width: 481px) { .hrl-mgrid__day { padding-inline: .2rem; } }
432
+ .hrl-mgrid__day--muted { color: var(--hrl-muted); }
433
+ .hrl-mgrid__num { font-weight: 600; }
434
+ .hrl-mgrid__tag { font-size: var(--hrl-t-micro); color: var(--hrl-ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
435
+ /* The day number's link covers its whole cell and keeps the cell's ink, which holds 4.5:1 on every tint. */
436
+ .hrl-mgrid__day a { color: inherit; }
437
+ .hrl-mgrid__day a::after { content: ""; position: absolute; inset: -1px; border-radius: var(--hrl-radius-sm); }
438
+ /* Legend swatch: a cell in badge form; the state modifiers below tint it. */
439
+ .hrl-mgrid__key { background: var(--hrl-card); border: 1px solid var(--hrl-line); color: var(--hrl-ink-2); }
307
440
  .hrl-mgrid__day--present { background: var(--hrl-ok-bg); border-color: transparent; }
308
441
  .hrl-mgrid__day--half { background: linear-gradient(135deg, var(--hrl-ok-bg) 50%, var(--hrl-card) 50%); }
309
442
  .hrl-mgrid__day--leave { background: var(--hrl-info-bg); border-color: transparent; }
443
+ .hrl-mgrid__day--half-leave { background: linear-gradient(135deg, var(--hrl-info-bg) 50%, var(--hrl-card) 50%); }
310
444
  .hrl-mgrid__day--holiday { background: var(--hrl-warn-bg); border-color: transparent; }
311
445
  .hrl-mgrid__day--weekend { background: var(--hrl-bg); }
312
446
  .hrl-mgrid__day--absent { background: var(--hrl-bad-bg); border-color: transparent; }
313
- .hrl-mgrid__flag { position: absolute; top: 3px; right: 4px; width: 7px; height: 7px; border-radius: 999px; background: var(--hrl-bad); }
447
+ .hrl-mgrid__day--today { box-shadow: inset 0 0 0 2px var(--hrl-accent); }
448
+ .hrl-mgrid__day--pending { border: 1px dashed var(--hrl-ink-2); }
449
+ .hrl-mgrid__flag { position: absolute; top: 3px; right: 4px; width: 7px; height: 7px; border-radius: var(--hrl-radius-pill); background: var(--hrl-bad); }
314
450
  .hrl-legend { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .6rem; }
451
+ .hrl-legend .hrl-mgrid__flag { position: static; display: inline-block; }
452
+ @media (min-width: 768px) { .hrl-mgrid--cal .hrl-mgrid__day { min-height: 88px; } }
315
453
 
316
454
  /* Utility */
317
455
  .hrl-muted { color: var(--hrl-muted); }
318
- .hrl-small { font-size: .8rem; }
456
+ .hrl-small { font-size: var(--hrl-t-small); }
319
457
  .hrl-mt { margin-top: .85rem; }
320
458
  .hrl-right { margin-left: auto; }
321
459
  .hrl-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
460
+ .hrl-row--mb { margin-bottom: var(--hrl-s-3); }
461
+ .hrl-row--tabs > .hrl-btn { padding-inline: var(--hrl-s-2); } /* four approvals tabs fit one row beside the rail at 768px */
322
462
 
323
463
  /* Long-form policy text. Constrained measure — a policy read across the
324
464
  full width of a laptop is a policy nobody finishes. */
325
- .hrl-prose { max-width: 68ch; }
465
+ .hrl-prose { max-width: 68ch; overflow-wrap: anywhere; }
326
466
  .hrl-prose p { margin: 0 0 .8rem; line-height: 1.6; }
327
467
  .hrl-prose p:last-child { margin-bottom: 0; }
328
468
 
@@ -395,5 +535,5 @@
395
535
  }
396
536
 
397
537
  /* Notifications link, shown only when the host configures one. */
398
- .hrl-bell { display: inline-flex; align-items: center; gap: .4rem; min-height: 44px; font-size: .86rem; font-weight: 600; }
399
- .hrl-bell__count { min-width: 1.4rem; padding: 0 .4rem; border-radius: 9999px; background: var(--hrl-accent); color: var(--hrl-accent-ink); font-size: .75rem; text-align: center; }
538
+ .hrl-bell { display: inline-flex; align-items: center; gap: .4rem; min-height: var(--hrl-target); font-size: var(--hrl-t-small); font-weight: 600; }
539
+ .hrl-bell__count { min-width: 1.4rem; padding: 0 .4rem; border-radius: var(--hrl-radius-pill); background: var(--hrl-accent); color: var(--hrl-accent-ink); font-size: var(--hrl-t-micro); text-align: center; }
@@ -11,7 +11,7 @@ module HrLite
11
11
  end
12
12
 
13
13
  def new
14
- @asset = Asset.new
14
+ @asset = Asset.new(category: nil) # no chip preselected over the column default
15
15
  end
16
16
 
17
17
  def create
@@ -11,6 +11,36 @@ module HrLite
11
11
  @employees = @employees.select { |user| visible.include?(user.id) } if visible
12
12
  @records = AttendanceRecord.for_date(@date).where(user_id: @employees.map(&:id)).index_by(&:user_id)
13
13
  @flagged_count = @records.values.count(&:flagged?)
14
+ # "How many people punched where": off-site punches count as one place.
15
+ @offices = OfficeLocation.active.to_a
16
+ @places = %i[check_in check_out].index_with do |side|
17
+ @records.values.select { |r| r.public_send("#{side}_at") }.map do |r|
18
+ OfficeLocation.place(r.public_send("#{side}_lat"), r.public_send("#{side}_lng"), @offices)&.split(" · ")&.first || "No GPS"
19
+ end.tally
20
+ end
21
+ end
22
+
23
+ # Mon–Fri hours per person (Saturday never counts) against
24
+ # config.work_hours[:week], pro rata for holidays and full-day leave.
25
+ # The current week counts only the days already over.
26
+ def week
27
+ @monday = parse_date_param(params[:date]).beginning_of_week(:monday)
28
+ calendar = WorkingCalendar.new(@monday..(@monday + 4))
29
+ days = (@monday..[ @monday + 4, Date.current - 1 ].min).select { |d| calendar.working_day?(d) }
30
+ visible = hr_access.visible_user_ids("attendance.view")
31
+ employees = HrLite.active_employees(on: @monday)
32
+ employees = employees.select { |user| visible.include?(user.id) } if visible
33
+ records = AttendanceRecord.where(user_id: employees.map(&:id), date: days).group_by(&:user_id)
34
+ on_leave = days.index_with { |d| LeaveRequest.active_on(d).where(half_day: false).pluck(:user_id) }
35
+ week_hours = HrLite.config.work_hours&.dig(:week)
36
+
37
+ @rows = employees.map do |user|
38
+ worked = records.fetch(user.id, []).filter_map(&:worked_duration)
39
+ hours = worked.sum / 3600.0
40
+ target = week_hours && week_hours * days.count { |d| on_leave[d].exclude?(user.id) } / 5.0
41
+ { user: user, hours: hours, avg: worked.any? ? hours / worked.size : nil, target: target,
42
+ below: target.present? && target.positive? && hours < target }
43
+ end
14
44
  end
15
45
 
16
46
  # One employee's month + the regularization form for ?date=.
@@ -19,6 +49,7 @@ module HrLite
19
49
  @month = parse_month_param(params[:month])
20
50
  @day_status = DayStatus.new(user: @employee, range: @month.beginning_of_month..@month.end_of_month)
21
51
  @counts = @day_status.counts
52
+ @counts[:absent] -= 1 if @month.all_month.cover?(Date.current) && @day_status.for(Date.current).kind == :absent
22
53
  @edit_date = params[:date].present? ? parse_date_param(params[:date]) : nil
23
54
  @edit_record = @edit_date && AttendanceRecord.find_or_initialize_by(user_id: @employee.id, date: @edit_date)
24
55
  end
@@ -8,7 +8,7 @@ module HrLite
8
8
  # screen and each sees only their own rows.
9
9
  class ApprovalsController < ApplicationController
10
10
  def index
11
- @approvals = paginate(mine.includes(:step, :subject).order(:created_at))
11
+ @approvals = paginate(Approval.pending_for(hr_current_user).includes(:step, :subject).order(:created_at))
12
12
  @delegations = ApprovalDelegation.live_on(Date.current)
13
13
  .where(from_user_id: hr_current_user.id)
14
14
  .includes(:to_user)
@@ -16,16 +16,5 @@ module HrLite
16
16
  .where(to_user_id: hr_current_user.id)
17
17
  .includes(:from_user)
18
18
  end
19
-
20
- private
21
-
22
- # Rows addressed to this person, plus rows belonging to anybody who has
23
- # delegated to them while they are away.
24
- def mine
25
- standing_in_for = ApprovalDelegation.live_on(Date.current)
26
- .where(to_user_id: hr_current_user.id)
27
- .pluck(:from_user_id)
28
- Approval.pending.where(approver_id: [ hr_current_user.id, *standing_in_for ])
29
- end
30
19
  end
31
20
  end
@@ -8,6 +8,13 @@ module HrLite
8
8
  @record = AttendanceRecord.find_by(user_id: hr_current_user.id, date: Date.current)
9
9
  @day_status = DayStatus.new(user: hr_current_user, range: @month.beginning_of_month..@month.end_of_month)
10
10
  @counts = @day_status.counts
11
+ @counts[:absent] -= 1 if @month.all_month.cover?(Date.current) && @day_status.for(Date.current).kind == :absent
12
+ @pending_dates = RegularizationRequest.pending.where(user_id: hr_current_user.id, date: @month.all_month).pluck(:date).to_set
13
+ # ?day= opens the day detail; garbage (parsed as today), other months and the future open nothing.
14
+ day = parse_date_param(params[:day])
15
+ @day = day if params[:day] == day.iso8601 && @month.all_month.cover?(day) && day <= Date.current
16
+ @day_info = @day_status.for(@day) if @day
17
+ @day_ticket = RegularizationRequest.where(user_id: hr_current_user.id, date: @day).recent_first.first if @day
11
18
  end
12
19
 
13
20
  def check_in
@@ -4,6 +4,7 @@ module HrLite
4
4
  year = params[:year].to_i
5
5
  @year = year.between?(2000, 2100) ? year : Date.current.year
6
6
  @holidays = Holiday.where(date: Date.new(@year, 1, 1)..Date.new(@year, 12, 31)).order(:date)
7
+ @next_holiday_id = Holiday.company_wide.upcoming.pick(:id)
7
8
  end
8
9
  end
9
10
  end