Skip to content

refactor(shapes): add display values system for shape visual properties#8121

Merged
steveruizok merged 12 commits intodefault-shape-customizationfrom
styles-2-clean-2
Mar 1, 2026
Merged

refactor(shapes): add display values system for shape visual properties#8121
steveruizok merged 12 commits intodefault-shape-customizationfrom
styles-2-clean-2

Conversation

@steveruizok
Copy link
Copy Markdown
Collaborator

@steveruizok steveruizok commented Mar 1, 2026

In order to make shape visual properties (colors, fonts, stroke widths, etc.) configurable and overridable by SDK consumers, this PR introduces a display values system. Each ShapeUtil now declares a getDisplayValues() function that derives all visual properties from shape props and theme, and a getDisplayValueOverrides() function that consumers can use via ShapeUtil.configure() to customize any visual property.

This replaces the previous pattern where shape utils directly read theme colors and style constants internally, making those decisions opaque to consumers. Now every visual property flows through a single, configurable pipeline.

Original branch: styles-2-clean

const CustomGeoShapeUtil = GeoShapeUtil.configure({
  getDisplayValueOverrides(_editor, shape, isDarkMode) {
    return {
      // Make locked shapes red
      ...(shape.isLocked && {
        fillColor: DefaultColorThemePalette[isDarkMode ? 'darkMode' : 'lightMode'].red.solid,
      }),
      // Use monospace italic for ellipses
      ...(shape.props.geo === 'ellipse' && {
        labelFontFamily: 'monospace',
        labelFontStyle: 'italic',
      }),
    }
  },
})

Code changes

Area Net LOC Description
Shape utils +647 All built-in shape utils refactored to derive visual properties (colors, fonts, stroke widths, fill, alignment, etc.) through display values. Geo, Note, Arrow, Draw, Highlight, Text, Line, and Frame have full implementations; Bookmark, Embed, Image, and Video have empty stubs. Internal helpers (noteHelpers, arrowLabel, GeoShapeBody) updated to accept resolved values. SelectTool child states (PointingHandle, Translating) resolve note dimensions from display values.
Shared infrastructure +4 New getDisplayValues() helper and ShapeOptionsWithDisplayValues interface. New PatternFill component and defaultFills mapping. P3 color space detection moved from React hook to tlenvReactive. RichTextLabel/PlainTextLabel accept resolved values instead of style tokens. Removed ShapeFill and useColorSpace.
Examples +101 New display-options example demonstrating getDisplayValueOverrides. Updated configure-shape-util README.
Tests -1 Updated grid-align-on-create.test.ts to use inline note size instead of removed NOTE_SIZE export.
Other +274 Updated index.ts exports, both API reports, eslint-disable comments for empty interfaces.

Change type

  • api

Test plan

  1. Open the examples app and verify shapes render correctly (geo, note, arrow, draw, highlight, text, line, frame)
  2. Toggle dark mode and verify colors update
  3. Try the new display-options example to confirm overrides work
  4. Verify P3 color rendering on supported displays
  • Unit tests

Release notes

  • Add display values system for customizing shape visual properties via ShapeUtil.configure()
  • Add getDisplayValues() and ShapeOptionsWithDisplayValues to public API
  • Add getDisplayValueOverrides option for overriding colors, fonts, sizing, and other visual properties per shape
  • Move P3 color space detection to tlenvReactive.supportsP3ColorSpace
  • Breaking! RichTextLabel and RichTextSVG props changed from style tokens to resolved values
  • Breaking! NoteShapeOptions renamed to NoteShapeUtilOptions
  • Breaking! HighlightShapeOptions.underlayOpacity and overlayOpacity moved to display values
  • Breaking! Removed ShapeFill component, useColorSpace hook, NOTE_SIZE and NOTE_CENTER_OFFSET constants, getArrowLabelFontSize function

API changes

  • Added ShapeOptionsWithDisplayValues<Shape, DisplayValues> interface
  • Added getDisplayValues() helper function
  • Added display value interfaces for all built-in shapes: GeoShapeUtilDisplayValues, NoteShapeUtilDisplayValues, ArrowShapeUtilDisplayValues, DrawShapeUtilDisplayValues, HighlightShapeUtilDisplayValues, TextShapeUtilDisplayValues, LineShapeUtilDisplayValues, FrameShapeUtilDisplayValues, EmbedShapeUtilDisplayValues, BookmarkShapeUtilDisplayValues, ImageShapeUtilDisplayValues, VideoShapeUtilDisplayValues
  • Added options interfaces: GeoShapeUtilOptions, NoteShapeUtilOptions, LineShapeUtilOptions, EmbedShapeUtilOptions, BookmarkShapeUtilOptions, ImageShapeUtilOptions
  • Added tlenvReactive.supportsP3ColorSpace
  • Breaking! Changed RichTextLabelProps to use fontFamily, textAlign, verticalAlign (resolved strings) instead of font, align, verticalAlign (style tokens)
  • Breaking! Changed RichTextSVGProps similarly, adding lineHeight
  • Breaking! Renamed NoteShapeOptionsNoteShapeUtilOptions
  • Breaking! Moved HighlightShapeOptions.underlayOpacity/overlayOpacity to HighlightShapeUtilDisplayValues
  • Breaking! Removed ShapeFill, PatternFill (from ShapeFill.tsx), useColorSpace, NOTE_SIZE, NOTE_CENTER_OFFSET, getArrowLabelFontSize

Note

High Risk
Large refactor across most built-in shape utils plus multiple public API and prop-type breaking changes (e.g. label components and shape option types), which can affect rendering/export behavior and downstream integrators.

Overview
Introduces a "display values" system so shape visuals (colors, fonts, stroke widths, fills, alignment, etc.) are computed via per-ShapeUtil getDisplayValues() and consumer-overridable getDisplayValueOverrides(), with a shared getDisplayValues() helper exported in the public API.

Refactors built-in shape utils (e.g. Geo, Arrow, Note, Draw, Highlight, Text, Line, Frame) and related helpers to render from these resolved values (including new PatternFill/fill mapping and P3 color-gamut detection via tlenvReactive.supportsP3ColorSpace), and updates exports/docs plus a new examples entry demonstrating display overrides.

Written by Cursor Bugbot for commit 5c5aba4. This will update automatically on new commits. Configure here.

