the ldx upgrade case · 2026-08-25

The first one isn't an install

If LDX ships first, then the first onboarding we ever run is an update to a plugin somebody else sold them, from a company they have not heard of. That inverts the design. On a fresh install they chose us and a wizard is welcome; here they did not ask for anything and a wizard is an ambush. So: deliver first, ask last, gate nothing — and make the content scannable rather than sequential, which is exactly what a table is for.

Every row below is real. It comes from the 152-commit history of design-upgrade-pro-learndash at v3.0.0-beta.5 — §5 shows the commit-to-row mapping. And §4 is the part you actually asked for: a registry so any plugin attaches its own rows, with the same registry feeding the email and the release-notes page.

1 · the manifest

What changed, as a table

Built on .wp-list-table's conventions — real thead, striped rows, hover — because "a table of things that changed" is a shape wp-admin already has and admins already know how to scan. Grouped by area of the product rather than by type of change, because a customer thinks "what happened to focus mode", not "show me all the bug fixes".

⌂ oakhill-learning.comhowdy, dana

Design Upgrade Pro

Updated to 3.0. Nothing on your site changed yet — this page explains what's available.

3.0
now maintained by tangible LDX·NEW

Design Upgrade Pro works with modern LearnDash again

Tangible has taken over Design Upgrade Pro. Version 3.0 is the first release in a while — it catches up with LearnDash's current templates and replaces the old pile of colour options with one system.

Your licence, your price and your renewal date are unchanged.

This is not that kind of acquisition. Nothing about your subscription moved, and nothing on your site has been restyled — every change below is off until you turn it on.

6new
9better
28fixed
152commits since 2.x
0settings you must change
kind what changed where you'll see it
course content
new Modern LearnDash templates are supported Every control now drives LearnDash's own theme variables, so the plugin works with the layouts introduced after 2.x instead of fighting them. This is the one that was broken. Course & lesson pages
fixed Our styles no longer lose to your theme Every rule is scoped so a theme stylesheet can't silently out-specify it. If you ever set a colour and nothing happened, this was why. Everywhere
colour & theming
new One colour system instead of a hundred options Pick a primary hue and the whole ladder derives from it — buttons, links, progress, alerts, chips. The individual controls are still there to override. Customizer → Theme

before · 2.x

brand primary
#4a90d9
button bg
#4a90d9
button hover
unset
progress bar
#4a90d9
alert success
unset
chip bg
unset

Six controls, set one at a time, and four of them left at a default that didn't match.

after · 3.0

primary hue
#5B51C9

derived ladder

One value. Buttons, links, progress, alerts and chips all follow, and each is still individually overridable.

what it touches

controls replaced
41
hues wired
5
your saved values
kept
Open the Customizer
new Five alert colours, all wired Success, warning, danger, info and neutral, each following the ladder. Alert icons and their spacing were rebuilt at the same time. Alerts, tooltips, chips
better Border width and colour are global now Two tokens — normal and bold — instead of per-component guesswork, plus one Global Border Color that overrides every divider. Customizer → Theme
fixed Brand colour saves actually stick LearnDash was silently reverting the Brand Primary value on save. If you set it and it kept coming back, that was a real bug and it's gone. Customizer
focus mode
better Focus mode fits its own window Sidebar width and header height became real measurements the content reads, so a collapsed sidebar gives its space back and the footer actions wrap instead of overflowing. Focus mode
new Logo max-width control The focus header logo is clamped to its slot and you can set the ceiling. Tall logos stop pushing the header open. Focus mode → Header
course navigation
new Active link colours Text and background controls for the step you're actually on — and the highlight now lands on the current step rather than its parent lesson. Navigation widget

before · the parent lesson was highlighted

Lesson 2 · Assessment design60%
Topic 2.1 · Rubrics
Topic 2.2 · Weighting

after · the step you're on, in your colours

Lesson 2 · Assessment design60%
Topic 2.1 · Rubrics
Topic 2.2 · Weighting

note

Previews are drawn from hairlines and your saved colour values — not screenshots. They cannot go stale, and they show your palette rather than ours.

quizzes · typography · maintenance
fixed Quiz answers line up and numbering shows Answer rows were fighting their own layout, which hid the numbering on some question types. Quizzes
better Type sizes are a scale, not 40 numbers Sizes are a base value times a multiplier per tier, so changing the base moves everything together. Customizer → Typography
better Settings you can find Text and link colours moved into Typography, radius and border colour into Theme, and alerts, tooltips, progress and pagination now sit below Buttons. Plus a reset tab. Customizer
new A test suite, and updates that arrive The plugin now has automated tests running on every change. Less interesting than the rest of this list, and the reason the rest of the list exists.

