Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Also online at [p10node.github.io/office-engine](https://p10node.github.io/offic

- [Features](docs/features.md): format coverage, editing, saving, limitations
- [API](docs/api.md): props, custom views, `mount()`, `<office-viewer>`, core API, CSP
- [Theming](docs/theming.md): design tokens, light / dark, loading the stylesheet yourself
- [Real-time collaboration](docs/collaboration.md)
- [Development](docs/development.md): playground, architecture, demo deploy, release

Expand All @@ -75,7 +76,8 @@ Also online at [p10node.github.io/office-engine](https://p10node.github.io/offic
- **File menu**: new, open (also DOC / XLS / PPT / RTF / OpenDocument / Pages / Keynote / Numbers / CSV / text / Markdown / HTML / SVG / diagrams), import CSV, rename, download as PDF / HTML / text / CSV, document details, print
- **Comments**: threads with replies, resolve, edit / delete, @mentions; on DOCX text, XLSX cells and PPTX slides, stored in the file the way Word, Excel and PowerPoint store them
- **Real-time collaboration**: live edits, avatars, collaborators' cursors, offline edits sync on reconnect
- **Dark mode**: `theme="dark"` or `"auto"` (follows the OS); documents keep their own colors
- **Dark mode and theming**: `theme="dark"` or `"auto"` (follows the OS), or a theme object with design tokens for colors, type and shape ([Theming](docs/theming.md)); documents keep their own colors
- **Translatable UI**: every text of the bars, menus and dialogs from one typed table; English by default, German shipped (`locale="de"`), any text replaceable through `strings`
- **Embed anywhere**: React component, hook, vanilla `mount()`, `<office-viewer>` web component, single `<script>` file

Full list, format coverage and limitations: [Features](docs/features.md).
Expand Down
107 changes: 103 additions & 4 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DocViewer } from '@p10node/office-engine'
<DocViewer src={file} allowSave={false} allowDownload={false} /> // no Save/Download, no image download
<DocViewer src={file} fileMenu={false} /> // no File menu
<DocViewer src={file} theme="dark" /> // dark chrome; "auto" follows the OS setting
<DocViewer src={file} locale="de" /> // German texts
```

### File menu
Expand Down Expand Up @@ -46,7 +47,7 @@ PDF goes through the browser print dialog ("Save as PDF"), so text stays selecta
/>
```

CSS is auto-injected, no import needed. Container must have a height.
CSS is auto-injected, no import needed (or import `@p10node/office-engine/styles.css` with `injectStyles={false}`). Container must have a height. Colors, type and shape: [Theming](theming.md).

| Prop | Type | Default | Notes |
|--------------------|------------------------------------------------|-----------|--------------------------------------------------------------------------------------------------------------------------|
Expand All @@ -60,7 +61,10 @@ CSS is auto-injected, no import needed. Container must have a height.
| `credentials` | `RequestCredentials` | – | Cross-origin cookies |
| `worker` | `() => Worker \| false` | inline | Custom worker (CSP), `false` = main thread |
| `styleTarget` | `Document \| ShadowRoot` | document | Where CSS is injected |
| `theme` | `'light' \| 'dark' \| 'auto'` | `'light'` | Color scheme of the chrome; `'auto'` follows `prefers-color-scheme`. Documents keep their own colors |
| `injectStyles` | `boolean` | `true` | Inject the stylesheet; false when the page loads `@p10node/office-engine/styles.css` itself |
| `theme` | `'light' \| 'dark' \| 'auto' \| ThemeOptions` | `'light'` | Color scheme of the chrome; `'auto'` follows `prefers-color-scheme`; an object adds token overrides ([Theming](theming.md)). Documents keep their own colors |
| `locale` | `'en' \| 'de'` | `'en'` | Language of the texts (bars, menus, dialogs, messages); numbers and dates follow it. See [UI texts](#ui-texts) |
| `strings` | `StringsOverride` | – | Texts replacing the locale's, key by key (a group may be given in part) |
| `editable` | `boolean` | `false` | Open in edit mode (Pages, Keynote and Numbers files stay view only) |
| `allowEdit` | `boolean` | `true` | Show Edit/View button |
| `allowSave` | `boolean` | `true` | Show Save/Download button, enable Ctrl+S |
Expand All @@ -69,6 +73,7 @@ CSS is auto-injected, no import needed. Container must have a height.
| `allowOpen` | `boolean` | `true` | File → New / Open (off during collaboration) |
| `captureSelectAll` | `boolean` | `true` | Ctrl/⌘+A from anywhere on the page selects the document; `false`: from the viewer only |
| `fileMenuItems` | `FileMenuItem[]` | – | Extra File menu entries (`label`, `onClick`, `icon`, `hint`) |
| `icons` | `Icons` | built-in | Your own icons: a map by `IconName` or a resolver `(name, { size }) => node`; see [Icons](#icons) |
| `onRename` | `(name) => void` | – | File → Rename result (extension included) |
| `onClose` | `() => void` | – | Adds File → Close |
| `onSave` | `(blob, name) => void` | download | Receives saved file (downloads by default): the Office file, or a Markdown / HTML / SVG file as itself |
Expand Down Expand Up @@ -126,7 +131,101 @@ const { client, error, progress } = useDocument(src)
if (client?.kind === 'xlsx') return <div className="de-root"><XlsxView client={client} /></div>
```

The views style themselves from CSS variables on `.de-root` (`--de-bg`, `--de-chrome`, `--de-accent`…): override them to match your app. Add `de-dark` to the root element for the dark theme.
The views style themselves from design tokens on their root: `useThemeRoot(theme)` gives its class name and style (see [Theming](theming.md)).

### Icons

Every icon the viewer draws, toolbars, menus, zoom, outline, Present and the add-sheet tab included, has a name (`IconName`, all of them in `ICON_NAMES`). `icons` replaces any of them, either as a map or as a resolver that is called for every icon:

```tsx
import { DocViewer, type IconName } from '@p10node/office-engine'

<DocViewer src={file} icons={{ undo: <MyUndo />, redo: <MyRedo /> }} />
```

A name the map leaves out, or a resolver returning `undefined` / `null`, keeps the built-in icon, so a set can cover just part of the names. Your icon is rendered inside `<span class="de-icon">` whose font-size is the icon's size in pixels: draw it at `1em` (a bare `<svg>` child is sized to `1em` for you) and with `currentColor`, so it follows the theme. The resolver also gets `{ size }` in pixels.

With [Lucide](https://lucide.dev) (`lucide-react`):

```tsx
import { Bold, Italic, ListTree, Plus, Presentation, Redo2, Replace, Search, Undo2, X, ZoomIn, ZoomOut, type LucideIcon } from 'lucide-react'

const LUCIDE: Partial<Record<IconName, LucideIcon>> = {
undo: Undo2, redo: Redo2, bold: Bold, italic: Italic, search: Search, replace: Replace,
zoomIn: ZoomIn, zoomOut: ZoomOut, outline: ListTree, close: X, present: Presentation, sheetAdd: Plus,
}
const lucide = (name: IconName) => {
const C = LUCIDE[name]
return C && <C size="1em" strokeWidth={1.8} />
}

<DocViewer src={file} icons={lucide} />
```

With [Phosphor](https://phosphoricons.com) (`@phosphor-icons/react`):

```tsx
import { ArrowArcLeft, ArrowArcRight, ArrowsLeftRight, ListBullets, MagnifyingGlass, MagnifyingGlassMinus, MagnifyingGlassPlus, Plus, PresentationChart, TextB, TextItalic, X, type Icon } from '@phosphor-icons/react'

const PHOSPHOR: Partial<Record<IconName, Icon>> = {
undo: ArrowArcLeft, redo: ArrowArcRight, bold: TextB, italic: TextItalic, search: MagnifyingGlass, replace: ArrowsLeftRight,
zoomIn: MagnifyingGlassPlus, zoomOut: MagnifyingGlassMinus, outline: ListBullets, close: X, present: PresentationChart, sheetAdd: Plus,
}
const phosphor = (name: IconName) => {
const C = PHOSPHOR[name]
return C && <C size="1em" />
}
```

The standalone views get the same icons from `IconsProvider`, which `DocViewer` uses internally; a nested provider falls back to the enclosing one for names it leaves out:

```tsx
import { IconsProvider, XlsxView } from '@p10node/office-engine'

<IconsProvider icons={lucide}>
<div className="de-root"><XlsxView client={client} /></div>
</IconsProvider>
```

### UI texts

Every text the viewer shows (titles, labels, menu entries, placeholders, dialogs, toasts, status lines such as "Page 1 of 5") comes from one typed table, `Strings`. English (`en`) is the default and German (`de`) ships with the package; `locale` picks one, and numbers and dates in the UI (zoom, counts, file sizes, comment times) are formatted with its `Intl` locale (`en-US`, `de-CH`).

`strings` replaces single texts and is merged over the locale key by key, so a group can be given in part. A value is a string, or a function where the text takes numbers or names:

```tsx
<DocViewer
src={file}
locale="de"
strings={{
file: { menu: 'Ablage' },
docx: { status: (page, pages, words) => `${page}/${pages} · ${words} Wörter` },
}}
/>
```

Views used on their own get their texts from a `StringsProvider` (English without one); `useStrings()` reads them in your own components:

```tsx
import { StringsProvider, useStrings, XlsxView } from '@p10node/office-engine'

<StringsProvider locale="de" strings={{ xlsx: { addSheet: 'Neues Blatt' } }}>
<div className="de-root"><XlsxView client={client} /></div>
</StringsProvider>
```

A locale of your own is a complete table: start from a copy of `en` (or `de`) and type it as `Strings`, so the compiler names every missing key, then pass it whole as `strings` (with `locale` set to the closest shipped one for number formats, or with its own `locale` value inside the table, a BCP 47 tag such as `'fr-CH'`):

```ts
import { en, type Strings } from '@p10node/office-engine'

export const fr: Strings = { ...en, locale: 'fr-CH', common: { ...en.common, cancel: 'Annuler' /* … */ } }
// <DocViewer src={file} strings={fr} />
```

To ship a locale with the engine, add `src/react/strings/<code>.ts` with a `Strings` table, list it in `LOCALES` and the `Locale` type (`src/react/strings/index.tsx`) and in `LOCALE_NAMES` (`src/embed.ts`); `test/strings.test.ts` checks that it has every key and no empty text.

Not translated: texts that end up in the saved document (a new sheet's name, the author "Me", the text of a new text box), font names, number format previews, and messages from the parsing worker that the UI shows as they are.

## Without a React build

Expand All @@ -143,7 +242,7 @@ The views style themselves from CSS variables on `.de-root` (`--de-bg`, `--de-ch
</script>
```

`<office-viewer>`: attributes `src`, `name`, `editable`, `toolbar="false"`, `thumbnails="false"`, `save="false"`, `download="false"`, `file-menu="false"`, `open="false"`, `capture-select-all="false"`, `theme="dark"` (or `"auto"`), `collab` / `room` / `user` / `base` (real-time collaboration; `user` also writes the comments), `comments="false"`; property `.file = File`; events `load`, `error`, `comment` (`detail`: a `CommentEvent`).
`<office-viewer>`: attributes `src`, `name`, `editable`, `toolbar="false"`, `thumbnails="false"`, `save="false"`, `download="false"`, `file-menu="false"`, `open="false"`, `capture-select-all="false"`, `theme="dark"` (or `"auto"`), `locale="de"`, `collab` / `room` / `user` / `base` (real-time collaboration; `user` also writes the comments), `comments="false"`; properties `.file = File`, `.icons` (map or resolver, as `icons` above) and `.strings = { … }` (see [UI texts](#ui-texts)); events `load`, `error`, `comment` (`detail`: a `CommentEvent`).

## Core API (no React)

Expand Down
4 changes: 2 additions & 2 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,11 @@ Diagram sources open as a document holding the drawing (an SVG picture, editable

### Toolbar

Single row, buttons that don't fit collapse into `⋮`; custom-drawn dropdowns (font/style preview, number formats with examples, ↑↓/Enter/Esc/type-ahead keys, never steals focus from the document); font size box `− 11 +` accepts direct input; cell borders picked from an icon grid; color pickers have a palette plus a **Custom** row (`+` opens the system color picker, recent custom colors are remembered across sessions).
Single row, buttons that don't fit collapse into `⋮`; custom-drawn dropdowns (font/style preview, number formats with examples, ↑↓/Enter/Esc/type-ahead keys, never steals focus from the document); font size box `− 11 +` accepts direct input; cell borders picked from an icon grid; color pickers have a palette plus a **Custom** row (`+` opens the system color picker, recent custom colors are remembered across sessions). Every icon can be replaced with the host's own set (Lucide, Phosphor or any other) through the `icons` prop, see [API](api.md#icons).

### Dark mode

`theme="dark"`, or `"auto"` to follow the OS / browser setting (`prefers-color-scheme`), darkens the chrome: bars, toolbar, menus, dialogs, sheet headers and tabs, thumbnails, popovers, scrollbars. Pages, slides and cells keep the colors the document gives them, like paper in a dark PDF viewer. Everything is driven by CSS variables on `.de-root`, so a host can also tune the palette.
`theme="dark"`, or `"auto"` to follow the OS / browser setting (`prefers-color-scheme`), darkens the chrome: bars, toolbar, menus, dialogs, sheet headers and tabs, thumbnails, popovers, scrollbars. Pages, slides and cells keep the colors the document gives them, like paper in a dark PDF viewer. Everything is driven by design tokens (CSS variables on `.de-root`), which a host sets through a theme object or its own CSS: see [Theming](theming.md).

## Comments

Expand Down
90 changes: 90 additions & 0 deletions docs/theming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Theming

The viewer's chrome (bars, toolbars, menus, dialogs, panels, sheet headers, tabs) takes every color, font, radius and
shadow from design tokens: CSS custom properties named `--de-*` on the viewer root (`.de-root`). The document itself
(pages, slides, cells) keeps its own colors in every theme.

## The `theme` prop

```tsx
<DocViewer src={file} theme="auto" /> // light, dark, or following the OS

<DocViewer
src={file}
theme={{
mode: 'auto',
tokens: { accent: '#7c3aed', accentHover: '#6d28d9', font: 'Inter, sans-serif', radius: '4px' }, // both modes
light: { bg: '#f4f4f5', chrome: '#ffffff' },
dark: { bg: '#18181b', chrome: '#27272a', accent: '#a78bfa' },
}}
/>
```

`tokens` apply in both modes, `light` and `dark` go over them in their mode, and anything left out keeps the built-in
value of that mode. Names are camelCase (`accentHover` sets `--de-accent-hover`); values are any CSS value, so a token can
point at the host's own variables: `{ accent: 'var(--brand-500)' }`. `createTheme()` types a theme object written apart
from the prop, and `THEME_TOKENS` lists every token.

`mount()` takes the same `theme` option; `<office-viewer theme="dark">` takes a mode.

## Plain CSS

The tokens are ordinary custom properties, so a stylesheet can set them as well:

```css
.my-app .de-root { --de-accent: #7c3aed; --de-font: Inter, sans-serif; }
.my-app .de-root.de-dark { --de-accent: #a78bfa; }
```

Class names starting with `de-` are stable and can be styled further; the tokens are the supported way to change
colors, type and shape.

## Loading the stylesheet yourself

By default the viewer injects its stylesheet once into the document (or the shadow root in `styleTarget`). To load it
yourself, for a Content Security Policy without inline styles, a bundler that extracts CSS, or a cascade layer of your own,
turn injection off and import the file:

```tsx
import '@p10node/office-engine/styles.css'
<DocViewer src={file} injectStyles={false} />
```

```css
/* the viewer's rules below the host's own, whatever their specificity */
@layer office-engine, app;
@import '@p10node/office-engine/styles.css' layer(office-engine);
```

The stylesheet is not put in a layer of its own: rules in a layer lose to every unlayered rule, so a host's element
reset (`button { … }`) would restyle the viewer's controls. Importing it into a layer is the host's choice.

## Your own root

The views (`DocxView`, `XlsxView`, `PptxView`) can be mounted without `DocViewer` (see [API](api.md)); `useThemeRoot()`
gives the class name and style their root needs:

```tsx
const root = useThemeRoot(theme)
return <div className={root.className} style={root.style}><XlsxView client={client} /></div>
```

## Tokens

| Group | Tokens |
|---|---|
| Surfaces and text | `bg` (around the document), `chrome` (bars), `pop` (menus, dialogs), `ebar` (formatting toolbar), `text`, `muted`, `ink`, `inkStrong`, `inkSoft`, `border`, `sep`, `tbSep` |
| Accent and states | `accent`, `accentHover`, `accentInk` (text on the accent), `accentSoft`, `on`, `onInk`, `onInkStrong` (pressed buttons), `hover`, `hoverSolid`, `hoverSoft`, `disabled`, `danger`, `dangerBorder`, `ok` |
| Inputs and tabs | `inputBg`, `inputBorder`, `swatchBorder`, `tabHover`, `tabOn`, `tabInk` |
| Overlays | `popShadow`, `shadow`, `shadowSm`, `shadowMd`, `shadowLg`, `backdrop`, `toastBg`, `toastInk`, `tipBg`, `tipInk` |
| Type and shape | `font`, `fontMono`, `fontSize`, `radiusSm`, `radius`, `radiusLg`, `radiusXl` |
| Sheets | `grid`, `head`, `headInk`, `headBorder`, `cornerBorder`, `freeze` |
| Comments | `cmBg`, `cmOn`, `cmLine`, `cmLineOn`, `cmCard`, `cmQuote` |
| Find | `find`, `findOn` (text matches), `findCell`, `findCellOn`, `findCellLine` (cell matches) |
| Files, people, connection | `kindDocx`, `kindXlsx`, `kindPptx`, `kindInk` (badges), `avatarInk` (text on a person's color), `statusIdle`, `statusLive`, `statusBusy`, `statusError` |
| Slideshow | `presentBg`, `presentInk` |
| Paper | `page`, `pageInk`, `sheetInk`, `skeleton` (loading pages), `phBorder`, `phA`, `phB`, `phInk` (placeholders), `phOutline`, `phHint` (empty text boxes) |
| Source editor | `srcBg`, `srcGutter`, `srcSel`, `srcFont`, `srcComment`, `srcKeyword`, `srcString`, `srcOperator`, `srcNumber`, `srcHeading`, `srcAttr`, `srcTag`, `srcMeta` |

The paper tokens are the same in both modes: a document is drawn on white paper with black ink unless it says
otherwise. Built-in values: `src/react/styles.css`, the `.de-root` and `.de-root.de-dark` blocks.
Loading