diff --git a/.distignore b/.distignore
index eab5009..0109218 100644
--- a/.distignore
+++ b/.distignore
@@ -11,6 +11,7 @@
/components/sass
/config.rb
/deploy
+/docs
/DEVELOPERS.md
/Gruntfile.js
/node_modules
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..200b724
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,55 @@
+name: Deploy Documentation
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ name: Build docs
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build docs
+ run: npm run build:docs
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: docs/.vuepress/dist
+
+ deploy:
+ name: Deploy to GitHub Pages
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index 8b1cd31..2ab11ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,8 @@ node_modules/
deploy/
build/
package-lock.json
-.vscode/
\ No newline at end of file
+.vscode/
+.php-cs-fixer.cache
+docs/.vuepress/dist
+docs/.vuepress/.temp
+docs/.vuepress/.cache
\ No newline at end of file
diff --git a/DEVELOPERS.md b/DEVELOPERS.md
index 73d2748..2ea7561 100755
--- a/DEVELOPERS.md
+++ b/DEVELOPERS.md
@@ -1,8 +1,16 @@
# Developer Instructions #
+## Documentation ##
+
+Full user and developer documentation is available at **https://methnen.github.io/m-chart/**. The developer section covers PHP hooks, JavaScript events, and the admin UI hooks API.
+
## Build Environment Install ##
-`npm install`
+```
+npm install
+```
+
+> **Note:** `package.json` includes a `postinstall` script that runs `npm run build` automatically after `npm install` completes. This means a full build will fire on first install — this is expected.
## Build Commands ##
@@ -18,7 +26,7 @@ Build CSS only:
npm run build:css
```
-Build JS (minify helpers):
+Build JS (minifies `components/js/m-chart-chartjs-helper.js`):
```
npm run build:js
@@ -30,6 +38,12 @@ Build block only:
npm run build:block
```
+Build admin React app only:
+
+```
+npm run build:admin-ui
+```
+
Convert readme.txt to README.md:
```
@@ -38,14 +52,143 @@ npm run build:readme
## Watch Commands ##
-Watch everything (CSS, JS, and block):
+Watch everything (CSS, JS, block, admin app, and readme):
```
npm run watch
```
+Individual watch targets are also available:
+
+| Command | Watches |
+|---------|---------|
+| `npm run watch:admin-ui` | React admin app |
+| `npm run watch:block` | Gutenberg block |
+| `npm run watch:css` | SCSS → CSS |
+| `npm run watch:js` | `m-chart-chartjs-helper.js` |
+| `npm run watch:readme` | `readme.txt` → `README.md` |
+
+## Translations (i18n) ##
+
+PHP translations use `.po` / `.mo` files managed in Poedit. JavaScript translations require additional steps because `wp-scripts` bundles multiple source files into a single compiled file, and WordPress needs handle-named JSON files to load them.
+
+All locale files (`.po`, `.mo`, `.l10n.php`) live in `components/languages/`.
+
+### Workflow
+
+1. Open the `.po` file for the locale you are updating (e.g. `components/languages/m-chart-zh_CN.po`) in Poedit.
+2. **Catalog > Update from Sources** to scan for new/changed translatable strings in both PHP and JS source files.
+3. Translate any new or updated strings.
+4. Save in Poedit (this generates the `.mo` and `.l10n.php` files automatically).
+5. Generate per-source-file JSON translation files:
+
+```
+wp i18n make-json components/languages/m-chart-zh_CN.po --no-purge
+```
+
+6. Merge the hash-based JSON files into handle-named files that WordPress can find:
+
+```
+npm run build:i18n
+```
+
+Repeat steps 1–6 for each locale.
+
+### Why the merge step is needed
+
+`wp i18n make-json` generates one JSON file per source file, named with the md5 hash of the source path (e.g. `components/admin-ui-src/components/AxisRows.js`). However, WordPress looks up translations using the md5 hash of the *compiled* file path (e.g. `components/admin-ui/index.js`). Since these hashes don't match, WordPress falls back to looking for `{domain}-{locale}-{handle}.json`. The `build:i18n` script merges the per-source-file JSONs into these handle-named files:
+
+- `m-chart-{locale}-m-chart-admin-ui.json` — admin UI translations
+- `m-chart-{locale}-m-chart-editor.json` — block editor translations
+
+### Poedit configuration
+
+Each `.po` file includes Poedit search path headers so that source scanning works correctly. These should exclude:
+
+- `*.min.js` — minified files (duplicates of source)
+- `node_modules` — third-party dependencies
+- `components/external` — vendored libraries
+
+If creating a new locale, copy these headers from an existing `.po` file (e.g. `m-chart-en_US.po`).
+
## Deployment ##
Deploy to WordPress.org via GitHub Actions:
Actions tab → "Deploy to WordPress.org" → "Run workflow"
+
+Before triggering the workflow:
+- Bump the version number in `m-chart.php`, `class-m-chart.php`, and `readme.txt`
+- Run `npm run build` and commit all compiled assets
+- Run `npm run build:readme` and commit the updated `README.md`
+- Target the `main` branch when running the workflow
+
+---
+
+## Admin UI Architecture ##
+
+The chart post-edit screen uses a React app (`components/admin-ui-src/`) compiled to `components/admin-ui/` by `@wordpress/scripts`. As of v2.0 the React admin UI is used for all charting libraries — the previous jQuery + Handlebars stack has been removed.
+
+### Source layout
+
+```
+components/admin-ui-src/
+ index.js Entry point — mounts portals into each meta box div
+ context/
+ ChartAdminContext.js Single shared reducer (all components read/write here)
+ hooks/
+ useChartRefresh.js Debounced AJAX fetch for updated chart args
+ useFormSubmissionGuard.js Gates Save/Publish buttons on state.formEnabled; blocks
+ form submission while a chart refresh is in flight
+ useImageGeneration.js Captures Chart.js canvas → base64 PNG → hidden textarea
+ useLongPress.js 500ms pointer-event long-press (tab rename on mobile)
+ utils/
+ measureTextWidth.js Canvas-based text measurement utility
+ components/
+ ChartMetaBox.js Root for the Chart meta box (preview + settings)
+ ChartPreview.js Imperative Chart.js instance managed via refs
+ ChartSettings.js Settings form container
+ TypeAndThemeRow.js Type / Theme / Height inputs
+ ParseAndFlagsRow.js Parse direction, Labels, Legend, Shared tooltip
+ AxisRows.js Vertical/horizontal axis title + units, Y-min
+ ShortcodeAndImageRow.js Shortcode display, image URL, library hidden input
+ SpreadsheetMetaBox.js Root for the Data meta box
+ SheetTabs.js Tab bar (conditionally shown for multi-sheet types)
+ SheetTab.js Individual tab — click/dblclick/long-press rename, delete
+ JspreadsheetWrapper.js Thin imperative wrapper around a Jspreadsheet worksheet
+ CsvControls.js CSV import (fetch + FormData) and export (temp form POST)
+ SubtitleField.js Controlled subtitle input (replaces subtitle-field.php)
+```
+
+### Data flow
+
+1. PHP localises initial state into `window.m_chart_admin` via `wp_localize_script` (see `current_screen()` in `class-m-chart-admin.php`). The object contains: plugin metadata (`slug`, `version`), settings (`performance`, `image_support`, `image_multiplier`, etc.), the active `library`, chart post meta, spreadsheet data, available chart types and themes, a nonce, the AJAX URL, and initial chart args.
+2. `ChartAdminContext` seeds a `useReducer` from that object — all components share one context.
+3. User changes (settings, spreadsheet, title, subtitle) update context state.
+4. `useChartRefresh` debounces 300 ms then POSTs to `admin-ajax.php?action=m_chart_get_chart_args`.
+5. The response updates `chartArgs` in context; `ChartPreview` patches its Chart.js instance.
+6. On form submit, `SpreadsheetMetaBox` serialises all sheet data to the hidden `textarea[name="m-chart[data]"]`.
+
+### Mount points (PHP)
+
+| PHP method | Mount div | Component |
+|---|---|---|
+| `edit_form_before_permalink()` | `#m-chart-subtitle-root` | `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - /> -
-
-
-
-
-
-
-