Skip to content

feat(graph-uikit): add @gravity-ui/graph-uikit kit for uikit apps - #429

Draft
draedful wants to merge 32 commits into
v2from
feat/graph-uikit
Draft

draedful wants to merge 32 commits into
v2from
feat/graph-uikit

Conversation

@draedful

@draedful draedful commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds a new v2 package, @gravity-ui/graph-uikit, for building graphs inside @gravity-ui/uikit applications. It has two layers:

  • Kit: building blocks to compose your own graph.
  • Facade: useUikitGraph and UikitGraphCanvas, which wire the whole kit in by default.

Canvas rendering and LOD stay the core of the library. Every canvas level now gets the uikit look, and React is used only at the Detailed level.

Core (packages/graph) is not modified. Code that should move into core is tagged // TODO(graph-core): <what> — <why>; see the list below.

What's inside

  • Theme bridge. Graph colors follow the active uikit theme, with a global or scoped ThemeProvider.

    • Tokens are read from the graph root in a passive effect, after the provider's own layout effect.
    • The mapping is UIKIT_GRAPH_TOKENS.
    • useUikitGraph({ colors, tokens }) overrides single values or the whole table.
    • Palette and typography values that don't fit into TGraphColors live in a per-graph signal store.
  • Canvas text. renderUikitText / measureUikitText mirror uikit <Text>: variants, colors, ellipsis, ellipsisLines, whiteSpace, wordBreak. The typography is built from the per-variant --g-text-* tokens.

  • UikitBlock. A canvas block with a uikit surface on every LOD level:

    • radius 8 and uikit borders;
    • hover and selection;
    • statuses through block.meta.theme, plus block.meta.description.

    Override renderContent(ctx, rect, level) for custom content.

  • UikitConnection. Uikit line colors, screen-pixel line and arrow widths, and a uikit label pill.

  • UikitGraphBlock. The Detailed-level React skeleton: GraphBlock with a uikit Box that matches the canvas surface pixel for pixel. It respects the colors override.

  • Interactions.

    • UikitInteractionLayer, GraphTooltip, GraphContextMenu and useHoveredEntity, built on uikit Popup/Menu. They work on every LOD level, including React blocks.
    • The native context menu is intercepted only while a GraphContextMenu is mounted.
  • Toolbar. GraphUikitToolbar is a floating toolbar of uikit buttons grouped into cards.

    • Only the zoom group ships, via group(...zoomGroup({ reset, viewport, selection })): zoom in and out with a multiplicative step, 1:1, fit to viewport and fit selection.
    • Disabled states follow the camera and the selection.
    • Project-specific groups are built with group(...) and custom actions.
    • This adds the runtime dependency @gravity-ui/icons.
  • Modes. view / edit. Edit mode allows dragging blocks and creating connections through @gravity-ui/graph-connection-editing: Shift+drag between blocks, or anchor-to-anchor.

  • Repo additions.

    • Storybook Plugins/Uikit stories (View, Edit, CustomBlock, four-theme switcher).
    • E2E specs in a separate uikit bundle and page (apps/e2e/tests/uikit, including toolbar.spec.ts).
    • The package contract and README.
    • Repo wiring: build filters, release-please config and manifest, tsconfig-strict list, root README.

Decisions worth knowing

  • Publish build. tsconfig.publish.json uses skipLibCheck: true. uikit's declarations import react-beautiful-dnd types that uikit ships only as a devDependency (TS7016). The e2e and contract fixtures keep skipLibCheck: false and use a small ambient type shim instead. The README tells consumers to install @types/react-beautiful-dnd.
  • Contract. The contract has no Node runtime probe (uikit's ESM build imports .css) and no types/node-esm fixture (uikit's own declarations fail under Node16).
  • Canvas text rules:
    • wordBreak: "break-all" behaves like "break-word";
    • at least one line is always drawn;
    • ellipsisLines truncates the last kept line on its own, like CSS line-clamp.
  • Hover point. It comes from the last real pointer move, read in the capture phase. Core's synthetic camera-emulation events never feed it.
  • Public API. The exports are narrowed to the documented API, and the shared defaults are deep-frozen.

