Somebody else's page
wp-admin gives us a rectangle, and the top of that rectangle is a notice dumping ground. Everything on this page follows from taking that seriously. The brief from the call was "not overly playful, like tangible one, but more wordpress-y, but still tangible identifiable" — so the question is not how to decorate a settings form, it is which signals survive containment.
Two decisions from the same call make this a frame rather than a screen: the wizard is core and inherits the installed plugin's colour, and the consent step is skipped rather than pre-checked when the licence already carries consent — so the step list is resolved at runtime, not authored. §3 is about the consequence nobody named: a progress rail you cannot draw until the licence returns.
This is greenfield, and it replaces something worse. An
environment audit found zero onboarding across every Tangible plugin — no
activation hook, no wizard, no first-run state; a clean activation of Loops & Logic
4.2.4 writes no options at all. What does exist is a non-dismissible,
site-wide notice notice-error when a licence key is
missing, plus a fake "update available" row on the Plugins list. Both violate our own
rule that licence UI lives in the plugin's Settings tab and never in an admin-wide
banner. So the first job of §3's licence step is to earn the right to delete that
notice — a nag that fires forever is the current design, and anything with an
ending beats it.
Five things core owns, and we don't touch
From the design DNA brief's constraint map. These are not stylistic preferences — each one has a failure mode that only shows up for some users, which is the worst kind.
| Core owns | The failure if we take it | What we do instead |
|---|---|---|
The primary button #2271b1 |
An indigo CTA beside a core-blue button makes two competing primaries. surfaces.css already wrote the rule down: the second coloured thing always kills the first one's pull. |
Use .button-primary unstyled. Indigo is demoted to structure: grid, rules, sheet numbers, meter fill. |
| Link colour, and 8 admin colour schemes that recolour it per user | A hard-coded link colour looks broken only for the user on Midnight or Ocean. You will never see it. | Never set a colour on a. Inherit. |
Form controls and their #2271b1 focus ring |
Fighting core specificity breaks focus visibility — an accessibility regression traded for a nicer border radius. | Zero restyling. This is the hardest rule here and the one most likely to be broken for aesthetic reasons. |
The notice zone at the top of .wrap |
Any designed hero gets yellow banners inserted into the middle of it, by plugins we don't control. | Design the first ~120px as expendable. Stress-tested in §9. |
| The admin font stack | A webfont means FOUT on a page the user opened because something needs doing. | No webfont. Monospace is used for labels only, and every platform has one. |
Four ways to own a rectangle
The indigo hairline grid was doing the "this is our paper" job, and it is the wrong device here for two reasons. It is already spent — the DDD viewer wears it, and a signature used twice in different registers stops signalling either. And its identity came from owning the page: a blueprint field reads as structure across a full sheet and as decoration inside somebody else's 900px column. Perfect for a proprietary surface. Noisy here.
So: four candidates, same screen, all four dropping the grid. What stays constant in every one is what now has to carry the identity — the mono micro-label, the 1.5px ink rule, hairlines over shadows, 4px radii, and the copy voice. If none of these read as ours, the problem was never the grid.
SearchSync
Where should search run?
You can change this later. Local indexing needs nothing else from you.
White panel on WP's grey — structurally identical to a .postbox.
Everything ours is in weight: the 1.5px ink rule under the heading, the mono
step count, the line-height. Maximum nativeness. The risk is real
and worth naming: at a glance this may read as core with better typography rather
than as Tangible at all.
SearchSync
- engine
- indexes
- Posts, pages, courses · 1,248 items
- reindex
- On publish, and nightly at 03:00
- changeable
- Yes, any time, in Settings
wp-admin is made of tables, so leaning in is more native than resisting. The fixed mono label column is the whole device — and it does something the others don't: it forces every step to state its consequences as facts, which suits a product whose copy voice is already "state the fact plainly". Weakness: a poor fit for steps that are genuinely a choice between three things rather than a set of values.
SearchSync
Where should search run?
You can change this later.
One coloured gesture per screen — a tinted band with a 1.5px accent rule under it — and plain white beneath. This is the one that solves per-plugin theming properly, because a single band can be any colour without becoming noise, where a grid in a colour you didn't choose always is.
SearchSync
Where should search run?
You can change this later. Local indexing needs nothing else from you.
White ground, and the structural colour moved from indigo to olive
#507F06. This is the option that changes the
palette rather than the layout: olive is already in the brand set, it has
never carried a surface anywhere, and it is far enough from core's
#2271b1 that it can never be mistaken for a control. One
filled mark, olive rules, everything else neutral. Weakness: green in wp-admin
carries a learned meaning — success, active, update
available — so it may read as a status rather than as a brand.
The doubt was about cream as well as the grid, so every option above sits on
white — including the panel header strips, which were
#FBF7F0 and are now a neutral #F7F7F8.
Worth being clear about what that costs: cream was the cheapest warm signal we had,
it is native by construction as a tinted .postbox header, and
without it none of the four is warm at all. Options 1–3 are now
neutral surfaces distinguished only by weight and structure; option 4 buys warmth back
through the palette instead of the ground.
That is a real fork, not a detail: either Tangible-in-wp-admin is warm and we
need a warm device that isn't cream, or it is neutral and the identity is carried
entirely by typography and rules. I lean neutral, because every warm ground
fights #f0f0f1 and the fight is the thing that reads as
foreign — but it is a brand call, not a technical one.
Neither loser is wasted. The ledger survives as the interior pattern for any step that is a set of values rather than a choice — §6's done screen is already a ledger, and §5's payload table is one too. And the rule is the correct fallback if the band is one gesture too many: it is strictly less than letterhead, so retreating costs nothing but deleting a div.
Everything below §2 still wears the incumbent cream-and-grid ground.
Left deliberately: those sections are about flow, consent and structure, and re-skinning
them before a ground is chosen would mean doing it twice. Read them for what they argue,
not for how they look — the surface is one .tgbl-ground rule away
from whichever option wins.
From the three artifacts I could read, that round explored four named
themes with authored intent strings: Warm Signal
(#6359D6 on #FDFAF8, 12px radii),
WP Native (#2271b1 on #f0f0f1,
3–4px, display face deleted), Dark Console, and Blueprint
(#1E6BB8 on #EEF1F4, cool not warm, faint
grid). So indigo-on-cream and the grid are both spent, and a cool steel-grey ground was
considered there too — which is why I have not proposed one here.
What I don't have is that round's rejected options or the reasoning. If there is a record — a doc, a thread, or more artifacts than the three I was given — point me at it and I will check these four against it before you spend review time. Olive and salmon are the two brand colours I can see no evidence were tried, which is why option 4 reaches for olive; if olive was already killed there, that is exactly the kind of thing I would rather learn now.
Takeover or in-place?
The one real structural decision. Both are drawn below with identical content so the frame is the only variable.
Where should search run?
You can change this later. Local indexing needs nothing else from you.
sheet 03 / 05 · searchsync 2.1
The mechanism is already proven in-house. A dormant
tangible-wizard-module from 2021 — zero consumers, dead
backend, placeholder copy from another product — nonetheless got the plumbing
right: activation transient, hidden dashboard page, full-page
<!DOCTYPE> takeover, an opt-out filter, and guards for
multisite and bulk activation. Worth reading for the guards alone; it also already
had a licence step and a telemetry step, which means it anticipated this call's
framing five years early.
The notice problem disappears — no .wrap, no
notices. We own the viewport, so the contained grid may sit on the sheet's own
ground. Costs: hiding the admin menu is a heavy move, and it must always carry a
visible Exit to WordPress or it reads as a trap.
Never traps anyone, survives being abandoned halfway, and the menu keeps the plugin's other pages one click away. Costs: it inherits the notice zone, and it is unmistakably a settings page — less ceremony for a first run that genuinely is an occasion.
Hands-on teardown of LifterLMS, Tutor LMS and LearnPress, plus LearnDash from its shipped source: 4 of 4 take over the full page, hiding the admin menu, bar and footer. So the sheet is not a bold choice, it is the convention — and a plugin that doesn't take over on first activation is the odd one out.
They split on how, and it matters. LifterLMS and LearnDash keep a
normal admin page and display:none the furniture
— cheap, and nonces, capabilities and enqueueing all keep behaving. Tutor and
LearnPress render a fully custom template with the chrome absent from
the DOM: total control, at the price of re-implementing the plumbing. For a shell
shared across plugins, take the CSS-hide route, and add LearnDash's blanket
.notice { display:none } — otherwise the notice zone follows
you into the takeover, which is exactly what LearnPress's activation screen shows.
One more piece of convention worth copying: the wizard page is registered and then removed from the menu, so it stays URL-addressable forever. That is what makes "resume setup" possible without a second page.
A rail you can't draw yet
Because consent is skipped when the licence already carries it, the step list is a result, not a constant — and the licence step is what produces it. So the rail has two lives: before the lookup returns, and after. Most wizards fake this by hard-coding five steps and quietly no-op'ing one. That is the version that feels sneaky later.
All four LMS wizards open with a welcome screen that asks nothing — the plugin's name, one or two reassuring sentences, one button. I had skipped it as ceremony and gone straight to the licence. That was wrong twice over.
It is conventional, so its absence reads as abrupt. But more usefully: it is where the licence lookup hides. Fire the request as the welcome screen paints, and by the time the reader has finished two sentences and pressed the button, the step list is resolved and the rail can be drawn correctly the first time. The awkward pending licence state below stops being the normal case and becomes the honest fallback for when the network is slow or the key is bad. A convention I nearly dismissed turns out to be load-bearing for the one hard problem on this page.
No fake number. The rail says pending licence rather than claiming 1 of 5, because we do not yet know whether it is four or five. Saying the true thing costs one row.
Five rows, four steps. The skipped one is drawn present and accounted for — greyed number, not asked · on file, and one line saying why with a link to change it in the account, not here.
Five kinds of step, and that's all
Julia described a plugin's own steps in one sentence on the call — pick an indexing method, then supply connection fields, then see whether it worked. Three archetypes fall out of that, and core adds two. Every plugin's "special sauce" has to be expressible as a sequence of these, or the frame is wrong.
| Archetype | Owner | Job | Shown |
|---|---|---|---|
| Licence | core | Identify the account. Resolves the rest of the flow. Always first, never skippable, absent entirely on free builds. | §3 |
| Consent | core | Email and diagnostics, asked separately. Present only when not already on file. | §5 |
| Choice | plugin | Pick one of a small closed set. Radio cards. | §2 |
| Credentials | plugin | Core form controls, unstyled, in our panel. Ends in a test, not a save. | below |
| Verified state | plugin | Report the result with a real number. Its own step because failure needs somewhere to live. | below |
Connect to Algolia
We'll test the connection before saving anything.
Needs write access. The search-only key won't do.
Inputs are core's, untouched — including the blue focus ring, which is the thing most likely to get "improved". Ours is the mono label above each field, the paper, and the hairline. The primary action is Test connection, not Save: a credentials step that saves without testing just moves the failure somewhere less helpful.
Connected
oakhill_posts reachable · 1,248 items queued ·
first sync in about 4 minutes
initial index
424 of 1,248 · you can leave this page
the same step, failing
The meter is the one place a filled accent bar is allowed, because it is data, not a control. Failure states name the actual cause — the mono 401 and "usually the search-only key" — instead of "something went wrong". This is where the copy voice earns its keep.
Show the payload, not a policy link
Gabriel's frame was "people have to perceive a fair exchange of value", and he named the competitors to study precisely because they are good at the opposite. The single move that separates us from them costs nothing: print the actual values from this site instead of describing them behind a link. A reader who can see the four lines we would send does not need to trust us.
Two optional things
Two questions, and no is a complete answer to both — the plugin works exactly the same either way. We ask rather than assume, so you do have to answer.
Send us diagnostics?
It tells us which PHP and WordPress versions to keep supporting. This is the whole payload, as it stands on this site right now:
| site | oakhill-learning.com |
| php | 8.2.14 |
| wordpress | 6.8.1 |
| searchsync | 2.1.0 |
No post content, no user data, no visitor data. Once a week, to
tangible.one.
Release notes by email?
What changed and what broke, roughly monthly, to dana@oakhill-learning.com. Separate from anything to do with your licence or your receipts — those arrive either way.
This is the Wordfence pattern, and it is here on evidence. The teardown of six competitors found exactly one decent flow — a forced, unbiased, unbundled Yes/No that refuses to submit on a non-answer — shipping at 4M+ installs. So nobody has to choose between a decent consent flow and a viable business. Three properties are doing the work: the two options are the same size and the same weight, nothing is pre-selected, and Continue is disabled until both are answered. An unchecked box is compliant and quietly means no; this actually asks, which is both more honest and higher-yield.
The two asks are never bundled, because bundling is where the worst
finding lands: WP Mail SMTP pre-fills the admin's email and subscribes it to their
list on Save and Continue, with no checkbox at all — so a user who visibly
declines telemetry is subscribed anyway. And note the destination is named:
tangible.one. Awesome Motive route telemetry to unbranded
throwaway domains — miusage.com,
aiousage.com, wpmailsmtpusage.com — as
house convention. If the payload is defensible, the domain can carry our name.
One optional thing
diagnostics · on
| site | oakhill-learning.com |
| php | 8.2.14 |
| wordpress | 6.8.1 |
| searchsync pro | 2.1.0 |
Gabriel wants version diagnostics default-on for pro, justified by purchase terms. Drawn honestly that is a statement with an off switch, not a pre-ticked box — a pre-ticked box implies a choice we have actually already made. Two open items: the terms don't say this yet, and nobody has decided whether Turn it off exists at all.
anonymous_data, with no constant or filter that can
turn it off. Ours is four lines you can read on screen before you answer. That contrast
is not a moral flourish, it is the reason we can afford to ask rather than
assume: a payload small enough to print is a payload you don't have to hide.
Gabriel recalled LearnDash trading a coupon for an email. No such string exists in LearnDash's shipped wizard — its email ask is a 48-hour demo, not a discount. The coupon is LifterLMS, and it is a rail-level step literally named "Coupon":
"Help Improve LifterLMS & Get a Coupon … Click 'Allow' to and we'll send you a coupon immediately."
(The broken sentence ships verbatim.) It is genuinely opt-in — nothing is written
unless you click Allow. But there is no email field: it sends the
site's admin_email, harvested from WP settings and never shown
on screen. So the thing being exchanged is not disclosed at the moment of exchange,
which is the same failure as a policy link — and it is avoidable for free. If we ever
offer an incentive, print the address we are about to use, the way
§8 does.
One structural advantage worth naming, since it is the whole reason we can be both honest and efficient. The bad examples buy their opt-in rates with friction reduction — pre-ticked boxes, blocked exits, refusals that record nothing and re-fire forever. We get the same friction reduction from §3 instead: a licence key belongs to an account that may already have consented at checkout, so we skip the question rather than rig it. Same outcome, opposite ethics.
Say what you're about to do to their site
The best single thing found in the whole competitive sweep, and it is LearnDash's: before writing anything, its final step enumerates every side effect — pages it will create, content it will import, settings it will flip, plugins it will install — each individually toggleable. It is the same principle as §5's payload table pointed at the filesystem instead of the network: show the actual thing, not a description of it.
It should be a shell-level guarantee rather than one plugin's good manners. Core renders this step from what the plugin steps declared, which means no Tangible plugin can quietly create a page — the frame won't let it.
Before we change anything
Everything below is on by default and every line is optional. Nothing has been written yet.
Two deliberate details. The primary button says Make these changes, not Finish — the label names the consequence. And the one item that touches the user's own theme is off by default, because the shell can reasonably presume consent for things it owns and never for things it doesn't. That distinction is the whole rule, and it is checkable in review.
No confetti
SearchSync is running
Here's what changed on this site.
| licence | Creator · 3 of 5 sites |
| index | Algolia · oakhill_posts |
| indexed | 1,248 items |
| search page | /search |
worth doing next
Two settings we didn't ask about during setup: which post types to include, and whether to index drafts. Both are safe to leave alone.
A done step that says "You're all set!" wastes the one moment the user is definitely paying attention. State what changed, with real numbers, then name the two things we deliberately didn't ask — which doubles as an apology for the questions we did ask.
Where you land is where convention collapses, and it is the most revealing
screen in any of these products. LifterLMS drops you into
post-new.php?post_type=course — onboarding ends by starting the
work. Tutor LMS, having just built you three courses, instead lands you on a
~7,900px marketing wall with six separate "Get Pro" buttons and no
link to the courses. Both took the same five minutes of the user's attention; only one
spent it on the user. Our primary action is Try a search for that reason.
One frame, whatever colour
Gabriel: the wizard "should inherit from the plugin that you're installing."
Below is the identical markup with only --tgbl-accent and
--tgbl-grid overridden. Because the accent is structural, no
value of it can break a control or fail contrast — the worst case is a grid you can
barely see, which is also the best case.
LearnPress's wizard is what unconstrained plugin theming produces: a purple logo, an amber progress bar, a WordPress-blue button and 400-weight grey headings — three unrelated colour stories on one screen, none of them load-bearing. That is not carelessness, it is the predictable result of letting each surface bring its own palette. Hence the contract in §10: a plugin gets an accent, a logo and an illustration slot. Type comes from WP, the button comes from core, and everything structural comes from the shell.
The LDX case: value first, ask last
The acquired cohort did not install anything — an update landed. Gabriel's sequence was to lead with what they got, reassure on price, and only then ask. Titus's version became the instruction: "a scroll where they can look at pretty pictures and pretty text and the new stuff they can do." So this is not a wizard: no rail, no step count, nothing gated, and the ask is at the bottom where it can be ignored.
LDX Design
Updated to 3.0. See what's new
ldx design 3.0 · maintained again
Working with modern LearnDash layouts, finally
Tangible now maintains LDX Design. Your licence, your price and your renewal date are unchanged — this is not that kind of acquisition.
Compatible with LearnDash 4.x layouts
The focus-mode and course-grid templates render correctly again. Nothing to reconfigure — existing settings carry over.
Settings that admit what they do
Forty-one options reorganised into six groups, each with the sentence we wish had been there. Nothing removed.
Updates arrive again
Two years of PHP 8.2 and 8.3 deprecation notices are gone. 28 fixes
one ask, and it is genuinely optional
We want to know what to fix next, and the people who tell us are the people on the list. Nothing changes if you skip this.
The email is pre-filled from the licence, not the ask — we already know it, so pretending to collect it would be theatre. The checkbox is unticked, and No thanks is a real button. Note the 1.5px ink rule doing structural work here: it is the only thing separating the gift from the ask, and it needs to be unmistakable so nobody feels the ask was smuggled in.
The conditions it actually ships into
A design that only works on a clean install does not work. Two tests below; the first is the one that kills most plugin screens.
Survives because it never competed. There is no hero, no title block, nothing above our container that a notice can land inside. The bordered edge is doing the work the page used to do — it says this rectangle is one coherent thing, which is legible even with four banners stacked above it. This is also the argument against a designed page header: it would be the first casualty, on somebody else's site, invisibly to us.
SearchSync
Two optional things
The buttons went red with the scheme and nothing else moved, because we never set a colour on a control or a link. Our indigo is nowhere near them — it is in the grid and the sheet number.
And this is handled by construction rather than by discipline:
anywhere the design genuinely needs the interactive colour it reads
var(--wp-admin-theme-color, #2271b1), which WordPress
itself sets and every colour scheme consumes. It is one of only five real custom
properties core exposes — and Tangible Fields' TUI build already bridges it the
same way, which makes this the ADR-010 path rather than a new one.
What core owns, what the plugin brings
Gabriel: "the stuff that's true of every plugin is what we deal with in the core. And then the rest has to be essentially extendable or hookable by the plugin to put in its special sauce." Sketched, not specified — the shape matters more than the names at this stage.
| Core | Plugin | |
|---|---|---|
| Frame | The whole shell: sheet vs panel, rail, header strip, actions row, tokens, and which frame gets used for which trigger. | Nothing. A plugin cannot restyle the frame — that is the point of having one. |
| Theme | The token set and every non-accent value. | Three values: --tgbl-accent, --tgbl-grid, a 19px mark. Nothing else is overridable. |
| Steps | Licence (always first, resolves the flow) and Consent (present only if needed). | Any number of Choice / Credentials / Verified-state steps, contributed through a filter and slotted after the core pair. |
| Copy | Everything on the core steps, including the consent wording — this is the part with legal exposure and it must not be per-plugin. | Its own steps' copy. |
| Completion | Records that onboarding ran, at what version, and what was skipped. | Declares what "done" means for it, so a half-configured plugin can re-offer the rest. |
There are two incompatible framework generations — Loops & Logic
Pro is still on the legacy plugin-framework while everything
else moved to the namespaced one plus a split-out updater. A shared onboarding module
has to load under both, or Pro is excluded from its own product's setup flow.
And the existing settings.css is inlined with no
handle, so it cannot be dequeued — new admin CSS lands on top of
roughly 25 hardcoded hexes rather than replacing them. Scoping everything under
.tgbl-admin is what makes that survivable, and it is now a
requirement rather than good manners.
Ordered by cost to change later
Everything in §3 rests on it. It also implies free builds have a different flow rather than a shorter one, since there is no key to resolve — so a free plugin's consent step cannot be skipped, ever, and its rail starts resolved. Worth confirming that free onboarding is in scope at all.
It deliberately re-introduces visibility of a decision Gabriel wanted invisible — his reason for skipping was to avoid offering an unconsent opportunity. My claim is that a visible-but-not-actionable row keeps the yes and the trust, but it is his call, and it is the kind of thing that looks like a rounding error until somebody blogs about it.
Drawn with one. Also blocked on a real prerequisite nobody owns: the terms of service do not currently cover default-on collection, so §5's right-hand screen is not shippable until they do.
§2 recommends both because the trigger differs. If that is too much for a first cut, the panel is the safer single build — it degrades to a normal settings page, works abandoned, and needs no menu-hiding. The sheet is the better experience for the case that matters most.
A 19px plugin mark is drawn top-left of every frame. It may be the one piece of decoration that earns nothing, given the plugin's name is right next to it and the admin menu already says where you are.