Stage 1: app skeleton — Electron window, sidebar/editor layout, light/dark CSS, esbuild build, typecheck
This commit is contained in:
commit
6fac0d013b
12 changed files with 2488 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
*.asar
|
||||||
|
releases/
|
||||||
|
.DS_Store
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Folio Contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
25
README.md
Normal file
25
README.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Folio
|
||||||
|
|
||||||
|
A simple, local, open-source desktop writing app for organizing a book.
|
||||||
|
|
||||||
|
- **Fully offline** — no cloud, no accounts, no telemetry.
|
||||||
|
- **Your folders** — each book lives in a folder you choose; chapters are plain Markdown.
|
||||||
|
- **Drag-and-drop reordering** of chapters.
|
||||||
|
- **Light & dark** themes that follow the system.
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev # starts esbuild watch + Electron
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building the renderer/bundle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE](LICENSE).
|
||||||
62
esbuild.config.mjs
Normal file
62
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import esbuild from "esbuild";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const watch = process.argv.includes("--watch");
|
||||||
|
|
||||||
|
const copyAssets = {
|
||||||
|
name: "copy-assets",
|
||||||
|
setup(build) {
|
||||||
|
build.onEnd(() => {
|
||||||
|
fs.mkdirSync("dist", { recursive: true });
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join("src", "renderer", "index.html"),
|
||||||
|
path.join("dist", "index.html")
|
||||||
|
);
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join("src", "renderer", "index.css"),
|
||||||
|
path.join("dist", "index.css")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const common = {
|
||||||
|
bundle: true,
|
||||||
|
sourcemap: true,
|
||||||
|
logLevel: "info",
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = {
|
||||||
|
entryPoints: ["src/main/index.ts"],
|
||||||
|
platform: "node",
|
||||||
|
external: ["electron"],
|
||||||
|
outfile: "dist/main.js",
|
||||||
|
plugins: [copyAssets],
|
||||||
|
...common,
|
||||||
|
};
|
||||||
|
|
||||||
|
const preload = {
|
||||||
|
entryPoints: ["src/main/preload.ts"],
|
||||||
|
platform: "node",
|
||||||
|
external: ["electron"],
|
||||||
|
outfile: "dist/preload.js",
|
||||||
|
plugins: [copyAssets],
|
||||||
|
...common,
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = {
|
||||||
|
entryPoints: ["src/renderer/index.ts"],
|
||||||
|
platform: "browser",
|
||||||
|
outfile: "dist/renderer.js",
|
||||||
|
...common,
|
||||||
|
};
|
||||||
|
|
||||||
|
const builds = [main, preload, renderer];
|
||||||
|
|
||||||
|
if (watch) {
|
||||||
|
const contexts = await Promise.all(builds.map((opts) => esbuild.context(opts)));
|
||||||
|
await Promise.all(contexts.map((ctx) => ctx.watch()));
|
||||||
|
} else {
|
||||||
|
await Promise.all(builds.map((opts) => esbuild.build(opts)));
|
||||||
|
}
|
||||||
2095
package-lock.json
generated
Normal file
2095
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
package.json
Normal file
26
package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "folio",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "A simple, local, open-source desktop writing app.",
|
||||||
|
"productName": "Folio",
|
||||||
|
"author": "Folio Contributors",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node esbuild.config.mjs",
|
||||||
|
"build:watch": "node esbuild.config.mjs --watch",
|
||||||
|
"electron": "electron .",
|
||||||
|
"dev": "concurrently -k \"npm run build:watch\" \"wait-on dist/main.js && npm run electron\"",
|
||||||
|
"start": "npm run build && npm run electron",
|
||||||
|
"lint": "tsc --noEmit",
|
||||||
|
"package": "electron-builder"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.5.0",
|
||||||
|
"concurrently": "^9.0.0",
|
||||||
|
"electron": "^32.0.0",
|
||||||
|
"esbuild": "^0.24.0",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"wait-on": "^8.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/main/index.ts
Normal file
35
src/main/index.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { app, BrowserWindow } from "electron";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1280,
|
||||||
|
height: 780,
|
||||||
|
minWidth: 960,
|
||||||
|
minHeight: 640,
|
||||||
|
title: "Folio",
|
||||||
|
webPreferences: {
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
preload: path.join(__dirname, "preload.js"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.loadFile(path.join(__dirname, "index.html"));
|
||||||
|
mainWindow.webContents.on("did-finish-load", () =>
|
||||||
|
console.log("[folio] renderer loaded")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createWindow();
|
||||||
|
app.on("activate", () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
if (process.platform !== "darwin") app.quit();
|
||||||
|
});
|
||||||
3
src/main/preload.ts
Normal file
3
src/main/preload.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
console.log("[folio] dom ready");
|
||||||
|
});
|
||||||
160
src/renderer/index.css
Normal file
160
src/renderer/index.css
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
/* System-font stack only — no remote fonts. */
|
||||||
|
:root {
|
||||||
|
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
"Helvetica Neue", Arial, sans-serif;
|
||||||
|
--font-serif: Georgia, "Times New Roman", serif;
|
||||||
|
--radius: 8px;
|
||||||
|
--gap: 16px;
|
||||||
|
|
||||||
|
/* Light theme */
|
||||||
|
--color-bg: #ffffff;
|
||||||
|
--color-panel: #f7f7f8;
|
||||||
|
--color-border: #e2e2e7;
|
||||||
|
--color-text: #1a1a1d;
|
||||||
|
--color-muted: #6b6b76;
|
||||||
|
--color-accent: #2563eb;
|
||||||
|
--color-accent-hover: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-bg: #0f0f12;
|
||||||
|
--color-panel: #18181e;
|
||||||
|
--color-border: #2a2a32;
|
||||||
|
--color-text: #e6e6ec;
|
||||||
|
--color-muted: #9a9aa6;
|
||||||
|
--color-accent: #4a94f0;
|
||||||
|
--color-accent-hover: #6ab0ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
flex: 0 0 280px;
|
||||||
|
min-width: 240px;
|
||||||
|
max-width: 360px;
|
||||||
|
background: var(--color-panel);
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding-bottom: var(--gap);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: var(--gap);
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter-list li {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
transition: background 0.12s ease, color 0.12s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter-list li:hover {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter-list li.active {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding-top: var(--gap);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s ease, border-color 0.12s ease;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-ghost {
|
||||||
|
border: 1px dashed var(--color-border);
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-ghost:hover {
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--color-bg);
|
||||||
|
padding: var(--gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-placeholder {
|
||||||
|
max-width: 560px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-placeholder p {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
30
src/renderer/index.html
Normal file
30
src/renderer/index.html
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Folio</title>
|
||||||
|
<link rel="stylesheet" href="./index.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root">
|
||||||
|
<aside class="sidebar" id="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<span class="sidebar-title">Chapters</span>
|
||||||
|
</div>
|
||||||
|
<ul class="chapter-list" id="chapterList"></ul>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<button class="button button-ghost" id="newChapterBtn">
|
||||||
|
+ New Chapter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<main class="editor" id="editor">
|
||||||
|
<div class="editor-placeholder">
|
||||||
|
<p>Select a chapter to begin writing.</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script src="./renderer.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
src/renderer/index.ts
Normal file
8
src/renderer/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
const root = document.getElementById("root")!;
|
||||||
|
|
||||||
|
root.addEventListener("dragover", (e) => e.preventDefault());
|
||||||
|
root.addEventListener("drop", (e) => e.preventDefault());
|
||||||
|
|
||||||
|
document.getElementById("newChapterBtn")!.addEventListener("click", () => {
|
||||||
|
alert("Chapter creation coming soon.");
|
||||||
|
});
|
||||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2022",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"lib": ["es2022", "dom"],
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue