From bb774a506673196e0bba2ee8b361331792065225 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sat, 13 Jun 2026 16:47:45 +0100 Subject: [PATCH] Phase 10B as a shell-only Word Companion foundation. --- PlotDirector.WordCompanion/README.md | 21 + .../config/development.json | 5 + .../manifest.template.xml | 87 +++ PlotDirector.WordCompanion/manifest.xml | 87 +++ PlotDirector.WordCompanion/package-lock.json | 546 ++++++++++++++++++ PlotDirector.WordCompanion/package.json | 14 + .../scripts/render-manifest.js | 35 ++ .../WordCompanionHostController.cs | 23 + .../ViewModels/WordCompanionViewModels.cs | 9 + PlotLine/Views/WordCompanionHost/Index.cshtml | 53 ++ PlotLine/wwwroot/css/word-companion.css | 109 ++++ PlotLine/wwwroot/js/word-companion-host.js | 31 + 12 files changed, 1020 insertions(+) create mode 100644 PlotDirector.WordCompanion/README.md create mode 100644 PlotDirector.WordCompanion/config/development.json create mode 100644 PlotDirector.WordCompanion/manifest.template.xml create mode 100644 PlotDirector.WordCompanion/manifest.xml create mode 100644 PlotDirector.WordCompanion/package-lock.json create mode 100644 PlotDirector.WordCompanion/package.json create mode 100644 PlotDirector.WordCompanion/scripts/render-manifest.js create mode 100644 PlotLine/Controllers/WordCompanionHostController.cs create mode 100644 PlotLine/ViewModels/WordCompanionViewModels.cs create mode 100644 PlotLine/Views/WordCompanionHost/Index.cshtml create mode 100644 PlotLine/wwwroot/css/word-companion.css create mode 100644 PlotLine/wwwroot/js/word-companion-host.js 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 + + + + + + + <Description resid="GetStarted.Description"/> + <LearnMoreUrl resid="Taskpane.Url"/> + </GetStarted> + <FunctionFile resid="Taskpane.Url"/> + <ExtensionPoint xsi:type="PrimaryCommandSurface"> + <OfficeTab id="TabHome"> + <Group id="PlotDirector.Companion.Group"> + <Label resid="Group.Label"/> + <Icon> + <bt:Image size="16" resid="Icon.16x16"/> + <bt:Image size="32" resid="Icon.32x32"/> + <bt:Image size="80" resid="Icon.80x80"/> + </Icon> + <Control xsi:type="Button" id="PlotDirector.Companion.OpenButton"> + <Label resid="OpenButton.Label"/> + <Supertip> + <Title resid="OpenButton.Label"/> + <Description resid="OpenButton.Tooltip"/> + </Supertip> + <Icon> + <bt:Image size="16" resid="Icon.16x16"/> + <bt:Image size="32" resid="Icon.32x32"/> + <bt:Image size="80" resid="Icon.80x80"/> + </Icon> + <Action xsi:type="ShowTaskpane"> + <TaskpaneId>PlotDirector.Companion.Taskpane</TaskpaneId> + <SourceLocation resid="Taskpane.Url"/> + </Action> + </Control> + </Group> + </OfficeTab> + </ExtensionPoint> + </DesktopFormFactor> + </Host> + </Hosts> + <Resources> + <bt:Images> + <bt:Image id="Icon.16x16" DefaultValue="{{iconUrl}}"/> + <bt:Image id="Icon.32x32" DefaultValue="{{iconUrl}}"/> + <bt:Image id="Icon.80x80" DefaultValue="{{iconUrl}}"/> + </bt:Images> + <bt:Urls> + <bt:Url id="Taskpane.Url" DefaultValue="{{taskpaneUrl}}"/> + </bt:Urls> + <bt:ShortStrings> + <bt:String id="Group.Label" DefaultValue="PlotDirector"/> + <bt:String id="OpenButton.Label" DefaultValue="Open PlotDirector Companion"/> + <bt:String id="GetStarted.Title" DefaultValue="PlotDirector Companion"/> + </bt:ShortStrings> + <bt:LongStrings> + <bt:String id="OpenButton.Tooltip" DefaultValue="Open the PlotDirector Companion task pane."/> + <bt:String id="GetStarted.Description" DefaultValue="Open the PlotDirector Companion task pane from the Home tab."/> + </bt:LongStrings> + </Resources> + </VersionOverrides> +</OfficeApp> 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 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" + xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" + xsi:type="TaskPaneApp"> + <Id>7e97f8e8-49ce-4a5d-99cc-5447a460a27f</Id> + <Version>0.1.0.0</Version> + <ProviderName>PlotDirector</ProviderName> + <DefaultLocale>en-US</DefaultLocale> + <DisplayName DefaultValue="PlotDirector Companion"/> + <Description DefaultValue="Companion tools for PlotDirector authors."/> + <IconUrl DefaultValue="https://localhost:7296/android-chrome-192x192.png"/> + <HighResolutionIconUrl DefaultValue="https://localhost:7296/android-chrome-192x192.png"/> + <SupportUrl DefaultValue="https://localhost:7296/word-companion"/> + <AppDomains> + <AppDomain>https://localhost:7296</AppDomain> + </AppDomains> + <Hosts> + <Host Name="Document"/> + </Hosts> + <DefaultSettings> + <SourceLocation DefaultValue="https://localhost:7296/word-companion"/> + </DefaultSettings> + <Permissions>Restricted</Permissions> + <VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0"> + <Hosts> + <Host xsi:type="Document"> + <DesktopFormFactor> + <GetStarted> + <Title resid="GetStarted.Title"/> + <Description resid="GetStarted.Description"/> + <LearnMoreUrl resid="Taskpane.Url"/> + </GetStarted> + <FunctionFile resid="Taskpane.Url"/> + <ExtensionPoint xsi:type="PrimaryCommandSurface"> + <OfficeTab id="TabHome"> + <Group id="PlotDirector.Companion.Group"> + <Label resid="Group.Label"/> + <Icon> + <bt:Image size="16" resid="Icon.16x16"/> + <bt:Image size="32" resid="Icon.32x32"/> + <bt:Image size="80" resid="Icon.80x80"/> + </Icon> + <Control xsi:type="Button" id="PlotDirector.Companion.OpenButton"> + <Label resid="OpenButton.Label"/> + <Supertip> + <Title resid="OpenButton.Label"/> + <Description resid="OpenButton.Tooltip"/> + </Supertip> + <Icon> + <bt:Image size="16" resid="Icon.16x16"/> + <bt:Image size="32" resid="Icon.32x32"/> + <bt:Image size="80" resid="Icon.80x80"/> + </Icon> + <Action xsi:type="ShowTaskpane"> + <TaskpaneId>PlotDirector.Companion.Taskpane</TaskpaneId> + <SourceLocation resid="Taskpane.Url"/> + </Action> + </Control> + </Group> + </OfficeTab> + </ExtensionPoint> + </DesktopFormFactor> + </Host> + </Hosts> + <Resources> + <bt:Images> + <bt:Image id="Icon.16x16" DefaultValue="https://localhost:7296/android-chrome-192x192.png"/> + <bt:Image id="Icon.32x32" DefaultValue="https://localhost:7296/android-chrome-192x192.png"/> + <bt:Image id="Icon.80x80" DefaultValue="https://localhost:7296/android-chrome-192x192.png"/> + </bt:Images> + <bt:Urls> + <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:7296/word-companion"/> + </bt:Urls> + <bt:ShortStrings> + <bt:String id="Group.Label" DefaultValue="PlotDirector"/> + <bt:String id="OpenButton.Label" DefaultValue="Open PlotDirector Companion"/> + <bt:String id="GetStarted.Title" DefaultValue="PlotDirector Companion"/> + </bt:ShortStrings> + <bt:LongStrings> + <bt:String id="OpenButton.Tooltip" DefaultValue="Open the PlotDirector Companion task pane."/> + <bt:String id="GetStarted.Description" DefaultValue="Open the PlotDirector Companion task pane from the Home tab."/> + </bt:LongStrings> + </Resources> + </VersionOverrides> +</OfficeApp> 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, ">") + .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"; +} +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>PlotDirector Companion + + + + +
+
+

PlotDirector

+

Companion

+

@statusText

+ @if (Model.IsAuthenticated) + { +

Signed in as @Model.DisplayName

+ } + else + { + + } +
+ +
+ Project + (Not connected) +
+ +
+ Book + (Not connected) +
+ +
+ Current Scene + (Not connected) +
+ +
+ Word Companion loading +
+
+ + + + 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"); + }); +})(); +