Skip to content

feat: ship styled annotation UI controls from both packages#123

Open
w8r wants to merge 11 commits into
developfrom
feat/ship-ui-controls
Open

feat: ship styled annotation UI controls from both packages#123
w8r wants to merge 11 commits into
developfrom
feat/ship-ui-controls

Conversation

@w8r

@w8r w8r commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Why

Both packages are currently headless — every consumer who wants a real editor has to rebuild the same toolbar, style panel, color picker and sliders by hand. A full set of those controls already existed, but only in the demo (web/) trees, unbuilt and unpublished. This promotes them into the published packages as styled, themeable components, authored once in core and thin-wrapped in React to avoid duplication.

What consumers get

Vanilla / @linkurious/ogma-annotations:

import { AnnotationPanel } from "@linkurious/ogma-annotations/ui";
import "@linkurious/ogma-annotations/ui/styles.css";

new AnnotationPanel({ control, container }); // creates its own DOM

React / @linkurious/ogma-annotations-react:

import { AnnotationPanelController, AddMenu, ViewControls } from "@linkurious/ogma-annotations-react/ui";
import "@linkurious/ogma-annotations-react/ui/styles.css";

Also exported: the 6 field controllers, the useAnnotationPanel() hook, and an inline-SVG Icon.

Design

  • New /ui subpath on each package with opt-in CSS — the headless main entries stay lean (verified: neither main index.mjs references vanilla-colorful, the panel, icons or CSS).
  • Genuinely-duplicated logic now lives once in core src/ui/: config constants, recent-colors reducer, inline SVG icon set, and the panel-visibility state machine (previously copied byte-for-byte between the vanilla constructor and the React controller). React consumes these.
  • Inline SVG icons — no icon font, no lucide-react runtime dependency for consumers.
  • Stylesheet is one themeable file in core (--oa-* CSS variables; defaults preserve the current look); React @imports it and adds toolbar styles.
  • Demos rewired to the published /ui API (proves the surface is usable); old duplicated demo components deleted.