steveruizok and others added 11 commits February 28, 2026 13:36
… 134 broken links (#8103)

In order to catch broken internal links before they ship (like the ones
fixed in #8102), this PR replaces the `linkinator`-based HTTP crawler
with a static checker that validates links against the content database.
The old checker required a running server, couldn't fail the build, and
missed most broken links. The new checker runs after
`refresh-everything` and before `next build`, failing early if any
broken links are found.

Along the way, this fixes all 134 broken internal links found across the
docs:
- 25 content files with stale `/docs/*` paths (should be
`/sdk-features/*`), wrong heading anchors, double-prefixed paths, and
renamed example links
- API doc generator skipping `TypeAlias`/`Interface` namespace members
(the `// TODO: document these` that caused `T.Validatable` etc. to
generate broken links)
- Namespace member path generation producing standalone paths like
`T-Validatable` instead of anchor links like `T#Validatable`

The checker uses a `--fail` flag to control behavior: `yarn check-links
--fail` (used in `build`) exits non-zero on broken links, while `yarn
check-links` (used in dev) only warns. This ensures broken links never
block the dev server from starting.

Additionally, this adds a standalone `yarn check-external-links` script
for manually checking external (HTTP/HTTPS) links across all docs
content. It deduplicates URLs, uses HEAD with GET fallback (30s timeout,
10 concurrent requests), and retries transient failures with backoff. A
skip list filters out domains that block automated requests (npmjs.com,
shadertoy.com). This replaces the external link checking that was
previously handled by linkinator but is too slow/flaky for the build
pipeline.

Also refactors the hardcoded Framer path prefixes into a shared
`fetchFramerPaths()` utility that fetches the live Framer sitemap. Both
the link checker and `app/sitemap.ts` now use this, so the list stays up
to date automatically.

Fixes a broken external link to the removed `custom-toolbar` example in
the v2.1.0 release notes.

### Change type

- [x] `improvement`

### Test plan

1. `cd apps/docs && yarn refresh-everything && yarn check-links` —
should pass with 0 broken links (warnings only)
2. `yarn check-links --fail` — same result but would exit 1 if any
broken links existed
3. Manually add a broken link to a content file (e.g.
`[test](/nonexistent)`) and run `yarn check-links --fail` — should exit
1 with the broken link reported
4. Run `yarn build` from `apps/docs` — check-links runs with `--fail`
before `next build`
5. `yarn check-external-links` — checks all ~630 unique external URLs,
reports broken ones

- [ ] Unit tests
- [ ] End to end tests

### Release notes

- Fix 134 broken internal links across SDK docs, starter kits, and API
reference
- Add static broken link checker to docs build pipeline (replaces
linkinator)
- Add standalone external link checker script (`yarn
check-external-links`)
- Document `TypeAlias` and `Interface` members within API reference
namespaces
Closes #7472

Adds an example demonstrating how to create and customize text labels on
arrows. The example covers:

- Creating arrows with labels using `toRichText()` and the `richText`
prop
- Positioning labels along the arrow path with `labelPosition` (0 =
start, 0.5 = middle, 1 = end)
- Setting independent `labelColor` distinct from the arrow's `color`
- All four font styles: `draw`, `sans`, `serif`, `mono`
- Curved arrows with `bend`

### Change type

- [x] `feature`

### Test plan

1. Run `yarn dev` and navigate to the "Arrow labels" example under
Editor API
2. Verify arrows render with correct labels, positions, colors, and
fonts
3. Verify the curved arrow displays with a bend and centered label

### Release notes

- Add arrow labels example showing how to create and customize text
labels on arrows.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Adds new example/demo code and documentation only, with no changes to
core editor behavior.
> 
> **Overview**
> Adds a new Editor API example, **Arrow labels**, that seeds the canvas
with several arrow shapes showcasing labeled arrows via
`richText`/`toRichText()`.
> 
> Demonstrates label customization including `labelPosition`,
independent `labelColor`, multiple `font` styles, and a curved (`bend`)
arrow, plus a matching `README.md` to surface the example in the
examples catalog.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
fea149b. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Closes #8100

The `ShapesWithSVGs` component (used when Debug SVG is enabled) renders
`<Shape>` without a `<ShapeCullingProvider>` ancestor. Since the `Shape`
component calls `useShapeCulling()`, this throws: "useShapeCulling must
be used within ShapeCullingProvider". The `ShapesToDisplay` component
already had this provider but `ShapesWithSVGs` was missing it.

This wraps `ShapesWithSVGs` content with `<ShapeCullingProvider>`,
`<CullingController>`, and the Safari reflow workaround — matching what
`ShapesToDisplay` already does.

### Change type

- [x] `bugfix`

### Test plan

1. Open tldraw and create some shapes on the canvas
2. Open the debug menu and enable **Debug mode**
3. Enable **Debug SVG**
4. Verify the app no longer crashes and SVG debug copies appear below
each shape

### Release notes

- Fix crash when enabling Debug SVG with shapes on the canvas.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: localized change to canvas shape rendering that mainly
affects debug-only `Debug SVG` mode and reuses existing culling logic.
> 
> **Overview**
> Fixes a crash when enabling **Debug SVG** by ensuring shapes rendered
in that mode are wrapped in `ShapeCullingProvider` (so
`useShapeCulling()` always has a provider).
> 
> Simplifies the canvas rendering path by replacing the separate
`ShapesWithSVGs`/`ShapesToDisplay` branches with a single `ShapesLayer`
that toggles debug SVG copies internally while still running
`CullingController` and the Safari reflow workaround.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
aba4a4e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Introduce the foundational system for shape display values — a pattern
that lets each ShapeUtil declare how its visual properties (colors,
fonts, stroke widths, etc.) are derived from shape props and theme.

- Add ShapeOptionsWithDisplayValues interface and getDisplayValues()
  helper that merges base values with user-provided overrides
- Add DEFAULT_FILL_COLOR_NAMES mapping for fill style → theme color
- Add PatternFill component to replace the pattern case from ShapeFill
- Move P3 color space detection from a React hook (useColorSpace) into
  a reactive atom on tlenvReactive, so it can be read outside React
- Update RichTextLabel and PlainTextLabel to accept resolved display
  values (fontFamily, textAlign, verticalAlign) instead of style tokens
- Remove ShapeFill (superseded by per-shape fill rendering) and
  useColorSpace (superseded by tlenvReactive.supportsP3ColorSpace)
Refactor GeoShapeUtil to derive all visual properties through display
values. This is the most complex shape — it has stroke, fill, and a
rich text label with alignment, font, and sizing configuration.

- Define GeoShapeUtilDisplayValues and GeoShapeUtilOptions interfaces
- Move label sizing, growY calculation, and shape expansion logic from
  module-level functions into private methods on the util class so they
  can access display values through this.options
- Replace direct theme/constant lookups with display value properties
- Move GeoShapeBody from components/ subdirectory to flat file,
  accepting resolved colors as props instead of reading theme internally
Refactor NoteShapeUtil to derive visual properties through display
values. The note shape is unique because its dimensions (noteWidth,
noteHeight) are themselves configurable, and several external callers
(noteHelpers, Pointing tool state) need access to those dimensions.

- Define NoteShapeUtilDisplayValues with note dimensions, background
  color, border, and label properties
- Move label sizing and font adjustment logic into private methods
- Refactor noteHelpers to accept note dimensions via options objects
  instead of importing NOTE_SIZE/NOTE_CENTER_OFFSET constants
- Update Pointing tool state to resolve note dimensions from the util
Refactor ArrowShapeUtil to derive visual properties through display
values. Arrows have stroke, fill (for arrowheads), and a label with
font, padding, and border radius configuration.

- Define ArrowShapeUtilDisplayValues interface in arrow-types.ts and
  extend ArrowShapeOptions with ShapeOptionsWithDisplayValues
- Pass resolved colors and dimensions to the ArrowSvg component as
  props instead of reading theme internally
- Update arrowLabel.ts to resolve font and padding from display values
  instead of importing constants directly
- Remove getArrowLabelFontSize (superseded by display values)
Refactor DrawShapeUtil and HighlightShapeUtil to derive visual
properties through display values.

- DrawShapeUtil: define display values for stroke color/width, fill
  color, and pattern fallback; pass resolved values to DrawShapeSvg
- HighlightShapeUtil: define display values for stroke color/width
  and opacity; move P3 color selection into getDisplayValues using
  the new tlenvReactive.supportsP3ColorSpace; move underlayOpacity
  and overlayOpacity from static options to display values
…peUtil

Refactor the remaining shape utils to derive visual properties through
display values.

- TextShapeUtil: define display values for color, font family, size,
  line height, and font style; pass resolved values to text measurement
- LineShapeUtil: define display values for stroke color and width
- FrameShapeUtil: define display values for fill, stroke, and heading
  colors in both default and showColors modes
Add empty display value implementations to shapes that don't yet have
visual properties to expose (bookmark, embed, image, video), ensuring
all built-in shapes conform to the ShapeOptionsWithDisplayValues
interface. Also wire up the EmbedShapeUtil's showShadow as a display
value.

Update SelectTool child states (PointingHandle, Translating) to resolve
note dimensions from the NoteShapeUtil's display values instead of
importing the removed NOTE_SIZE and NOTE_CENTER_OFFSET constants.
- Export all new display value types and interfaces from the tldraw
  package entry point
- Update both editor and tldraw API reports to reflect the new public
  API surface
- Add a display-options example demonstrating how to use
  getDisplayValueOverrides to customize shape rendering
- Update configure-shape-util example README to reference the renamed
  NoteShapeUtilOptions
- Update grid-align-on-create test to use inline note size constant
  instead of the removed NOTE_SIZE export
@huppy-bot huppy-bot bot added the api API change label Mar 1, 2026
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 1, 2026

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

Project Deployment Actions Updated (UTC)
examples Ready Ready Preview Mar 1, 2026 3:47pm
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
analytics Ignored Ignored Preview Mar 1, 2026 3:47pm
chat-template Ignored Ignored Preview Mar 1, 2026 3:47pm
tldraw-docs Ignored Ignored Preview Mar 1, 2026 3:47pm
tldraw-shader Ignored Ignored Preview Mar 1, 2026 3:47pm
workflow-template Ignored Ignored Preview Mar 1, 2026 3:47pm

Request Review

Update the API report to include a new `labelExtraPadding` property on `GeoShapeUtilDisplayValues`. This reflects an added display option for extra padding around geo-shape labels and keeps the generated API docs in sync with the code.
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

// We cast here because editor.getShapeUtil('arrow') returns ShapeUtil<TLArrowShape>
// with generic options, but the actual instance has ArrowShapeOptions with display values.
return getDisplayValues(editor.getShapeUtil('arrow') as any, shape, false)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Arrow label sizing always uses light mode display values

Medium Severity

getArrowDisplayValues hardcodes isDarkMode to false and is used in labelSizeCache (a createComputedCache). The cache is keyed only by shape record, not by dark mode state. If a consumer provides getDisplayValueOverrides that changes size-related properties (like labelFontSize, labelPadding, or labelFontFamily) based on isDarkMode, the cached label size will always reflect light-mode values. This causes incorrect label sizing, geometry, and clip paths in dark mode for consumers using the new override system.

Additional Locations (1)

Fix in Cursor Fix in Web

? 'center'
: verticalAlign === 'end'
? 'flex-end'
: 'flex-start',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Legacy alignment values handled inconsistently between label components

Low Severity

RichTextLabel now receives pre-mapped textAlign values where legacy alignments like 'start-legacy' are mapped to 'start', resulting in justifyContent: 'flex-start'. Previously, ALL legacy alignment values produced justifyContent: 'center' due to the legacyAlign catch-all. Meanwhile, PlainTextLabel preserves the old behavior where legacy values still map to 'center'. Shapes with 'start-legacy' or 'end-legacy' alignment will now render with different text justification in RichTextLabel compared to before, and differently from PlainTextLabel.

Additional Locations (1)

Fix in Cursor Fix in Web

@steveruizok steveruizok changed the base branch from main to default-shape-customization March 1, 2026 16:44
@steveruizok steveruizok merged commit 8d8ce83 into default-shape-customization Mar 1, 2026
22 checks passed
@steveruizok steveruizok deleted the styles-2-clean-2 branch March 1, 2026 16:44
steveruizok added a commit that referenced this pull request Mar 4, 2026
github-merge-queue bot pushed a commit that referenced this pull request Mar 4, 2026
Update `next.mdx` release notes to cover all SDK-relevant PRs merged to
main since v4.4.0.

Highlights:
- Display values system (#8121) with breaking changes and migration
guide
- Click-through on transparent image pixels (#7942)
- `Editor.resizeToBounds()` (#8120)
- SVG sanitization (#7896)
- TypeScript enum-to-const refactoring (#8084)
- 14 bug fixes and 4 improvements

### Change type

- [x] `other`

### Test plan

1. Verify the release notes render correctly on the docs site

### Release notes

- Update next release notes with changes since v4.4.0.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: documentation-only changes updating release notes content
and date, with no runtime/code behavior impact.
> 
> **Overview**
> Updates `apps/docs/content/releases/next.mdx` for the upcoming release
by refreshing the date and replacing the brief blurb with expanded
release notes.
> 
> Documents new SDK surface area (`Geometry2d.ignoreHit`,
`Editor.resizeToBounds`, `sanitizeSvg`), highlights click-through on
transparent image pixels, and adds a list of recent improvements and bug
fixes (paste parenting, link/alt-text persistence, SVG sanitization
behavior, circular-dependency cleanup, and several crash/export/sync
fixes).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5f7dc0a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api API change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants