Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6e2fc17
feat(shell): dark/light/system theme + performance optimizations (#41)
alfirus Aug 7, 2026
b3cab39
Sync snapshot (2026-08-07) (#48)
merrick-2002 Aug 7, 2026
1a1247a
fix(sheets): open temp file r+ before fsync so Windows saves work (#47)
Jabberwocky238 Aug 7, 2026
091c599
fix(file-parse): keep xlsx and pptx attachment text verbatim (#51)
Waiel5 Aug 8, 2026
efb2470
fix(build): include the markdown app's dependencies in the third-part…
Waiel5 Aug 8, 2026
d8305ff
docs: catch the contributor docs up with the tree (#53)
Waiel5 Aug 8, 2026
6a2d1cd
fix(docx): keep the original root namespaces when regenerating a part…
Waiel5 Aug 9, 2026
d2e03aa
fix(docx): keep header and footer paragraphs right-to-left on save (#62)
Waiel5 Aug 9, 2026
300ad8f
fix(sheets): allow Save As without edits (#60)
Jabberwocky238 Aug 9, 2026
920dbf6
fix(file-parse): keep pptx line breaks and fields in document order (…
Waiel5 Aug 9, 2026
fbfe63a
fix(docx): do not read a nil paragraph border as a real one (#69)
Waiel5 Aug 9, 2026
d157d9d
docs(sheets): drop the stale claim that duplicate/hide/reorder are bl…
Waiel5 Aug 9, 2026
767b87b
fix(sheets): keep the cell alignment attributes the style model omits…
Waiel5 Aug 9, 2026
e249c3b
Sync snapshot (2026-08-09) (#75)
merrick-2002 Aug 9, 2026
185040f
Sync snapshot (2026-08-09) (#76)
merrick-2002 Aug 9, 2026
fd33934
Sync snapshot (2026-08-09) (#77)
merrick-2002 Aug 9, 2026
7023e63
Sync snapshot (2026-08-10) (#80)
merrick-2002 Aug 10, 2026
8e72b93
sync: upstream genoffice 7023e63 — dark/light theme, engine fixes, ma…
Aug 10, 2026
4c34aed
fix(shell): resolve remaining css merge conflicts + format pass
Aug 10, 2026
2b872d8
fix(shell): theme-color check — tokenize dark-mode raw colors
Aug 11, 2026
7d4f09b
fix(build): restore platform-optional deps lost in lockfile merge
Aug 11, 2026
01c105d
fix(shell): remove duplicated accountStatus IPC registration
Aug 11, 2026
0d4f0a5
fix(shell): re-expose aiOfficeCloud preload API lost in upstream merge
Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 5 additions & 4 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ body:
label: App
description: Which part of the suite is affected?
options:
- HermesOffice Docs (docs)
- HermesOffice Sheets (sheets)
- HermesOffice Slides (slides)
- HermesOffice PDF (pdf)
- GenOffice Docs (docs)
- GenOffice Sheets (sheets)
- GenOffice Slides (slides)
- GenOffice PDF (pdf)
- GenOffice Markdown (markdown)
- Shell / home screen
- AI panel / providers
- Build / packaging
Expand Down
9 changes: 5 additions & 4 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ body:
attributes:
label: App
options:
- HermesOffice Docs (docs)
- HermesOffice Sheets (sheets)
- HermesOffice Slides (slides)
- HermesOffice PDF (pdf)
- GenOffice Docs (docs)
- GenOffice Sheets (sheets)
- GenOffice Slides (slides)
- GenOffice PDF (pdf)
- GenOffice Markdown (markdown)
- Shell / home screen
- AI panel / providers
- Whole suite
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ jobs:
fi
npm run format:check

- name: Check theme colors
env:
FORMAT_BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before }}
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
FORMAT_BASE_REF=$(git rev-parse HEAD^1)
fi
npm run check:theme-colors

- name: Lint
run: npm run lint

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ packages/hermes-cloud/src/oauth-credentials.ts
/*.zip
/windows/

# Playwright artifacts
/test-results/

.cursor/rules/git-remote.mdc
__pycache__/
/notes/
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ apps/shell/build/THIRD-PARTY-NOTICES.txt

# Local-only acceptance drivers are intentionally outside CI.
scripts/drivers/driver.*.mjs

# Vendored third-party sources stay byte-identical to upstream.
packages/*/src/vendor/
47 changes: 47 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# CLAUDE.md

Guidance for AI agents and human contributors working in this repo.

## Theming rules (mandatory)

The suite supports light / dark / system UI themes. The switching mechanism is a
`data-theme` attribute on `<html>` plus CSS custom properties defined once in
`packages/ui/src/tokens.css` (light defaults in `:root`, overrides in
`[data-theme='dark']`, and a `prefers-color-scheme` media-query fallback for
system mode).

1. **UI chrome colors must use semantic tokens.** Never write raw `#hex` /
`rgb()` in renderer CSS rules or chrome-related inline styles — reference
`var(--surface)`, `var(--text)`, `var(--hover)`, etc. from
`packages/ui/src/tokens.css`. Raw values are allowed only on custom-property
definition lines (`--x: #...;` — token, accent, or app-scoped variable
definitions). CI enforces this for new/changed renderer CSS lines
(`tools/check-theme-colors.mjs`).
2. **Every new token gets both values.** Adding a token means adding it to all
three blocks in `tokens.css` (light, dark, system-dark fallback).
3. **Accent colors stay per-app.** Each app defines `--accent` /
`--accent-dark` / `--accent-soft` (and its dark-adjusted values) in its own
`styles.css`. Shared rules reference `var(--accent)` and inherit the app's
brand color.
4. **Document content never follows the theme.** Page surfaces, cell fills,
slide content, PDF page bitmaps, export/print stylesheets, chart palettes,
highlight color maps, stamps, and WordArt presets are document data: they
stay hardcoded, must not reference chrome tokens, and must render/export
identically in both themes. (Word-style "dark chrome, white paper".)
5. **Canvas-drawn UI affordances go through a constants table.** Konva/canvas
editing chrome (selection frames, guides, handles) reads from the app's
canvas color table (e.g. `canvas-colors.ts`) keyed by the current theme —
no inline hex in draw calls.

## Build gotchas

- App main-process code (`apps/*/src/main`) is compiled into the **shell**
build. After changing it, rebuild the shell or the change silently does not
run.
- In dev mode, preload changes require a rebuild — a stale preload leaves the
renderer blank.
- Workspace packages listed in an app's `dependencies` must also be added to
the `externalizeDepsPlugin` `exclude` list, or the packaged app crashes on
launch.
- `useI18n()`'s `t` is not referentially stable; never put it in a hook
dependency array. Store the key and translate at render time.
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ and issues labeled `good first issue` are small, well-scoped entry points.

## Repository layout

- `apps/*` — the five Electron apps (docs, sheets, slides, pdf, shell).
- `apps/*` — the six Electron apps (docs, sheets, slides, pdf, markdown, shell).
Each app is an npm workspace with its own `src/main` (Electron main
process), `src/renderer` (React UI), and `tests/`.
- `packages/*` — pure TypeScript engine and shared packages (no Electron
Expand All @@ -34,7 +34,7 @@ and issues labeled `good first issue` are small, well-scoped entry points.

## Getting started

Prerequisites: Node 20+, npm 10+, and a Rust toolchain (`cargo` on PATH,
Prerequisites: Node 22+, npm 10+, and a Rust toolchain (`cargo` on PATH,
needed only for the sheets xlsx sidecar).

```bash
Expand Down Expand Up @@ -119,7 +119,7 @@ within hours — the gate lives on your machine first.
## Building installers

Run these from the repository root — they regenerate the third-party
notices and build all five apps before packaging:
notices and build all six apps before packaging:

```bash
npm run dist:mac # dmg + zip
Expand Down
39 changes: 39 additions & 0 deletions LICENSE-UNICODE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
UNICODE LICENSE V3

COPYRIGHT AND PERMISSION NOTICE

Copyright © 1991-2026 Unicode, Inc.

NOTICE TO USER: Carefully read the following legal agreement. BY
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.

Permission is hereby granted, free of charge, to any person obtaining a
copy of data files and any associated documentation (the "Data Files") or
software and any associated documentation (the "Software") to deal in
the Data Files or Software without restriction, including without
limitation the rights to use, copy, modify, merge, publish, distribute,
and/or sell copies of the Data Files or Software, and to permit persons
to whom the Data Files or Software are furnished to do so, provided that
either (a) this copyright and permission notice appear with all copies of
the Data Files or Software, or (b) this copyright and permission notice
appear in associated Documentation.

THE DATA FILES AND SOFTWARE ARE 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 OF
THIRD PARTY RIGHTS.

IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THE DATA FILES OR SOFTWARE.

Except as contained in this notice, the name of a copyright holder shall
not be used in advertising or otherwise to promote the sale, use or
other dealings in these Data Files or Software without prior written
authorization of the copyright holder.
13 changes: 13 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ checkout.
Bundled fonts and their licenses are documented in
apps/docs/src/renderer/fonts/README.md.

<<<<<<< HEAD
---

HermesOffice
Expand All @@ -25,3 +26,15 @@ in full; all attribution to Mainfunc, Inc. and to the GenOffice project
remains intact. This fork adds its own branding and a native integration with
the Hermes Agent (Nous Research) as the default AI provider; upstream code
keeps its original copyright.
=======
Unicode Character Database

apps/pdf/src/shared/radicals.ts contains a generated mapping derived from
Unicode Character Database 17.0.0, EquivalentUnifiedIdeograph.txt
(2025-08-01):
https://www.unicode.org/Public/17.0.0/ucd/EquivalentUnifiedIdeograph.txt

Copyright © 1991-2026 Unicode, Inc. This data is distributed under Unicode
License v3. The complete copyright and permission notice is reproduced in
LICENSE-UNICODE.txt.
>>>>>>> upstream/main
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ acknowledge reports within 72 hours.
All application windows run with the full Electron renderer lockdown:

- `contextIsolation: true`, `nodeIntegration: false`, `sandbox: true` for every
document window and tab view (docs, sheets, slides, pdf, shell, updater).
document window and tab view (docs, sheets, slides, pdf, markdown, shell, updater).
- Renderers reach the main process only through typed, validated IPC channels
(payloads are schema-checked in the main process; sheets uses zod end to end).
- Every `shell.openExternal` call goes through a single shared gate
Expand Down
10 changes: 10 additions & 0 deletions apps/docs/electron.vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { resolve } from 'node:path'
import react from '@vitejs/plugin-react'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'

// Resolve workspace packages from this checkout's sources: in a git worktree
// node_modules is a symlink into the main checkout, so bare specifiers would
// silently bundle the other checkout's (possibly stale) code.
const localAlias = {
'@hermesoffice/docx-engine': resolve(__dirname, '../../packages/docx-engine/src/index.ts'),
}

export default defineConfig({
// Main and preload use only electron + node builtins; bundle everything so
// the packaged app doesn't rely on node_modules at runtime.
Expand All @@ -9,10 +17,12 @@ export default defineConfig({
// (same setup as apps/slides).
main: {
plugins: [externalizeDepsPlugin({ exclude: ['@hermesoffice/electron-utils'] })],
resolve: { alias: localAlias },
},
preload: {},
renderer: {
plugins: [react()],
resolve: { alias: localAlias },
server: {
// Overridable so multiple hermesoffice dev instances can coexist (default 5173).
port: Number(process.env.DOCS_DEV_PORT) || 5173,
Expand Down
1 change: 1 addition & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"electron-builder": "^26.0.12",
"electron-vite": "^5.0.0",
"jsdom": "^28.0.0",
"marked": "^17.0.6",
"opentype.js": "^2.0.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
Expand Down
74 changes: 66 additions & 8 deletions apps/docs/src/main/docs-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import { basename, dirname, join } from 'node:path'
import { BrowserWindow, Menu, WebContentsView, app, dialog, ipcMain, shell } from 'electron'
import {
appMenuLabels,
configuredDefaultSaveDir,
contextMenuLabels,
fetchRemoteImage,
installContextMenu,
installNavigationGuard,
safeExternalUrl,
showOpenDialogWithMemory,
showSaveDialogWithMemory,
toggleDevToolsItem,
windowMenuTemplate,
} from '@hermesoffice/electron-utils'
import { createI18n, getUiLang, normalizeLang, setUiLang } from '@hermesoffice/i18n'
Expand Down Expand Up @@ -1947,14 +1949,14 @@ async function openDialog(event: IpcMainInvokeEvent, options: OpenDialogOptions)
}

async function saveDialog(event: IpcMainInvokeEvent, options: SaveDialogOptions) {
return showSaveDialogWithMemory(dialog, dialogParent(event), options)
// before any pick is remembered, bare-name suggestions anchor in the
// configurable default save folder instead of Electron's Downloads pin
return showSaveDialogWithMemory(dialog, dialogParent(event), options, defaultSaveDir())
}

/** default folder where new files land on their first (silent) save; shared with the other editors via shell */
/** default folder where new files land on their first (silent) save; shared with the other editors via shell. User-configurable (app-settings.json), falls back to <Documents>/HermesOffice. */
export function defaultSaveDir(): string {
const dir = join(app.getPath('documents'), 'HermesOffice')
mkdirSync(dir, { recursive: true })
return dir
return configuredDefaultSaveDir(app)
}

/** first free path for fileName inside dir: name.ext, name-2.ext, name-3.ext… */
Expand Down Expand Up @@ -2157,7 +2159,13 @@ function allowPdfWrite(wcId: number, filePath: string): void {
pdfWritablePaths.set(wcId, set)
}

// Fidelity-harness escape hatch: headless runs have no save dialog to authorize
// paths, so an explicitly configured directory (set only by our test scripts)
// is treated as pre-authorized for PDF export.
const testExportDir = process.env.GENOFFICE_TEST_EXPORT_DIR || null

function canPdfWrite(wcId: number, filePath: string): boolean {
if (testExportDir && filePath.startsWith(testExportDir + '/')) return true
return pdfWritablePaths.get(wcId)?.has(filePath) === true
}

Expand Down Expand Up @@ -3337,6 +3345,26 @@ function sendCommand(command: MenuCommand, payload?: string): void {
activeDocsWebContents()?.send('menu:command', command, payload)
}

/**
* Per-tab View-menu toggle state (AI Sidebar / Dark Mode), reported by each
* renderer whenever it changes. The template can't hardcode `checked` — the
* state lives in the renderer and differs per tab — so builds read the active
* tab's last report, and reports from the active tab patch the built menu in
* place (buildDocsMenu also re-runs on every tab focus switch).
* Defaults mirror the renderer's initial state: sidebar shown, light canvas.
*/
const viewMenuStateByWebContents = new Map<number, { aiSidebar: boolean; darkCanvas: boolean }>()

function activeViewMenuState(): { aiSidebar: boolean; darkCanvas: boolean } {
const id = activeDocsWebContents()?.id
return (
(id !== undefined ? viewMenuStateByWebContents.get(id) : undefined) ?? {
aiSidebar: true,
darkCanvas: false,
}
)
}

/** shell-injected items appended to the File menu (e.g. Back to Home); persists
* across the internal rebuilds pushRecent() triggers */
let extraFileMenuItems: MenuItemConstructorOptions[] = []
Expand Down Expand Up @@ -3477,11 +3505,23 @@ export function buildDocsMenu(): void {
{ label: tm('menuPageWidth'), click: () => sendCommand('zoom-page-width') },
{ label: tm('menuWholePage'), click: () => sendCommand('zoom-whole-page') },
{ type: 'separator' },
{ label: tm('menuAiSidebar'), click: () => sendCommand('toggle-ai') },
{ label: tm('menuDarkMode'), click: () => sendCommand('toggle-dark') },
{
id: 'docs-menu-ai-sidebar',
type: 'checkbox',
checked: activeViewMenuState().aiSidebar,
label: tm('menuAiSidebar'),
click: () => sendCommand('toggle-ai'),
},
{
id: 'docs-menu-dark-mode',
type: 'checkbox',
checked: activeViewMenuState().darkCanvas,
label: tm('menuDarkMode'),
click: () => sendCommand('toggle-dark'),
},
{ type: 'separator' },
{ role: 'togglefullscreen', label: tm('menuFullscreen') },
...(isDev ? [{ role: 'toggleDevTools' as const }] : []),
...(isDev ? [toggleDevToolsItem(appMenuLabels(getUiLang()))] : []),
],
},
{
Expand Down Expand Up @@ -3644,6 +3684,24 @@ interface DocsCloseState {
const closeCheckWaiters = new Map<number, (state: DocsCloseState) => void>()
const closeSaveWaiters = new Map<number, (ok: boolean) => void>()

ipcMain.on('docs:view-menu-state', (event, state: unknown) => {
const s = state as { aiSidebar?: unknown; darkCanvas?: unknown } | null
const next = { aiSidebar: s?.aiSidebar === true, darkCanvas: s?.darkCanvas === true }
if (!viewMenuStateByWebContents.has(event.sender.id)) {
const id = event.sender.id
event.sender.once('destroyed', () => viewMenuStateByWebContents.delete(id))
}
viewMenuStateByWebContents.set(event.sender.id, next)
// patch the live menu only for the active tab; an inactive tab's state gets
// picked up by the buildDocsMenu run its next focus triggers
if (event.sender.id !== activeDocsWebContents()?.id) return
const menu = Menu.getApplicationMenu()
const ai = menu?.getMenuItemById('docs-menu-ai-sidebar')
if (ai) ai.checked = next.aiSidebar
const dark = menu?.getMenuItemById('docs-menu-dark-mode')
if (dark) dark.checked = next.darkCanvas
})

ipcMain.on('docs:close-check-result', (event, state: unknown) => {
const waiter = closeCheckWaiters.get(event.sender.id)
if (!waiter) return
Expand Down
Loading
Loading