Skip to content

feat: SW-2100 Add molecule structure primitive (SMILES → 2D)#182

Open
owilliams-tetrascience wants to merge 8 commits into
mainfrom
claude/rdkit-component-wrapper-36a448
Open

feat: SW-2100 Add molecule structure primitive (SMILES → 2D)#182
owilliams-tetrascience wants to merge 8 commits into
mainfrom
claude/rdkit-component-wrapper-36a448

Conversation

@owilliams-tetrascience

Copy link
Copy Markdown
Collaborator

Summary

Adds a chemical-structure primitive to the kit (SW-2100 / VIZ-3) — the library's first cheminformatics component. Renders a 2D structure from a SMILES string, with a thumbnail and a hover-tooltip wrapper, for showing compound structures in tables, scatter tooltips, and detail cards (HTS Hit Finder).

New in src/components/composed/MoleculeStructure/, exported from src/index.ts:

  • MoleculeRenderer — core SMILES → SVG; lazy-loads RDKit, follows dark mode, renders a fallback for invalid SMILES instead of throwing.
  • StructureThumbnail — fixed-size (size={48}) or responsive square, for table cells / list rows / cards, with optional caption.
  • StructureTooltip — hover popover revealing the structure; matches the kit's tooltip styling.
  • useRDKit / moleculeToSvg / loadRDKit / configureRDKit — preload hook + synchronous SVG helper so a structure can feed the interactive scatter's string-returning tooltip.content.

Dependency strategy (the important part)

RDKit's ~6.6 MB WASM is never bundled. It's isolated in rdkit-loader.ts and lazy-loaded once per page, so pages that never render a molecule pay nothing (verified: built rdkit-loader.js is ~3 KB, no WASM in dist). @rdkit/rdkit is an optional peerDependency (+ devDependency for types); by default the loader pulls the script/WASM from a pinned jsDelivr CDN, and consumers can self-host or pass a preloaded instance via configureRDKit. RDKit's Emscripten stdout/stderr is routed to console.debug so invalid input never spams the host console.

Maintenance note for reviewers: rdkit-js entered a maintenance transition in April 2026 (prior maintainer stepped down; transfer in progress). The version is pinned (2025.3.4-1.0.0); revisit before bumping.

Shared change (separate commit)

The interactive-scatter integration surfaced a pre-existing bug: ChartTooltip rendered tooltip.content as escaped text, so the documented rich-HTML tooltips (badges, and now molecule SVGs) showed as literal tags. Fixed to render content as HTML (fix(charts):). Content originates from developer-authored content functions / the library's own line builders, not raw untrusted input.

Type of Change

  • Feature (new functionality)
  • Bug fix (shared ChartTooltip rendered content as escaped text)
  • Refactor
  • Documentation
  • Chore (build, CI, dependencies)
  • Breaking change

Checklist

  • yarn lint passes
  • yarn build passes (no WASM bundled; loader ~3 KB)
  • yarn test:all passes (unit 818 + storybook 699; touched areas re-verified: 21 molecule + scatter play tests)
  • Storybook stories added/updated (Design Patterns/Molecule Structure)
  • Code coverage remains the same or increased

Testing

Stories and play tests exercise real RDKit — Vite ?url imports resolve the loader script + WASM to asset URLs served by whichever server is active (dev Storybook, the vitest browser runner, the Vercel static build), so there's no CDN dependency in CI and no behavioural split between what a human sees and what tests run.

Stories: Thumbnail, ThumbnailWithLabel, Responsive, InvalidStructure, Tooltip, InDataTable, InInteractiveScatter, Core.

Zephyr: parameters.zephyr.testCaseId intentionally left "" on all stories — apply the zephyr_sync label to generate/backfill IDs.

Verification

  • Deploys to preview environment for manual verification
  • All CI/E2E checks pass

Verified live in Storybook (real RDKit rendering, navigator.webdriver false): correct aspirin/caffeine/paracetamol depictions; invalid-SMILES fallback; dark-mode legibility (light bonds + brightened heteroatoms — soft blue N, red O); structures inside a DataTable cell; and the structure SVG rendering inside an InteractiveScatter hover tooltip.

Screenshots

UI change — verified in Storybook (thumbnail, DataTable cell, StructureTooltip popover, and the scatter hover tooltip). Reviewers can browse Design Patterns/Molecule Structure in the preview deploy.

