@natjswenson/devlog 0.1.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.
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/SKILL.md +174 -0
- package/bin/devlog.js +316 -0
- package/config.example.json +17 -0
- package/examples/react/DevLogPage.css +372 -0
- package/examples/react/DevLogPage.jsx +170 -0
- package/examples/react/README.md +74 -0
- package/examples/react/devlog-config.js +24 -0
- package/examples/react/useDevLogEntries.js +95 -0
- package/package.json +49 -0
- package/preview/App.jsx +84 -0
- package/preview/README.md +25 -0
- package/preview/demo.js +181 -0
- package/preview/index.html +18 -0
- package/preview/main.jsx +15 -0
- package/preview/preview.css +132 -0
- package/preview/vite.config.js +10 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/* DevLogPage — entry feed styles
|
|
2
|
+
*
|
|
3
|
+
* Self-contained: defines its own CSS variables in a default :root scope.
|
|
4
|
+
* Override any of the --devlog-* vars in your own stylesheet to match
|
|
5
|
+
* your site's theme.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.devlog-page {
|
|
9
|
+
--devlog-fg: #1a1a1a;
|
|
10
|
+
--devlog-fg-secondary: #4a4a4a;
|
|
11
|
+
--devlog-fg-tertiary: #888;
|
|
12
|
+
--devlog-bg-surface: #f7f7f7;
|
|
13
|
+
--devlog-bg-elevated: #f0f0f0;
|
|
14
|
+
--devlog-border: #e5e5e5;
|
|
15
|
+
--devlog-border-hover: #cccccc;
|
|
16
|
+
--devlog-transition-fast: 0.15s ease;
|
|
17
|
+
--devlog-transition-normal: 0.3s ease;
|
|
18
|
+
|
|
19
|
+
max-width: 720px;
|
|
20
|
+
margin: 0 auto;
|
|
21
|
+
padding: 24px;
|
|
22
|
+
color: var(--devlog-fg);
|
|
23
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@media (prefers-color-scheme: dark) {
|
|
27
|
+
.devlog-page {
|
|
28
|
+
--devlog-fg: #e8e8e8;
|
|
29
|
+
--devlog-fg-secondary: #b0b0b0;
|
|
30
|
+
--devlog-fg-tertiary: #777;
|
|
31
|
+
--devlog-bg-surface: #1a1a1a;
|
|
32
|
+
--devlog-bg-elevated: #222;
|
|
33
|
+
--devlog-border: #2a2a2a;
|
|
34
|
+
--devlog-border-hover: #3a3a3a;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ─── Project tabs ──────────────────────────────────────────────────── */
|
|
39
|
+
|
|
40
|
+
.devlog-tabs {
|
|
41
|
+
display: flex;
|
|
42
|
+
gap: 8px;
|
|
43
|
+
margin-bottom: 24px;
|
|
44
|
+
border-bottom: 1px solid var(--devlog-border);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.devlog-tab {
|
|
48
|
+
background: none;
|
|
49
|
+
border: none;
|
|
50
|
+
padding: 12px 16px;
|
|
51
|
+
font-size: 13px;
|
|
52
|
+
font-weight: 500;
|
|
53
|
+
color: var(--devlog-fg-tertiary);
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
letter-spacing: -0.01em;
|
|
56
|
+
border-bottom: 2px solid transparent;
|
|
57
|
+
margin-bottom: -1px;
|
|
58
|
+
transition: color var(--devlog-transition-fast), border-color var(--devlog-transition-fast);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.devlog-tab:hover {
|
|
62
|
+
color: var(--devlog-fg);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.devlog-tab--active {
|
|
66
|
+
color: var(--devlog-fg);
|
|
67
|
+
border-bottom-color: var(--devlog-fg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ─── Entry list ────────────────────────────────────────────────────── */
|
|
71
|
+
|
|
72
|
+
.devlog-list {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.devlog-entry {
|
|
78
|
+
padding: 28px 0;
|
|
79
|
+
border-bottom: 1px solid var(--devlog-border);
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
transition: background var(--devlog-transition-fast);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.devlog-entry:first-child {
|
|
85
|
+
border-top: 1px solid var(--devlog-border);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.devlog-entry:hover {
|
|
89
|
+
background: var(--devlog-bg-surface);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.devlog-entry--expanded,
|
|
93
|
+
.devlog-entry--expanded:hover {
|
|
94
|
+
cursor: default;
|
|
95
|
+
background: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ─── Entry header ──────────────────────────────────────────────────── */
|
|
99
|
+
|
|
100
|
+
.devlog-header {
|
|
101
|
+
display: flex;
|
|
102
|
+
justify-content: space-between;
|
|
103
|
+
align-items: flex-start;
|
|
104
|
+
gap: 24px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.devlog-header__left {
|
|
108
|
+
flex: 1;
|
|
109
|
+
min-width: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.devlog-date {
|
|
113
|
+
font-size: 11px;
|
|
114
|
+
font-weight: 600;
|
|
115
|
+
color: var(--devlog-fg-tertiary);
|
|
116
|
+
letter-spacing: 0.1em;
|
|
117
|
+
text-transform: uppercase;
|
|
118
|
+
margin: 0 0 8px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.devlog-title {
|
|
122
|
+
font-size: 17px;
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
color: var(--devlog-fg);
|
|
125
|
+
letter-spacing: -0.02em;
|
|
126
|
+
margin: 0 0 6px;
|
|
127
|
+
line-height: 1.3;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.devlog-summary {
|
|
131
|
+
font-size: 14px;
|
|
132
|
+
font-weight: 400;
|
|
133
|
+
color: var(--devlog-fg-secondary);
|
|
134
|
+
letter-spacing: -0.01em;
|
|
135
|
+
margin: 0;
|
|
136
|
+
line-height: 1.5;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.devlog-chevron {
|
|
140
|
+
flex-shrink: 0;
|
|
141
|
+
color: var(--devlog-fg-tertiary);
|
|
142
|
+
transition: transform var(--devlog-transition-fast);
|
|
143
|
+
margin-top: 4px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.devlog-entry--expanded .devlog-chevron {
|
|
147
|
+
transform: rotate(180deg);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* ─── Entry content (expanded) ──────────────────────────────────────── */
|
|
151
|
+
|
|
152
|
+
.devlog-content-wrapper {
|
|
153
|
+
display: grid;
|
|
154
|
+
grid-template-rows: 0fr;
|
|
155
|
+
transition: grid-template-rows var(--devlog-transition-normal);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.devlog-entry--expanded .devlog-content-wrapper {
|
|
159
|
+
grid-template-rows: 1fr;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.devlog-content-inner {
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.devlog-content {
|
|
167
|
+
padding-top: 24px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.devlog-content h2 {
|
|
171
|
+
font-size: 13px;
|
|
172
|
+
font-weight: 600;
|
|
173
|
+
color: var(--devlog-fg-tertiary);
|
|
174
|
+
letter-spacing: 0.08em;
|
|
175
|
+
text-transform: uppercase;
|
|
176
|
+
margin: 28px 0 12px;
|
|
177
|
+
padding-bottom: 8px;
|
|
178
|
+
border-bottom: 1px solid var(--devlog-border);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.devlog-content h2:first-child {
|
|
182
|
+
margin-top: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.devlog-content p {
|
|
186
|
+
font-size: 15px;
|
|
187
|
+
font-weight: 400;
|
|
188
|
+
color: var(--devlog-fg-secondary);
|
|
189
|
+
line-height: 1.7;
|
|
190
|
+
letter-spacing: -0.01em;
|
|
191
|
+
margin: 0 0 12px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.devlog-content ul {
|
|
195
|
+
list-style: none;
|
|
196
|
+
padding: 0;
|
|
197
|
+
margin: 0 0 12px;
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
gap: 6px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.devlog-content li {
|
|
204
|
+
font-size: 14px;
|
|
205
|
+
font-weight: 400;
|
|
206
|
+
color: var(--devlog-fg-secondary);
|
|
207
|
+
line-height: 1.6;
|
|
208
|
+
letter-spacing: -0.01em;
|
|
209
|
+
padding-left: 16px;
|
|
210
|
+
position: relative;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.devlog-content li::before {
|
|
214
|
+
content: '\2013';
|
|
215
|
+
position: absolute;
|
|
216
|
+
left: 0;
|
|
217
|
+
color: var(--devlog-fg-tertiary);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.devlog-content a {
|
|
221
|
+
color: var(--devlog-fg);
|
|
222
|
+
text-decoration: underline;
|
|
223
|
+
text-underline-offset: 2px;
|
|
224
|
+
text-decoration-color: var(--devlog-fg-tertiary);
|
|
225
|
+
transition: text-decoration-color var(--devlog-transition-fast);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.devlog-content a:hover {
|
|
229
|
+
text-decoration-color: var(--devlog-fg);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.devlog-content code {
|
|
233
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
234
|
+
font-size: 13px;
|
|
235
|
+
background: var(--devlog-bg-elevated);
|
|
236
|
+
border: 1px solid var(--devlog-border);
|
|
237
|
+
border-radius: 4px;
|
|
238
|
+
padding: 2px 6px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.devlog-content pre {
|
|
242
|
+
background: var(--devlog-bg-elevated);
|
|
243
|
+
border: 1px solid var(--devlog-border);
|
|
244
|
+
border-radius: 6px;
|
|
245
|
+
padding: 16px;
|
|
246
|
+
margin: 0 0 12px;
|
|
247
|
+
overflow-x: auto;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.devlog-content pre code {
|
|
251
|
+
background: none;
|
|
252
|
+
border: none;
|
|
253
|
+
padding: 0;
|
|
254
|
+
font-size: 13px;
|
|
255
|
+
line-height: 1.6;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.devlog-content blockquote {
|
|
259
|
+
margin: 0 0 12px;
|
|
260
|
+
padding: 0 0 0 16px;
|
|
261
|
+
border-left: 2px solid var(--devlog-border-hover);
|
|
262
|
+
color: var(--devlog-fg-secondary);
|
|
263
|
+
font-style: normal;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* ─── Loading skeleton ──────────────────────────────────────────────── */
|
|
267
|
+
|
|
268
|
+
.devlog-skeleton {
|
|
269
|
+
padding: 28px 0;
|
|
270
|
+
border-bottom: 1px solid var(--devlog-border);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.devlog-skeleton:first-child {
|
|
274
|
+
border-top: 1px solid var(--devlog-border);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.devlog-skeleton__line {
|
|
278
|
+
height: 12px;
|
|
279
|
+
background: var(--devlog-bg-elevated);
|
|
280
|
+
border-radius: 4px;
|
|
281
|
+
animation: devlog-pulse 1.5s ease-in-out infinite;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.devlog-skeleton__line--date {
|
|
285
|
+
width: 80px;
|
|
286
|
+
margin-bottom: 10px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.devlog-skeleton__line--title {
|
|
290
|
+
width: 60%;
|
|
291
|
+
height: 16px;
|
|
292
|
+
margin-bottom: 8px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.devlog-skeleton__line--summary {
|
|
296
|
+
width: 85%;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@keyframes devlog-pulse {
|
|
300
|
+
0%, 100% { opacity: 0.4; }
|
|
301
|
+
50% { opacity: 0.8; }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* ─── Empty & error states ──────────────────────────────────────────── */
|
|
305
|
+
|
|
306
|
+
.devlog-empty,
|
|
307
|
+
.devlog-error {
|
|
308
|
+
text-align: center;
|
|
309
|
+
padding: 80px 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.devlog-empty__text,
|
|
313
|
+
.devlog-error__text {
|
|
314
|
+
font-size: 14px;
|
|
315
|
+
color: var(--devlog-fg-tertiary);
|
|
316
|
+
letter-spacing: -0.01em;
|
|
317
|
+
margin: 0 0 16px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ─── Buttons ───────────────────────────────────────────────────────── */
|
|
321
|
+
|
|
322
|
+
.devlog-btn {
|
|
323
|
+
background: none;
|
|
324
|
+
border: 1px solid var(--devlog-border);
|
|
325
|
+
border-radius: 6px;
|
|
326
|
+
padding: 10px 20px;
|
|
327
|
+
font-size: 13px;
|
|
328
|
+
font-weight: 500;
|
|
329
|
+
color: var(--devlog-fg);
|
|
330
|
+
cursor: pointer;
|
|
331
|
+
letter-spacing: -0.01em;
|
|
332
|
+
transition: border-color var(--devlog-transition-fast), background var(--devlog-transition-fast);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.devlog-btn:hover {
|
|
336
|
+
border-color: var(--devlog-border-hover);
|
|
337
|
+
background: var(--devlog-bg-surface);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* ─── Load more ─────────────────────────────────────────────────────── */
|
|
341
|
+
|
|
342
|
+
.devlog-load-more {
|
|
343
|
+
display: flex;
|
|
344
|
+
justify-content: center;
|
|
345
|
+
padding: 40px 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* ─── Responsive ────────────────────────────────────────────────────── */
|
|
349
|
+
|
|
350
|
+
@media (max-width: 640px) {
|
|
351
|
+
.devlog-header {
|
|
352
|
+
flex-direction: column;
|
|
353
|
+
gap: 0;
|
|
354
|
+
}
|
|
355
|
+
.devlog-chevron {
|
|
356
|
+
display: none;
|
|
357
|
+
}
|
|
358
|
+
.devlog-title {
|
|
359
|
+
font-size: 15px;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@media (prefers-reduced-motion: reduce) {
|
|
364
|
+
.devlog-content-wrapper,
|
|
365
|
+
.devlog-chevron {
|
|
366
|
+
transition: none;
|
|
367
|
+
}
|
|
368
|
+
.devlog-skeleton__line {
|
|
369
|
+
animation: none;
|
|
370
|
+
opacity: 0.6;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import ReactMarkdown from 'react-markdown';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import { useDevLogEntries } from './useDevLogEntries.js';
|
|
5
|
+
import { DEVLOG_PROJECTS } from './devlog-config.js';
|
|
6
|
+
import './DevLogPage.css';
|
|
7
|
+
|
|
8
|
+
const ENTRIES_PER_PAGE = 10;
|
|
9
|
+
|
|
10
|
+
function formatDate(dateStr) {
|
|
11
|
+
const [year, month, day] = dateStr.split('-');
|
|
12
|
+
const date = new Date(year, month - 1, day);
|
|
13
|
+
return date.toLocaleDateString('en-US', {
|
|
14
|
+
month: 'short',
|
|
15
|
+
day: 'numeric',
|
|
16
|
+
year: 'numeric',
|
|
17
|
+
}).toUpperCase();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ChevronDown() {
|
|
21
|
+
return (
|
|
22
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
23
|
+
<polyline points="6 9 12 15 18 9" />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* DevLogPage renders the entries from a single project of your daily-dev-log repo.
|
|
30
|
+
*
|
|
31
|
+
* Props:
|
|
32
|
+
* project (string) active project key (must match a folder in your dev-log repo)
|
|
33
|
+
* projects (Project[]) optional — list of projects to render as tabs.
|
|
34
|
+
* Defaults to DEVLOG_PROJECTS from devlog-config.js.
|
|
35
|
+
* Hidden if length <= 1.
|
|
36
|
+
* onProjectChange (function) called with key when a tab is clicked.
|
|
37
|
+
* Wire this to your router (e.g. navigate(`/devlog/${key}`)).
|
|
38
|
+
* config (object) optional config override { repoOwner, repoName, branch }.
|
|
39
|
+
* Used by the preview app; adopters should set values in
|
|
40
|
+
* devlog-config.js instead.
|
|
41
|
+
*/
|
|
42
|
+
export default function DevLogPage({
|
|
43
|
+
project,
|
|
44
|
+
projects = DEVLOG_PROJECTS,
|
|
45
|
+
onProjectChange,
|
|
46
|
+
config,
|
|
47
|
+
}) {
|
|
48
|
+
const activeProject = project || projects[0]?.key;
|
|
49
|
+
const { entries, loadedContent, loading, error, fetchEntryContent, retry } = useDevLogEntries(activeProject, config);
|
|
50
|
+
const [expandedEntry, setExpandedEntry] = useState(null);
|
|
51
|
+
const [visibleCount, setVisibleCount] = useState(ENTRIES_PER_PAGE);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
setExpandedEntry(null);
|
|
55
|
+
setVisibleCount(ENTRIES_PER_PAGE);
|
|
56
|
+
}, [activeProject]);
|
|
57
|
+
|
|
58
|
+
const handleToggle = useCallback((filename) => {
|
|
59
|
+
if (expandedEntry === filename) {
|
|
60
|
+
setExpandedEntry(null);
|
|
61
|
+
} else {
|
|
62
|
+
setExpandedEntry(filename);
|
|
63
|
+
if (!loadedContent.has(filename)) {
|
|
64
|
+
fetchEntryContent(filename);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, [expandedEntry, loadedContent, fetchEntryContent]);
|
|
68
|
+
|
|
69
|
+
const visibleEntries = entries.slice(0, visibleCount);
|
|
70
|
+
const hasMore = visibleCount < entries.length;
|
|
71
|
+
const showTabs = projects && projects.length > 1;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="devlog-page">
|
|
75
|
+
{showTabs && (
|
|
76
|
+
<div className="devlog-tabs">
|
|
77
|
+
{projects.map((p) => (
|
|
78
|
+
<button
|
|
79
|
+
key={p.key}
|
|
80
|
+
className={`devlog-tab${p.key === activeProject ? ' devlog-tab--active' : ''}`}
|
|
81
|
+
onClick={() => onProjectChange?.(p.key)}
|
|
82
|
+
>
|
|
83
|
+
{p.label}
|
|
84
|
+
</button>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{loading && (
|
|
90
|
+
<div className="devlog-list" aria-busy="true" aria-label="Loading entries">
|
|
91
|
+
{[0, 1, 2].map((i) => (
|
|
92
|
+
<div key={i} className="devlog-skeleton">
|
|
93
|
+
<div className="devlog-skeleton__line devlog-skeleton__line--date" />
|
|
94
|
+
<div className="devlog-skeleton__line devlog-skeleton__line--title" />
|
|
95
|
+
<div className="devlog-skeleton__line devlog-skeleton__line--summary" />
|
|
96
|
+
</div>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{error && (
|
|
102
|
+
<div className="devlog-error">
|
|
103
|
+
<p className="devlog-error__text">Failed to load entries: {error}</p>
|
|
104
|
+
<button className="devlog-btn" onClick={retry}>Retry</button>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{!loading && !error && entries.length === 0 && (
|
|
109
|
+
<div className="devlog-empty">
|
|
110
|
+
<p className="devlog-empty__text">No entries yet.</p>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{!loading && !error && entries.length > 0 && (
|
|
115
|
+
<>
|
|
116
|
+
<div className="devlog-list">
|
|
117
|
+
{visibleEntries.map((entry) => {
|
|
118
|
+
const isExpanded = expandedEntry === entry.file;
|
|
119
|
+
const content = loadedContent.get(entry.file);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<article
|
|
123
|
+
key={entry.file}
|
|
124
|
+
className={`devlog-entry${isExpanded ? ' devlog-entry--expanded' : ''}`}
|
|
125
|
+
onClick={() => handleToggle(entry.file)}
|
|
126
|
+
>
|
|
127
|
+
<div className="devlog-header">
|
|
128
|
+
<div className="devlog-header__left">
|
|
129
|
+
<p className="devlog-date">{formatDate(entry.date)}</p>
|
|
130
|
+
<h2 className="devlog-title">{entry.title}</h2>
|
|
131
|
+
<p className="devlog-summary">{entry.summary}</p>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="devlog-chevron" aria-hidden="true">
|
|
134
|
+
<ChevronDown />
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div className="devlog-content-wrapper">
|
|
139
|
+
<div className="devlog-content-inner">
|
|
140
|
+
{isExpanded && content && (
|
|
141
|
+
<div className="devlog-content" onClick={(e) => e.stopPropagation()}>
|
|
142
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
143
|
+
{content}
|
|
144
|
+
</ReactMarkdown>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
{isExpanded && !content && (
|
|
148
|
+
<div className="devlog-content">
|
|
149
|
+
<p className="devlog-empty__text">Loading...</p>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</article>
|
|
155
|
+
);
|
|
156
|
+
})}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{hasMore && (
|
|
160
|
+
<div className="devlog-load-more">
|
|
161
|
+
<button className="devlog-btn" onClick={() => setVisibleCount((c) => c + ENTRIES_PER_PAGE)}>
|
|
162
|
+
Load More
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# React example — devlog
|
|
2
|
+
|
|
3
|
+
Drop-in React components for rendering your daily dev log on any React-based site.
|
|
4
|
+
|
|
5
|
+
## What's here
|
|
6
|
+
|
|
7
|
+
| File | What to do with it |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `devlog-config.js` | **Edit this.** Set `repoOwner` and `repoName` to your dev-log repo. |
|
|
10
|
+
| `useDevLogEntries.js` | Hook that fetches the manifest + entry markdown. No edits needed. |
|
|
11
|
+
| `DevLogPage.jsx` | The page component. No edits needed. |
|
|
12
|
+
| `DevLogPage.css` | Self-contained styles with CSS variables you can override. |
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
1. Copy these four files into your React project (e.g. `src/devlog/`).
|
|
17
|
+
2. Install peer deps:
|
|
18
|
+
```sh
|
|
19
|
+
npm install react-markdown remark-gfm
|
|
20
|
+
```
|
|
21
|
+
3. Edit `devlog-config.js` to point at your repo.
|
|
22
|
+
4. Mount the page somewhere in your app:
|
|
23
|
+
|
|
24
|
+
```jsx
|
|
25
|
+
import DevLogPage from './devlog/DevLogPage.jsx';
|
|
26
|
+
|
|
27
|
+
function MyDevLogRoute() {
|
|
28
|
+
return <DevLogPage project="myproject" />;
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## With multiple projects + routing
|
|
33
|
+
|
|
34
|
+
```jsx
|
|
35
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
36
|
+
import DevLogPage from './devlog/DevLogPage.jsx';
|
|
37
|
+
|
|
38
|
+
const PROJECTS = [
|
|
39
|
+
{ key: 'project-a', label: 'Project A' },
|
|
40
|
+
{ key: 'project-b', label: 'Project B' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function DevLogRoute() {
|
|
44
|
+
const { project = 'project-a' } = useParams();
|
|
45
|
+
const navigate = useNavigate();
|
|
46
|
+
return (
|
|
47
|
+
<DevLogPage
|
|
48
|
+
project={project}
|
|
49
|
+
projects={PROJECTS}
|
|
50
|
+
onProjectChange={(key) => navigate(`/devlog/${key}`)}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Theming
|
|
57
|
+
|
|
58
|
+
Override the `--devlog-*` variables on `.devlog-page` (or any ancestor) in your own stylesheet:
|
|
59
|
+
|
|
60
|
+
```css
|
|
61
|
+
.devlog-page {
|
|
62
|
+
--devlog-fg: #000;
|
|
63
|
+
--devlog-bg-surface: #fafafa;
|
|
64
|
+
/* ...etc */
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Variables: `--devlog-fg`, `--devlog-fg-secondary`, `--devlog-fg-tertiary`, `--devlog-bg-surface`, `--devlog-bg-elevated`, `--devlog-border`, `--devlog-border-hover`.
|
|
69
|
+
|
|
70
|
+
A reasonable dark mode fires automatically via `prefers-color-scheme`.
|
|
71
|
+
|
|
72
|
+
## Want to use a non-React stack?
|
|
73
|
+
|
|
74
|
+
The hook and page are React-specific, but the underlying data is just static JSON + Markdown on GitHub. See the **Data contract** section of the top-level README.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hi! 👋 You're looking at the demo config.
|
|
3
|
+
*
|
|
4
|
+
* Step 1: replace `repoOwner` and `repoName` with YOUR daily-dev-log repo.
|
|
5
|
+
* Step 2: replace the placeholder projects with your actual ones.
|
|
6
|
+
* Step 3: feel a small wave of accomplishment, then ship something.
|
|
7
|
+
*
|
|
8
|
+
* (If you skip these steps, the page will silently render nothing,
|
|
9
|
+
* and you will be left to wonder why. Don't do that to yourself.)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const DEVLOG_CONFIG = {
|
|
13
|
+
repoOwner: 'yourusername',
|
|
14
|
+
repoName: 'daily-dev-log',
|
|
15
|
+
branch: 'main',
|
|
16
|
+
get baseUrl() {
|
|
17
|
+
return `https://raw.githubusercontent.com/${this.repoOwner}/${this.repoName}/${this.branch}`;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const DEVLOG_PROJECTS = [
|
|
22
|
+
{ key: 'midnight-side-quest', label: 'Midnight Side Quest' },
|
|
23
|
+
{ key: 'todays-existential-crisis', label: "Today's Existential Crisis" },
|
|
24
|
+
];
|