A dead simple time tracker that keeps everything in local storage. Next.js, no accounts and no server.
handcodedlocalstoragenextjsreactself-hostedserverlesstime-trackingtimer
1.entryContainer {
2 background-color: var(--surface);
3 color: var(--text);
4 margin-bottom: 10px;
5 display: grid;
6 grid-template-columns: 170px 1fr 44px 44px;
7 align-items: stretch;
8 border: 1px solid var(--border);
9 border-radius: var(--radius);
10 transition:
11 border-color 200ms ease,
12 box-shadow 200ms ease,
13 transform 180ms ease;
14 overflow: hidden;
15}
16
17.entryContainer.editing {
18 grid-template-columns: 300px 1fr 44px 44px;
19}
20
21.entryContainer:hover {
22 border-color: var(--border-green);
23}
24
25.selected {
26 border-color: var(--green);
27 box-shadow: 0 0 0 1px var(--green), 0 0 24px var(--green-dim);
28}
29
30.zoom {
31 transform: scale(1.01);
32}
33
34@media (max-width: 1023.99px) {
35 .entryContainer {
36 grid-template-columns: 1fr 1fr;
37 grid-template-rows: auto auto auto;
38 text-align: center;
39 }
40}
41
42.entryForm {
43 display: contents;
44}
45
46.entryNoteInput {
47 border: none;
48 padding: 10px 0;
49 font-size: 0.95em;
50 width: 100%;
51 border-bottom: 1px solid var(--border-green);
52 background: transparent;
53 color: var(--text-bright);
54 outline: none;
55 letter-spacing: 0.02em;
56}
57
58.entryNoteInput:focus-visible {
59 border-bottom-color: var(--green);
60 box-shadow: 0 1px 0 0 var(--green);
61}
62
63.entryTime {
64 font-weight: 600;
65 font-size: 1.1em;
66 font-variant-numeric: tabular-nums;
67 letter-spacing: 0.02em;
68 padding: 12px 14px;
69 height: 100%;
70 background-color: var(--bg-deep);
71 color: var(--green-bright);
72 text-align: center;
73 display: flex;
74 justify-content: center;
75 align-items: center;
76 box-sizing: border-box;
77 flex-direction: column;
78 gap: 4px;
79 border-right: 1px solid var(--border);
80 min-width: 0;
81}
82
83.entryTimeEditing {
84 align-items: stretch;
85 gap: 8px;
86 text-align: left;
87 padding: 12px;
88}
89
90.entryDuration {
91 font-size: 0.95em;
92 letter-spacing: 0.04em;
93 text-align: center;
94 color: var(--green-bright);
95 font-weight: 700;
96 padding-bottom: 6px;
97 border-bottom: 1px solid var(--border);
98}
99
100.entryTime span {
101 font-size: 0.7em;
102 opacity: 0.7;
103 font-weight: 400;
104 color: var(--text-muted);
105 text-transform: uppercase;
106 letter-spacing: 0.1em;
107}
108
109.entryTimeRow {
110 display: flex;
111 align-items: center;
112 gap: 6px;
113 width: 100%;
114}
115
116.entryTimeRowLabel {
117 font-size: 0.58em;
118 font-weight: 600;
119 text-transform: uppercase;
120 letter-spacing: 0.18em;
121 color: var(--text-muted);
122 min-width: 28px;
123 text-align: right;
124 flex-shrink: 0;
125}
126
127.entryTimeInput {
128 flex: 1 1 auto;
129 font-size: 0.72em;
130 font-family: var(--font-mono);
131 font-weight: 400;
132 padding: 4px 6px;
133 border: 1px solid var(--border-green);
134 background: var(--surface-2);
135 color: var(--text);
136 border-radius: 2px;
137 width: 100%;
138 min-width: 0;
139 box-sizing: border-box;
140 color-scheme: dark;
141}
142
143.entryTimeInput:focus-visible {
144 outline: none;
145 border-color: var(--green);
146 box-shadow: 0 0 0 2px var(--green-dim);
147}
148
149/* Trim and recolor the native calendar picker indicator so the seconds
150 digit never collides with it on Chromium. */
151.entryTimeInput::-webkit-calendar-picker-indicator {
152 filter: invert(0.55) sepia(0.2) saturate(3) hue-rotate(80deg);
153 padding: 0;
154 margin-left: 4px;
155 cursor: pointer;
156 opacity: 0.6;
157}
158
159.entryTimeInput::-webkit-calendar-picker-indicator:hover {
160 opacity: 1;
161}
162
163@media (max-width: 1023.99px) {
164 .entryContainer,
165 .entryContainer.editing {
166 grid-template-columns: 1fr 1fr;
167 grid-template-rows: auto auto auto;
168 }
169 .entryTime {
170 grid-column: 1 / span 2;
171 border-right: 0;
172 border-bottom: 1px solid var(--border);
173 }
174 .entryTimeEditing {
175 padding: 14px 16px;
176 }
177 .entryTimeRow {
178 gap: 10px;
179 }
180 .entryTimeRowLabel {
181 min-width: 32px;
182 }
183}
184
185.entryNote {
186 padding: 14px 18px;
187 align-self: center;
188 font-size: 0.92em;
189 letter-spacing: 0.01em;
190}
191
192.entryNoteEmpty {
193 padding: 0;
194}
195
196.entryNote small {
197 display: block;
198 color: var(--green-bright);
199 opacity: 0.7;
200 margin-top: 6px;
201 font-size: 0.72em;
202 letter-spacing: 0.08em;
203}
204
205@media (max-width: 1023.99px) {
206 .entryNote {
207 grid-column: 1 / span 2;
208 }
209}
210
211.entryButton {
212 font-size: 1em;
213 cursor: pointer;
214 border: 0;
215 border-left: 1px solid var(--border);
216 margin: 0;
217 padding: 0;
218 height: 100%;
219 background: transparent;
220 color: var(--text-muted);
221 display: flex;
222 align-items: center;
223 justify-content: center;
224 transition:
225 background 180ms ease,
226 color 180ms ease;
227}
228
229.entryButton:hover {
230 background: var(--green-dim);
231 color: var(--green-bright);
232}
233
234.entryButton:focus-visible {
235 outline: none;
236 background: var(--green-dim);
237 box-shadow: inset 0 0 0 1px var(--green);
238}
239
240@media (max-width: 1023.99px) {
241 .entryButton {
242 padding: 10px;
243 }
244}
245
246.entrySubmit {
247 color: var(--green-bright);
248}
249
250.entrySubmit:hover {
251 background: var(--green-dim);
252 color: var(--green-hi);
253}
254
255.entryEdit:hover {
256 color: var(--amber-bright);
257 background: var(--amber-dim);
258}
259
260.entryRemove:hover {
261 color: var(--terracotta-bright);
262 background: var(--terracotta-dim);
263}