Copilot AI review requested due to automatic review settings July 16, 2026 21:01
@owilliams-tetrascience
owilliams-tetrascience requested review from a team as code owners July 16, 2026 21:01
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ts-lib-ui-kit-storybook Ready Ready Preview, Comment Jul 17, 2026 7:01pm

Request Review

@owilliams-tetrascience owilliams-tetrascience changed the title SW-2100 Add molecule structure primitive (SMILES → 2D) feat: SW-2100 Add molecule structure primitive (SMILES → 2D) Jul 16, 2026
@owilliams-tetrascience
owilliams-tetrascience force-pushed the claude/rdkit-component-wrapper-36a448 branch from 23c94aa to 13013eb Compare July 16, 2026 21:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new composed Molecule Structure primitive (SMILES → 2D SVG) to the UI kit with lazy-loaded RDKit WASM, plus enables rich-HTML chart tooltips to support embedding molecule SVGs (and other markup) in hover content.

Changes:

  • Introduces MoleculeRenderer, StructureThumbnail, StructureTooltip, and RDKit loading helpers (useRDKit, loadRDKit, moleculeToSvg, configureRDKit) under src/components/composed/MoleculeStructure/.
  • Exports the new composed module from src/index.ts and adds Storybook stories/play tests for the new primitives.
  • Updates ChartTooltip to render tooltip lines as HTML (via dangerouslySetInnerHTML) so tooltip.content can return rich markup.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
yarn.lock Adds lockfile entry for @rdkit/rdkit to support Storybook/tests and optional peer usage.
package.json Declares @rdkit/rdkit as an optional peerDependency and devDependency.
src/index.ts Exports the new MoleculeStructure composed module from the library entrypoint.
src/components/composed/MoleculeStructure/use-rdkit.ts React hook to load RDKit lazily and expose status/error.
src/components/composed/MoleculeStructure/rdkit-loader.ts Lazy loader + CDN/self-host configuration + moleculeToSvg helper.
src/components/composed/MoleculeStructure/MoleculeRenderer.tsx Core SMILES → SVG renderer with loading + invalid/failure fallback UI.
src/components/composed/MoleculeStructure/StructureThumbnail.tsx Square thumbnail wrapper for table/list/card usage.
src/components/composed/MoleculeStructure/StructureTooltip.tsx Tooltip wrapper rendering a structure popover with theme-aware contrast.
src/components/composed/MoleculeStructure/MoleculeStructure.stories.tsx Storybook coverage (incl. play tests) exercising real RDKit via Vite ?url assets.
src/components/composed/MoleculeStructure/index.ts Barrel exports for new MoleculeStructure API surface.
src/components/charts/ChartTooltip/ChartTooltip.tsx Switches tooltip lines from escaped text to HTML rendering.

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

Comment thread src/components/charts/ChartTooltip/ChartTooltip.tsx Outdated
Comment thread src/components/charts/ChartTooltip/ChartTooltip.tsx Outdated
Comment thread src/components/composed/MoleculeStructure/MoleculeRenderer.tsx Outdated
owilliams-tetrascience and others added 2 commits July 16, 2026 17:26
Add a cheminformatics primitive for rendering 2D chemical structures from
SMILES, the kit's first cheminformatics component (VIZ-3 / HTS Hit Finder).

- MoleculeRenderer: core SMILES → SVG, lazy-loads RDKit.js
- StructureThumbnail: fixed-size or responsive square for table cells
- StructureTooltip: hover popover wrapper
- useRDKit / moleculeToSvg: preload + sync SVG helper for the interactive
  scatter's string-returning tooltip.content

RDKit's ~6.6 MB WASM is isolated in the loader and lazy-loaded once per page,
so non-chem consumers pay nothing. @rdkit/rdkit is an optional peerDependency;
assets load from a pinned jsDelivr CDN by default, or self-hosted/preloaded via
configureRDKit. The loader routes RDKit's Emscripten stdout/stderr to
console.debug so invalid SMILES don't spam the host console.

Stories and play tests both exercise REAL RDKit — the WASM is resolved through
Vite `?url` imports so it's served identically in dev Storybook, the vitest
browser runner, and the Vercel build, with no CDN dependency and no
human-vs-CI behavioural split.

Stories cover thumbnail, responsive, invalid-SMILES fallback, dark mode,
tooltip, DataTable cell, and InteractiveScatter custom tooltip. Exported from
src/index.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The design-system chart tooltip (useChartTooltip / ChartTooltip) rendered each
content line as escaped text, so the `tooltip.content` contract — which returns
HTML strings (`<br>` separators, and rich markup like `<b>` or badges) — showed
literal tags instead of formatted content. Render lines via
dangerouslySetInnerHTML so authored content (including a molecule SVG in the
interactive scatter) renders as intended. Content originates from developer
content functions and the library's own line builders, not raw untrusted input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
owilliams-tetrascience and others added 2 commits July 17, 2026 10:14
Address PR review: rendering every chart tooltip line via
dangerouslySetInnerHTML turned consumer-supplied strings (point labels,
trace names, metadata) into an XSS surface across all charts using
useChartTooltip. Make HTML rendering explicit opt-in instead — escaped
text is the default again.

- ChartTooltip: render lines as escaped text unless `anchor.html` is set;
  key lines by index (not full content, which could be a large SVG string).
- useChartTooltip / TooltipConfig: add an `html` flag threaded to the anchor.
- ScatterPlotInteractive: forward `tooltip.html`; the CustomTooltips story
  and the molecule scatter story opt in (they return trusted markup).
- ChartTooltip tests: cover both the escaped-by-default and opt-in paths.
- MoleculeRenderer: correct the `onError` doc — it may fire more than once
  (StrictMode, or when drawing inputs change), not exactly once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…6a448' into claude/rdkit-component-wrapper-36a448

# Conflicts:
#	src/components/charts/ChartTooltip/ChartTooltip.tsx
#	src/components/composed/MoleculeStructure/MoleculeRenderer.tsx
…RDKit

Per review: drop the StructureThumbnail and StructureTooltip components —
MoleculeRenderer is the single primitive. Hovering a structure is now shown
by composing MoleculeRenderer with the existing HoverCard primitive (see the
InHoverCard and InDataTable stories) rather than a bespoke component.

- Remove StructureThumbnail / StructureTooltip and their exports.
- RDKit loading is air-gapped by default: the loader dynamically imports the
  consumer-installed @rdkit/rdkit (optional peer) instead of injecting a
  jsDelivr CDN script. No CDN, nothing fetched from the network by default.
  configureRDKit({ wasmSrc }) points at a self-hosted WASM; importFactory
  overrides how the factory is obtained (and makes the loader unit-testable).
- MoleculeRenderer accessibility: aria-busy while loading, labelled loading
  and error states, role="img" with an aria-label for the structure.
- Stories: MoleculeRenderer is the meta component; add Sizes + InHoverCard;
  DataTable renders MoleculeRenderer directly with a hover-card ID column.
- Rewrite the loader unit tests for the dynamic-import path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t/scatter tooltip changes

Simplify per review: the molecule primitive no longer touches the shared chart
tooltip. Revert ChartTooltip, useChartTooltip, ScatterPlotInteractive, and its
TooltipConfig to their pre-molecule state (drop the opt-in HTML tooltip flag and
the added ChartTooltip tests), and remove the InInteractiveScatter story that
depended on it. Molecule usage is demonstrated with MoleculeRenderer directly,
the HoverCard primitive, and a DataTable cell.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the width/height props — the structure is vector and just fills its
container (fixed square internal draw resolution). The Default story now shows
this by rendering into a resizable box; remove the Sizes and InDataTable
stories.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🟢 Lines 96.76% (🎯 83%)
⬆️ +0.04%
22815 / 23578
🟢 Statements 96.76% (🎯 83%)
⬆️ +0.04%
22815 / 23578
🟢 Functions 95.76% (🎯 74%)
⬇️ -0.15%
994 / 1038
🟢 Branches 90.28% (🎯 81%)
⬆️ +0.01%
4385 / 4857
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/composed/MoleculeStructure/MoleculeRenderer.tsx 100% 70.58% 100% 100%
src/components/composed/MoleculeStructure/rdkit-loader.ts 100% 92.1% 75% 100%
src/components/composed/MoleculeStructure/use-rdkit.ts 100% 100% 100% 100%
Generated in workflow #1028 for commit 2671135 by the Vitest Coverage Report Action

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