Core candidates (grep -rn "TODO(graph-core)" packages/graph-uikit/src)

  • Graph-level mouseleave when the pointer leaves the root, and a graph-level contextmenu event.
  • A public "component → entity kind/id" API, and a "world rect → viewport rect" helper.
  • A hover state on Block.
  • In core text layout:
    • ellipsis and line clamp in layoutText;
    • line height is parsed from the font string, so "500 13px" becomes 50013;
    • the measureText cache is never cleared.
  • A configurable default block class, and extensible TGraphColors groups.
  • Per-graph extension storage.

Testing

  • ✅ pnpm run build.
  • ✅ graph-uikit: 24 suites and 101 unit tests; typecheck and lint are clean.
  • ✅ Typecheck of storybook and e2e, the e2e bundle, test:type-configs and test:release-contract.
  • ⚠️ Not run yet: the Playwright E2E (pnpm --filter @gravity-ui/graph-e2e run test:e2e tests/uikit) and the package contract (pnpm --filter @gravity-ui/graph-uikit run test:package-contract). The dev sandbox blocked local port binding and registry access.
  • ⚠️ No visual check of the Storybook stories yet: the four themes, canvas versus React parity, tooltip and menu.

🤖 Generated with Claude Code

draedful and others added 25 commits October 20, 2026 03:15
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…graphy

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>
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tore

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>
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>
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… edit mode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Clear the kit hover when the pointer leaves the graph root (core emits
  no graph mouseleave there) and restore it on re-entry over the same
  target, where core emits no mouseenter.
- Hide GraphTooltip while the context menu is open.
- Release the pressed pointer on window blur and root pointercancel.
- Drop the context-menu state when items() is empty or null and when the
  last GraphContextMenu unmounts, so it cannot reopen later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- React block surfaces read the graph colors GraphCanvas mirrors
  (--graph-block-bg/-border/-border-selected) with uikit token fallbacks,
  so a colors override matches the canvas block on every level.
- Resolve typography from the kit defaults instead of uikit's runtime
  TEXT_VARIANTS; getUikitTextStyle falls back to body-1 for unknown
  variants instead of crashing.
- UikitConnection draws arrows with screen-pixel widths, skips the label
  pill when its text is too small to draw and re-renders on typography
  changes.
- UikitBlock skips the description when no caption line fits.
- Set graph colors before the palette in one batch, so palette
  subscribers see the new colors.
- Fix the radius comment in UIKIT_GRAPH_CONSTANTS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- index.ts lists the documented API with explicit named exports; helpers
  such as layout, entity and theme-resolution internals are no longer
  exported. UikitGraphStore is exported as a type only.
- Deep-freeze the shared defaults (DEFAULT_UIKIT_PALETTE,
  DEFAULT_UIKIT_TYPOGRAPHY, UIKIT_GRAPH_TOKENS, UIKIT_GRAPH_CONSTANTS) and
  type them read-only; renderUikitText returns a new empty layout per
  call.
- useUikitGraph({ tokens }) sets the uikit token table the theme is read
  from (store $tokens, default UIKIT_GRAPH_TOKENS).
- Tag the palette and default block class as core candidates.

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

- README: ThemeProvider is required; theme token table and the tokens
  option; how edit mode creates connections; unregistered block types
  fall back to core Block; prefer @types/react-beautiful-dnd over an
  ambient shim.
- Contract: assert the uikit, react and react-dom peer ranges.
- Storybook: scoped ThemeProvider that fills the viewport height.
- E2E: the connection tooltip is anchored above the pointer (off-centre
  hover); switching view to edit at runtime enables anchor connections.

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

Copy link
Copy Markdown
Contributor

Preview is ready.

draedful and others added 4 commits October 20, 2026 17:37
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
draedful and others added 3 commits October 20, 2026 18:18
…ped group

The group clips its children with overflow: hidden, and uikit draws the button focus ring outside the button box, so it was cut off (a lone action button got no ring at all). Draw it inside with uikit's public --g-button-focus-outline-offset and set an explicit ring color so it stays visible on brand-filled buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The default reset title was "Actual size", so the accessible name did not contain the visible "1:1" label. Use "Actual size (1:1)" and update the unit tests, README and E2E.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…styles in the contract

Fit selection, fit to viewport and 1:1 all end at scale 1 with the default blocks, so also assert that the selected block is centered in the viewport. Reword the misleading hit-box comment on the fit-to-viewport tolerance (it is rounding slack). The contract now checks that the toolbar group styles are shipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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