✨ recent project
This commit is contained in:
@@ -71,6 +71,42 @@ export async function writeSession(data: Record<string, unknown>): Promise<void>
|
|||||||
await writeFile(sessionFile(), JSON.stringify(data), 'utf-8')
|
await writeFile(sessionFile(), JSON.stringify(data), 'utf-8')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectConfig {
|
||||||
|
projectTitle?: string
|
||||||
|
authorName?: string
|
||||||
|
penName?: string
|
||||||
|
authorAddress?: string
|
||||||
|
authorEmail?: string
|
||||||
|
authorPhone?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROJECT_CONFIG_FIELDS: (keyof ProjectConfig)[] = [
|
||||||
|
'projectTitle', 'authorName', 'penName', 'authorAddress', 'authorEmail', 'authorPhone'
|
||||||
|
]
|
||||||
|
|
||||||
|
export { PROJECT_CONFIG_FIELDS }
|
||||||
|
|
||||||
|
const projectConfigFile = (): string => join(hohoffDir(), 'project.json')
|
||||||
|
|
||||||
|
export async function readProjectConfig(): Promise<ProjectConfig> {
|
||||||
|
try {
|
||||||
|
return JSON.parse(await readFile(projectConfigFile(), 'utf-8'))
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeProjectConfig(updates: Partial<ProjectConfig>): Promise<void> {
|
||||||
|
await mkdir(hohoffDir(), { recursive: true })
|
||||||
|
const existing = await readProjectConfig()
|
||||||
|
const merged = { ...existing, ...updates }
|
||||||
|
// Remove undefined values
|
||||||
|
for (const key of Object.keys(merged) as (keyof ProjectConfig)[]) {
|
||||||
|
if (merged[key] === undefined) delete merged[key]
|
||||||
|
}
|
||||||
|
await writeFile(projectConfigFile(), JSON.stringify(merged, null, 2), 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
function applyOrder(nodes: FileNode[], savedNames: string[]): FileNode[] {
|
function applyOrder(nodes: FileNode[], savedNames: string[]): FileNode[] {
|
||||||
const map = new Map(nodes.map((n) => [n.name, n]))
|
const map = new Map(nodes.map((n) => [n.name, n]))
|
||||||
const ordered = savedNames.filter((n) => map.has(n)).map((n) => map.get(n)!)
|
const ordered = savedNames.filter((n) => map.has(n)).map((n) => map.get(n)!)
|
||||||
|
|||||||
@@ -2,18 +2,17 @@ import { join, basename } from 'path'
|
|||||||
import { homedir } from 'os'
|
import { homedir } from 'os'
|
||||||
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
||||||
|
|
||||||
|
export interface RecentProject {
|
||||||
|
path: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface GlobalConfig {
|
export interface GlobalConfig {
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
projectPath?: string
|
projectPath?: string
|
||||||
projectTitle?: string
|
|
||||||
fontSize?: number
|
fontSize?: number
|
||||||
theme?: 'dark' | 'light'
|
theme?: 'dark' | 'light'
|
||||||
// Manuscript metadata
|
recentProjects?: RecentProject[]
|
||||||
authorName?: string
|
|
||||||
penName?: string
|
|
||||||
authorAddress?: string
|
|
||||||
authorEmail?: string
|
|
||||||
authorPhone?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_DIR = join(homedir(), '.hohoff')
|
const CONFIG_DIR = join(homedir(), '.hohoff')
|
||||||
@@ -22,7 +21,12 @@ const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|||||||
let _config: GlobalConfig = {}
|
let _config: GlobalConfig = {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_config = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
|
const raw = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
|
||||||
|
// Migrate old string[] recentProjects to RecentProject[]
|
||||||
|
if (Array.isArray(raw.recentProjects) && typeof raw.recentProjects[0] === 'string') {
|
||||||
|
raw.recentProjects = (raw.recentProjects as string[]).map((p) => ({ path: p, title: basename(p) }))
|
||||||
|
}
|
||||||
|
_config = raw
|
||||||
} catch {
|
} catch {
|
||||||
// first run or file missing — use fallbacks
|
// first run or file missing — use fallbacks
|
||||||
}
|
}
|
||||||
@@ -35,8 +39,8 @@ export const getDraftRoot = (): string =>
|
|||||||
export const getApiKey = (): string | undefined =>
|
export const getApiKey = (): string | undefined =>
|
||||||
_config.apiKey ?? process.env.ANTHROPIC_API_KEY
|
_config.apiKey ?? process.env.ANTHROPIC_API_KEY
|
||||||
|
|
||||||
export const getProjectTitle = (): string =>
|
export const getProjectTitle = (projectTitle?: string): string =>
|
||||||
_config.projectTitle?.trim() || basename(getDraftRoot())
|
projectTitle?.trim() || basename(getDraftRoot())
|
||||||
|
|
||||||
export function readGlobalConfig(): GlobalConfig {
|
export function readGlobalConfig(): GlobalConfig {
|
||||||
return { ..._config }
|
return { ..._config }
|
||||||
@@ -47,3 +51,19 @@ export function writeGlobalConfig(updates: Partial<GlobalConfig>): void {
|
|||||||
mkdirSync(CONFIG_DIR, { recursive: true })
|
mkdirSync(CONFIG_DIR, { recursive: true })
|
||||||
writeFileSync(CONFIG_FILE, JSON.stringify(_config, null, 2), 'utf-8')
|
writeFileSync(CONFIG_FILE, JSON.stringify(_config, null, 2), 'utf-8')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addRecentProject(path: string, title: string): void {
|
||||||
|
const existing = _config.recentProjects ?? []
|
||||||
|
const deduped = [{ path, title }, ...existing.filter((p) => p.path !== path)].slice(0, 10)
|
||||||
|
writeGlobalConfig({ recentProjects: deduped })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateRecentProjectTitle(path: string, title: string): void {
|
||||||
|
const existing = _config.recentProjects ?? []
|
||||||
|
const updated = existing.map((p) => p.path === path ? { path, title } : p)
|
||||||
|
writeGlobalConfig({ recentProjects: updated })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecentProjects(): RecentProject[] {
|
||||||
|
return _config.recentProjects ?? []
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { app, BrowserWindow, shell, nativeImage, Menu, dialog } from 'electron'
|
import { app, BrowserWindow, shell, nativeImage, Menu, dialog } from 'electron'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
import { registerIpcHandlers } from './ipcHandlers'
|
import { registerIpcHandlers, setOnProjectChanged } from './ipcHandlers'
|
||||||
import { getDraftRoot } from './globalConfig'
|
import { getDraftRoot, getRecentProjects, writeGlobalConfig } from './globalConfig'
|
||||||
|
|
||||||
app.setName('Hohoff')
|
app.setName('Hohoff')
|
||||||
|
|
||||||
@@ -10,7 +10,14 @@ function send(win: BrowserWindow, action: string): void {
|
|||||||
if (!win.isDestroyed()) win.webContents.send('menu:action', action)
|
if (!win.isDestroyed()) win.webContents.send('menu:action', action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _menuWin: BrowserWindow | null = null
|
||||||
|
|
||||||
|
export function rebuildAppMenu(): void {
|
||||||
|
if (_menuWin) buildAppMenu(_menuWin)
|
||||||
|
}
|
||||||
|
|
||||||
function buildAppMenu(win: BrowserWindow): void {
|
function buildAppMenu(win: BrowserWindow): void {
|
||||||
|
_menuWin = win
|
||||||
const isMac = process.platform === 'darwin'
|
const isMac = process.platform === 'darwin'
|
||||||
|
|
||||||
const template: Electron.MenuItemConstructorOptions[] = [
|
const template: Electron.MenuItemConstructorOptions[] = [
|
||||||
@@ -58,6 +65,29 @@ function buildAppMenu(win: BrowserWindow): void {
|
|||||||
accelerator: 'CmdOrCtrl+Shift+O',
|
accelerator: 'CmdOrCtrl+Shift+O',
|
||||||
click: () => send(win, 'openProject')
|
click: () => send(win, 'openProject')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Open Recent',
|
||||||
|
submenu: (() => {
|
||||||
|
const recents = getRecentProjects()
|
||||||
|
if (recents.length === 0) {
|
||||||
|
return [{ label: 'No Recent Projects', enabled: false }]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
...recents.map((r) => ({
|
||||||
|
label: `${r.title} (${r.path})`,
|
||||||
|
click: () => win.webContents.send('menu:action', `openRecent:${r.path}`)
|
||||||
|
})),
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
{
|
||||||
|
label: 'Clear Recent',
|
||||||
|
click: () => {
|
||||||
|
writeGlobalConfig({ recentProjects: [] })
|
||||||
|
rebuildAppMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})()
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Save',
|
label: 'Save',
|
||||||
@@ -243,6 +273,7 @@ app.whenReady().then(() => {
|
|||||||
registerIpcHandlers()
|
registerIpcHandlers()
|
||||||
const mainWindow = createWindow()
|
const mainWindow = createWindow()
|
||||||
buildAppMenu(mainWindow)
|
buildAppMenu(mainWindow)
|
||||||
|
setOnProjectChanged(rebuildAppMenu)
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
|||||||
@@ -2,13 +2,21 @@ import { ipcMain, dialog, BrowserWindow } from 'electron'
|
|||||||
import { readFileSync, writeFileSync, unlinkSync } from 'fs'
|
import { readFileSync, writeFileSync, unlinkSync } from 'fs'
|
||||||
import { extname, basename, join } from 'path'
|
import { extname, basename, join } from 'path'
|
||||||
import { tmpdir } from 'os'
|
import { tmpdir } from 'os'
|
||||||
import { listDraftFiles, readMarkdownFile, writeMarkdownFile, getProjectWordCount, saveOrderFile, readSession, writeSession, saveRevision, listRevisions, loadRevision, deleteRevision, renameFileOrDir, deleteFileOrDir, createMarkdownFile, createSubdirectory, moveFileOrDir, readStoryBibleFile, openStoryBibleFile, writeStoryBibleFile, searchAcrossFiles, replaceInFiles, readAllDraftFiles } from './fileSystem'
|
import { listDraftFiles, readMarkdownFile, writeMarkdownFile, getProjectWordCount, saveOrderFile, readSession, writeSession, saveRevision, listRevisions, loadRevision, deleteRevision, renameFileOrDir, deleteFileOrDir, createMarkdownFile, createSubdirectory, moveFileOrDir, readStoryBibleFile, openStoryBibleFile, writeStoryBibleFile, searchAcrossFiles, replaceInFiles, readAllDraftFiles, readProjectConfig, writeProjectConfig, PROJECT_CONFIG_FIELDS } from './fileSystem'
|
||||||
import type { SearchOptions } from './fileSystem'
|
import type { SearchOptions, ProjectConfig } from './fileSystem'
|
||||||
import { streamMessage, resetClient } from './aiService'
|
import { streamMessage, resetClient } from './aiService'
|
||||||
import type { AIPayload, Attachment } from '../renderer/types/editor'
|
import type { AIPayload, Attachment } from '../renderer/types/editor'
|
||||||
import { readGlobalConfig, writeGlobalConfig, getProjectTitle } from './globalConfig'
|
import { readGlobalConfig, writeGlobalConfig, getProjectTitle, addRecentProject, updateRecentProjectTitle } from './globalConfig'
|
||||||
import type { GlobalConfig } from './globalConfig'
|
import type { GlobalConfig } from './globalConfig'
|
||||||
|
|
||||||
|
type MergedConfig = GlobalConfig & ProjectConfig
|
||||||
|
|
||||||
|
let _onProjectChanged: (() => void) | null = null
|
||||||
|
|
||||||
|
export function setOnProjectChanged(cb: () => void): void {
|
||||||
|
_onProjectChanged = cb
|
||||||
|
}
|
||||||
|
|
||||||
export function registerIpcHandlers(): void {
|
export function registerIpcHandlers(): void {
|
||||||
ipcMain.handle('fs:listFiles', async () => {
|
ipcMain.handle('fs:listFiles', async () => {
|
||||||
return await listDraftFiles()
|
return await listDraftFiles()
|
||||||
@@ -168,13 +176,59 @@ export function registerIpcHandlers(): void {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('config:read', (): GlobalConfig => {
|
ipcMain.handle('config:read', async (): Promise<MergedConfig> => {
|
||||||
return readGlobalConfig()
|
const global = readGlobalConfig()
|
||||||
|
const project = await readProjectConfig()
|
||||||
|
|
||||||
|
// One-time migration: if global has project fields, move them to project config
|
||||||
|
const legacyGlobal = global as Record<string, unknown>
|
||||||
|
const migrationFields = PROJECT_CONFIG_FIELDS.filter((f) => legacyGlobal[f] !== undefined)
|
||||||
|
if (migrationFields.length > 0 && Object.keys(project).length === 0) {
|
||||||
|
const migrated: Partial<ProjectConfig> = {}
|
||||||
|
for (const f of migrationFields) {
|
||||||
|
(migrated as Record<string, unknown>)[f] = legacyGlobal[f]
|
||||||
|
}
|
||||||
|
await writeProjectConfig(migrated)
|
||||||
|
// Remove from global config
|
||||||
|
const cleaned = { ...legacyGlobal }
|
||||||
|
for (const f of migrationFields) delete cleaned[f]
|
||||||
|
writeGlobalConfig(cleaned as Partial<GlobalConfig>)
|
||||||
|
return { ...global, ...migrated }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...global, ...project }
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('config:write', (_event, updates: Partial<GlobalConfig>): void => {
|
ipcMain.handle('config:write', async (_event, updates: Partial<MergedConfig>): Promise<void> => {
|
||||||
writeGlobalConfig(updates)
|
const globalUpdates: Partial<GlobalConfig> = {}
|
||||||
|
const projectUpdates: Partial<ProjectConfig> = {}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(updates)) {
|
||||||
|
if ((PROJECT_CONFIG_FIELDS as string[]).includes(key)) {
|
||||||
|
(projectUpdates as Record<string, unknown>)[key] = value
|
||||||
|
} else {
|
||||||
|
(globalUpdates as Record<string, unknown>)[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(globalUpdates).length > 0) writeGlobalConfig(globalUpdates)
|
||||||
|
if (Object.keys(projectUpdates).length > 0) await writeProjectConfig(projectUpdates)
|
||||||
|
|
||||||
if (updates.apiKey !== undefined) resetClient()
|
if (updates.apiKey !== undefined) resetClient()
|
||||||
|
|
||||||
|
if (updates.projectPath) {
|
||||||
|
const projectCfg = await readProjectConfig()
|
||||||
|
const { basename } = require('path') as typeof import('path')
|
||||||
|
addRecentProject(updates.projectPath, projectCfg.projectTitle?.trim() || basename(updates.projectPath))
|
||||||
|
_onProjectChanged?.()
|
||||||
|
} else if (updates.projectTitle !== undefined) {
|
||||||
|
const currentPath = readGlobalConfig().projectPath
|
||||||
|
if (currentPath) {
|
||||||
|
const { basename } = require('path') as typeof import('path')
|
||||||
|
updateRecentProjectTitle(currentPath, updates.projectTitle.trim() || basename(currentPath))
|
||||||
|
_onProjectChanged?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('config:pickFolder', async (event): Promise<string | null> => {
|
ipcMain.handle('config:pickFolder', async (event): Promise<string | null> => {
|
||||||
@@ -198,7 +252,8 @@ export function registerIpcHandlers(): void {
|
|||||||
const bodyHtml = await marked(normalized)
|
const bodyHtml = await marked(normalized)
|
||||||
|
|
||||||
const safeTitle = fileName.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<')
|
const safeTitle = fileName.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<')
|
||||||
const projectTitle = getProjectTitle().toUpperCase()
|
const projectCfgForPdf = await readProjectConfig()
|
||||||
|
const projectTitle = getProjectTitle(projectCfgForPdf.projectTitle).toUpperCase()
|
||||||
const chapterTitle = fileName.toUpperCase()
|
const chapterTitle = fileName.toUpperCase()
|
||||||
|
|
||||||
const html = `<!DOCTYPE html>
|
const html = `<!DOCTYPE html>
|
||||||
@@ -298,7 +353,8 @@ export function registerIpcHandlers(): void {
|
|||||||
|
|
||||||
ipcMain.handle('export:projectPdf', async (event): Promise<void> => {
|
ipcMain.handle('export:projectPdf', async (event): Promise<void> => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
const projectName = getProjectTitle()
|
const projectCfg = await readProjectConfig()
|
||||||
|
const projectName = getProjectTitle(projectCfg.projectTitle)
|
||||||
|
|
||||||
const result = await dialog.showSaveDialog(win!, {
|
const result = await dialog.showSaveDialog(win!, {
|
||||||
defaultPath: `${projectName}.pdf`,
|
defaultPath: `${projectName}.pdf`,
|
||||||
@@ -318,12 +374,11 @@ export function registerIpcHandlers(): void {
|
|||||||
const projectTitle = projectName.toUpperCase()
|
const projectTitle = projectName.toUpperCase()
|
||||||
|
|
||||||
// ── Cover page ──────────────────────────────────────────────────────────────
|
// ── Cover page ──────────────────────────────────────────────────────────────
|
||||||
const cfg = readGlobalConfig()
|
const authorLegal = projectCfg.authorName?.trim() ?? ''
|
||||||
const authorLegal = cfg.authorName?.trim() ?? ''
|
const authorByline = projectCfg.penName?.trim() || authorLegal
|
||||||
const authorByline = cfg.penName?.trim() || authorLegal
|
const authorAddr = projectCfg.authorAddress?.trim() ?? ''
|
||||||
const authorAddr = cfg.authorAddress?.trim() ?? ''
|
const authorEmail = projectCfg.authorEmail?.trim() ?? ''
|
||||||
const authorEmail = cfg.authorEmail?.trim() ?? ''
|
const authorPhone = projectCfg.authorPhone?.trim() ?? ''
|
||||||
const authorPhone = cfg.authorPhone?.trim() ?? ''
|
|
||||||
|
|
||||||
// Count words from docs already in memory, rounded to nearest 1,000
|
// Count words from docs already in memory, rounded to nearest 1,000
|
||||||
const totalWords = docs.reduce((sum, doc) => {
|
const totalWords = docs.reduce((sum, doc) => {
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ export default function App(): JSX.Element {
|
|||||||
} else if (action === 'openProject') {
|
} else if (action === 'openProject') {
|
||||||
const picked = await window.api.pickProjectFolder()
|
const picked = await window.api.pickProjectFolder()
|
||||||
if (picked) await switchProject(picked)
|
if (picked) await switchProject(picked)
|
||||||
|
} else if (action.startsWith('openRecent:')) {
|
||||||
|
await switchProject(action.slice('openRecent:'.length))
|
||||||
} else if (action === 'exportPDF') {
|
} else if (action === 'exportPDF') {
|
||||||
if (activeFilePath && activeFileContent) {
|
if (activeFilePath && activeFileContent) {
|
||||||
const fileName = activeFilePath.split('/').pop()?.replace(/\.md$/, '') ?? 'document'
|
const fileName = activeFilePath.split('/').pop()?.replace(/\.md$/, '') ?? 'document'
|
||||||
|
|||||||
@@ -100,20 +100,6 @@ export function SettingsDialog({ onClose, onProjectChanged, isSetup }: Props): J
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-field">
|
|
||||||
<label className="settings-label" htmlFor="settings-project-title">Project Title</label>
|
|
||||||
<input
|
|
||||||
id="settings-project-title"
|
|
||||||
className="settings-input"
|
|
||||||
type="text"
|
|
||||||
value={projectTitle}
|
|
||||||
onChange={(e) => setProjectTitle(e.target.value)}
|
|
||||||
placeholder="My Novel"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
<p className="settings-hint">Used as the title in PDF exports. Defaults to the project folder name if left blank.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="settings-field">
|
<div className="settings-field">
|
||||||
<label className="settings-label" htmlFor="settings-api-key">Anthropic API Key</label>
|
<label className="settings-label" htmlFor="settings-api-key">Anthropic API Key</label>
|
||||||
<input
|
<input
|
||||||
@@ -131,6 +117,20 @@ export function SettingsDialog({ onClose, onProjectChanged, isSetup }: Props): J
|
|||||||
|
|
||||||
<div className="settings-section-divider"><span>Manuscript</span></div>
|
<div className="settings-section-divider"><span>Manuscript</span></div>
|
||||||
|
|
||||||
|
<div className="settings-field">
|
||||||
|
<label className="settings-label" htmlFor="settings-project-title">Project Title</label>
|
||||||
|
<input
|
||||||
|
id="settings-project-title"
|
||||||
|
className="settings-input"
|
||||||
|
type="text"
|
||||||
|
value={projectTitle}
|
||||||
|
onChange={(e) => setProjectTitle(e.target.value)}
|
||||||
|
placeholder="My Novel"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
<p className="settings-hint">Used as the title in PDF exports. Defaults to the project folder name if left blank.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="settings-field">
|
<div className="settings-field">
|
||||||
<label className="settings-label" htmlFor="settings-author-name">Author Name</label>
|
<label className="settings-label" htmlFor="settings-author-name">Author Name</label>
|
||||||
<input
|
<input
|
||||||
@@ -201,7 +201,7 @@ export function SettingsDialog({ onClose, onProjectChanged, isSetup }: Props): J
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-footer">
|
<div className="settings-footer">
|
||||||
<span className="settings-config-path">Config: ~/.hohoff/config.json</span>
|
<span className="settings-config-path">Global: ~/.hohoff/config.json · Project: .hohoff/project.json</span>
|
||||||
<div className="settings-footer-actions">
|
<div className="settings-footer-actions">
|
||||||
{!isSetup && (
|
{!isSetup && (
|
||||||
<button className="settings-btn settings-btn--secondary" onClick={onClose}>Cancel</button>
|
<button className="settings-btn settings-btn--secondary" onClick={onClose}>Cancel</button>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user