Skip to content

feat(viewer): ready-made controls for a headless viewer - #10

Open
mkroemer wants to merge 13 commits into
p10node:mainfrom
mkroemer:ui-controller-controls
Open

mkroemer wants to merge 13 commits into
p10node:mainfrom
mkroemer:ui-controller-controls

Conversation

@mkroemer

Copy link
Copy Markdown

Builds on the formatting PR (#9) and #8. Until those are merged this PR shows their commits too; the controls part is the last commit.

For a host that wants the engine's controls, but in its own layout around a headless viewer:

const ctl = createViewerController()

<header><ZoomBar controller={ctl} /><FormatBar controller={ctl} /><StatusText controller={ctl} /></header>
<aside><OutlineList controller={ctl} /></aside>
<DocViewer src={file} controller={ctl} headless />
<footer><SheetTabs controller={ctl} /></footer>

ZoomBar, StatusText, OutlineList (headings, sheets or slides), SheetTabs and FormatBar are each driven by the controller alone. They render through the host's components, icons, texts and design tokens like the built-in toolbars (OfficeUIProvider and useThemeRoot outside DocViewer). The views and their built-in chrome are unchanged.

A container of controls gets useThemeRoot(theme, style, { host: true }): sized by its content, not clipping, and stacked above the viewer that follows, so the format bar's size list and overflow menu are not cut off. SheetTabs keeps the focus in the grid, so a formula being typed stays open on the sheet switched to and can point at its cells, as with the built-in tabs.

docs/api.md has a "Ready-made controls" section under headless mode. e2e/chrome.spec.ts mounts them around a headless viewer: status, zoom and the headings outline follow and drive a DOCX, the format bar bolds the selection, sheet tabs switch sheets, and a cross-sheet reference typed across a tab switch. bun run typecheck, bun test and the e2e suite pass in CI.

🤖 Generated with Claude Code

markus-kroemer and others added 13 commits November 14, 2026 10:43
…ects

Every color in the stylesheet is now a --de-* token on .de-root, with the
dark values on .de-root.de-dark: find highlights, collaboration status,
avatars, slideshow, tooltips, file badges, paper and placeholders, and the
source editor's syntax colors (moved to the root so a theme reaches them).
Fonts, radii and shadows are tokens too. Rendering is unchanged: a
computed-style comparison against the previous stylesheet over DOCX, XLSX,
PPTX and Markdown, light and dark, differs only in equivalent font stacks
and the color-button swatch edge, which now follows swatch-border (visible
in dark).

- theme accepts { mode, tokens, light, dark } with typed camelCase token
  names (ThemeTokens); THEME_TOKENS, createTheme, themeStyle, useThemeRoot
- injectStyles={false} and @p10node/office-engine/styles.css for hosts that
  load the stylesheet themselves (CSP, extracted CSS, cascade layers)
- docs/theming.md; unit tests keep THEME_TOKENS and the stylesheet in step
  and reject literal colors outside token definitions; e2e for theme
  objects in both modes and for injectStyles=false

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every icon the chrome draws now goes through `Icon`: the zoom buttons,
the outline toggle and its close button, find & replace, Present and the
add-sheet tab used text glyphs and get the names `zoomIn`, `zoomOut`,
`present`, `sheetAdd`, `replace` (with `outline` and `close` reused),
drawn like today.

- `IconName` is the union of the built-in names (exported with
  `ICON_NAMES`); icon props in the toolbars, menus and `FileMenuItem`
  are typed with it.
- `icons` on DocViewer, `mount()` and `<office-viewer>` (`.icons`
  property) takes a partial map or a resolver `(name, { size })`;
  a missing entry or `undefined` / `null` keeps the built-in icon.
  Host icons render in `span.de-icon`, sized by font-size.
- `IconsProvider` gives standalone views the same icons; nested
  providers fall back to the enclosing one.
- Docs with Lucide and Phosphor resolvers; unit test for precedence,
  e2e spec for DOCX, XLSX and PPTX chrome.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s props

The viewer's texts come from one typed table, `Strings` (src/react/strings/en.ts),
with a complete German table next to it (de.ts, Swiss spelling, Word / Excel /
PowerPoint terms). `DocViewer`, `mount()` and `<office-viewer>` take `locale`
('en' | 'de', default 'en') and `strings`, a partial table merged over the
locale key by key; `StringsProvider` / `useStrings()` give standalone views the
same texts. Numbers and dates in the UI go through Intl with the table's locale;
the English output is unchanged.

This change converts the top bar, the details rows, the common dialogs, the
zoom control, the shared toolbar controls, the source pane and the picture view.
The shared groups (`edit`, `find`, `table`, `format`) are in the table already,
so the document, sheet and slide views can be converted in separate changes.

A unit test checks that every locale has all keys of `en`, no empty text and no
sharp s in German.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host can render its own Button, Select, Separator, Dialog and Toast in the
engine's toolbars and dialogs (`components` prop, UIComponentsProvider), for
a design system's controls where design tokens are not enough. Each one left
out keeps the built-in one, whose markup is unchanged.

- every toolbar button goes through Button: Btn, the color, picker and
  overflow toggles, the font size steps; Dropdown/Select through Select
- PromptDialog and ConfirmDialog build their actions and body and render
  them through Dialog; Toast keeps its timer and renders through Toast
- OfficeUIProvider carries icons, components, locale and strings to views
  mounted without DocViewer
- the overflow button's title is its accessible name ("More tools"), so the
  unused "More" text goes
- docs/theming.md "Your own components"; unit tests for each component and
  nested providers; e2e with host components: toolbar, formatting keeps the
  selection, overflow at a narrow width, the link prompt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xts from the strings table

The document view's status line, toolbar, context menu, outline, find bar, page setup and header / footer dialogs and the tracked-change tooltips take their texts from the docx, pageSetup and headerFooter groups. The Page 1 of 5 footer preset inserts its text in the UI's language. e2e/locale-docx.spec.ts checks the German texts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s from the strings table

The sheet view's status line, document details, sheet tabs, context menu, find options, print notice and the toolbar (number formats, borders, merge, alignment, rotation, rows, functions, sort, view) take their texts from the xlsx group. Number format previews stay as they are. e2e/locale-xlsx.spec.ts checks the German texts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heir texts from the strings table

The slide view's toolbar, shape and slide menus, status line and present button, the comments panel and its notices, the File menu, the details dialog, the unsupported-file message and the collaboration chip take their texts from the pptx, comments, file and collab groups. The empty text box placeholder is a data-placeholder attribute shown through attr(), so it follows the locale. e2e/locale.spec.ts checks the German bar, File menu, details dialog and slide view, a partial override and the English default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host that draws its own controls around the document gets
`<DocViewer controller={createViewerController()} headless />`.
`headless` leaves the viewer with the document alone: no top bar, zoom
control, outline panel or button, sheet tabs, slide list or Present
button. While editing, the formatting toolbar keeps its row, and an
explicit `toolbar` brings back the top bar (File menu, comments, Save).

The controller (src/react/controller.ts, framework-free) holds the
state - kind, loading, editing, canEdit, dirty, saving, canUndo/canRedo,
zoom, page, outline (headings, sheets or slides), status - and runs the
commands: setEditing, save, undo, redo, zoomIn/zoomOut/setZoom, goto,
gotoPage, present, print, fileAction, toggleComments. The viewer and its
mounted view publish to it and register their commands; without one from
the host the viewer keeps its own, so nothing else changes. When the
viewer or its view unmounts, its part of the state is cleared, so a
controller that outlives it shows no document.
`useViewerState(ctl)` reads the state in React.

- DOCX: headings are read for the controller whether or not the outline
  panel is open (once edits rest), with the current one, which stays the
  heading jumped to while its page is in view; DocxEditor reports its
  undo/redo history after each step, offered only while editing.
- XLSX: the visible sheets as the outline; undo/redo counted per edit,
  cleared when a sheet is added, deleted, moved, renamed or imported, as
  the worker's history is. A sheet switched to through the controller
  keeps a formula being typed open, for cross-sheet references, as the
  built-in tabs do.
- PPTX: slides titled by their title placeholder, read by a title-only
  worker call (`pptx.titles`, no layout) only while a host subscribes
  (`observed`, `watchObserved`), and kept current as slides are edited;
  symbol-font characters in titles as their Unicode look-alikes. Undo and
  redo through the controller do nothing outside edit mode.
- `onLoad` fires once per opened document, also with an inline callback.
- mount() returns the controller; <office-viewer headless> has .controller.
- e2e harness: PLAYWRIGHT_CHANNEL picks an installed browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One formatting API for documents, cells and slide text, so a host can draw
its own formatting toolbar: `ViewerState.format` (a TextFormat: bold,
italic, underline, strike, font, size, color, highlight, align, list) and
`ViewerController.format(command)`.

- DocxEditor and PptxEditor publish their formatting state to subscribers
  (`subscribeState`) beside the toolbar's `onState`; a subscriber hears the
  current state at once, so a controller attached while text is selected
  reports its format without waiting for the selection to change
- src/react/edit/format.ts maps the editors' states and the active cell's
  CSS to TextFormat and commands to editor calls or cell format patches
- the format is null while viewing, and in a presentation unless text is
  being edited; a command a kind has no use for does nothing
- spreadsheet undo, redo and format commands through the controller run
  only while editing, and canUndo / canRedo follow the editing state
- docs: "Formatting" under headless mode; unit tests for the mappings;
  e2e: bold and size through the controller in a document (saved), bold
  and fill on a cell, and an undo while viewing that leaves the cell as is

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ZoomBar, StatusText, OutlineList, SheetTabs and FormatBar, each driven by a
ViewerController alone, so a host gets the viewer's controls in a layout of
its own around `<DocViewer controller={ctl} headless />`. They render
through the host's components, icons, texts and design tokens like the
built-in toolbars (OfficeUIProvider and useThemeRoot outside DocViewer).
The views and their built-in chrome are unchanged.

A container of controls gets useThemeRoot(theme, style, { host: true }):
sized by its content, not clipping, and stacked above the viewer that
follows, so the format bar's size list and overflow menu are not cut off.

- docs: "Ready-made controls" under headless mode
- e2e (chrome.spec.ts, `chrome` flag in the fixture): status, zoom and the
  headings outline follow and drive a DOCX; the format bar bolds the
  selection; sheet tabs switch sheets, and keep a formula being typed
  open, like the built-in tabs, for a reference to another sheet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants