feat: Tailwind Gutenberg - #157
Draft
timothy-mugo wants to merge 14 commits into
Draft
timothy-mugo wants to merge 14 commits into
timothy-mugo wants to merge 14 commits into
Conversation
… special symbols in tooltip components
timothy-mugo
force-pushed
the
task/implement-tailwind-css-plugin
branch
from
September 21, 2026 13:02
ee18d1b to
b1aeea4
Compare
Bundles @tailwindcss/browser as a static asset (not imported into canvas.js itself, since it self-executes against whatever `document` it runs in) and injects it directly into the post-editor iframe's own document, where its built-in MutationObserver picks up class changes and recompiles reactively. This replaces the spec's `compile()`/`build()` pseudocode, which does not match the package's actual API (a self-executing global bundle, no exports) - verified by installing it and reading dist/index.global.js before writing any of this. Also adds an `editor.BlockListBlock` filter applying twgClasses to the live canvas wrapper: the existing extraProps filter only affects serialized save() markup, not the DOM rendered while editing, so the canvas compiler would never see the classes without it. Preflight is intentionally excluded from the canvas import to avoid resetting editor chrome styles. Iframe injection uses the querySelector approach (not the spec's component-route-first preference) with a MutationObserver + capturing load listener for resilience across device-preview remounts, chosen for predictability over unstable/private Gutenberg APIs given end-to-end testing is deferred until all milestones are done. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
No Node.js exists in this deployment's production runtime (PHP-FPM Alpine,
confirmed in the earlier spec compatibility review), so there is no
server-side Tailwind compile step. Instead, canvas.js locates the <style>
tag @tailwindcss/browser writes into the editor iframe, and POSTs its
current text to a new REST endpoint after each non-autosave save. PHP does
a dumb write to a content-hashed file in wp-content/uploads/twg/, pruning
older files down to the 2 most recent.
Twg\Class_Index walks parse_blocks() output on save_post to maintain a
site-wide option of every class seen (additive only; full rescan/prune is
a later settings-page action). Twg\Rest exposes GET/POST /twg/v1/css, the
POST route gated on current_user_can('edit_posts') since it writes
attacker-controllable bytes to a publicly served file.
Since this WordPress instance is used headless (also from the earlier
review), there is no PHP-rendered frontend page to wp_enqueue_style()
onto — GET /twg/v1/css is what the external frontend app is expected to
poll/fetch to link the current stylesheet itself.
Verified with standalone PHP harnesses (WP functions stubbed) for the
block-tree class-collection recursion and the file hash/write/prune
logic, since neither needs a live WordPress instance to check.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tools/generate-index.mjs uses Tailwind's real Node compile() API (verified by installing tailwindcss and reading dist/lib.js - the .d.ts for the "." export is misleading, but `compile` is a genuine named export in the actual runtime bundle) to batch-compile a curated set of candidate utility names (colors/spacing/radius/shadows/font-size/font-weight extracted straight from the installed theme.css, plus a static list of common parameterless utilities) and parse the real output CSS for each class's declaration. Nothing is guessed: only classes that actually produced CSS end up in assets/class-index.json (3,291 entries, ~21KB gzipped), committed since there's no build step that regenerates it in production. The editor UI uses @wordpress/components' FormTokenField for the chip/ keyboard interaction (Enter/Tab/Space commit, Backspace-removes-last-chip, already handled and accessible) rather than hand-rolling it, since that's the single riskiest surface to get wrong with no live testing available until every milestone is done. Trade-off: FormTokenField re-filters suggestions by plain substring containment against the full typed text (verified in its source), which would silently discard a fuzzy-subsequence ranking tier - so js/worker.js only ranks prefix-vs-contains, not the spec's third "fuzzy subsequence" tier. The declaration preview line is driven by an exact match against the current input rather than a highlighted-suggestion index for the same reason (FormTokenField doesn't expose one). SuggestionList.jsx from the spec's file tree isn't created, since FormTokenField renders its own suggestion list. The worker still owns what FormTokenField can't do on its own: rejecting arbitrary-value queries (w-[37px]) outright, splitting the variant prefix on the last ":", and listing variants (not utilities) when nothing has been typed for the next segment yet. Also fixes a bug from Milestone 2: markCompiledStyleTag matched any existing untyped <style> tag already in the iframe head (e.g. the theme's own editor styles), not specifically the one @tailwindcss/browser adds - it only looked at mutation.addedNodes correctly, scoping the match to newly inserted elements. Verified separately (Node harness) that tailwindcss's build() is genuinely cumulative across calls, so the M3 upload path does capture the complete stylesheet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Twg\Render hooks render_block to inject twgClasses into dynamic blocks' server-rendered markup (Query Loop, Navigation, third-party render_callback blocks) using WP_HTML_Tag_Processor, matching the spec's approach exactly. It skips any block type it can confirm is NOT dynamic (WP_Block_Type::is_dynamic()) since those already have their classes baked in at save time via the Milestone 1 extraProps filter - render_block fires for every block on every page load, so this avoids redundant work on the common case. None of this org's own ~48 viz/* blocks are dynamic (confirmed during the original spec compatibility review), so in practice this only matters for core dynamic blocks and any third-party ones - lower priority than the spec's milestone ordering implies, but still worth having for completeness. Verified the control flow (static-skip, empty-content guard, multi-class splitting, safe default for unregistered block types) with a standalone PHP harness mocking WP_HTML_Tag_Processor and WP_Block_Type_Registry, since the real WP_HTML_Tag_Processor needs a live WordPress to exercise directly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lestone 6) Settings -> Tailwind Classes, backed by twg_settings (own dedicated page, not house's usual General-Settings-fields convention - justified since the AJAX-free rebuild-index action and status table don't fit there). frontend_mode is a 2-way local/cdn toggle rather than the spec's 4-way cli/editor-cache/cdn/auto, since CLI was already ruled out entirely back in Milestone 3 (no Node.js in this deployment). theme_css, safelist, and preflight now actually flow through to canvas.js instead of sitting inert: theme_css and preflight change what gets imported into the editor's compile, and safelist classes get applied to a hidden marker element in the iframe so @tailwindcss/browser's DOM-based class collector picks them up. suggest_limit flows through to worker.js's result cap. load_on_frontend gates whether GET /twg/v1/css returns a URL at all, for sites whose frontend app already bundles Tailwind itself. "Rebuild CSS" from the spec's button pair is deliberately not built: this architecture has no server-side compiler at all (confirmed back in Milestone 3), so there is nothing such a button could actually trigger - the frontend stylesheet is always whatever the editor most recently compiled and POSTed. The status panel explains this directly instead of shipping a button that can't do anything. "Rebuild index" is implemented as a single synchronous admin-post request (Class_Index::rebuild(), full rescan including the wp_block post type per the spec's own risk callout on reusable blocks/patterns) rather than the spec's batched/paginated AJAX-with-progress-bar version - a reasonable scope cut for most sites, called out here since very large sites may need the paginated version later. Verified Settings_Page::sanitize() and get_safelist_classes() with a standalone PHP harness (WP functions stubbed): valid input passthrough, invalid frontend_mode falling back to the default, boolean coercion, and newline-separated safelist parsing all check out. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…estone 7) Adds the remaining spec polish items: - Block toolbar button and a real Ctrl/Cmd+Shift+T shortcut (@wordpress/keyboard-shortcuts' registerShortcut + useShortcut, verified against the installed package - modifier "primaryShift" resolves to exactly Cmd+Shift on Mac / Ctrl+Shift on Windows/Linux) that both open the block sidebar and expand the panel. PanelBody is now fully controlled (opened/onToggle) instead of initialOpen so external triggers can actually expand it. - Conflict handling: a block's existing "Additional CSS Class(es)" (attributes.className) now renders as a read-only note above the token field, per the spec's Editor UI section - this had been missed in Milestone 4. - uninstall.php, gated on a new opt-in "delete_data_on_uninstall" setting (off by default): removes the three plugin options and the generated CSS files, never post content. Matches the spec's stated deactivation behavior exactly (deactivating alone never deletes anything). - wp_set_script_translations() for twg-editor (the only bundle using __()) plus load_plugin_textdomain() for the PHP-side strings, both pointed at a new languages/ directory. No translations exist yet, but the JS strings are now wired to be translatable at all - without this call they never could be, even once .json translation files exist. - readme.txt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each editor session's @tailwindcss/browser instance only ever sees the classes visible in that one canvas. Since the upload path in Milestone 3 treats the result as the single site-wide stylesheet (one file, one twg_current_css option), saving post B after post A would upload a stylesheet containing only post B's classes - silently breaking post A's frontend styling the moment anyone else saved anything. This was guaranteed to surface on the very first two-post test and nowhere before that, since Milestone 3's own tests only ever exercised a single post. Twg\Class_Index has been write-only until now - populated on save_post, rebuildable, shown in the status panel, but nothing ever read it back to affect compiled output. It's now merged into the safelist handed to canvas.js, using the same hidden-marker-element mechanism Milestone 6 already built for the settings-page safelist field. Every editor session now compiles a superset covering every class ever seen on the site, so any save produces a complete stylesheet regardless of which post triggered it. Also hardens injectIntoIframe: the doc.body readiness check that injectSafelist relied on has moved into the function's main early-return guard. It was possible for a not-yet-ready iframe document to skip the safelist marker but still complete the rest of the injection (style tag + script), permanently locked out of a retry by the script[data-twg-canvas] guard for that iframe instance - which would have produced the exact same silent breakage this fix addresses, just triggered by timing instead of by which post was open. readme.txt's "Tested up to" also corrected to 7.1, matching the actual runtime image (wordpress:7.1.0-fpm-alpine) rather than a guess. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every frontend delivery mechanism built so far (GET /wp-json/twg/v1/css) assumed a headless setup, where a separate frontend app fetches the URL and links to it itself. Classes still land in rendered markup either way (that's a separate, unrelated mechanism), but nothing ever attached the CSS file that defines what those classes do to an actual WordPress page render - so previewing a post directly through WordPress (not through the external frontend app) showed unstyled classes with no stylesheet backing them at all. Hooks wp_enqueue_scripts and enqueues Compiler::get_current()'s URL, gated on both frontend_mode being "local" (in "cdn" mode nothing here should load anything - that's the external app's job) and load_on_frontend. No `ver` query string, matching the existing content-hashed-filename caching model rather than adding a redundant cache-busting param on top of it. Verified the four gating branches (cdn mode, load_on_frontend off, no CSS generated yet, and the enqueue-should-fire case) with a standalone PHP harness stubbing the relevant WP functions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
File layout, build/lint/index-generation commands, the two REST routes, and the testing approach used so far (no automated test suite; an isolated throwaway WordPress instance plus standalone PHP harnesses stubbing WP functions for pure logic). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
timothy-mugo
force-pushed
the
task/implement-tailwind-css-plugin
branch
2 times, most recently
from
September 21, 2026 13:12
5e5ed3a to
3e6b2f2
Compare
… into task/implement-tailwind-css-plugin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Tailwind Gutenberg
Adds a plugin that lets editors type Tailwind utility classes onto any Gutenberg block, with autocomplete, live preview, and frontend output. It works with both Wordpress and headless CMS setup.
Features
@tailwindcss/browserinjected into the post-editor iframe, toolbar button + Ctrl/Cmd+Shift+T shortcut.GET /wp-json/twg/v1/cssexposes it for the external frontend app.render_block), a settings page (frontend mode, theme/safelist/preflight), anduninstall.phpcleanup.FormTokenFieldinstead of a hand-rolled token field; no "Rebuild CSS" button, since there's nothing for it to trigger).Type of change
fix:)feat:)BREAKING CHANGE:)refactor:/chore:)docs:)Affected package(s)
@devgateway/dvz-wp-commons@devgateway/create-wp-customizer@devgateway/upgrade-wp-customizerplugins/wp-react-blocks-pluginplugins/wp-react-custom-apiplugins/wp-react-custom-rest-menuChecklist
pnpm buildpasses locally