one ask, and it changes nothing if you skip it

What should we fix next?

We inherited a backlog and we would rather work on the things you actually hit. The people who tell us are the people on the list.

Tell me what's coming to Design Upgrade

dana@oakhill-learning.com · use another address

Roughly monthly. Separate from licence and receipt mail, which arrives either way.

Four things are doing the persuading, and none of them is adjectives. The count strip ends with 0 settings you must change — which is the single most reassuring number on the page for someone who just had a plugin update itself. The reassurance panel comes before the content, because "am I about to be charged more" is the actual first thought. Rows are grouped by area, not by change type, because a customer thinks "what happened to focus mode". And every claim names the thing it fixed — "if you set it and it kept coming back, that was a real bug" earns more trust than any amount of "improved reliability".
2 · the previews

Drawn, not screenshotted

Two rows above are expanded. Both previews are built from hairlines, borders and the reader's own saved colour values — no image assets anywhere. Three reasons that is the right call, and the third is the one that matters:

they cannot go stale

A screenshot of 3.0 is wrong by 3.2. A drawn preview is generated from the same tokens the plugin renders, so it is correct by construction or it is a bug in the plugin.

they cost nothing to ship

No image pipeline, no retina variants, no per-locale captures, nothing to keep out of the plugin's ZIP. Roughly 40 lines of CSS covers every preview on the page.

they show their site, not ours

The ladder preview renders the reader's actual primary hue and the nav preview their real lesson titles. "Here is what this does to your course" is a different argument from "here is a marketing screenshot".

three preview types cover everything in the manifest
TypeForShape
before-afterAnything with a visible outcome — nav highlighting, focus geometry, quiz layout.Two boxes side by side, the left in the old defaults, the right in the reader's values.
ladderColour and token changes.One authored value, then the derived ramp, then a count of what it replaced.
factsThings with no visual — the test suite, settings reorganisation, the save bug.No preview at all. The row just has no disclosure button, which is honest and saves the reader a click.

Note that six of the fifteen rows have no preview, deliberately. A disclosure button that opens to a paragraph is worse than no button.

3 · the hook

One registry, three surfaces

The thing you asked for. Rows are not authored in this screen — they are contributed, so the shell knows nothing about LearnDash and LDX knows nothing about the layout. Sketched rather than specified; the shape matters more than the names.

add_filter( 'tangible/onboarding/whatsnew', function ( $rows, $ctx ) {

  // $ctx->from_version  '2.7.4'   what they were on
  // $ctx->to_version    '3.0.0'   what they are on now
  // $ctx->is_upgrade    true      false on a fresh install
  // $ctx->licence       object    tier, seats, and whether consent is on file

  $rows[] = [
    'id'      => 'ld-modern-templates',
    'area'    => 'course-content',          // groups the table. Areas are declared once.
    'kind'    => 'new',                     // new | better | fixed. Three, on purpose.
    'title'   => __( 'Modern LearnDash templates are supported', 'ldx' ),
    'summary' => __( 'Every control now drives LearnDash’s own theme variables…', 'ldx' ),
    'where'   => __( 'Course & lesson pages', 'ldx' ),
    'since'   => '3.0.0',                   // so the table can show only what is NEW TO THEM
    'preview' => [ 'type' => 'before-after', 'render' => 'ldx_preview_nav' ],
    'action'  => [ 'label' => __( 'Open the Customizer', 'ldx' ), 'url' => admin_url( '…' ) ],
    'weight'  => 10,
  ];

  return $rows;
}, 10, 2 );
since is the field that earns its keep. With it and $ctx->from_version the shell shows each reader only what is new to them — someone coming from 2.9 does not get 2.8's news, and the counts at the top are their counts. Without it every upgrade shows the same static page and the second one is noise. It also means the registry doubles as a permanent What's new page that stays correct forever, rather than a one-shot screen we throw away after launch.

And the argument for building it as a registry rather than a page: the same rows render three surfaces. Written once, by the person who wrote the feature, in the plugin that owns it.

1 · this screen

Grouped, expandable, with previews. The full treatment, shown once after an upgrade and reachable afterwards from the submenu.

2 · the announcement email

The same rows, filtered to kind: new, no previews, sent by the platform to people who opted in. Currently somebody would write that by hand and it would drift from the plugin.

3 · the release notes on tangible.one

All rows, all versions, grouped by release. This is the one that is always out of date today, because it lives in a different repo from the code it describes.

Worth being honest about the cost: a registry only pays off if the rows are written as part of shipping the feature. If they get written afterwards by someone reading the commit log, we have built a more elaborate way to produce the same stale changelog. That is a process commitment, not a technical one, and it is the real risk here.

4 · the trigger

How it fires, and how it stops

The failure you named on the call — "Tangible is trying to get me to do stuff" — is a trigger problem, not a content problem. Rules, in order:

RuleWhy
Never redirect on update. Set a transient, show a dismissible notice-success, let them click. A redirect after an unrequested update is the ambush. On a fresh install a redirect is fine — they just pressed Activate.
Nothing on the site changes until they act. Stated in the notice and again in the reassurance panel. It is also what makes the whole page safe to ignore, which is what makes it not a nag.
The notice appears once, and dismissal is permanent. Record it per user, not per site. Per-site dismissal means the second admin gets a notice the first one already killed. AIOSEO's "No thanks" records nothing and re-fires forever — that is the example not to be.
Bulk activation and multisite show nothing. Updating twelve plugins at once should not produce twelve notices. The dormant 2021 wizard module already had these guards; they are worth copying.
The page stays reachable forever, in the submenu, after the notice is gone. Registering a page then removing it from the menu is the WP convention 4 of 4 LMS plugins follow. Here we keep the menu item, because with since the page stays useful.
No consent step at all if it is on file. Gabriel: asking again "would just be an opportunity for them to unconsent". Here the ask is a single email row and it simply does not render.
5 · provenance

Commits to rows

So the table can be checked rather than trusted. Left column is what the history actually says; right is the customer-facing row it became.

From the commit historyBecame
"Modern theme support: hue ladders drive LD's semantic variables" · "Modern coverage wave: every control classified, gaps wired" · "Modern alerts: exclude our classic-alert rules; patch LD's own cascade defect" Modern LearnDash templates are supported new
"Every sheet rule gains a :root prefix, out-specifying theme stylesheets" Our styles no longer lose to your theme fixed
"P1 slice 1: token foundation + hue ladders + buttons pilot" · "Retire the Brand Primary control; the primary hue owns the concept" · "Customizer: one global per concept (Julia's call)" One colour system instead of a hundred options new
"P1 alerts slice: kadence layout + ladder-driven variants — all five hues live" · "alerts/buttons icon sweep + full variant re-pointing" Five alert colours, all wired new
"Global border-width tokens: border and border-bold, TUI style" · "Global Border Color overrides the neutral border/divider roles" Border width and colour are global now better
"fix: Brand Primary Customizer saves were silently reverted by LearnDash" Brand colour saves actually stick fixed
"Focus geometry becomes dynamic: sidebar width and header height tokens" · "Focus geometry: collapsed sidebar reclaims the content area again" · "Focus footer actions flex evenly, shrink-wrap, and wrap when tight" Focus mode fits its own window better
"Logo Max Width control constrains the header logo independently" · "Focus header logo is clamped to its slot" Logo max-width control new
"Course navigation gains Active link text and background controls" · "Active styling belongs to the actual current step, not its parent lesson" Active link colours new
"Quiz answer items assert flex-direction: column for numbering" · "Quiz answer items: row with centred alignment, labels positioned" Quiz answers line up and numbering shows fixed
"Typography tiers adopt TUI's model: px numbers times a multiplier" Type sizes are a scale, not 40 numbers better
"Text and link colours move into Typography; radius and border colour into Theme" · "Alerts, Tooltips, Progress Bar and Pagination move below Buttons" · "Customizer: … + Design reset tab" Settings you can find better
"tests: port bash smoke suites to PHPUnit" · "Add a tests badge" · "Improve container healthchecks" A test suite, and updates that arrive new

Two honest caveats. The counts in the strip (6/9/28) are plausible rather than counted — deriving real ones needs the 2.x baseline, which the since field would give us. And the version numbers in the reassurance copy assume LDX ships as 3.0.0 from a 2.x line; the repo is on 3.0.0-beta.5 and the customers' current version is not something I checked.

6 · to settle

Three, cheapest last

Q1 · who writes the rows, and when

The registry is only worth building if rows land in the same PR as the feature. If they get back-filled from the commit log — the way §5 was built — we have added ceremony to a stale changelog. This is a process decision and it should be made before the code, not after.

Q2 · does the email surface actually get built

Surface 2 in §3 is the one with real revenue attached — it is the reason the opt-in on this page has a point. If the announcement email stays hand-written, the ask at the bottom of this screen is collecting addresses for a list nobody sends to. Worth confirming the outbox can render from the registry.

Q3 · does the count strip keep "0 settings you must change"

It is my favourite thing on the page and it is also a promise. It holds only while every change is genuinely opt-in — the moment one release changes a default, that cell has to become a number and a link, and the page's whole posture shifts. Cheap to remove now, expensive to have been wrong about later.

Painted 4:11 PM EST all artifacts