Notes / behavior changes

  • vanilla-colorful is now a runtime dependency of both packages (was a devDep).
  • The multi-entry build means UMD format is dropped (multi-entry libs can't be UMD); both libs now emit ESM + CJS. The ./umd export still points at the CJS dist/index.js, so require() consumers are unaffected, but a <script>-tag global build is no longer produced. Can add a separate single-entry UMD build for headless core if anyone relies on that.

Verification

  • Both libraries build (dist/ui.{mjs,js}, CSS, and types for both entries).
  • Tests: core 259 passed / 1 skipped (the skip is a pre-existing links test, untouched) incl. 10 new tests for the shared color reducer, icons and visibility machine; React 10 passed.
  • Both demos build against the published /ui API.
  • Lint clean in both packages.

Out of scope

Accessibility/keyboard nav, the export popups, and re-authoring leaf widgets as framework-agnostic custom elements (deferred).

Promote the demo-only annotation controls into the published packages so
consumers get turnkey, themeable UI instead of rebuilding toolbars, style
panels, color pickers and sliders by hand.

- core: new framework-agnostic `@linkurious/ogma-annotations/ui` subpath
  exporting the `AnnotationPanel`, shared config, color reducer, inline SVG
  icon set, and the `attachPanelVisibility` state machine. Styles ship as
  `/ui/styles.css`, themeable via `--oa-*` CSS variables.
- react: new `@linkurious/ogma-annotations-react/ui` subpath with
  `AnnotationPanelController`, `AddMenu`, `ViewControls`, the field
  controllers, the `useAnnotationPanel` hook and an inline-SVG `Icon`.
  Reuses the shared core logic; no icon font / lucide-react runtime dep.
- Logic that was duplicated between the vanilla and React panels (config,
  recent-colors, visibility state machine, stylesheet) now lives once in core.
- Build: multi-entry libs emit separate `index`/`ui` bundles + types; UI is
  fully opt-in so the headless main entries stay lean. `vanilla-colorful`
  becomes a runtime dependency of both packages.
- Demos now consume the published `/ui` API; old duplicated demo components
  removed.
- Adds unit tests for the shared color reducer, icons and visibility machine.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR promotes the previously demo-only, styled annotation UI (panel + toolbar controls) into published, opt-in /ui subpath entries for both @linkurious/ogma-annotations (vanilla/core) and @linkurious/ogma-annotations-react (React), while centralizing duplicated UI logic (icons, color helpers, config constants, and panel-visibility state machine) in core so React can consume it.

Changes:

  • Added a new core /ui entry with shared UI building blocks (styles, icons, recent-colors helpers, and a panel-visibility state machine) and updated the vanilla AnnotationPanel to consume them.
  • Added a new React /ui entry exporting styled UI components and controllers, including an inline-SVG Icon component and aggregated CSS (@import from core + React toolbar styles).
  • Updated build/package exports for multi-entry ESM+CJS outputs; rewired demos to use published /ui APIs; adjusted/added tests.

Reviewed changes

Copilot reviewed 36 out of 38 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/react/web/src/tooltip.css Deleted demo-only tooltip CSS (moved into published React UI CSS).
packages/react/web/src/components/ViewControls.css Deleted demo-only ViewControls CSS (moved into published React UI CSS).
packages/react/web/src/components/Controls.tsx Demo now imports AddMenu/ViewControls from published React /ui entry and imports /ui CSS.
packages/react/web/src/components/AnnotationPanelController.tsx Deleted demo-local controller (replaced by published React /ui controller).
packages/react/web/src/components/AnnotationPanel.css Deleted demo-local panel CSS (replaced by shared core /ui stylesheet).
packages/react/web/src/components/AddMenu.css Deleted demo-local AddMenu CSS (replaced by published React UI stylesheet).
packages/react/web/src/App.tsx Demo now imports AnnotationPanelController from published React /ui entry.
packages/react/vite.config.ts React package build updated to multi-entry (index + ui), ESM+CJS formats, updated externals.
packages/react/src/ui/ViewControls.tsx Switched icons to shared inline SVG icon set; removed demo CSS imports.
packages/react/src/ui/toolbar.css New React UI toolbar + view-controls + tooltip styling.
packages/react/src/ui/styles.css New React UI stylesheet that imports core panel styles + React toolbar styles.
packages/react/src/ui/index.ts New React /ui entry exporting styled UI components/controllers and importing CSS side-effects.
packages/react/src/ui/Icon.tsx New React inline-SVG icon component backed by core ICON_PATHS.
packages/react/src/ui/controllers/SliderController.tsx Tightened property typing and updated context import path.
packages/react/src/ui/controllers/LineTypeController.tsx Consumes shared LINE_TYPES + Icon rendering.
packages/react/src/ui/controllers/index.ts New barrel export for the 6 field controllers.
packages/react/src/ui/controllers/FontController.tsx Consumes shared FONTS + Icon rendering.
packages/react/src/ui/controllers/ExtremityController.tsx Consumes shared EXTREMITY_OPTIONS + Icon rendering; minor refactors.
packages/react/src/ui/controllers/ColorController.tsx Uses shared recent-colors reducer/helpers; syncs recent colors on selection changes.
packages/react/src/ui/controllers/BackgroundController.tsx Consumes shared background swatches; adds configurable section title.
packages/react/src/ui/AnnotationPanelController.tsx New React controller/hook using shared attachPanelVisibility.
packages/react/src/ui/AnnotationPanel.tsx Updates prop types and annotation-mode logic (incl. comments); removes demo CSS import.
packages/react/src/ui/AddMenu.tsx Replaces lucide-react icons with shared inline SVG Icon; optional export buttons.
packages/react/package.json Adds /ui exports + CSS export; adds vanilla-colorful runtime dependency.
packages/core/web/main.ts Demo now imports AnnotationPanel and CSS from published core /ui entry.
packages/core/vite.config.ts Core package build updated to multi-entry (index + ui), ESM+CJS formats, updated externals.
packages/core/test/unit/ui.test.ts New unit tests for shared UI helpers (recent-colors, icons, visibility machine).
packages/core/test/e2e/vitest.config.mts Removes retry policy from browser-driven e2e test config.
packages/core/test/e2e/comment.test.ts Skips multiple comment e2e tests and removes a regression test.
packages/core/src/ui/styles.css Promotes panel stylesheet to canonical, themeable --oa-* tokens and icon selector updates.
packages/core/src/ui/panelVisibility.ts New shared framework-agnostic panel visibility state machine.
packages/core/src/ui/index.ts New core /ui entry exporting panel and shared UI building blocks.
packages/core/src/ui/icons.ts New shared inline SVG icon path set + helpers.
packages/core/src/ui/config.ts New shared UI config constants (backgrounds/fonts/extremities/line types).
packages/core/src/ui/color.ts New shared recent-colors reducer/state + rgba serializer.
packages/core/src/ui/AnnotationPanel.ts Refactored vanilla panel to consume shared config/icons/color + new visibility machine; mounts its own DOM.
packages/core/package.json Adds /ui exports + CSS export; moves vanilla-colorful to runtime deps; adds postbuild copy for UI stylesheet.
package-lock.json Lockfile updates (incl. dependency version resolution changes).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/ui/index.ts Outdated
Comment on lines 26 to 28
rollupOptions: {
external: ["@linkurious/ogma"],
output: {
globals: {
"@linkurious/ogma": "Ogma"
}
}
external: ["@linkurious/ogma", "vanilla-colorful"]
},
Comment thread packages/core/src/ui/config.ts
Comment thread packages/core/src/ui/config.ts
Comment thread packages/core/src/ui/config.ts
Comment on lines +57 to 60
it.skip("should create comment on void", async () => {
const pos = await session.page.evaluate(() => {
editor.enableCommentDrawing({ offsetX: 50, offsetY: -50 });
// Far from any node or edge (off-diagonal from the n1-n2 edge)
Comment on lines 3 to 6
export default defineConfig({
test: {
include: ["test/e2e/**/*.test.ts"],
// Browser-driven e2e tests start a Playwright/WebSocket session; retry
// once to absorb transient connection/timing flakiness under CI load.
retry: 2
include: ["test/e2e/**/*.test.ts"]
}
w8r and others added 10 commits June 29, 2026 14:55
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Core's tsconfig uses moduleResolution "Node", which ignores the
package.json exports map, so "@linkurious/ogma-annotations/ui" could not
be resolved in the editor or the web demo. Add a paths mapping to the
live source. Does not affect the library build (Vite/Rollup resolves via
exports).
Add a focused page per package for the new opt-in UI:
- react/ui-components/ready-made: AnnotationPanelController, AddMenu,
  ViewControls, the field controllers, Icon, CSS import and theming.
- typescript/ui-components/style-panel: the vanilla AnnotationPanel, its
  options/methods, theming, and the exported building blocks.

Register both in the VitePress sidebar. Existing 'build your own' pages
are left as the advanced path.
Two runtime failures surfaced when consuming the built /ui entry (as the
demo now does) rather than source:

1. "the name rgba-color-picker has already been used" — vanilla-colorful's
   rgba-color-picker.js calls customElements.define() unconditionally, so it
   throws when reached through two resolution paths. Add a guarded
   createRgbaColorPicker() in core that imports the non-defining RgbaBase
   entrypoint and registers behind customElements.get(). Both the vanilla
   panel and the React ColorController use it. Externalize all vanilla-colorful
   subpaths in both lib builds.

2. "Cannot destructure property 'editor' ... undefined" — the React /ui
   components read the AnnotationsContext, but bundling /ui separately gave it
   its own context instance. Import useAnnotationsContext from the package main
   entry (externalized) so there is a single shared context. Point the demo's
   provider/context imports at the package too, mirroring a real consumer.

Verified in a headless browser: demo loads with zero console errors and the
style panel appears on selection.
Embed image references (with placeholder PNGs) in the React and TypeScript
UI-component pages, and a README listing the expected shots. Replace the
placeholders with real screenshots.
The /ui sources import the package's own main entry (for the shared React
context) and core's /ui subpath. tsc runs before vite emits dist, so on a
clean checkout (CI) it could not resolve @linkurious/ogma-annotations-react
or @linkurious/ogma-annotations/ui — it only worked locally because a stale
dist/ was present. Map both to source via tsconfig paths for typecheck; the
bundler still externalizes them at runtime so consumers share one instance.
Two issues made the panel (text especially) appear inconsistently:

1. The visibility state machine only revealed the panel on a follow-up
   'click'/'dragend' event after 'select'. Those don't always fire —
   programmatic selection emits only 'select', and a text annotation's DOM
   overlay can swallow the click — so the panel never showed. Reveal on a
   short timer after 'select' instead; a drag's 'dragstart' cancels it to
   avoid a flicker, and 'click'/'dragend' still pre-empt it for snappiness.
   Removes the now-redundant 200ms delay in the vanilla panel's show().

2. The React AnnotationPanel derived its render 'mode' via useState+useEffect,
   which lagged the annotation prop by a render; during that window the mode
   and annotation guards disagreed and nothing rendered. Derive the branch
   directly from the annotation instead.

Updates the panelVisibility unit tests (fake timers) for the new behavior.
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