@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,95 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
2
|
+
import { DEVLOG_CONFIG as DEFAULT_CONFIG } from './devlog-config.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse YAML-ish frontmatter from a markdown string.
|
|
6
|
+
* Returns { metadata: { title, date, project, summary }, body: string }
|
|
7
|
+
*/
|
|
8
|
+
function parseFrontmatter(raw) {
|
|
9
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
10
|
+
if (!match) return { metadata: {}, body: raw };
|
|
11
|
+
|
|
12
|
+
const frontmatter = match[1];
|
|
13
|
+
const body = match[2];
|
|
14
|
+
const metadata = {};
|
|
15
|
+
|
|
16
|
+
for (const line of frontmatter.split('\n')) {
|
|
17
|
+
const m = line.match(/^(\w+)\s*:\s*"?([^"]*)"?\s*$/);
|
|
18
|
+
if (m) metadata[m[1]] = m[2].trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { metadata, body };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hook for fetching dev log entries from a daily-dev-log repo on GitHub.
|
|
26
|
+
*
|
|
27
|
+
* Defaults to DEVLOG_CONFIG from ./devlog-config.js — pass `configOverride`
|
|
28
|
+
* to inject runtime values (used by the preview app, generally not needed).
|
|
29
|
+
*
|
|
30
|
+
* @param {string} project - Project folder name (e.g. "myproject")
|
|
31
|
+
* @param {object} [configOverride] - { repoOwner, repoName, branch, baseUrl? }
|
|
32
|
+
* @returns {{ entries, loadedContent, loading, error, fetchEntryContent, retry }}
|
|
33
|
+
*/
|
|
34
|
+
export function useDevLogEntries(project, configOverride) {
|
|
35
|
+
const config = useMemo(() => {
|
|
36
|
+
const merged = { ...DEFAULT_CONFIG, ...(configOverride || {}) };
|
|
37
|
+
if (!configOverride?.baseUrl) {
|
|
38
|
+
merged.baseUrl = `https://raw.githubusercontent.com/${merged.repoOwner}/${merged.repoName}/${merged.branch}`;
|
|
39
|
+
}
|
|
40
|
+
return merged;
|
|
41
|
+
}, [configOverride]);
|
|
42
|
+
|
|
43
|
+
const [entries, setEntries] = useState([]);
|
|
44
|
+
const [loading, setLoading] = useState(true);
|
|
45
|
+
const [error, setError] = useState(null);
|
|
46
|
+
const contentCache = useRef(new Map());
|
|
47
|
+
const [loadedContent, setLoadedContent] = useState(new Map());
|
|
48
|
+
|
|
49
|
+
const fetchManifest = useCallback(async () => {
|
|
50
|
+
setLoading(true);
|
|
51
|
+
setError(null);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const url = `${config.baseUrl}/${project}/manifest.json`;
|
|
55
|
+
const res = await fetch(url);
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
if (res.status === 404) {
|
|
58
|
+
setEntries([]);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Failed to fetch manifest (${res.status})`);
|
|
62
|
+
}
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
setEntries(data.entries || []);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
setError(err.message);
|
|
67
|
+
} finally {
|
|
68
|
+
setLoading(false);
|
|
69
|
+
}
|
|
70
|
+
}, [project, config]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
fetchManifest();
|
|
74
|
+
}, [fetchManifest]);
|
|
75
|
+
|
|
76
|
+
const fetchEntryContent = useCallback(async (filename) => {
|
|
77
|
+
if (contentCache.current.has(filename)) return;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const url = `${config.baseUrl}/${project}/${filename}`;
|
|
81
|
+
const res = await fetch(url);
|
|
82
|
+
if (!res.ok) throw new Error(`Failed to fetch entry (${res.status})`);
|
|
83
|
+
const raw = await res.text();
|
|
84
|
+
const { body } = parseFrontmatter(raw);
|
|
85
|
+
|
|
86
|
+
contentCache.current.set(filename, body);
|
|
87
|
+
setLoadedContent(new Map(contentCache.current));
|
|
88
|
+
} catch (err) {
|
|
89
|
+
contentCache.current.set(filename, `*Error loading entry: ${err.message}*`);
|
|
90
|
+
setLoadedContent(new Map(contentCache.current));
|
|
91
|
+
}
|
|
92
|
+
}, [project, config]);
|
|
93
|
+
|
|
94
|
+
return { entries, loadedContent, loading, error, fetchEntryContent, retry: fetchManifest };
|
|
95
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@natjswenson/devlog",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Daily dev log generator — Claude Code skill + preview app for publishing git-based dev logs to your site",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Nate Swenson",
|
|
7
|
+
"homepage": "https://github.com/natejswenson/devlog",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/natejswenson/devlog.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/natejswenson/devlog/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"devlog",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"claude-skill",
|
|
19
|
+
"build-in-public",
|
|
20
|
+
"git",
|
|
21
|
+
"blog"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"bin": {
|
|
25
|
+
"devlog": "bin/devlog.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin/",
|
|
29
|
+
"preview/",
|
|
30
|
+
"examples/",
|
|
31
|
+
"SKILL.md",
|
|
32
|
+
"config.example.json",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
41
|
+
"kleur": "^4.1.5",
|
|
42
|
+
"prompts": "^2.4.2",
|
|
43
|
+
"react": "^18.3.0",
|
|
44
|
+
"react-dom": "^18.3.0",
|
|
45
|
+
"react-markdown": "^9.0.0",
|
|
46
|
+
"remark-gfm": "^4.0.0",
|
|
47
|
+
"vite": "^5.4.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/preview/App.jsx
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import DevLogPage from '../examples/react/DevLogPage.jsx';
|
|
3
|
+
import { DEMO_BASE, DEMO_PROJECTS, DEMO_PROJECT_KEY } from './demo.js';
|
|
4
|
+
|
|
5
|
+
const owner = import.meta.env.VITE_DEVLOG_OWNER;
|
|
6
|
+
const repo = import.meta.env.VITE_DEVLOG_REPO;
|
|
7
|
+
const branch = import.meta.env.VITE_DEVLOG_BRANCH || 'main';
|
|
8
|
+
const projectsRaw = import.meta.env.VITE_DEVLOG_PROJECTS;
|
|
9
|
+
|
|
10
|
+
const isDemo = !owner || !repo;
|
|
11
|
+
|
|
12
|
+
let realProjects = [];
|
|
13
|
+
try {
|
|
14
|
+
realProjects = projectsRaw ? JSON.parse(projectsRaw) : [];
|
|
15
|
+
} catch {
|
|
16
|
+
realProjects = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const config = isDemo
|
|
20
|
+
? { repoOwner: 'demo', repoName: 'demo', branch: 'main', baseUrl: DEMO_BASE }
|
|
21
|
+
: { repoOwner: owner, repoName: repo, branch };
|
|
22
|
+
|
|
23
|
+
const projects = isDemo
|
|
24
|
+
? DEMO_PROJECTS
|
|
25
|
+
: (realProjects.length > 0 ? realProjects : null);
|
|
26
|
+
|
|
27
|
+
function DemoBanner() {
|
|
28
|
+
return (
|
|
29
|
+
<div className="demo-banner" role="alert">
|
|
30
|
+
<div className="demo-banner__row">
|
|
31
|
+
<strong>👋 demo mode</strong>
|
|
32
|
+
<span>
|
|
33
|
+
You're looking at fake entries. Wipe this and point at your real dev log:
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
<pre className="demo-banner__code">npx @natjswenson/devlog init</pre>
|
|
37
|
+
<div className="demo-banner__small">
|
|
38
|
+
Or set <code>VITE_DEVLOG_OWNER</code>, <code>VITE_DEVLOG_REPO</code>, <code>VITE_DEVLOG_PROJECTS</code> in <code>preview/.env.local</code> and restart vite.
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function NoProjectsScreen() {
|
|
45
|
+
return (
|
|
46
|
+
<div className="empty-screen">
|
|
47
|
+
<h1>You found the preview, congratulations.</h1>
|
|
48
|
+
<p>
|
|
49
|
+
Env vars say you have a repo (<code>{owner}/{repo}</code>) but no projects.
|
|
50
|
+
That's like buying a stage and forgetting to invite a band.
|
|
51
|
+
</p>
|
|
52
|
+
<p>Add at least one project to your config:</p>
|
|
53
|
+
<pre>{`{
|
|
54
|
+
"projects": [
|
|
55
|
+
{ "key": "myproject", "path": "...", "remote": "${owner}/myproject" }
|
|
56
|
+
]
|
|
57
|
+
}`}</pre>
|
|
58
|
+
<p>
|
|
59
|
+
Then run <code>npx @natjswenson/devlog preview</code> again, or set
|
|
60
|
+
<code> VITE_DEVLOG_PROJECTS</code> directly in <code>preview/.env.local</code>.
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default function App() {
|
|
67
|
+
const [activeKey, setActiveKey] = useState((projects && projects[0]?.key) || DEMO_PROJECT_KEY);
|
|
68
|
+
|
|
69
|
+
if (!isDemo && !projects) {
|
|
70
|
+
return <NoProjectsScreen />;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
{isDemo && <DemoBanner />}
|
|
76
|
+
<DevLogPage
|
|
77
|
+
project={activeKey}
|
|
78
|
+
projects={projects}
|
|
79
|
+
config={config}
|
|
80
|
+
onProjectChange={setActiveKey}
|
|
81
|
+
/>
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# preview/
|
|
2
|
+
|
|
3
|
+
Standalone Vite app that renders your published dev log locally.
|
|
4
|
+
|
|
5
|
+
## Run via CLI (recommended)
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx @natjswenson/devlog preview
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The CLI reads `~/.claude/skills/devlog/config.json` and passes the values to Vite via env vars.
|
|
12
|
+
|
|
13
|
+
## Deploy as a standalone site
|
|
14
|
+
|
|
15
|
+
If you don't have a personal site yet, this directory is a complete React/Vite app you can deploy as your dev log site.
|
|
16
|
+
|
|
17
|
+
1. Set env vars (Vercel/Netlify/Cloudflare Pages — wherever):
|
|
18
|
+
- `VITE_DEVLOG_OWNER` = your GitHub username
|
|
19
|
+
- `VITE_DEVLOG_REPO` = your dev-log repo name (e.g. `daily-dev-log`)
|
|
20
|
+
- `VITE_DEVLOG_BRANCH` = `main` (or `master`)
|
|
21
|
+
- `VITE_DEVLOG_PROJECTS` = JSON array, e.g. `[{"key":"myproject","label":"My Project"}]`
|
|
22
|
+
2. Build command: `vite build` (root: this directory)
|
|
23
|
+
3. Output: `dist/`
|
|
24
|
+
|
|
25
|
+
For local-only dev (without the CLI), set the same env vars in a `.env.local` next to this `vite.config.js` and run `vite`.
|
package/preview/demo.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo mode for the preview app.
|
|
3
|
+
*
|
|
4
|
+
* When the user runs the preview without env vars (or without going through
|
|
5
|
+
* `npx @natjswenson/devlog init`), we render fake entries against a
|
|
6
|
+
* pretend GitHub URL. The real DevLogPage component does its normal thing;
|
|
7
|
+
* a tiny fetch override intercepts requests to the demo URL and serves
|
|
8
|
+
* canned data.
|
|
9
|
+
*
|
|
10
|
+
* Goal: show the user what their dev log will look like, while loudly
|
|
11
|
+
* encouraging them to wipe this out and point it at their real repo.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const DEMO_BASE = 'https://demo-mode.local';
|
|
15
|
+
export const DEMO_PROJECT_KEY = 'placeholder-empire';
|
|
16
|
+
export const DEMO_PROJECTS = [
|
|
17
|
+
{ key: DEMO_PROJECT_KEY, label: 'The Placeholder Empire' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function offsetDate(daysAgo) {
|
|
21
|
+
const d = new Date();
|
|
22
|
+
d.setDate(d.getDate() - daysAgo);
|
|
23
|
+
return d.toISOString().slice(0, 10);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ENTRIES = [
|
|
27
|
+
{
|
|
28
|
+
date: offsetDate(0),
|
|
29
|
+
title: 'You are looking at fake data',
|
|
30
|
+
summary: "Hi. These aren't your entries. Your entries are 30 seconds away.",
|
|
31
|
+
body: `## What I Built
|
|
32
|
+
|
|
33
|
+
Nothing. I'm a placeholder. A handsome one, but still a placeholder.
|
|
34
|
+
|
|
35
|
+
You're seeing this screen because the preview app couldn't find env vars
|
|
36
|
+
pointing at your dev-log repo. Once that's fixed, this entire feed gets
|
|
37
|
+
replaced with real entries, generated by the \`/devlog\` skill from your
|
|
38
|
+
actual git commits.
|
|
39
|
+
|
|
40
|
+
## What's Next
|
|
41
|
+
|
|
42
|
+
You. Setting things up. Probably while half-watching a YouTube tutorial
|
|
43
|
+
about something unrelated. We believe in you.
|
|
44
|
+
|
|
45
|
+
## Public Commits
|
|
46
|
+
|
|
47
|
+
- [demo] make placeholder more passive-aggressive ([abcd123](#))
|
|
48
|
+
- [demo] add a tiny bit of charm ([def4567](#))
|
|
49
|
+
`,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
date: offsetDate(1),
|
|
53
|
+
title: 'How to make this screen go away',
|
|
54
|
+
summary: 'Two paths: the lazy one (recommended) and the manual one (also fine).',
|
|
55
|
+
body: `## The lazy path
|
|
56
|
+
|
|
57
|
+
\`\`\`sh
|
|
58
|
+
npx @natjswenson/devlog init
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
Answer four prompts. The CLI creates your dev-log repo on GitHub, installs
|
|
62
|
+
the Claude Code skill, and writes your config. Run \`npx @natjswenson/devlog preview\`
|
|
63
|
+
again. This entry vanishes. You feel powerful.
|
|
64
|
+
|
|
65
|
+
## The manual path
|
|
66
|
+
|
|
67
|
+
Set these in \`preview/.env.local\` (or your shell), then restart vite:
|
|
68
|
+
|
|
69
|
+
\`\`\`
|
|
70
|
+
VITE_DEVLOG_OWNER=your-github-username
|
|
71
|
+
VITE_DEVLOG_REPO=daily-dev-log
|
|
72
|
+
VITE_DEVLOG_BRANCH=main
|
|
73
|
+
VITE_DEVLOG_PROJECTS=[{"key":"myproject","label":"My Project"}]
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
## What's Next
|
|
77
|
+
|
|
78
|
+
The real preview, with your real entries. Try it.
|
|
79
|
+
`,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
date: offsetDate(3),
|
|
83
|
+
title: "Why you'd actually want this",
|
|
84
|
+
summary: 'Build in public, but with style. And without remembering to write blog posts.',
|
|
85
|
+
body: `## What I Built
|
|
86
|
+
|
|
87
|
+
The whole point: you commit code as usual. You run \`/devlog\` in Claude Code.
|
|
88
|
+
The skill reads today's commits and writes a *narrative* entry — not "fix typo,
|
|
89
|
+
fix typo again, ok actually fix it" but real prose about what you built and why.
|
|
90
|
+
|
|
91
|
+
That entry gets pushed to your dev-log repo. Your site (or this preview app,
|
|
92
|
+
deployed to Vercel/Netlify/Cloudflare) renders it.
|
|
93
|
+
|
|
94
|
+
The result: you ship in public without ever opening a blog post editor.
|
|
95
|
+
|
|
96
|
+
## What's Next
|
|
97
|
+
|
|
98
|
+
You'll set this up. You'll ship something on day one. You'll feel slightly
|
|
99
|
+
smug about it on the train tomorrow. We're rooting for you.
|
|
100
|
+
|
|
101
|
+
## Public Commits
|
|
102
|
+
|
|
103
|
+
- [demo] write hopeful pep talk ([eeee101](#))
|
|
104
|
+
`,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
date: offsetDate(7),
|
|
108
|
+
title: "Things this is not",
|
|
109
|
+
summary: 'A short list, for the avoidance of disappointment.',
|
|
110
|
+
body: `## Not features
|
|
111
|
+
|
|
112
|
+
- A blog CMS. There are sixty of those. Use one if you want one.
|
|
113
|
+
- A social network. Please don't.
|
|
114
|
+
- An AI ghostwriter for marketing copy. The narratives come from *your*
|
|
115
|
+
commits. Garbage in, garbage out.
|
|
116
|
+
- A way to make your past coding choices look better in retrospect. Sorry.
|
|
117
|
+
|
|
118
|
+
## Is features
|
|
119
|
+
|
|
120
|
+
- A way to ship dev log entries without context-switching out of Claude Code.
|
|
121
|
+
- A static, no-backend pipeline (manifest.json + markdown on GitHub).
|
|
122
|
+
- Components you can drop into your own React site, or the preview app you
|
|
123
|
+
can deploy as a standalone dev log.
|
|
124
|
+
|
|
125
|
+
## What's Next
|
|
126
|
+
|
|
127
|
+
Replace this fake entry with a real one. It's right there. Just go.
|
|
128
|
+
`,
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const MANIFEST = {
|
|
133
|
+
entries: ENTRIES.map((e) => ({
|
|
134
|
+
date: e.date,
|
|
135
|
+
file: `${e.date}.md`,
|
|
136
|
+
title: e.title,
|
|
137
|
+
summary: e.summary,
|
|
138
|
+
})),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
function entryMarkdown(entry) {
|
|
142
|
+
return `---
|
|
143
|
+
title: "${entry.title.replace(/"/g, '\\"')}"
|
|
144
|
+
date: ${entry.date}
|
|
145
|
+
project: ${DEMO_PROJECT_KEY}
|
|
146
|
+
summary: "${entry.summary.replace(/"/g, '\\"')}"
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
${entry.body}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function demoResponse(url) {
|
|
153
|
+
if (url.endsWith('manifest.json')) {
|
|
154
|
+
return new Response(JSON.stringify(MANIFEST), {
|
|
155
|
+
status: 200,
|
|
156
|
+
headers: { 'content-type': 'application/json' },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const m = url.match(/(\d{4}-\d{2}-\d{2})\.md$/);
|
|
160
|
+
if (m) {
|
|
161
|
+
const entry = ENTRIES.find((e) => e.date === m[1]);
|
|
162
|
+
if (entry) {
|
|
163
|
+
return new Response(entryMarkdown(entry), {
|
|
164
|
+
status: 200,
|
|
165
|
+
headers: { 'content-type': 'text/plain' },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return new Response('demo: not found', { status: 404 });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function installDemoFetch() {
|
|
173
|
+
const original = window.fetch.bind(window);
|
|
174
|
+
window.fetch = async (input, init) => {
|
|
175
|
+
const url = typeof input === 'string' ? input : input?.url || String(input);
|
|
176
|
+
if (url.startsWith(DEMO_BASE)) {
|
|
177
|
+
return demoResponse(url);
|
|
178
|
+
}
|
|
179
|
+
return original(input, init);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>devlog preview</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
10
|
+
}
|
|
11
|
+
body { margin: 0; }
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div id="root"></div>
|
|
16
|
+
<script type="module" src="/main.jsx"></script>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
package/preview/main.jsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import App from './App.jsx';
|
|
4
|
+
import { installDemoFetch } from './demo.js';
|
|
5
|
+
import './preview.css';
|
|
6
|
+
|
|
7
|
+
if (!import.meta.env.VITE_DEVLOG_OWNER || !import.meta.env.VITE_DEVLOG_REPO) {
|
|
8
|
+
installDemoFetch();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
createRoot(document.getElementById('root')).render(
|
|
12
|
+
<React.StrictMode>
|
|
13
|
+
<App />
|
|
14
|
+
</React.StrictMode>
|
|
15
|
+
);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/* preview-app chrome — demo banner + empty states. The DevLogPage
|
|
2
|
+
* component brings its own styles. Keep this file tiny.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
.demo-banner {
|
|
6
|
+
margin: 16px auto 0;
|
|
7
|
+
max-width: 720px;
|
|
8
|
+
padding: 16px 20px;
|
|
9
|
+
border: 1px dashed #d97706;
|
|
10
|
+
background: #fffbeb;
|
|
11
|
+
color: #78350f;
|
|
12
|
+
border-radius: 8px;
|
|
13
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
14
|
+
font-size: 13px;
|
|
15
|
+
line-height: 1.5;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@media (prefers-color-scheme: dark) {
|
|
19
|
+
.demo-banner {
|
|
20
|
+
background: rgba(217, 119, 6, 0.08);
|
|
21
|
+
color: #fcd34d;
|
|
22
|
+
border-color: #b45309;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.demo-banner__row {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: baseline;
|
|
29
|
+
gap: 12px;
|
|
30
|
+
flex-wrap: wrap;
|
|
31
|
+
margin-bottom: 10px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.demo-banner__row strong {
|
|
35
|
+
font-size: 13px;
|
|
36
|
+
letter-spacing: -0.01em;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.demo-banner__code {
|
|
40
|
+
margin: 0 0 8px;
|
|
41
|
+
padding: 8px 12px;
|
|
42
|
+
background: rgba(0, 0, 0, 0.04);
|
|
43
|
+
border-radius: 4px;
|
|
44
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
45
|
+
font-size: 13px;
|
|
46
|
+
color: inherit;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@media (prefers-color-scheme: dark) {
|
|
50
|
+
.demo-banner__code {
|
|
51
|
+
background: rgba(255, 255, 255, 0.06);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.demo-banner__small {
|
|
56
|
+
font-size: 12px;
|
|
57
|
+
opacity: 0.85;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.demo-banner code {
|
|
61
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
background: rgba(0, 0, 0, 0.05);
|
|
64
|
+
border-radius: 3px;
|
|
65
|
+
padding: 1px 5px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@media (prefers-color-scheme: dark) {
|
|
69
|
+
.demo-banner code {
|
|
70
|
+
background: rgba(255, 255, 255, 0.08);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.empty-screen {
|
|
75
|
+
max-width: 640px;
|
|
76
|
+
margin: 80px auto;
|
|
77
|
+
padding: 24px;
|
|
78
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
79
|
+
color: #1a1a1a;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@media (prefers-color-scheme: dark) {
|
|
83
|
+
.empty-screen {
|
|
84
|
+
color: #e8e8e8;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.empty-screen h1 {
|
|
89
|
+
font-size: 22px;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
letter-spacing: -0.02em;
|
|
92
|
+
margin: 0 0 16px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.empty-screen p {
|
|
96
|
+
font-size: 15px;
|
|
97
|
+
line-height: 1.6;
|
|
98
|
+
margin: 0 0 12px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.empty-screen code {
|
|
102
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
103
|
+
font-size: 13px;
|
|
104
|
+
background: rgba(0, 0, 0, 0.05);
|
|
105
|
+
border-radius: 4px;
|
|
106
|
+
padding: 2px 6px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@media (prefers-color-scheme: dark) {
|
|
110
|
+
.empty-screen code {
|
|
111
|
+
background: rgba(255, 255, 255, 0.08);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.empty-screen pre {
|
|
116
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
117
|
+
font-size: 13px;
|
|
118
|
+
line-height: 1.5;
|
|
119
|
+
background: rgba(0, 0, 0, 0.04);
|
|
120
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
121
|
+
border-radius: 6px;
|
|
122
|
+
padding: 14px 16px;
|
|
123
|
+
overflow-x: auto;
|
|
124
|
+
margin: 0 0 12px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@media (prefers-color-scheme: dark) {
|
|
128
|
+
.empty-screen pre {
|
|
129
|
+
background: rgba(255, 255, 255, 0.04);
|
|
130
|
+
border-color: rgba(255, 255, 255, 0.08);
|
|
131
|
+
}
|
|
132
|
+
}
|