diff --git a/PlotDirector.WordCompanion/README.md b/PlotDirector.WordCompanion/README.md
new file mode 100644
index 0000000..5e7dd93
--- /dev/null
+++ b/PlotDirector.WordCompanion/README.md
@@ -0,0 +1,21 @@
+# PlotDirector Word Companion
+
+This project contains the Microsoft Word task pane add-in shell for PlotDirector.
+
+Phase 10B keeps the add-in deliberately small:
+
+- The manifest adds one ribbon button: **Open PlotDirector Companion**.
+- The task pane opens the PlotDirector-hosted `/word-companion` route.
+- The PlotDirector page initialises Office.js only.
+- No Word document APIs are called in this phase.
+
+## Development
+
+The development manifest URL is configured in `config/development.json`.
+
+```powershell
+npm install
+npm run build
+npm run validate
+```
+
diff --git a/PlotDirector.WordCompanion/config/development.json b/PlotDirector.WordCompanion/config/development.json
new file mode 100644
index 0000000..cbdcaa9
--- /dev/null
+++ b/PlotDirector.WordCompanion/config/development.json
@@ -0,0 +1,5 @@
+{
+ "appDomain": "https://localhost:7296",
+ "taskpaneUrl": "https://localhost:7296/word-companion",
+ "iconUrl": "https://localhost:7296/android-chrome-192x192.png"
+}
diff --git a/PlotDirector.WordCompanion/manifest.template.xml b/PlotDirector.WordCompanion/manifest.template.xml
new file mode 100644
index 0000000..a73b896
--- /dev/null
+++ b/PlotDirector.WordCompanion/manifest.template.xml
@@ -0,0 +1,87 @@
+
+
+ 7e97f8e8-49ce-4a5d-99cc-5447a460a27f
+ 0.1.0.0
+ PlotDirector
+ en-US
+
+
+
+
+
+
+ {{appDomain}}
+
+
+
+
+
+
+
+ Restricted
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PlotDirector.Companion.Taskpane
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PlotDirector.WordCompanion/manifest.xml b/PlotDirector.WordCompanion/manifest.xml
new file mode 100644
index 0000000..49f84eb
--- /dev/null
+++ b/PlotDirector.WordCompanion/manifest.xml
@@ -0,0 +1,87 @@
+
+
+ 7e97f8e8-49ce-4a5d-99cc-5447a460a27f
+ 0.1.0.0
+ PlotDirector
+ en-US
+
+
+
+
+
+
+ https://localhost:7296
+
+
+
+
+
+
+
+ Restricted
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PlotDirector.Companion.Taskpane
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PlotDirector.WordCompanion/package-lock.json b/PlotDirector.WordCompanion/package-lock.json
new file mode 100644
index 0000000..74d7247
--- /dev/null
+++ b/PlotDirector.WordCompanion/package-lock.json
@@ -0,0 +1,546 @@
+{
+ "name": "plotdirector-word-companion",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "plotdirector-word-companion",
+ "version": "0.1.0",
+ "devDependencies": {
+ "office-addin-manifest": "^2.1.5"
+ }
+ },
+ "node_modules/@microsoft/app-manifest": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@microsoft/app-manifest/-/app-manifest-1.0.6.tgz",
+ "integrity": "sha512-0+abQ2zlrq7VA/Rv/MwBKvN1CPe5FNmfM6a1bsinOXltpGduKDNlvmBTQ6LPUE2rXMA35VCmXMxkXXfn5IoVDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/fs-extra": "^11.0.1",
+ "@types/strip-bom": "^4.0.1",
+ "ajv": "^8.18.0",
+ "ajv-draft-04": "^1.0.0",
+ "ajv-formats": "^3.0.1",
+ "node-fetch": "^3.3.2",
+ "strip-bom": "^5.0.0"
+ }
+ },
+ "node_modules/@microsoft/app-manifest/node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
+ "node_modules/@types/fs-extra": {
+ "version": "11.0.4",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
+ "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/jsonfile": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/jsonfile": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
+ "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "25.9.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz",
+ "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": ">=7.24.0 <7.24.7"
+ }
+ },
+ "node_modules/@types/strip-bom": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-4.0.1.tgz",
+ "integrity": "sha512-d3RZcMmYRuL5f+jG5YM5ISs9z/HCwI2xjqXxhLon54dlpBWfVLVStOFTalO7UcX7RXaIJ6oylqOTg3Cbu6zU1Q==",
+ "deprecated": "This is a stub types definition. strip-bom provides its own type definitions, so you do not need this installed.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "strip-bom": "*"
+ }
+ },
+ "node_modules/adm-zip": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
+ "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-draft-04": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
+ "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "ajv": "^8.5.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
+ "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "license": "MIT",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/office-addin-manifest": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/office-addin-manifest/-/office-addin-manifest-2.1.5.tgz",
+ "integrity": "sha512-C8fvAQwG/Q/MpSXeIwkuLNUCLAfuE4rswou9ZXckGJovJSKrWskSsQKlAstSR+k68ph1nSZWLs4VZchnpn/Vsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@microsoft/app-manifest": "^1.0.2",
+ "adm-zip": "0.5.16",
+ "chalk": "^2.4.2",
+ "commander": "^13.0.0",
+ "fs-extra": "^7.0.1",
+ "node-fetch": "^2.6.1",
+ "office-addin-usage-data": "^2.1.0",
+ "uuid": "^8.3.2",
+ "xml2js": "^0.5.0"
+ },
+ "bin": {
+ "office-addin-manifest": "cli.js"
+ }
+ },
+ "node_modules/office-addin-usage-data": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/office-addin-usage-data/-/office-addin-usage-data-2.1.0.tgz",
+ "integrity": "sha512-/0UtF9IBUU7uRxX2LuY8koiWvIxFl9+t3UyN6Qwq37OC8e22riC2rBZHH4WZbJqeADqk3jqGQJL6aoY2Vy0RTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^13.0.0"
+ },
+ "bin": {
+ "office-addin-usage-data": "cli.js"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sax": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
+ "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=11.0.0"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-5.0.0.tgz",
+ "integrity": "sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "7.24.6",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
+ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/xml2js": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
+ "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ }
+ }
+}
diff --git a/PlotDirector.WordCompanion/package.json b/PlotDirector.WordCompanion/package.json
new file mode 100644
index 0000000..0064ac4
--- /dev/null
+++ b/PlotDirector.WordCompanion/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "plotdirector-word-companion",
+ "version": "0.1.0",
+ "private": true,
+ "description": "PlotDirector Companion Microsoft Word add-in shell.",
+ "scripts": {
+ "build": "node scripts/render-manifest.js",
+ "manifest": "node scripts/render-manifest.js",
+ "validate": "npm run manifest && office-addin-manifest validate manifest.xml"
+ },
+ "devDependencies": {
+ "office-addin-manifest": "^2.1.5"
+ }
+}
diff --git a/PlotDirector.WordCompanion/scripts/render-manifest.js b/PlotDirector.WordCompanion/scripts/render-manifest.js
new file mode 100644
index 0000000..77b57e4
--- /dev/null
+++ b/PlotDirector.WordCompanion/scripts/render-manifest.js
@@ -0,0 +1,35 @@
+const fs = require("fs");
+const path = require("path");
+
+const projectRoot = path.resolve(__dirname, "..");
+const environment = process.env.PLOTDIRECTOR_COMPANION_ENV || "development";
+const configPath = path.join(projectRoot, "config", `${environment}.json`);
+const templatePath = path.join(projectRoot, "manifest.template.xml");
+const manifestPath = path.join(projectRoot, "manifest.xml");
+
+if (!fs.existsSync(configPath)) {
+ throw new Error(`Missing Word Companion manifest config: ${configPath}`);
+}
+
+const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
+const requiredKeys = ["appDomain", "taskpaneUrl", "iconUrl"];
+for (const key of requiredKeys) {
+ if (!config[key]) {
+ throw new Error(`Missing required Word Companion manifest config value: ${key}`);
+ }
+}
+
+const escapeXml = (value) =>
+ String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+
+let manifest = fs.readFileSync(templatePath, "utf8");
+for (const [key, value] of Object.entries(config)) {
+ manifest = manifest.replaceAll(`{{${key}}}`, escapeXml(value));
+}
+
+fs.writeFileSync(manifestPath, manifest, "utf8");
+console.log(`Rendered ${path.relative(projectRoot, manifestPath)} for ${environment}.`);
diff --git a/PlotLine/Controllers/WordCompanionHostController.cs b/PlotLine/Controllers/WordCompanionHostController.cs
new file mode 100644
index 0000000..0fee7d5
--- /dev/null
+++ b/PlotLine/Controllers/WordCompanionHostController.cs
@@ -0,0 +1,23 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using PlotLine.Services;
+using PlotLine.ViewModels;
+
+namespace PlotLine.Controllers;
+
+[AllowAnonymous]
+[Route("word-companion")]
+public sealed class WordCompanionHostController(ICurrentUserService currentUser) : Controller
+{
+ [HttpGet("")]
+ public IActionResult Index()
+ {
+ return View(new WordCompanionHostViewModel
+ {
+ IsAuthenticated = currentUser.IsAuthenticated,
+ DisplayName = currentUser.DisplayName,
+ LoginUrl = Url.Action("Login", "Account", new { returnUrl = "/word-companion" }) ?? "/Account/Login?returnUrl=%2Fword-companion"
+ });
+ }
+}
+
diff --git a/PlotLine/ViewModels/WordCompanionViewModels.cs b/PlotLine/ViewModels/WordCompanionViewModels.cs
new file mode 100644
index 0000000..957fd10
--- /dev/null
+++ b/PlotLine/ViewModels/WordCompanionViewModels.cs
@@ -0,0 +1,9 @@
+namespace PlotLine.ViewModels;
+
+public sealed class WordCompanionHostViewModel
+{
+ public bool IsAuthenticated { get; init; }
+ public string? DisplayName { get; init; }
+ public string LoginUrl { get; init; } = string.Empty;
+}
+
diff --git a/PlotLine/Views/WordCompanionHost/Index.cshtml b/PlotLine/Views/WordCompanionHost/Index.cshtml
new file mode 100644
index 0000000..291d22c
--- /dev/null
+++ b/PlotLine/Views/WordCompanionHost/Index.cshtml
@@ -0,0 +1,53 @@
+@model PlotLine.ViewModels.WordCompanionHostViewModel
+@{
+ Layout = null;
+ var statusText = Model.IsAuthenticated ? "Connected to PlotDirector" : "Please sign in to PlotDirector";
+}
+
+
+
+
+
+ PlotDirector Companion
+
+
+
+
+
+
+
+
+ Project
+ (Not connected)
+
+
+
+ Book
+ (Not connected)
+
+
+
+ Current Scene
+ (Not connected)
+
+
+
+
+
+
+
+
diff --git a/PlotLine/wwwroot/css/word-companion.css b/PlotLine/wwwroot/css/word-companion.css
new file mode 100644
index 0000000..50396e0
--- /dev/null
+++ b/PlotLine/wwwroot/css/word-companion.css
@@ -0,0 +1,109 @@
+:root {
+ --companion-ink: #1e252b;
+ --companion-muted: #66717b;
+ --companion-paper: #fbfaf7;
+ --companion-panel: #ffffff;
+ --companion-line: #d8ddd8;
+ --companion-accent: #2f6f63;
+ --companion-accent-dark: #22534a;
+ --companion-soft: #eef5f2;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-width: 280px;
+ color: var(--companion-ink);
+ background: var(--companion-paper);
+ font-family: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
+}
+
+.word-companion-shell {
+ display: grid;
+ gap: 12px;
+ width: 100%;
+ max-width: 420px;
+ min-height: 100vh;
+ padding: 16px;
+}
+
+.word-companion-header {
+ display: grid;
+ gap: 6px;
+ border-bottom: 1px solid var(--companion-line);
+ padding-bottom: 14px;
+}
+
+.word-companion-eyebrow {
+ margin: 0;
+ color: var(--companion-accent-dark);
+ font-size: 0.72rem;
+ font-weight: 800;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.word-companion-header h1 {
+ margin: 0;
+ font-size: 1.55rem;
+ line-height: 1.1;
+}
+
+.word-companion-status,
+.word-companion-user,
+.word-companion-footer {
+ margin: 0;
+ color: var(--companion-muted);
+ font-size: 0.9rem;
+}
+
+.word-companion-status {
+ color: var(--companion-accent-dark);
+ font-weight: 700;
+}
+
+.word-companion-sign-in {
+ display: inline-flex;
+ justify-content: center;
+ width: fit-content;
+ border: 1px solid var(--companion-accent);
+ border-radius: 6px;
+ padding: 6px 10px;
+ background: var(--companion-accent);
+ color: #ffffff;
+ font-size: 0.88rem;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.word-companion-section {
+ display: grid;
+ gap: 4px;
+ border: 1px solid var(--companion-line);
+ border-radius: 8px;
+ padding: 12px;
+ background: var(--companion-panel);
+}
+
+.word-companion-section span {
+ color: var(--companion-muted);
+ font-size: 0.76rem;
+ font-weight: 800;
+ text-transform: uppercase;
+}
+
+.word-companion-section strong {
+ min-width: 0;
+ font-size: 0.98rem;
+ overflow-wrap: anywhere;
+}
+
+.word-companion-footer {
+ border-top: 1px solid var(--companion-line);
+ margin-top: auto;
+ padding-top: 12px;
+}
+
diff --git a/PlotLine/wwwroot/js/word-companion-host.js b/PlotLine/wwwroot/js/word-companion-host.js
new file mode 100644
index 0000000..8c1dd91
--- /dev/null
+++ b/PlotLine/wwwroot/js/word-companion-host.js
@@ -0,0 +1,31 @@
+(() => {
+ const officeStatus = document.querySelector("[data-office-status]");
+ const connectionStatus = document.querySelector("[data-connection-status]");
+
+ const setOfficeStatus = (message) => {
+ if (officeStatus) {
+ officeStatus.textContent = message;
+ }
+ };
+
+ const setConnectionStatus = (message) => {
+ if (connectionStatus) {
+ connectionStatus.textContent = message;
+ }
+ };
+
+ if (!window.Office || typeof window.Office.onReady !== "function") {
+ setOfficeStatus("Word Companion ready");
+ return;
+ }
+
+ window.Office.onReady()
+ .then(() => {
+ setOfficeStatus("Word Companion ready");
+ })
+ .catch(() => {
+ setConnectionStatus("Unable to connect to PlotDirector.");
+ setOfficeStatus("Word Companion unavailable");
+ });
+})();
+