Skip to content

feat(viewer): replaceable UI components for toolbars and dialogs - #7

Open
mkroemer wants to merge 7 commits into
p10node:mainfrom
mkroemer:ui-primitives
Open

mkroemer wants to merge 7 commits into
p10node:mainfrom
mkroemer:ui-primitives

Conversation

@mkroemer

Copy link
Copy Markdown

Builds on #6 (UI texts), #5 (icons) and #4 (theming). Until those are merged this PR shows their commits too; the components part is the last commit.

This lets a host render the toolbars and dialogs with its own design system's controls, for when design tokens are not enough. Each component a host leaves out keeps the built-in one, whose markup is unchanged.

<DocViewer
  src={file}
  editable
  components={{
    Button: ({ title, icon, label, children, active, expanded, disabled, onClick, onMouseDown }) => (
      <MyIconButton tooltip={title} aria-label={title} pressed={active} aria-expanded={expanded}
        disabled={disabled} onClick={onClick} onMouseDown={onMouseDown}>
        {icon}{label}{children}
      </MyIconButton>
    ),
    Separator: () => <MyDivider vertical />,
  }}
/>

There are five: Button, Select, Separator, Dialog and Toast, with typed props (ButtonProps, SelectProps, DialogProps, ToastProps).

  • Every toolbar button goes through Button: the plain buttons, the toggles that open the color, table and border pickers, the overflow button and the font size steps. The dropdowns go through Select.
  • PromptDialog and ConfirmDialog build their body and actions and render them through Dialog; Toast keeps its timer and renders through Toast.
  • components is a prop of DocViewer, and UIComponentsProvider gives it to views mounted on their own. OfficeUIProvider carries icons, components, locale and strings in one provider.
  • The overflow menu measures each toolbar item from a wrapper of its own, so a host component needs no ref.
  • The overflow button's title is now its accessible name ("More tools"), so the unused "More" text is gone from the strings table.

The one thing a host component has to get right is the focus: the document must keep it while a toolbar control is used, or the command loses its selection. docs/theming.md ("Your own components") says how, for Button (pass onMouseDown on) and Select (open on mouse down, handle keys without focusing the list), and lists every prop.

test/ui.test.tsx covers each component and nested providers. e2e/components.spec.ts runs the viewer with host components: the toolbar renders them, formatting through a host button keeps the selection, the overflow menu still works at a narrow width, and the link prompt opens and submits through a host dialog. bun run typecheck, bun test and the e2e suite pass in CI.

🤖 Generated with Claude Code

markus-kroemer and others added 7 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>

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