project-logbook 0.3.0 → 0.3.2
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/README.md +14 -11
- package/dist/commands/build.js +57 -34
- package/dist/commands/lint.js +6 -14
- package/dist/index.js +3 -3
- package/dist/lib/build-helpers.js +2 -0
- package/dist/lib/git-helpers.d.ts +11 -0
- package/dist/lib/git-helpers.js +28 -5
- package/dist/lib/image-helpers.js +2 -0
- package/dist/lib/template-helpers.d.ts +8 -0
- package/dist/lib/template-helpers.js +78 -0
- package/dist/lib/template-types.d.ts +3 -0
- package/dist/lib/templates.js +4 -65
- package/dist/linters/frontmatter.js +21 -1
- package/dist/linters/placeholders.js +1 -3
- package/dist/templates/CONTRIBUTING.md +2 -2
- package/dist/templates/steer.txt +2 -2
- package/dist/templates/styles.css +52 -0
- package/dist/utils/timeline-helpers.d.ts +4 -0
- package/dist/utils/timeline-helpers.js +19 -0
- package/package.json +2 -1
- package/src/templates/CONTRIBUTING.md +2 -2
- package/src/templates/steer.txt +2 -2
- package/src/templates/styles.css +52 -0
package/README.md
CHANGED
|
@@ -21,14 +21,17 @@ npm i project-loogbook -g
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## Commands
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
|
|
25
|
+
| Command | Description |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `logbook init` | Initialize configuration and directory. |
|
|
28
|
+
| `logbook new <id> <slug>` | Create a new entry folder with templates. |
|
|
29
|
+
| `logbook start <id>` | Mark a logbook entry as active (writes `.logbook-active` lockfile). The active entry is also auto-detected from the current Git branch — a branch named `feat/LB-123-my-feature` will automatically resolve to entry `LB-123`. |
|
|
30
|
+
| `logbook release` | Release the active logbook entry (removes `.logbook-active` lockfile). |
|
|
31
|
+
| `logbook log <message>` | Append a timestamped log entry to the active `log.md`. |
|
|
32
|
+
| `logbook list` | List all logbook entries with their status. |
|
|
33
|
+
| `logbook lint` | Validate structure, frontmatter, and internal links. |
|
|
34
|
+
| `logbook build` | Compile logbook entries into a static HTML site (default: `public/`). |
|
|
35
|
+
| `logbook preview` | Build and open the logbook in your default browser. |
|
|
36
|
+
| `logbook upgrade` | Synchronize core project files (like `CONTRIBUTING.md`) with latest templates. |
|
|
37
|
+
| `logbook steer` | Output agentic protocol for AI assistants. |
|
package/dist/commands/build.js
CHANGED
|
@@ -11,8 +11,19 @@ import { getGitCommits, getGitTags } from '../lib/git-helpers.js';
|
|
|
11
11
|
import { formatRelativeDate, getMonthYear, getSortTime } from '../utils/date.js';
|
|
12
12
|
import { getLogbookEntries } from '../utils/fs.js';
|
|
13
13
|
import { getAboutContent } from '../lib/about-content.js';
|
|
14
|
+
import { groupTimelineItems } from '../utils/timeline-helpers.js';
|
|
14
15
|
const pkg = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
15
16
|
const version = pkg.version;
|
|
17
|
+
/** Normalises a frontmatter field that may be a scalar string or a YAML list of strings. */
|
|
18
|
+
const toDisplayString = (v) => {
|
|
19
|
+
if (typeof v === 'string')
|
|
20
|
+
return v || undefined;
|
|
21
|
+
if (Array.isArray(v)) {
|
|
22
|
+
const joined = v.filter((x) => typeof x === 'string').join(' + ');
|
|
23
|
+
return joined || undefined;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
};
|
|
16
27
|
export async function build() {
|
|
17
28
|
const config = getConfig();
|
|
18
29
|
const logbookDir = join(process.cwd(), config.logbookDir);
|
|
@@ -102,9 +113,9 @@ export async function build() {
|
|
|
102
113
|
tags: Array.isArray(data.tags) ? data.tags.filter((t) => typeof t === 'string') : [],
|
|
103
114
|
workspaces: entryWorkspaces,
|
|
104
115
|
title: typeof data.title === 'string' ? data.title : undefined,
|
|
105
|
-
harness:
|
|
106
|
-
llm:
|
|
107
|
-
prompter:
|
|
116
|
+
harness: toDisplayString(data.harness),
|
|
117
|
+
llm: toDisplayString(data.llm),
|
|
118
|
+
prompter: toDisplayString(data.prompter),
|
|
108
119
|
});
|
|
109
120
|
}
|
|
110
121
|
// Fetch global git commits and tags concurrently.
|
|
@@ -134,18 +145,49 @@ export async function build() {
|
|
|
134
145
|
return bEnd - aEnd;
|
|
135
146
|
});
|
|
136
147
|
await Promise.all(timelineEntries.map((entry, i) => renderPost(entry, i, timelineEntries, { logbookDir, outputDir, config, version, buildTime })));
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
// Build the full merged, chronologically sorted groups once, then paginate the flat item list.
|
|
149
|
+
const allGroups = groupTimelineItems(timelineEntries, commitItems, tagItems);
|
|
150
|
+
const flatItems = allGroups.flatMap((g) => g.items);
|
|
151
|
+
const pageSize = 50;
|
|
152
|
+
const totalPages = Math.max(1, Math.ceil(flatItems.length / pageSize));
|
|
153
|
+
for (let i = 0; i < totalPages; i++) {
|
|
154
|
+
const currentPage = i + 1;
|
|
155
|
+
const pageItems = flatItems.slice(i * pageSize, (i + 1) * pageSize);
|
|
156
|
+
// Re-group this page's items by month for rendering.
|
|
157
|
+
const pageGroups = [];
|
|
158
|
+
for (const item of pageItems) {
|
|
159
|
+
const month = item.monthGroup;
|
|
160
|
+
let group = pageGroups.find((g) => g.month === month);
|
|
161
|
+
if (!group) {
|
|
162
|
+
group = { month, items: [] };
|
|
163
|
+
pageGroups.push(group);
|
|
164
|
+
}
|
|
165
|
+
group.items.push(item);
|
|
166
|
+
}
|
|
167
|
+
const timelineContent = timelineTemplate({
|
|
168
|
+
projectName: config.projectName,
|
|
169
|
+
version,
|
|
170
|
+
buildTime,
|
|
171
|
+
groups: pageGroups,
|
|
172
|
+
readmeHtml,
|
|
173
|
+
aboutHtml,
|
|
174
|
+
jiraBaseUrl: config.jiraBaseUrl,
|
|
175
|
+
jiraPrefix: config.jiraPrefix,
|
|
176
|
+
currentPage,
|
|
177
|
+
totalPages,
|
|
178
|
+
}); // prettier-ignore
|
|
179
|
+
const buildMeta = `Generated by ${config.projectName} v${version} • ${buildTime}`;
|
|
180
|
+
const timelineHtml = layout({
|
|
181
|
+
title: 'Timeline',
|
|
182
|
+
projectName: config.projectName,
|
|
183
|
+
basePath: './',
|
|
184
|
+
buildMeta,
|
|
185
|
+
header: timelineContent.split('</header>')[0] + '</header>',
|
|
186
|
+
content: timelineContent.split('</header>')[1],
|
|
187
|
+
});
|
|
188
|
+
const fileName = currentPage === 1 ? 'index.html' : `index-${currentPage}.html`;
|
|
189
|
+
await fs.writeFile(join(outputDir, fileName), timelineHtml);
|
|
190
|
+
}
|
|
149
191
|
await buildProjectMdFiles(process.cwd(), outputDir, [config.logbookDir, config.outputDir, 'node_modules'], {
|
|
150
192
|
config,
|
|
151
193
|
version,
|
|
@@ -153,22 +195,3 @@ export async function build() {
|
|
|
153
195
|
});
|
|
154
196
|
console.log(chalk.green(`\nSuccessfully built logbook to ${config.outputDir}/`));
|
|
155
197
|
}
|
|
156
|
-
function groupTimelineItems(entries, commits, tags) {
|
|
157
|
-
const toMs = (item) => {
|
|
158
|
-
if (item.kind === 'entry')
|
|
159
|
-
return item.dateEnd ? new Date(item.dateEnd).getTime() : new Date(item.dateStart).getTime();
|
|
160
|
-
return new Date(item.timestamp).getTime();
|
|
161
|
-
};
|
|
162
|
-
const all = [...entries, ...commits, ...tags];
|
|
163
|
-
all.sort((a, b) => toMs(b) - toMs(a));
|
|
164
|
-
const groups = [];
|
|
165
|
-
for (const item of all) {
|
|
166
|
-
let group = groups.find((g) => g.month === item.monthGroup);
|
|
167
|
-
if (!group) {
|
|
168
|
-
group = { month: item.monthGroup, items: [] };
|
|
169
|
-
groups.push(group);
|
|
170
|
-
}
|
|
171
|
-
group.items.push(item);
|
|
172
|
-
}
|
|
173
|
-
return groups;
|
|
174
|
-
}
|
package/dist/commands/lint.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function lint() {
|
|
|
20
20
|
// 2. Entry-level checks
|
|
21
21
|
const logbookEntries = await getLogbookEntries(logbookDir);
|
|
22
22
|
let passedCount = 0;
|
|
23
|
-
let skippedCount = 0;
|
|
23
|
+
let skippedCount = 0; // Re-introducing skippedCount
|
|
24
24
|
for (const entry of logbookEntries) {
|
|
25
25
|
if (!entry.hasIndex) {
|
|
26
26
|
console.error(chalk.red(` [MISSING] ${entry.slug}/index.md`));
|
|
@@ -62,17 +62,9 @@ export async function lint() {
|
|
|
62
62
|
* meaning it should be exempt from linting.
|
|
63
63
|
*/
|
|
64
64
|
function isDraft(entry) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
dataValues.includes('[WRITE_SUMMARY_HERE]') ||
|
|
71
|
-
dataValues.includes('[PROMPTER]') ||
|
|
72
|
-
dataValues.includes('[HARNESS]') ||
|
|
73
|
-
dataValues.includes('[LLM]');
|
|
74
|
-
const contentWithoutCode = entry.content.replace(/`[^`]*`/g, '');
|
|
75
|
-
const bodyHasPlaceholders = contentWithoutCode.includes('TODO:') ||
|
|
76
|
-
entry.content.includes('Write a polished, highly readable "short story" of the change here.');
|
|
77
|
-
return frontmatterHasPlaceholders || bodyHasPlaceholders;
|
|
65
|
+
const frontmatter = entry.data; // Correctly access the data property
|
|
66
|
+
// An entry is a draft if:
|
|
67
|
+
// 1. The 'dateStart' key is missing from frontmatter (i.e., undefined or null).
|
|
68
|
+
// 2. OR the string representation of 'dateStart' is '[DATE_START]'.
|
|
69
|
+
return !frontmatter?.dateStart || String(frontmatter.dateStart) === 'DATE_START';
|
|
78
70
|
}
|
package/dist/index.js
CHANGED
|
@@ -75,12 +75,12 @@ program
|
|
|
75
75
|
await list(options);
|
|
76
76
|
});
|
|
77
77
|
program
|
|
78
|
-
.command('log <message
|
|
78
|
+
.command('log <message>')
|
|
79
79
|
.description('Append a timestamped log entry to the active log.md (e.g. logbook log "Did a thing")')
|
|
80
80
|
.option('-i, --id <id>', 'Override active logbook entry ID')
|
|
81
|
-
.action(async (
|
|
81
|
+
.action(async (message, options) => {
|
|
82
82
|
const { log } = await import('./commands/log.js');
|
|
83
|
-
await log(
|
|
83
|
+
await log([message], options);
|
|
84
84
|
});
|
|
85
85
|
program
|
|
86
86
|
.command('steer')
|
|
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { unified } from 'unified';
|
|
4
4
|
import remarkParse from 'remark-parse';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
5
6
|
import remarkRehype from 'remark-rehype';
|
|
6
7
|
import rehypeSlug from 'rehype-slug';
|
|
7
8
|
import rehypeFormat from 'rehype-format';
|
|
@@ -39,6 +40,7 @@ function visitLinks(node, visitor) {
|
|
|
39
40
|
}
|
|
40
41
|
const processor = unified()
|
|
41
42
|
.use(remarkParse)
|
|
43
|
+
.use(remarkGfm)
|
|
42
44
|
.use(remarkRehype)
|
|
43
45
|
.use(rehypeSlug)
|
|
44
46
|
.use(rehypeRewriteMdLinks)
|
|
@@ -2,9 +2,20 @@ import type { GitCommit } from './template-types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Parse the raw output of `git log --pretty=format:"%H|%s|%aI"` into GitCommit objects.
|
|
4
4
|
* Pure function (no I/O) — unit-testable without spawning git.
|
|
5
|
+
*
|
|
6
|
+
* Uses indexOf/lastIndexOf to delimit fields so commit messages containing `|`
|
|
7
|
+
* are preserved correctly instead of being silently truncated.
|
|
5
8
|
*/
|
|
6
9
|
export declare function parseGitLogOutput(raw: string, now?: Date): GitCommit[];
|
|
10
|
+
/**
|
|
11
|
+
* Fetch git commits that touched `dir`, up to `maxCount`.
|
|
12
|
+
* Returns an empty array if git is unavailable or the directory is not a repo.
|
|
13
|
+
*/
|
|
7
14
|
export declare function getGitCommits(dir: string, maxCount?: number): Promise<GitCommit[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Fetch all git tags sorted by creation date (newest first).
|
|
17
|
+
* Returns an empty array if git is unavailable or the directory is not a repo.
|
|
18
|
+
*/
|
|
8
19
|
export declare function getGitTags(): Promise<{
|
|
9
20
|
name: string;
|
|
10
21
|
timestamp: string;
|
package/dist/lib/git-helpers.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { simpleGit } from 'simple-git';
|
|
3
|
+
function getGit() {
|
|
4
|
+
return simpleGit(process.cwd());
|
|
5
|
+
}
|
|
3
6
|
/**
|
|
4
7
|
* Parse the raw output of `git log --pretty=format:"%H|%s|%aI"` into GitCommit objects.
|
|
5
8
|
* Pure function (no I/O) — unit-testable without spawning git.
|
|
9
|
+
*
|
|
10
|
+
* Uses indexOf/lastIndexOf to delimit fields so commit messages containing `|`
|
|
11
|
+
* are preserved correctly instead of being silently truncated.
|
|
6
12
|
*/
|
|
7
13
|
export function parseGitLogOutput(raw, now = new Date()) {
|
|
8
14
|
if (!raw.trim())
|
|
@@ -11,7 +17,13 @@ export function parseGitLogOutput(raw, now = new Date()) {
|
|
|
11
17
|
.split('\n')
|
|
12
18
|
.filter(Boolean)
|
|
13
19
|
.map((line) => {
|
|
14
|
-
const
|
|
20
|
+
const firstPipe = line.indexOf('|');
|
|
21
|
+
const lastPipe = line.lastIndexOf('|');
|
|
22
|
+
if (firstPipe === -1 || firstPipe === lastPipe)
|
|
23
|
+
return null;
|
|
24
|
+
const sha = line.slice(0, firstPipe);
|
|
25
|
+
const message = line.slice(firstPipe + 1, lastPipe);
|
|
26
|
+
const timestamp = line.slice(lastPipe + 1);
|
|
15
27
|
if (!sha || !message || !timestamp)
|
|
16
28
|
return null;
|
|
17
29
|
const relativeTime = formatRelativeTime(timestamp, now);
|
|
@@ -42,20 +54,31 @@ function formatRelativeTime(isoTimestamp, now) {
|
|
|
42
54
|
const diffYear = Math.floor(diffMonth / 12);
|
|
43
55
|
return `${diffYear} year${diffYear === 1 ? '' : 's'} ago`;
|
|
44
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Fetch git commits that touched `dir`, up to `maxCount`.
|
|
59
|
+
* Returns an empty array if git is unavailable or the directory is not a repo.
|
|
60
|
+
*/
|
|
45
61
|
export async function getGitCommits(dir, maxCount = 100) {
|
|
46
62
|
try {
|
|
47
|
-
const
|
|
48
|
-
const raw = await git.raw(['log', '--follow', `-n`, String(maxCount), '--pretty=format:%H|%s|%aI', '--', dir]);
|
|
63
|
+
const raw = await getGit().raw(['log', '--follow', `-n`, String(maxCount), '--pretty=format:%H|%s|%aI', '--', dir]);
|
|
49
64
|
return parseGitLogOutput(raw);
|
|
50
65
|
}
|
|
51
66
|
catch {
|
|
52
67
|
return [];
|
|
53
68
|
}
|
|
54
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Fetch all git tags sorted by creation date (newest first).
|
|
72
|
+
* Returns an empty array if git is unavailable or the directory is not a repo.
|
|
73
|
+
*/
|
|
55
74
|
export async function getGitTags() {
|
|
56
75
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
76
|
+
const raw = await getGit().raw([
|
|
77
|
+
'tag',
|
|
78
|
+
'-l',
|
|
79
|
+
'--sort=-creatordate',
|
|
80
|
+
'--format=%(creatordate:iso8601)|%(refname:short)',
|
|
81
|
+
]);
|
|
59
82
|
return raw
|
|
60
83
|
.split('\n')
|
|
61
84
|
.filter(Boolean)
|
|
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import { join, dirname, relative } from 'node:path';
|
|
3
3
|
import { unified } from 'unified';
|
|
4
4
|
import remarkParse from 'remark-parse';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
5
6
|
import remarkRehype from 'remark-rehype';
|
|
6
7
|
import rehypeSlug from 'rehype-slug';
|
|
7
8
|
import rehypeFormat from 'rehype-format';
|
|
@@ -65,6 +66,7 @@ export async function mdToHtmlWithImages(md) {
|
|
|
65
66
|
imageProcessor.reset();
|
|
66
67
|
const processorWithImages = unified()
|
|
67
68
|
.use(remarkParse)
|
|
69
|
+
.use(remarkGfm)
|
|
68
70
|
.use(remarkRehype)
|
|
69
71
|
.use(imageProcessor.rehypeCollectImages)
|
|
70
72
|
.use(rehypeSlug)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TimelineEntry, TimelineCommitItem, TimelineTagItem, EntryLink, GitCommit } from './template-types.js';
|
|
2
|
+
export declare const navLink: (entry: EntryLink, dir: "prev" | "next") => string;
|
|
3
|
+
export declare const renderTagsAndWorkspaces: (tags: string[] | undefined, workspaces: string[] | undefined) => string;
|
|
4
|
+
export declare const renderCommits: (commits: GitCommit[] | undefined) => string;
|
|
5
|
+
export declare const renderTimelineEntryItem: (e: TimelineEntry) => string;
|
|
6
|
+
export declare const renderTimelineCommitItem: (c: TimelineCommitItem, jiraBaseUrl?: string, jiraPrefix?: string) => string;
|
|
7
|
+
export declare const renderTimelineTagItem: (t: TimelineTagItem) => string;
|
|
8
|
+
export declare const paginationLinks: (currentPage: number, totalPages: number) => string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { linkJiraIds } from './jira-helpers.js';
|
|
2
|
+
const html = (strings, ...values) => {
|
|
3
|
+
return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
|
|
4
|
+
};
|
|
5
|
+
export const navLink = (entry, dir) => {
|
|
6
|
+
const label = dir === 'prev' ? '← Previous' : 'Next →';
|
|
7
|
+
const t = `${entry.ticket ? `${entry.ticket}: ` : ''}${entry.title}`;
|
|
8
|
+
return `<a href="../${entry.slug}/index.html" class="post-nav-link"><span class="post-nav-label">${label}</span><span class="post-nav-title">${t}</span></a>`;
|
|
9
|
+
};
|
|
10
|
+
const renderWorkspaces = (workspaces) => {
|
|
11
|
+
if (!workspaces || workspaces.length === 0)
|
|
12
|
+
return '';
|
|
13
|
+
return workspaces.map((ws) => `<span class="tag-badge tag-workspace">${ws}</span>`).join('');
|
|
14
|
+
};
|
|
15
|
+
export const renderTagsAndWorkspaces = (tags, workspaces) => {
|
|
16
|
+
const tagsHtml = tags && tags.length > 0
|
|
17
|
+
? tags.map((tag) => `<span class="tag-badge tag-${tag.replace('#', '')}">${tag}</span>`).join('')
|
|
18
|
+
: '';
|
|
19
|
+
const wsHtml = renderWorkspaces(workspaces);
|
|
20
|
+
if (!tagsHtml && !wsHtml)
|
|
21
|
+
return '';
|
|
22
|
+
return html `<div class="tags-wrapper">${tagsHtml}${wsHtml}</div>`;
|
|
23
|
+
};
|
|
24
|
+
// Renders a compact list of git commits to embed in the Technical Log tab.
|
|
25
|
+
export const renderCommits = (commits) => {
|
|
26
|
+
if (!commits || commits.length === 0)
|
|
27
|
+
return '';
|
|
28
|
+
const rows = commits
|
|
29
|
+
.map((c) => `<div class="commit-item"><span class="commit-sha">${c.sha}</span><span class="commit-message">${c.message}</span><span class="commit-time" title="${c.timestamp}">${c.relativeTime}</span></div>`)
|
|
30
|
+
.join('');
|
|
31
|
+
return `<div class="commit-list"><h3 class="commit-list-heading">Git Commits</h3>${rows}</div>`;
|
|
32
|
+
};
|
|
33
|
+
export const renderTimelineEntryItem = (e) => html `<div class="timeline-item" data-entry-slug="${e.slug}">
|
|
34
|
+
<a class="timeline-card" href="./${e.slug}/index.html"
|
|
35
|
+
><div class="item-content">
|
|
36
|
+
<div class="item-meta">
|
|
37
|
+
<span data-date="${e.dateStart}">${e.displayDate}</span> • <span class="sort-time">${e.sortTime}</span> •
|
|
38
|
+
${e.llm ? ` ${e.llm} via ` : ''}${e.harness}${e.prompter ? ` / ${e.prompter}` : ''}
|
|
39
|
+
</div>
|
|
40
|
+
<h3 class="item-title">
|
|
41
|
+
<span class="new-badge" style="display:none;">NEW</span>${e.ticket ? `${e.ticket}: ` : ''}${e.title}
|
|
42
|
+
</h3>
|
|
43
|
+
<div class="item-summary">${e.summary}</div>
|
|
44
|
+
${renderTagsAndWorkspaces(e.tags, e.workspaces)}
|
|
45
|
+
</div>
|
|
46
|
+
<span class="item-arrow">›</span></a
|
|
47
|
+
>
|
|
48
|
+
</div>`;
|
|
49
|
+
export const renderTimelineCommitItem = (c, jiraBaseUrl, jiraPrefix) => {
|
|
50
|
+
const message = jiraBaseUrl && jiraPrefix ? linkJiraIds(c.message, jiraBaseUrl, jiraPrefix) : c.message;
|
|
51
|
+
return `<div class="timeline-item timeline-item--commit"><div class="commit-chip"><span class="commit-sha">${c.sha}</span><span class="commit-message">${message}</span><span class="commit-time" title="${c.timestamp}" data-date="${c.timestamp}">${c.relativeTime}</span></div></div>`;
|
|
52
|
+
};
|
|
53
|
+
export const renderTimelineTagItem = (t) => {
|
|
54
|
+
const formatted = new Date(t.timestamp)
|
|
55
|
+
.toLocaleString('de-DE', {
|
|
56
|
+
year: 'numeric',
|
|
57
|
+
month: '2-digit',
|
|
58
|
+
day: '2-digit',
|
|
59
|
+
hour: '2-digit',
|
|
60
|
+
minute: '2-digit',
|
|
61
|
+
second: '2-digit',
|
|
62
|
+
hour12: false,
|
|
63
|
+
})
|
|
64
|
+
.replace(',', '');
|
|
65
|
+
return `<div class="timeline-item timeline-item--tag"><div class="tag-chip"><span class="tag-chip-icon">🏷</span><span class="tag-chip-name">${t.name}</span><span class="tag-chip-date" title="${t.timestamp}">${formatted}</span></div></div>`;
|
|
66
|
+
};
|
|
67
|
+
export const paginationLinks = (currentPage, totalPages) => {
|
|
68
|
+
let links = '';
|
|
69
|
+
if (currentPage > 1) {
|
|
70
|
+
const prevPageLink = currentPage === 2 ? 'index.html' : `index-${currentPage - 1}.html`;
|
|
71
|
+
links += `<a href="./${prevPageLink}" class="pagination-link pagination-link--prev">← Previous Page</a>`;
|
|
72
|
+
}
|
|
73
|
+
if (currentPage < totalPages) {
|
|
74
|
+
const nextPageLink = `index-${currentPage + 1}.html`;
|
|
75
|
+
links += `<a href="./${nextPageLink}" class="pagination-link pagination-link--next">Next Page →</a>`;
|
|
76
|
+
}
|
|
77
|
+
return links ? `<nav class="pagination-nav">${links}</nav>` : '';
|
|
78
|
+
};
|
|
@@ -18,6 +18,8 @@ export interface TimelineTemplateProps {
|
|
|
18
18
|
aboutHtml: string;
|
|
19
19
|
jiraBaseUrl?: string;
|
|
20
20
|
jiraPrefix?: string;
|
|
21
|
+
currentPage?: number;
|
|
22
|
+
totalPages?: number;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Configuration for a month group in the timeline
|
|
@@ -40,6 +42,7 @@ export interface TimelineEntry {
|
|
|
40
42
|
dateEnd?: string;
|
|
41
43
|
displayDate: string;
|
|
42
44
|
sortTime: string;
|
|
45
|
+
monthGroup: string;
|
|
43
46
|
harness?: string;
|
|
44
47
|
llm?: string;
|
|
45
48
|
prompter?: string;
|
package/dist/lib/templates.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { navLink, renderTagsAndWorkspaces, renderCommits, renderTimelineEntryItem, renderTimelineCommitItem, renderTimelineTagItem, paginationLinks, } from './template-helpers.js';
|
|
2
2
|
const html = (strings, ...values) => {
|
|
3
3
|
return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
|
|
4
4
|
};
|
|
@@ -28,70 +28,8 @@ export const layout = ({ title, header, content, projectName, basePath = './', d
|
|
|
28
28
|
<footer>${buildMeta ? `<p class="build-time">${buildMeta}</p>` : ''}</footer>
|
|
29
29
|
<script src="${basePath}logbook.js"></script>
|
|
30
30
|
</body></html>`;
|
|
31
|
-
const navLink = (entry, dir) => {
|
|
32
|
-
const label = dir === 'prev' ? '← Previous' : 'Next →';
|
|
33
|
-
const t = `${entry.ticket ? `${entry.ticket}: ` : ''}${entry.title}`;
|
|
34
|
-
return `<a href="../${entry.slug}/index.html" class="post-nav-link"><span class="post-nav-label">${label}</span><span class="post-nav-title">${t}</span></a>`;
|
|
35
|
-
};
|
|
36
|
-
const renderWorkspaces = (workspaces) => {
|
|
37
|
-
if (!workspaces || workspaces.length === 0)
|
|
38
|
-
return '';
|
|
39
|
-
return workspaces.map((ws) => `<span class="tag-badge tag-workspace">${ws}</span>`).join('');
|
|
40
|
-
};
|
|
41
|
-
const renderTagsAndWorkspaces = (tags, workspaces) => {
|
|
42
|
-
const tagsHtml = tags && tags.length > 0
|
|
43
|
-
? tags.map((tag) => `<span class="tag-badge tag-${tag.replace('#', '')}">${tag}</span>`).join('')
|
|
44
|
-
: '';
|
|
45
|
-
const wsHtml = renderWorkspaces(workspaces);
|
|
46
|
-
if (!tagsHtml && !wsHtml)
|
|
47
|
-
return '';
|
|
48
|
-
return html `<div class="tags-wrapper">${tagsHtml}${wsHtml}</div>`;
|
|
49
|
-
};
|
|
50
|
-
// Renders a compact list of git commits to embed in the Technical Log tab.
|
|
51
|
-
const renderCommits = (commits) => {
|
|
52
|
-
if (!commits || commits.length === 0)
|
|
53
|
-
return '';
|
|
54
|
-
const rows = commits
|
|
55
|
-
.map((c) => `<div class="commit-item"><span class="commit-sha">${c.sha}</span><span class="commit-message">${c.message}</span><span class="commit-time" title="${c.timestamp}">${c.relativeTime}</span></div>`)
|
|
56
|
-
.join('');
|
|
57
|
-
return `<div class="commit-list"><h3 class="commit-list-heading">Git Commits</h3>${rows}</div>`;
|
|
58
|
-
};
|
|
59
|
-
const renderTimelineEntryItem = (e) => html `<div class="timeline-item" data-entry-slug="${e.slug}">
|
|
60
|
-
<a class="timeline-card" href="./${e.slug}/index.html"
|
|
61
|
-
><div class="item-content">
|
|
62
|
-
<div class="item-meta">
|
|
63
|
-
<span data-date="${e.dateStart}">${e.displayDate}</span> • <span class="sort-time">${e.sortTime}</span> •
|
|
64
|
-
${e.llm ? ` ${e.llm} via ` : ''}${e.harness}${e.prompter ? ` / ${e.prompter}` : ''}
|
|
65
|
-
</div>
|
|
66
|
-
<h3 class="item-title">
|
|
67
|
-
<span class="new-badge" style="display:none;">NEW</span>${e.ticket ? `${e.ticket}: ` : ''}${e.title}
|
|
68
|
-
</h3>
|
|
69
|
-
<div class="item-summary">${e.summary}</div>
|
|
70
|
-
${renderTagsAndWorkspaces(e.tags, e.workspaces)}
|
|
71
|
-
</div>
|
|
72
|
-
<span class="item-arrow">›</span></a
|
|
73
|
-
>
|
|
74
|
-
</div>`;
|
|
75
|
-
const renderTimelineCommitItem = (c, jiraBaseUrl, jiraPrefix) => {
|
|
76
|
-
const message = jiraBaseUrl && jiraPrefix ? linkJiraIds(c.message, jiraBaseUrl, jiraPrefix) : c.message;
|
|
77
|
-
return `<div class="timeline-item timeline-item--commit"><div class="commit-chip"><span class="commit-sha">${c.sha}</span><span class="commit-message">${message}</span><span class="commit-time" title="${c.timestamp}">${c.relativeTime}</span></div></div>`;
|
|
78
|
-
};
|
|
79
|
-
const renderTimelineTagItem = (t) => {
|
|
80
|
-
const formatted = new Date(t.timestamp)
|
|
81
|
-
.toLocaleString('de-DE', {
|
|
82
|
-
year: 'numeric',
|
|
83
|
-
month: '2-digit',
|
|
84
|
-
day: '2-digit',
|
|
85
|
-
hour: '2-digit',
|
|
86
|
-
minute: '2-digit',
|
|
87
|
-
second: '2-digit',
|
|
88
|
-
hour12: false,
|
|
89
|
-
})
|
|
90
|
-
.replace(',', '');
|
|
91
|
-
return `<div class="timeline-item timeline-item--tag"><div class="tag-chip"><span class="tag-chip-icon">🏷</span><span class="tag-chip-name">${t.name}</span><span class="tag-chip-date" title="${t.timestamp}">${formatted}</span></div></div>`;
|
|
92
|
-
};
|
|
93
31
|
export const timelineTemplate = (props) => {
|
|
94
|
-
const { projectName, groups, readmeHtml, aboutHtml, jiraBaseUrl, jiraPrefix } = props;
|
|
32
|
+
const { projectName, groups, readmeHtml, aboutHtml, jiraBaseUrl, jiraPrefix, currentPage, totalPages } = props;
|
|
95
33
|
const timelineTab = html `<div class="timeline">
|
|
96
34
|
${groups
|
|
97
35
|
.map((g) => html `<section class="month-group">
|
|
@@ -107,9 +45,10 @@ export const timelineTemplate = (props) => {
|
|
|
107
45
|
.join('')}
|
|
108
46
|
</section>`)
|
|
109
47
|
.join('')}
|
|
48
|
+
${paginationLinks(currentPage, totalPages)}
|
|
110
49
|
</div>`;
|
|
111
50
|
return html `<header>
|
|
112
|
-
<h1>${projectName}</h1>
|
|
51
|
+
<h1><a href="./index.html">${projectName}</a></h1>
|
|
113
52
|
<p class="tagline">A brief summary of the recent changes to the project.</p>
|
|
114
53
|
</header>
|
|
115
54
|
${tabsComponent([
|
|
@@ -42,7 +42,23 @@ const linter = {
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
// 3.
|
|
45
|
+
// 3. Validate specific placeholder values
|
|
46
|
+
const placeholderFields = ['prompter', 'harness', 'llm', 'summary'];
|
|
47
|
+
for (const field of placeholderFields) {
|
|
48
|
+
const value = frontmatter[field];
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
for (const item of value) {
|
|
51
|
+
if (typeof item === 'string' && (item === field.toUpperCase() || item === 'WRITE_SUMMARY_HERE')) {
|
|
52
|
+
issues.push({
|
|
53
|
+
level: 'error',
|
|
54
|
+
category: 'frontmatter',
|
|
55
|
+
message: `Placeholder value "${item}" found in field "${field}". Please replace with actual content.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// 4. Tags validation
|
|
46
62
|
const allowedTags = config.tags?.allowed || [];
|
|
47
63
|
if (!frontmatter.tags || !Array.isArray(frontmatter.tags) || frontmatter.tags.length === 0) {
|
|
48
64
|
issues.push({
|
|
@@ -62,6 +78,10 @@ const linter = {
|
|
|
62
78
|
}
|
|
63
79
|
}
|
|
64
80
|
}
|
|
81
|
+
// TODO: Add conditional validation for body placeholders based on isDraft
|
|
82
|
+
// Currently the isDraft only checks frontmatter data for placeholders.
|
|
83
|
+
// If the body contains "TODO:" and isDraft is true, the current logic is to skip.
|
|
84
|
+
// However, if we want to lint placeholder values even in draft mode, we need a separate body linter.
|
|
65
85
|
return issues;
|
|
66
86
|
},
|
|
67
87
|
};
|
|
@@ -14,8 +14,6 @@ const linter = {
|
|
|
14
14
|
if (context.indexPath && (await fs.pathExists(context.indexPath))) {
|
|
15
15
|
rawFile = await fs.readFile(context.indexPath, 'utf8');
|
|
16
16
|
}
|
|
17
|
-
// Scan only the frontmatter block for bracket-style placeholders to avoid false
|
|
18
|
-
// positives when placeholder names are mentioned in the narrative body.
|
|
19
17
|
const frontmatterMatch = rawFile.match(/^---\n[\s\S]*?\n---/);
|
|
20
18
|
const frontmatterBlock = frontmatterMatch ? frontmatterMatch[0] : rawFile;
|
|
21
19
|
const frontmatterPlaceholders = [
|
|
@@ -37,7 +35,7 @@ const linter = {
|
|
|
37
35
|
});
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
|
-
// Check boilerplate body text against the full content (body only is fine here).
|
|
38
|
+
// Check boilerplate body text against the full content (body only is fine here) for all entries.
|
|
41
39
|
if (context.content.includes('TODO: ')) {
|
|
42
40
|
issues.push({
|
|
43
41
|
level: 'error',
|
|
@@ -37,10 +37,10 @@ As you code, keep the `log.md` updated in real-time using the `logbook log` comm
|
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
logbook log "Investigated root cause — found issue in src/lib/config.ts"
|
|
40
|
-
logbook log "Fixed the bug"
|
|
40
|
+
logbook log "Fixed the bug"
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
Each message is automatically prefixed with an ISO timestamp and appended as a bullet to the active `log.md`.
|
|
43
|
+
Each message is automatically prefixed with an ISO timestamp and appended as a bullet to the active `log.md`. One message per call. If you encounter a bug or change your design, log it immediately. This is the most valuable file for future debugging.
|
|
44
44
|
|
|
45
45
|
Only update the currently active logbook entry. Do not edit other existing entries while working.
|
|
46
46
|
|
package/dist/templates/steer.txt
CHANGED
|
@@ -11,8 +11,8 @@ Phase 2: Execute & Trace
|
|
|
11
11
|
1. Use `log.md` as a live technical scratchpad.
|
|
12
12
|
2. Log your work in real-time using the `logbook log "<message>"` command. Record every major decision, error, and pivot. Do not reconstruct this at the end.
|
|
13
13
|
Example: logbook log "Investigated root cause — found issue in src/lib/config.ts"
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
One message per call. For multiple entries, call the command once per message.
|
|
15
|
+
3. Stick to the active entry; never modify past entries in the logbook folder.
|
|
16
16
|
|
|
17
17
|
Phase 3: Synthesize
|
|
18
18
|
1. When implementation is finished, write the narrative in `index.md`.
|
|
@@ -11,6 +11,11 @@ body {
|
|
|
11
11
|
padding: 0;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
.home-link {
|
|
15
|
+
text-decoration: none;
|
|
16
|
+
color: inherit;
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
header {
|
|
15
20
|
background: var(--card-bg);
|
|
16
21
|
border-bottom: 1px solid var(--border);
|
|
@@ -25,6 +30,11 @@ header h1 {
|
|
|
25
30
|
letter-spacing: -0.025em;
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
header h1 a {
|
|
34
|
+
text-decoration: none !important;
|
|
35
|
+
color: black !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
.tagline {
|
|
29
39
|
color: var(--text-muted);
|
|
30
40
|
font-size: 1.125rem;
|
|
@@ -273,6 +283,44 @@ article h1 {
|
|
|
273
283
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
274
284
|
}
|
|
275
285
|
|
|
286
|
+
/* Pagination Links */
|
|
287
|
+
.pagination-nav {
|
|
288
|
+
display: flex;
|
|
289
|
+
justify-content: space-between;
|
|
290
|
+
margin-top: 2rem;
|
|
291
|
+
padding-top: 1.5rem;
|
|
292
|
+
border-top: 1px solid var(--border);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.pagination-link {
|
|
296
|
+
display: inline-flex;
|
|
297
|
+
align-items: center;
|
|
298
|
+
gap: 0.5rem;
|
|
299
|
+
font-size: 0.9375rem;
|
|
300
|
+
font-weight: 500;
|
|
301
|
+
color: var(--primary);
|
|
302
|
+
text-decoration: none;
|
|
303
|
+
padding: 0.5rem 1rem;
|
|
304
|
+
border-radius: 2rem;
|
|
305
|
+
transition:
|
|
306
|
+
background 0.2s,
|
|
307
|
+
color 0.2s;
|
|
308
|
+
background: var(--primary-soft);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.pagination-link:hover {
|
|
312
|
+
background: var(--primary);
|
|
313
|
+
color: white;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.pagination-link--prev {
|
|
317
|
+
margin-right: auto;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.pagination-link--next {
|
|
321
|
+
margin-left: auto;
|
|
322
|
+
}
|
|
323
|
+
|
|
276
324
|
.content-section {
|
|
277
325
|
animation: fadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
278
326
|
font-size: 1.125rem;
|
|
@@ -286,6 +334,10 @@ article h1 {
|
|
|
286
334
|
margin-bottom: 1rem;
|
|
287
335
|
}
|
|
288
336
|
|
|
337
|
+
.content-section img {
|
|
338
|
+
max-width: 100%;
|
|
339
|
+
}
|
|
340
|
+
|
|
289
341
|
@keyframes fadeIn {
|
|
290
342
|
from {
|
|
291
343
|
opacity: 0;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TimelineEntry, TimelineCommitItem, TimelineTagItem, TimelineGroup } from '../lib/template-types.js';
|
|
2
|
+
export declare function groupTimelineItems(entries: (TimelineEntry & {
|
|
3
|
+
monthGroup: string;
|
|
4
|
+
})[], commits: TimelineCommitItem[], tags: TimelineTagItem[]): TimelineGroup[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function groupTimelineItems(entries, commits, tags) {
|
|
2
|
+
const toMs = (item) => {
|
|
3
|
+
if (item.kind === 'entry')
|
|
4
|
+
return item.dateEnd ? new Date(item.dateEnd).getTime() : new Date(item.dateStart).getTime();
|
|
5
|
+
return new Date(item.timestamp).getTime();
|
|
6
|
+
};
|
|
7
|
+
const all = [...entries, ...commits, ...tags];
|
|
8
|
+
all.sort((a, b) => toMs(b) - toMs(a));
|
|
9
|
+
const groups = [];
|
|
10
|
+
for (const item of all) {
|
|
11
|
+
let group = groups.find((g) => g.month === item.monthGroup);
|
|
12
|
+
if (!group) {
|
|
13
|
+
group = { month: item.monthGroup, items: [] };
|
|
14
|
+
groups.push(group);
|
|
15
|
+
}
|
|
16
|
+
group.items.push(item);
|
|
17
|
+
}
|
|
18
|
+
return groups;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-logbook",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "A command-line tool for project logbooks.",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"demo-app"
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"rehype-format": "^5.0.1",
|
|
62
62
|
"rehype-slug": "^6.0.0",
|
|
63
63
|
"rehype-stringify": "^10.0.1",
|
|
64
|
+
"remark-gfm": "^4.0.1",
|
|
64
65
|
"remark-parse": "^11.0.0",
|
|
65
66
|
"remark-rehype": "^11.1.2",
|
|
66
67
|
"simple-git": "^3.36.0",
|
|
@@ -37,10 +37,10 @@ As you code, keep the `log.md` updated in real-time using the `logbook log` comm
|
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
logbook log "Investigated root cause — found issue in src/lib/config.ts"
|
|
40
|
-
logbook log "Fixed the bug"
|
|
40
|
+
logbook log "Fixed the bug"
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
Each message is automatically prefixed with an ISO timestamp and appended as a bullet to the active `log.md`.
|
|
43
|
+
Each message is automatically prefixed with an ISO timestamp and appended as a bullet to the active `log.md`. One message per call. If you encounter a bug or change your design, log it immediately. This is the most valuable file for future debugging.
|
|
44
44
|
|
|
45
45
|
Only update the currently active logbook entry. Do not edit other existing entries while working.
|
|
46
46
|
|
package/src/templates/steer.txt
CHANGED
|
@@ -11,8 +11,8 @@ Phase 2: Execute & Trace
|
|
|
11
11
|
1. Use `log.md` as a live technical scratchpad.
|
|
12
12
|
2. Log your work in real-time using the `logbook log "<message>"` command. Record every major decision, error, and pivot. Do not reconstruct this at the end.
|
|
13
13
|
Example: logbook log "Investigated root cause — found issue in src/lib/config.ts"
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
One message per call. For multiple entries, call the command once per message.
|
|
15
|
+
3. Stick to the active entry; never modify past entries in the logbook folder.
|
|
16
16
|
|
|
17
17
|
Phase 3: Synthesize
|
|
18
18
|
1. When implementation is finished, write the narrative in `index.md`.
|
package/src/templates/styles.css
CHANGED
|
@@ -11,6 +11,11 @@ body {
|
|
|
11
11
|
padding: 0;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
.home-link {
|
|
15
|
+
text-decoration: none;
|
|
16
|
+
color: inherit;
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
header {
|
|
15
20
|
background: var(--card-bg);
|
|
16
21
|
border-bottom: 1px solid var(--border);
|
|
@@ -25,6 +30,11 @@ header h1 {
|
|
|
25
30
|
letter-spacing: -0.025em;
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
header h1 a {
|
|
34
|
+
text-decoration: none !important;
|
|
35
|
+
color: black !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
.tagline {
|
|
29
39
|
color: var(--text-muted);
|
|
30
40
|
font-size: 1.125rem;
|
|
@@ -273,6 +283,44 @@ article h1 {
|
|
|
273
283
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
274
284
|
}
|
|
275
285
|
|
|
286
|
+
/* Pagination Links */
|
|
287
|
+
.pagination-nav {
|
|
288
|
+
display: flex;
|
|
289
|
+
justify-content: space-between;
|
|
290
|
+
margin-top: 2rem;
|
|
291
|
+
padding-top: 1.5rem;
|
|
292
|
+
border-top: 1px solid var(--border);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.pagination-link {
|
|
296
|
+
display: inline-flex;
|
|
297
|
+
align-items: center;
|
|
298
|
+
gap: 0.5rem;
|
|
299
|
+
font-size: 0.9375rem;
|
|
300
|
+
font-weight: 500;
|
|
301
|
+
color: var(--primary);
|
|
302
|
+
text-decoration: none;
|
|
303
|
+
padding: 0.5rem 1rem;
|
|
304
|
+
border-radius: 2rem;
|
|
305
|
+
transition:
|
|
306
|
+
background 0.2s,
|
|
307
|
+
color 0.2s;
|
|
308
|
+
background: var(--primary-soft);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.pagination-link:hover {
|
|
312
|
+
background: var(--primary);
|
|
313
|
+
color: white;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.pagination-link--prev {
|
|
317
|
+
margin-right: auto;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.pagination-link--next {
|
|
321
|
+
margin-left: auto;
|
|
322
|
+
}
|
|
323
|
+
|
|
276
324
|
.content-section {
|
|
277
325
|
animation: fadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
278
326
|
font-size: 1.125rem;
|
|
@@ -286,6 +334,10 @@ article h1 {
|
|
|
286
334
|
margin-bottom: 1rem;
|
|
287
335
|
}
|
|
288
336
|
|
|
337
|
+
.content-section img {
|
|
338
|
+
max-width: 100%;
|
|
339
|
+
}
|
|
340
|
+
|
|
289
341
|
@keyframes fadeIn {
|
|
290
342
|
from {
|
|
291
343
|
opacity: 0;
|