diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 93287829..cac4752a 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -1,6 +1,6 @@ { "name": "indiekit", - "version": "1.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { @@ -49,17 +49,17 @@ "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", "license": "MIT" }, - "node_modules/@esbuild/freebsd-x64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ - "x64" + "arm64" ], "license": "MIT", "optional": true, "os": [ - "freebsd" + "darwin" ], "engines": { "node": ">=18" @@ -102,6 +102,44 @@ "node": ">=18" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@indiekit/endpoint-auth": { "version": "1.0.0-beta.25", "resolved": "https://registry.npmjs.org/@indiekit/endpoint-auth/-/endpoint-auth-1.0.0-beta.25.tgz", @@ -120,20 +158,6 @@ "node": ">=20" } }, - "node_modules/@indiekit/endpoint-files": { - "version": "1.0.0-beta.25", - "resolved": "https://registry.npmjs.org/@indiekit/endpoint-files/-/endpoint-files-1.0.0-beta.25.tgz", - "integrity": "sha512-ql4InRaw70LO+zSTNU1gWluaXh86tvciYnlFD9bLLUV6j0ATNEsfkirNFD6VBHSV+0FxMuXGFnakkn48qrVbIA==", - "license": "MIT", - "dependencies": { - "@indiekit/error": "^1.0.0-beta.25", - "express": "^5.0.0", - "express-validator": "^7.0.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/@indiekit/endpoint-image": { "version": "1.0.0-beta.25", "resolved": "https://registry.npmjs.org/@indiekit/endpoint-image/-/endpoint-image-1.0.0-beta.25.tgz", @@ -166,26 +190,6 @@ "node": ">=20" } }, - "node_modules/@indiekit/endpoint-micropub": { - "version": "1.0.0-beta.25", - "resolved": "https://registry.npmjs.org/@indiekit/endpoint-micropub/-/endpoint-micropub-1.0.0-beta.25.tgz", - "integrity": "sha512-/NtKo94hlIWL4oP3TdokAYkxKPQooMMy31TwTfQisijSEGRGief25GvUzuhW3WGHWIVeGK/c5IevhhM8BV9NFg==", - "license": "MIT", - "dependencies": { - "@indiekit/error": "^1.0.0-beta.25", - "@indiekit/util": "^1.0.0-beta.25", - "@paulrobertlloyd/mf2tojf2": "^3.0.0", - "debug": "^4.3.2", - "express": "^5.0.0", - "lodash": "^4.17.21", - "markdown-it": "^14.0.0", - "newbase60": "^1.3.1", - "turndown": "^7.1.1" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/@indiekit/endpoint-posts": { "version": "1.0.0-beta.25", "resolved": "https://registry.npmjs.org/@indiekit/endpoint-posts/-/endpoint-posts-1.0.0-beta.25.tgz", @@ -205,47 +209,32 @@ "node": ">=20" } }, - "node_modules/@indiekit/endpoint-share": { - "version": "1.0.0-beta.25", - "resolved": "https://registry.npmjs.org/@indiekit/endpoint-share/-/endpoint-share-1.0.0-beta.25.tgz", - "integrity": "sha512-0UOG7vusxK5V2tVhpLTKwXmLQVCv8lsK+T6G8WECOFEEAj00xARamGj0vyPQAZAFeTNw4K3Iwncz8YJcHb2MEw==", + "node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub": { + "name": "@rmdes/indiekit-endpoint-micropub", + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-micropub/-/indiekit-endpoint-micropub-1.0.0-beta.29.tgz", + "integrity": "sha512-94aUGNMsTOHnFVwUicqfGvbeJ4/HAJLKeReMw0iSoryGfgIi2Hz52DV5Ehub6UzzdLon6loXOCTqTMpM3UgxuA==", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", + "@indiekit/util": "^1.0.0-beta.25", + "@paulrobertlloyd/mf2tojf2": "^3.0.0", + "debug": "^4.3.2", "express": "^5.0.0", - "express-validator": "^7.0.0" + "lodash": "^4.17.21", + "markdown-it": "^14.0.0", + "newbase60": "^1.3.1", + "turndown": "^7.1.1" }, "engines": { "node": ">=20" } }, - "node_modules/@indiekit/endpoint-syndicate": { - "version": "1.0.0-beta.25", - "resolved": "https://registry.npmjs.org/@indiekit/endpoint-syndicate/-/endpoint-syndicate-1.0.0-beta.25.tgz", - "integrity": "sha512-V5JAURGxHI5Nw07Sdgk3c1nhmsL+h+VT58/5zIWja9nBO0wBfaTA/JwKJZYrNH7x0EMow/H/2MoTX4/ETln5/A==", - "license": "MIT", - "dependencies": { - "@indiekit/error": "^1.0.0-beta.25", - "express": "^5.0.0", - "jsonwebtoken": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@indiekit/error": { - "version": "1.0.0-beta.25", - "resolved": "https://registry.npmjs.org/@indiekit/error/-/error-1.0.0-beta.25.tgz", - "integrity": "sha512-ZDM6cyC4qPaosv4Ji1gGObSYpOlHNMqys9v428E7/XvK1qT3uW5S8mAeqGu7ErbWdMZINe0ua0fuZwBlGmSPLg==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/@indiekit/frontend": { - "version": "1.0.0-beta.25", - "resolved": "https://registry.npmjs.org/@indiekit/frontend/-/frontend-1.0.0-beta.25.tgz", - "integrity": "sha512-iukVUIRlqvpvi5x8ld7viT6xOkTqtd4un2awf2ceQXOGyKt4dylHWHvO90K6eP4rMZ19alWVKxQ1lmAC4YIy5g==", + "node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend": { + "name": "@rmdes/indiekit-frontend", + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-frontend/-/indiekit-frontend-1.0.0-beta.27.tgz", + "integrity": "sha512-jvfkhTJJMkiWbDi15DqqHuvEt0BzbtdAuYsqL36t59e+Wa3PodGTV8SELl6REcbjyshuixgxOTBgkZVNTHoziQ==", "license": "MIT", "dependencies": { "@accessible-components/tag-input": "^0.2.0", @@ -270,11 +259,21 @@ "node": ">=20" } }, + "node_modules/@indiekit/error": { + "version": "1.0.0-beta.25", + "resolved": "https://registry.npmjs.org/@indiekit/error/-/error-1.0.0-beta.25.tgz", + "integrity": "sha512-ZDM6cyC4qPaosv4Ji1gGObSYpOlHNMqys9v428E7/XvK1qT3uW5S8mAeqGu7ErbWdMZINe0ua0fuZwBlGmSPLg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/@indiekit/indiekit": { "version": "1.0.0-beta.25", "resolved": "https://registry.npmjs.org/@indiekit/indiekit/-/indiekit-1.0.0-beta.25.tgz", "integrity": "sha512-AB63jCDtjdVW/bPl9obLoBnwtH6LVBCFNuiXggVlLNR6FlSwU396yMl1693XOR4vbkk00tOVS83KHQpbl/eYGw==", "license": "MIT", + "peer": true, "dependencies": { "@indiekit/endpoint-auth": "^1.0.0-beta.25", "@indiekit/endpoint-files": "^1.0.0-beta.25", @@ -316,6 +315,104 @@ "node": ">=20" } }, + "node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files": { + "name": "@rmdes/indiekit-endpoint-files", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-files/-/indiekit-endpoint-files-1.0.0.tgz", + "integrity": "sha512-RAKg+ZSxEHEwCgtyRlLelsNe6TqHtpDUi/wx7qEpiFQ2OeHyJvQ2uQcVRLra9hiD8OsFFF6syUs42dX0C8Slkg==", + "license": "MIT", + "dependencies": { + "@indiekit/error": "^1.0.0-beta.25", + "express": "^5.0.0", + "express-validator": "^7.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub": { + "name": "@rmdes/indiekit-endpoint-micropub", + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-micropub/-/indiekit-endpoint-micropub-1.0.0-beta.29.tgz", + "integrity": "sha512-94aUGNMsTOHnFVwUicqfGvbeJ4/HAJLKeReMw0iSoryGfgIi2Hz52DV5Ehub6UzzdLon6loXOCTqTMpM3UgxuA==", + "license": "MIT", + "dependencies": { + "@indiekit/error": "^1.0.0-beta.25", + "@indiekit/util": "^1.0.0-beta.25", + "@paulrobertlloyd/mf2tojf2": "^3.0.0", + "debug": "^4.3.2", + "express": "^5.0.0", + "lodash": "^4.17.21", + "markdown-it": "^14.0.0", + "newbase60": "^1.3.1", + "turndown": "^7.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share": { + "name": "@rmdes/indiekit-endpoint-share", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-share/-/indiekit-endpoint-share-1.0.2.tgz", + "integrity": "sha512-rIjdvto0k97zEwDxExsXBCkusbSP/Dbn9CEHYh2675XWFZuzCdZtq+oO9SrMui5dLVl4MuGIxfGnhinCgE+n0A==", + "license": "MIT", + "dependencies": { + "@indiekit/error": "^1.0.0-beta.25", + "express": "^5.0.0", + "express-validator": "^7.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate": { + "name": "@rmdes/indiekit-endpoint-syndicate", + "version": "1.0.0-beta.34", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-syndicate/-/indiekit-endpoint-syndicate-1.0.0-beta.34.tgz", + "integrity": "sha512-GGuJStYnqb4W8lHlRjpow8RRBf0BuQbnBP8QKnhCOw48UM+DyzUWVonGa5vxNJtOFw/n+A6LDzOHxr1VT11QbQ==", + "license": "MIT", + "dependencies": { + "@indiekit/error": "^1.0.0-beta.25", + "express": "^5.0.0", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@indiekit/indiekit": ">=1.0.0-beta.25" + } + }, + "node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend": { + "name": "@rmdes/indiekit-frontend", + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-frontend/-/indiekit-frontend-1.0.0-beta.27.tgz", + "integrity": "sha512-jvfkhTJJMkiWbDi15DqqHuvEt0BzbtdAuYsqL36t59e+Wa3PodGTV8SELl6REcbjyshuixgxOTBgkZVNTHoziQ==", + "license": "MIT", + "dependencies": { + "@accessible-components/tag-input": "^0.2.0", + "@indiekit/error": "^1.0.0-beta.25", + "@indiekit/util": "^1.0.0-beta.25", + "color": "^5.0.0", + "easymde": "^2.18.0", + "esbuild": "^0.27.0", + "iso-639-1": "^3.0.0", + "lightningcss": "^1.29.3", + "lodash": "^4.17.21", + "markdown-it": "^14.0.0", + "markdown-it-abbr": "^2.0.0", + "markdown-it-deflist": "^3.0.0", + "markdown-it-footnote": "^4.0.0", + "markdown-it-image-figures": "^2.1.1", + "markdown-it-prism": "^3.0.0", + "nunjucks": "^3.2.3", + "sharp": "^0.34.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@indiekit/post-type-article": { "version": "1.0.0-beta.25", "resolved": "https://registry.npmjs.org/@indiekit/post-type-article/-/post-type-article-1.0.0-beta.25.tgz", @@ -573,17 +670,17 @@ "@parcel/watcher-win32-x64": "2.5.6" } }, - "node_modules/@parcel/watcher-freebsd-x64": { + "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", - "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ - "x64" + "arm64" ], "license": "MIT", "optional": true, "os": [ - "freebsd" + "darwin" ], "engines": { "node": ">= 10.0.0" @@ -637,24 +734,6 @@ "node": ">=22" } }, - "node_modules/@rmdes/indiekit-endpoint-auth": { - "version": "1.0.0-beta.25", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-auth/-/indiekit-endpoint-auth-1.0.0-beta.25.tgz", - "integrity": "sha512-pdQCFi96NERxGUTSSO4mkbmY0lJSx61STNcf9uBWTBdOapekAsjENlOTHMAO2tcEJ00v+r7GAtBYZ0afwwHnbw==", - "license": "MIT", - "dependencies": { - "@indiekit/error": "^1.0.0-beta.25", - "@indiekit/util": "^1.0.0-beta.25", - "bcrypt": "^6.0.0", - "express": "^5.0.0", - "express-validator": "^7.0.0", - "jsonwebtoken": "^9.0.0", - "microformats-parser": "^2.0.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/@rmdes/indiekit-endpoint-conversations": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-conversations/-/indiekit-endpoint-conversations-2.1.6.tgz", @@ -668,6 +747,35 @@ "node": ">=20" } }, + "node_modules/@rmdes/indiekit-endpoint-conversations/node_modules/@indiekit/frontend": { + "name": "@rmdes/indiekit-frontend", + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-frontend/-/indiekit-frontend-1.0.0-beta.27.tgz", + "integrity": "sha512-jvfkhTJJMkiWbDi15DqqHuvEt0BzbtdAuYsqL36t59e+Wa3PodGTV8SELl6REcbjyshuixgxOTBgkZVNTHoziQ==", + "license": "MIT", + "dependencies": { + "@accessible-components/tag-input": "^0.2.0", + "@indiekit/error": "^1.0.0-beta.25", + "@indiekit/util": "^1.0.0-beta.25", + "color": "^5.0.0", + "easymde": "^2.18.0", + "esbuild": "^0.27.0", + "iso-639-1": "^3.0.0", + "lightningcss": "^1.29.3", + "lodash": "^4.17.21", + "markdown-it": "^14.0.0", + "markdown-it-abbr": "^2.0.0", + "markdown-it-deflist": "^3.0.0", + "markdown-it-footnote": "^4.0.0", + "markdown-it-image-figures": "^2.1.1", + "markdown-it-prism": "^3.0.0", + "nunjucks": "^3.2.3", + "sharp": "^0.34.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@rmdes/indiekit-endpoint-github": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-github/-/indiekit-endpoint-github-1.2.3.tgz", @@ -684,39 +792,6 @@ "@indiekit/indiekit": ">=1.0.0-beta.25" } }, - "node_modules/@rmdes/indiekit-endpoint-posts": { - "version": "1.0.0-beta.25", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-posts/-/indiekit-endpoint-posts-1.0.0-beta.25.tgz", - "integrity": "sha512-Rh519QYIOQK3pipUESfp1lv2mDZSwVX0W32KPsMwZrs77LFCu4KCI+YjedgSr7dwZZO0qH0j24QhJ7SOkn+DaA==", - "license": "MIT", - "dependencies": { - "@indiekit/endpoint-micropub": "^1.0.0-beta.25", - "@indiekit/error": "^1.0.0-beta.25", - "@indiekit/frontend": "^1.0.0-beta.25", - "@indiekit/util": "^1.0.0-beta.25", - "@paulrobertlloyd/mf2tojf2": "^3.0.0", - "express": "^5.0.0", - "express-validator": "^7.0.0", - "formatcoords": "^1.1.3" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@rmdes/indiekit-endpoint-share": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-share/-/indiekit-endpoint-share-1.0.2.tgz", - "integrity": "sha512-rIjdvto0k97zEwDxExsXBCkusbSP/Dbn9CEHYh2675XWFZuzCdZtq+oO9SrMui5dLVl4MuGIxfGnhinCgE+n0A==", - "license": "MIT", - "dependencies": { - "@indiekit/error": "^1.0.0-beta.25", - "express": "^5.0.0", - "express-validator": "^7.0.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/@rmdes/indiekit-endpoint-webmention-io": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-webmention-io/-/indiekit-endpoint-webmention-io-1.0.7.tgz", @@ -735,6 +810,49 @@ "@indiekit/indiekit": ">=1.0.0-beta.25" } }, + "node_modules/@rmdes/indiekit-endpoint-webmention-io/node_modules/@indiekit/frontend": { + "name": "@rmdes/indiekit-frontend", + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-frontend/-/indiekit-frontend-1.0.0-beta.27.tgz", + "integrity": "sha512-jvfkhTJJMkiWbDi15DqqHuvEt0BzbtdAuYsqL36t59e+Wa3PodGTV8SELl6REcbjyshuixgxOTBgkZVNTHoziQ==", + "license": "MIT", + "dependencies": { + "@accessible-components/tag-input": "^0.2.0", + "@indiekit/error": "^1.0.0-beta.25", + "@indiekit/util": "^1.0.0-beta.25", + "color": "^5.0.0", + "easymde": "^2.18.0", + "esbuild": "^0.27.0", + "iso-639-1": "^3.0.0", + "lightningcss": "^1.29.3", + "lodash": "^4.17.21", + "markdown-it": "^14.0.0", + "markdown-it-abbr": "^2.0.0", + "markdown-it-deflist": "^3.0.0", + "markdown-it-footnote": "^4.0.0", + "markdown-it-image-figures": "^2.1.1", + "markdown-it-prism": "^3.0.0", + "nunjucks": "^3.2.3", + "sharp": "^0.34.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@rmdes/indiekit-preset-eleventy": { + "version": "1.0.0-beta.33", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-preset-eleventy/-/indiekit-preset-eleventy-1.0.0-beta.33.tgz", + "integrity": "sha512-47Nyvg6FqeAUPN90zzfhEqijeFxAsclcmqTz1ITdk/F9XBscCaBKyrNiJ+Ywk/c5wWsMK5NL0DpP8v7k8fi+cw==", + "license": "MIT", + "dependencies": { + "camelcase-keys": "^10.0.0", + "plur": "^6.0.0", + "yaml": "^2.6.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@sindresorhus/slugify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-3.0.0.tgz", @@ -1077,6 +1195,63 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-9.0.0.tgz", + "integrity": "sha512-TO9xmyXTZ9HUHI8M1OnvExxYB0eYVS/1e5s7IDMTAoIcwUd+aNcFODs6Xk83mobk0velyHFQgA1yIrvYc6wclw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-10.0.2.tgz", + "integrity": "sha512-PVHCLVbJ7nWGal0lPAmBN5eSLjIynlMUk2EPmL9aPl6QyJ6+FoszTKwldPzkuVqg5teZbPTbb8Oenzyw9GSJRw==", + "license": "MIT", + "dependencies": { + "camelcase": "^9.0.0", + "map-obj": "6.0.0", + "quick-lru": "^7.3.0", + "type-fest": "^5.4.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/map-obj": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-6.0.0.tgz", + "integrity": "sha512-PwDvwt/tK70+luLw5k9ySLtzLAzwf7tZTY9GBj63Y010nHRPjwHcQTpTd5JwQqITC2ty7prtxBo71iwyYY0TAg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/type-fest": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", + "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/change-case": { "version": "5.4.4", "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", @@ -1852,6 +2027,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -2651,6 +2827,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -2684,17 +2861,17 @@ "lightningcss-win32-x64-msvc": "1.31.1" } }, - "node_modules/lightningcss-freebsd-x64": { + "node_modules/lightningcss-darwin-arm64": { "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", - "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", "cpu": [ - "x64" + "arm64" ], "license": "MPL-2.0", "optional": true, "os": [ - "freebsd" + "darwin" ], "engines": { "node": ">= 12.0.0" @@ -2857,6 +3034,7 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -3787,6 +3965,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-lru": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz", + "integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/radix3": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", @@ -4159,6 +4349,7 @@ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "license": "MIT", + "peer": true, "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" @@ -4309,6 +4500,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tar": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", diff --git a/node_modules/@esbuild/darwin-arm64/README.md b/node_modules/@esbuild/darwin-arm64/README.md new file mode 100644 index 00000000..c2c0398d --- /dev/null +++ b/node_modules/@esbuild/darwin-arm64/README.md @@ -0,0 +1,3 @@ +# esbuild + +This is the macOS ARM 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details. diff --git a/node_modules/@esbuild/freebsd-x64/bin/esbuild b/node_modules/@esbuild/darwin-arm64/bin/esbuild similarity index 56% rename from node_modules/@esbuild/freebsd-x64/bin/esbuild rename to node_modules/@esbuild/darwin-arm64/bin/esbuild index 4bdbb75e..b8c23e28 100755 Binary files a/node_modules/@esbuild/freebsd-x64/bin/esbuild and b/node_modules/@esbuild/darwin-arm64/bin/esbuild differ diff --git a/node_modules/@esbuild/freebsd-x64/package.json b/node_modules/@esbuild/darwin-arm64/package.json similarity index 62% rename from node_modules/@esbuild/freebsd-x64/package.json rename to node_modules/@esbuild/darwin-arm64/package.json index 26aceb43..2c15be10 100644 --- a/node_modules/@esbuild/freebsd-x64/package.json +++ b/node_modules/@esbuild/darwin-arm64/package.json @@ -1,7 +1,7 @@ { - "name": "@esbuild/freebsd-x64", + "name": "@esbuild/darwin-arm64", "version": "0.27.3", - "description": "The FreeBSD 64-bit binary for esbuild, a JavaScript bundler.", + "description": "The macOS ARM 64-bit binary for esbuild, a JavaScript bundler.", "repository": { "type": "git", "url": "git+https://github.com/evanw/esbuild.git" @@ -12,9 +12,9 @@ "node": ">=18" }, "os": [ - "freebsd" + "darwin" ], "cpu": [ - "x64" + "arm64" ] } diff --git a/node_modules/@esbuild/freebsd-x64/README.md b/node_modules/@esbuild/freebsd-x64/README.md deleted file mode 100644 index eb63d93c..00000000 --- a/node_modules/@esbuild/freebsd-x64/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# esbuild - -This is the FreeBSD 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details. diff --git a/node_modules/@img/sharp-darwin-arm64/LICENSE b/node_modules/@img/sharp-darwin-arm64/LICENSE new file mode 100644 index 00000000..37ec93a1 --- /dev/null +++ b/node_modules/@img/sharp-darwin-arm64/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/node_modules/@img/sharp-darwin-arm64/README.md b/node_modules/@img/sharp-darwin-arm64/README.md new file mode 100644 index 00000000..8220bf77 --- /dev/null +++ b/node_modules/@img/sharp-darwin-arm64/README.md @@ -0,0 +1,18 @@ +# `@img/sharp-darwin-arm64` + +Prebuilt sharp for use with macOS 64-bit ARM. + +## Licensing + +Copyright 2013 Lovell Fuller and others. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +[https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node b/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node new file mode 100644 index 00000000..9cb0adad Binary files /dev/null and b/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node differ diff --git a/node_modules/@img/sharp-darwin-arm64/package.json b/node_modules/@img/sharp-darwin-arm64/package.json new file mode 100644 index 00000000..df6b34a9 --- /dev/null +++ b/node_modules/@img/sharp-darwin-arm64/package.json @@ -0,0 +1,40 @@ +{ + "name": "@img/sharp-darwin-arm64", + "version": "0.34.5", + "description": "Prebuilt sharp for use with macOS 64-bit ARM", + "author": "Lovell Fuller ", + "homepage": "https://sharp.pixelplumbing.com", + "repository": { + "type": "git", + "url": "git+https://github.com/lovell/sharp.git", + "directory": "npm/darwin-arm64" + }, + "license": "Apache-2.0", + "funding": { + "url": "https://opencollective.com/libvips" + }, + "preferUnplugged": true, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + }, + "files": [ + "lib" + ], + "publishConfig": { + "access": "public" + }, + "type": "commonjs", + "exports": { + "./sharp.node": "./lib/sharp-darwin-arm64.node", + "./package": "./package.json" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ] +} diff --git a/node_modules/@img/sharp-libvips-darwin-arm64/README.md b/node_modules/@img/sharp-libvips-darwin-arm64/README.md new file mode 100644 index 00000000..7516f72d --- /dev/null +++ b/node_modules/@img/sharp-libvips-darwin-arm64/README.md @@ -0,0 +1,46 @@ +# `@img/sharp-libvips-darwin-arm64` + +Prebuilt libvips and dependencies for use with sharp on macOS 64-bit ARM. + +## Licensing + +This software contains third-party libraries +used under the terms of the following licences: + +| Library | Used under the terms of | +|---------------|-----------------------------------------------------------------------------------------------------------| +| aom | BSD 2-Clause + [Alliance for Open Media Patent License 1.0](https://aomedia.org/license/patent-license/) | +| cairo | Mozilla Public License 2.0 | +| cgif | MIT Licence | +| expat | MIT Licence | +| fontconfig | [fontconfig Licence](https://gitlab.freedesktop.org/fontconfig/fontconfig/blob/main/COPYING) (BSD-like) | +| freetype | [freetype Licence](https://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/FTL.TXT) (BSD-like) | +| fribidi | LGPLv3 | +| glib | LGPLv3 | +| harfbuzz | MIT Licence | +| highway | Apache-2.0 License, BSD 3-Clause | +| lcms | MIT Licence | +| libarchive | BSD 2-Clause | +| libexif | LGPLv3 | +| libffi | MIT Licence | +| libheif | LGPLv3 | +| libimagequant | [BSD 2-Clause](https://github.com/lovell/libimagequant/blob/main/COPYRIGHT) | +| libnsgif | MIT Licence | +| libpng | [libpng License](https://github.com/pnggroup/libpng/blob/master/LICENSE) | +| librsvg | LGPLv3 | +| libspng | [BSD 2-Clause, libpng License](https://github.com/randy408/libspng/blob/master/LICENSE) | +| libtiff | [libtiff License](https://gitlab.com/libtiff/libtiff/blob/master/LICENSE.md) (BSD-like) | +| libvips | LGPLv3 | +| libwebp | New BSD License | +| libxml2 | MIT Licence | +| mozjpeg | [zlib License, IJG License, BSD-3-Clause](https://github.com/mozilla/mozjpeg/blob/master/LICENSE.md) | +| pango | LGPLv3 | +| pixman | MIT Licence | +| proxy-libintl | LGPLv3 | +| zlib-ng | [zlib Licence](https://github.com/zlib-ng/zlib-ng/blob/develop/LICENSE.md) | + +Use of libraries under the terms of the LGPLv3 is via the +"any later version" clause of the LGPLv2 or LGPLv2.1. + +Please report any errors or omissions via +https://github.com/lovell/sharp-libvips/issues/new diff --git a/node_modules/@img/sharp-libvips-darwin-arm64/lib/glib-2.0/include/glibconfig.h b/node_modules/@img/sharp-libvips-darwin-arm64/lib/glib-2.0/include/glibconfig.h new file mode 100644 index 00000000..d43d3211 --- /dev/null +++ b/node_modules/@img/sharp-libvips-darwin-arm64/lib/glib-2.0/include/glibconfig.h @@ -0,0 +1,220 @@ +/* glibconfig.h + * + * This is a generated file. Please modify 'glibconfig.h.in' + */ + +#ifndef __GLIBCONFIG_H__ +#define __GLIBCONFIG_H__ + +#include + +#include +#include +#define GLIB_HAVE_ALLOCA_H + +#define GLIB_STATIC_COMPILATION 1 +#define GOBJECT_STATIC_COMPILATION 1 +#define GIO_STATIC_COMPILATION 1 +#define GMODULE_STATIC_COMPILATION 1 +#define GI_STATIC_COMPILATION 1 +#define G_INTL_STATIC_COMPILATION 1 +#define FFI_STATIC_BUILD 1 + +/* Specifies that GLib's g_print*() functions wrap the + * system printf functions. This is useful to know, for example, + * when using glibc's register_printf_function(). + */ +#define GLIB_USING_SYSTEM_PRINTF + +G_BEGIN_DECLS + +#define G_MINFLOAT FLT_MIN +#define G_MAXFLOAT FLT_MAX +#define G_MINDOUBLE DBL_MIN +#define G_MAXDOUBLE DBL_MAX +#define G_MINSHORT SHRT_MIN +#define G_MAXSHORT SHRT_MAX +#define G_MAXUSHORT USHRT_MAX +#define G_MININT INT_MIN +#define G_MAXINT INT_MAX +#define G_MAXUINT UINT_MAX +#define G_MINLONG LONG_MIN +#define G_MAXLONG LONG_MAX +#define G_MAXULONG ULONG_MAX + +typedef signed char gint8; +typedef unsigned char guint8; + +typedef signed short gint16; +typedef unsigned short guint16; + +#define G_GINT16_MODIFIER "h" +#define G_GINT16_FORMAT "hi" +#define G_GUINT16_FORMAT "hu" + + +typedef signed int gint32; +typedef unsigned int guint32; + +#define G_GINT32_MODIFIER "" +#define G_GINT32_FORMAT "i" +#define G_GUINT32_FORMAT "u" + + +#define G_HAVE_GINT64 1 /* deprecated, always true */ + +G_GNUC_EXTENSION typedef signed long long gint64; +G_GNUC_EXTENSION typedef unsigned long long guint64; + +#define G_GINT64_CONSTANT(val) (G_GNUC_EXTENSION (val##LL)) +#define G_GUINT64_CONSTANT(val) (G_GNUC_EXTENSION (val##ULL)) + +#define G_GINT64_MODIFIER "ll" +#define G_GINT64_FORMAT "lli" +#define G_GUINT64_FORMAT "llu" + + +#define GLIB_SIZEOF_VOID_P 8 +#define GLIB_SIZEOF_LONG 8 +#define GLIB_SIZEOF_SIZE_T 8 +#define GLIB_SIZEOF_SSIZE_T 8 + +typedef signed long gssize; +typedef unsigned long gsize; +#define G_GSIZE_MODIFIER "l" +#define G_GSSIZE_MODIFIER "l" +#define G_GSIZE_FORMAT "lu" +#define G_GSSIZE_FORMAT "li" + +#define G_MAXSIZE G_MAXULONG +#define G_MINSSIZE G_MINLONG +#define G_MAXSSIZE G_MAXLONG + +typedef gint64 goffset; +#define G_MINOFFSET G_MININT64 +#define G_MAXOFFSET G_MAXINT64 + +#define G_GOFFSET_MODIFIER G_GINT64_MODIFIER +#define G_GOFFSET_FORMAT G_GINT64_FORMAT +#define G_GOFFSET_CONSTANT(val) G_GINT64_CONSTANT(val) + +#define G_POLLFD_FORMAT "%d" + +#define GPOINTER_TO_INT(p) ((gint) (glong) (p)) +#define GPOINTER_TO_UINT(p) ((guint) (gulong) (p)) + +#define GINT_TO_POINTER(i) ((gpointer) (glong) (i)) +#define GUINT_TO_POINTER(u) ((gpointer) (gulong) (u)) + +typedef signed long gintptr; +typedef unsigned long guintptr; + +#define G_GINTPTR_MODIFIER "l" +#define G_GINTPTR_FORMAT "li" +#define G_GUINTPTR_FORMAT "lu" + +#define GLIB_MAJOR_VERSION 2 +#define GLIB_MINOR_VERSION 86 +#define GLIB_MICRO_VERSION 1 + +#define G_OS_UNIX + +#define G_VA_COPY va_copy + + +#define G_HAVE_ISO_VARARGS 1 + +/* gcc-2.95.x supports both gnu style and ISO varargs, but if -ansi + * is passed ISO vararg support is turned off, and there is no work + * around to turn it on, so we unconditionally turn it off. + */ +#if __GNUC__ == 2 && __GNUC_MINOR__ == 95 +# undef G_HAVE_ISO_VARARGS +#endif + +#define G_HAVE_GROWING_STACK 0 + +#ifndef _MSC_VER +# define G_HAVE_GNUC_VARARGS 1 +#endif + +#if defined(__SUNPRO_C) && (__SUNPRO_C >= 0x590) +#define G_GNUC_INTERNAL __attribute__((visibility("hidden"))) +#elif defined(__SUNPRO_C) && (__SUNPRO_C >= 0x550) +#define G_GNUC_INTERNAL __hidden +#elif defined (__GNUC__) && defined (G_HAVE_GNUC_VISIBILITY) +#define G_GNUC_INTERNAL __attribute__((visibility("hidden"))) +#else +#define G_GNUC_INTERNAL +#endif + +#define G_THREADS_ENABLED +#define G_THREADS_IMPL_POSIX + +#define G_ATOMIC_LOCK_FREE + +#define GINT16_TO_LE(val) ((gint16) (val)) +#define GUINT16_TO_LE(val) ((guint16) (val)) +#define GINT16_TO_BE(val) ((gint16) GUINT16_SWAP_LE_BE (val)) +#define GUINT16_TO_BE(val) (GUINT16_SWAP_LE_BE (val)) + +#define GINT32_TO_LE(val) ((gint32) (val)) +#define GUINT32_TO_LE(val) ((guint32) (val)) +#define GINT32_TO_BE(val) ((gint32) GUINT32_SWAP_LE_BE (val)) +#define GUINT32_TO_BE(val) (GUINT32_SWAP_LE_BE (val)) + +#define GINT64_TO_LE(val) ((gint64) (val)) +#define GUINT64_TO_LE(val) ((guint64) (val)) +#define GINT64_TO_BE(val) ((gint64) GUINT64_SWAP_LE_BE (val)) +#define GUINT64_TO_BE(val) (GUINT64_SWAP_LE_BE (val)) + +#define GLONG_TO_LE(val) ((glong) GINT64_TO_LE (val)) +#define GULONG_TO_LE(val) ((gulong) GUINT64_TO_LE (val)) +#define GLONG_TO_BE(val) ((glong) GINT64_TO_BE (val)) +#define GULONG_TO_BE(val) ((gulong) GUINT64_TO_BE (val)) +#define GINT_TO_LE(val) ((gint) GINT32_TO_LE (val)) +#define GUINT_TO_LE(val) ((guint) GUINT32_TO_LE (val)) +#define GINT_TO_BE(val) ((gint) GINT32_TO_BE (val)) +#define GUINT_TO_BE(val) ((guint) GUINT32_TO_BE (val)) +#define GSIZE_TO_LE(val) ((gsize) GUINT64_TO_LE (val)) +#define GSSIZE_TO_LE(val) ((gssize) GINT64_TO_LE (val)) +#define GSIZE_TO_BE(val) ((gsize) GUINT64_TO_BE (val)) +#define GSSIZE_TO_BE(val) ((gssize) GINT64_TO_BE (val)) +#define G_BYTE_ORDER G_LITTLE_ENDIAN + +#define GLIB_SYSDEF_POLLIN =1 +#define GLIB_SYSDEF_POLLOUT =4 +#define GLIB_SYSDEF_POLLPRI =2 +#define GLIB_SYSDEF_POLLHUP =16 +#define GLIB_SYSDEF_POLLERR =8 +#define GLIB_SYSDEF_POLLNVAL =32 + +/* No way to disable deprecation warnings for macros, so only emit deprecation + * warnings on platforms where usage of this macro is broken */ +#if defined(__APPLE__) || defined(_MSC_VER) || defined(__CYGWIN__) +#define G_MODULE_SUFFIX "so" GLIB_DEPRECATED_MACRO_IN_2_76 +#else +#define G_MODULE_SUFFIX "so" +#endif + +typedef int GPid; +#define G_PID_FORMAT "i" + +#define GLIB_SYSDEF_AF_UNIX 1 +#define GLIB_SYSDEF_AF_INET 2 +#define GLIB_SYSDEF_AF_INET6 30 + +#define GLIB_SYSDEF_MSG_OOB 1 +#define GLIB_SYSDEF_MSG_PEEK 2 +#define GLIB_SYSDEF_MSG_DONTROUTE 4 + +#define G_DIR_SEPARATOR '/' +#define G_DIR_SEPARATOR_S "/" +#define G_SEARCHPATH_SEPARATOR ':' +#define G_SEARCHPATH_SEPARATOR_S ":" + +#undef G_HAVE_FREE_SIZED + +G_END_DECLS + +#endif /* __GLIBCONFIG_H__ */ diff --git a/node_modules/@img/sharp-libvips-darwin-arm64/lib/index.js b/node_modules/@img/sharp-libvips-darwin-arm64/lib/index.js new file mode 100644 index 00000000..5092b4dd --- /dev/null +++ b/node_modules/@img/sharp-libvips-darwin-arm64/lib/index.js @@ -0,0 +1 @@ +module.exports = __dirname; diff --git a/node_modules/@img/sharp-libvips-darwin-arm64/lib/libvips-cpp.8.17.3.dylib b/node_modules/@img/sharp-libvips-darwin-arm64/lib/libvips-cpp.8.17.3.dylib new file mode 100644 index 00000000..377e5f87 Binary files /dev/null and b/node_modules/@img/sharp-libvips-darwin-arm64/lib/libvips-cpp.8.17.3.dylib differ diff --git a/node_modules/@img/sharp-libvips-darwin-arm64/package.json b/node_modules/@img/sharp-libvips-darwin-arm64/package.json new file mode 100644 index 00000000..7b3a933d --- /dev/null +++ b/node_modules/@img/sharp-libvips-darwin-arm64/package.json @@ -0,0 +1,36 @@ +{ + "name": "@img/sharp-libvips-darwin-arm64", + "version": "1.2.4", + "description": "Prebuilt libvips and dependencies for use with sharp on macOS 64-bit ARM", + "author": "Lovell Fuller ", + "homepage": "https://sharp.pixelplumbing.com", + "repository": { + "type": "git", + "url": "git+https://github.com/lovell/sharp-libvips.git", + "directory": "npm/darwin-arm64" + }, + "license": "LGPL-3.0-or-later", + "funding": { + "url": "https://opencollective.com/libvips" + }, + "preferUnplugged": true, + "publishConfig": { + "access": "public" + }, + "files": [ + "lib", + "versions.json" + ], + "type": "commonjs", + "exports": { + "./lib": "./lib/index.js", + "./package": "./package.json", + "./versions": "./versions.json" + }, + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ] +} diff --git a/node_modules/@img/sharp-libvips-darwin-arm64/versions.json b/node_modules/@img/sharp-libvips-darwin-arm64/versions.json new file mode 100644 index 00000000..fec67b15 --- /dev/null +++ b/node_modules/@img/sharp-libvips-darwin-arm64/versions.json @@ -0,0 +1,30 @@ +{ + "aom": "3.13.1", + "archive": "3.8.2", + "cairo": "1.18.4", + "cgif": "0.5.0", + "exif": "0.6.25", + "expat": "2.7.3", + "ffi": "3.5.2", + "fontconfig": "2.17.1", + "freetype": "2.14.1", + "fribidi": "1.0.16", + "glib": "2.86.1", + "harfbuzz": "12.1.0", + "heif": "1.20.2", + "highway": "1.3.0", + "imagequant": "2.4.1", + "lcms": "2.17", + "mozjpeg": "0826579", + "pango": "1.57.0", + "pixman": "0.46.4", + "png": "1.6.50", + "proxy-libintl": "0.5", + "rsvg": "2.61.2", + "spng": "0.7.4", + "tiff": "4.7.1", + "vips": "8.17.3", + "webp": "1.6.0", + "xml2": "2.15.1", + "zlib-ng": "2.2.5" +} \ No newline at end of file diff --git a/node_modules/@indiekit/endpoint-files/LICENSE b/node_modules/@indiekit/endpoint-files/LICENSE deleted file mode 100644 index a460806a..00000000 --- a/node_modules/@indiekit/endpoint-files/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Paul Robert Lloyd - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/node_modules/@indiekit/endpoint-files/views/file-form.njk b/node_modules/@indiekit/endpoint-files/views/file-form.njk deleted file mode 100644 index f17cacbf..00000000 --- a/node_modules/@indiekit/endpoint-files/views/file-form.njk +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "form.njk" %} - -{% block fieldset %} - {{ input({ - name: "file", - type: "file", - value: fieldData("file").value, - label: __("files.form.file.label"), - errorMessage: fieldData("file").errorMessage - }) | indent(2) }} -{% endblock %} - -{% block buttons %} -
- {{ button({ - text: __("files.form.submit"), - attributes: { - formenctype: "multipart/form-data" - } - }) | indent(4) }} - - {{ prose({ - text: "[" + __("files.form.cancel") + "](" + filesPath + ")" - }) | indent(4) }} -
-{% endblock %} \ No newline at end of file diff --git a/node_modules/@indiekit/endpoint-micropub/LICENSE b/node_modules/@indiekit/endpoint-micropub/LICENSE deleted file mode 100644 index a460806a..00000000 --- a/node_modules/@indiekit/endpoint-micropub/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Paul Robert Lloyd - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/node_modules/@indiekit/endpoint-micropub/README.md b/node_modules/@indiekit/endpoint-micropub/README.md deleted file mode 100644 index cede5cc4..00000000 --- a/node_modules/@indiekit/endpoint-micropub/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# @indiekit/endpoint-micropub - -Micropub endpoint for Indiekit. Enables publishing content to your website using the Micropub protocol. - -## Installation - -`npm install @indiekit/endpoint-micropub` - -> [!NOTE] -> This package is installed alongside `@indiekit/indiekit` - -## Usage - -To customise the behaviour of this plug-in, add `@indiekit/endpoint-micropub` to your configuration, specifying options as required: - -```json -{ - "@indiekit/endpoint-micropub": { - "mountPath": "/publisher" - } -} -``` - -## Options - -| Option | Type | Description | -| :---------- | :------- | :------------------------------------------------------------------------ | -| `mountPath` | `string` | Path to listen to Micropub requests. _Optional_, defaults to `/micropub`. | - -## Supported endpoint queries - -- Configuration: `/micropub?q=config` -- Media endpoint location: `/micropub?q=media-endpoint` -- Available syndication targets (list): `/micropub?q=syndicate-to` -- Supported queries: `/micropub?q=config` -- Supported vocabularies (list): `/micropub?q=post-types` -- Publication categories (list): `/micropub?q=category` -- Previously published posts (list): `/micropub?q=source` -- Source content: `/micropub?q=source&url=WEBSITE_URL` - -List queries support `filter`, `limit` and `offset` and parameters. For example, `/micropub?q=source&filter=web&limit=10&offset=10`. diff --git a/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/README.md b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/README.md new file mode 100644 index 00000000..c629b876 --- /dev/null +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/README.md @@ -0,0 +1,201 @@ +# @rmdes/indiekit-endpoint-micropub + +Micropub endpoint for Indiekit with custom type-based post discovery and pre-syndication markup support. Enables publishing content to your website using the [Micropub protocol](https://micropub.spec.indieweb.org/). + +## Fork Notice + +This is a fork of `@indiekit/endpoint-micropub` with two custom features: + +### 1. Pre-Syndication Markup Support + +Services like [IndieNews](https://news.indieweb.org/) require a `u-syndication` link in your HTML **before** they receive the syndication webmention. The upstream Micropub endpoint strips all `mp-*` properties (including `mp-syndicate-to`) before passing data to the preset's `postTemplate()`. + +This fork preserves `mp-syndicate-to` so that: +1. The property reaches the preset's `postTemplate()` +2. The preset can include it in frontmatter (as `mpSyndicateTo` in Eleventy) +3. The theme can render the `u-syndication` link +4. IndieNews (and similar services) can find the link when parsing the webmention + +**Technical change** in `lib/utils.js`: + +```javascript +// mp- properties to preserve for the template (needed for pre-syndication markup) +const preserveMpProperties = ["mp-syndicate-to"]; + +for (let key in templateProperties) { + if (key.startsWith("mp-") && !preserveMpProperties.includes(key)) { + delete templateProperties[key]; + } + // ... +} +``` + +### 2. Type-Based Post Type Discovery + +The external `@paulrobertlloyd/mf2tojf2` library only preserves standard microformat properties during mf2→JF2 conversion. Custom discovery properties were being stripped, making it impossible for custom post type plugins to trigger type detection. + +This fork adds a type-based discovery mechanism in `lib/post-type-discovery.js`: + +```javascript +// If post has a custom type (h value) that matches a configured post type +// This allows plugins to use h: "page" or similar for type-based discovery +if (properties.type && properties.type !== "entry" && postTypes[properties.type]) { + return properties.type; +} +``` + +This enables custom post type plugins like `@rmdes/indiekit-post-type-page` to work correctly. + +## Installation + +```bash +npm install @rmdes/indiekit-endpoint-micropub +``` + +### Using npm overrides (recommended) + +Add to your `package.json`: + +```json +{ + "overrides": { + "@indiekit/endpoint-micropub": "npm:@rmdes/indiekit-endpoint-micropub@^1.0.0-beta.28" + } +} +``` + +This replaces the upstream package with this fork without changing your plugin configuration. + +### Direct installation + +```javascript +import MicropubEndpoint from "@rmdes/indiekit-endpoint-micropub"; + +export default { + plugins: [ + new MicropubEndpoint({ + mountPath: "/micropub" // Optional, defaults to /micropub + }) + ] +}; +``` + +## Configuration + +### Options + +| Option | Type | Description | +| :---------- | :------- | :------------------------------------------------------------------------ | +| `mountPath` | `string` | Path to listen to Micropub requests. Optional, defaults to `/micropub`. | + +## Supported Endpoints + +### POST /micropub (Action Endpoint) + +Create, update, delete, or undelete posts using the Micropub protocol. + +**Actions:** +- `create` - Create new post (requires `create` or `post` scope) +- `update` - Update existing post (requires `update` scope) +- `delete` - Delete post (requires `delete` scope) +- `undelete` - Restore deleted post (requires `create` scope) + +**Update operations:** +- `replace` - Replace entire property value +- `add` - Add value to existing property +- `delete` - Delete property or specific values + +### GET /micropub (Query Endpoint) + +Query published posts and configuration. + +**Supported queries:** + +| Query | Description | Example | +|-------|-------------|---------| +| `config` | Full configuration | `/micropub?q=config` | +| `media-endpoint` | Media endpoint URL | `/micropub?q=media-endpoint` | +| `syndicate-to` | Available syndication targets | `/micropub?q=syndicate-to` | +| `post-types` | Supported post types | `/micropub?q=post-types` | +| `category` | Publication categories | `/micropub?q=category` | +| `channel` | Publication channels | `/micropub?q=channel` | +| `source` | Published posts (paginated) | `/micropub?q=source` | +| `source&url=URL` | Single post by URL | `/micropub?q=source&url=https://example.com/post` | + +**Pagination parameters:** +- `filter` - Filter results by string match +- `limit` - Maximum number of results +- `offset` - Skip first N results +- `after` - Cursor for next page +- `before` - Cursor for previous page + +Example: `/micropub?q=source&filter=web&limit=10&offset=10` + +## Features + +### Media Uploads + +Supports file uploads via multipart/form-data for `photo`, `video`, and `audio` properties. Files are uploaded to the media endpoint before post creation. + +### Post Type Discovery + +Implements the [Post Type Discovery](https://ptd.spec.indieweb.org/) algorithm with custom type-based detection: + +1. Event type (`type: "event"`) +2. **Custom h type** (fork feature - matches configured post type names) +3. Standard discovery properties (`rsvp`, `repost-of`, `like-of`, `in-reply-to`, `video`, `photo`) +4. Custom discovery properties (from post type config) +5. Collection (populated `children` array) +6. Article (`name` + `content`) +7. Note (default fallback) + +### Content Normalization + +Automatically converts between Markdown and HTML: +- Plaintext only → generates HTML via markdown-it +- HTML only → generates plaintext via Turndown +- Markdown conversion uses typographer and smart quotes + +### Soft Deletes + +Deleted posts are not removed from the database. They store `_deletedProperties` for restoration via the `undelete` action. + +## Requirements + +This endpoint requires: +- MongoDB database for storing post metadata +- IndieAuth authentication endpoint (`@indiekit/endpoint-auth` or `@rmdes/indiekit-endpoint-auth`) +- Media endpoint (`@indiekit/endpoint-media`) +- At least one post type plugin +- At least one preset plugin (Jekyll, Hugo, Eleventy, etc.) +- At least one store plugin (GitHub, GitLab, Gitea) + +## Related Plugins + +### Works With + +- **Post types:** `@indiekit/post-type-*`, `@rmdes/indiekit-post-type-page` +- **Syndicators:** `@rmdes/indiekit-syndicator-bluesky`, `@rmdes/indiekit-syndicator-mastodon`, `@rmdes/indiekit-syndicator-indienews` +- **Presets:** `@rmdes/indiekit-preset-eleventy`, `@indiekit/preset-jekyll`, `@indiekit/preset-hugo` +- **Stores:** `@indiekit/store-github`, `@indiekit/store-gitlab`, `@indiekit/store-gitea` + +### Optional + +- `@indiekit/endpoint-posts` or `@rmdes/indiekit-endpoint-posts` - Web UI for post management +- `@indiekit/endpoint-syndicate` or `@rmdes/indiekit-endpoint-syndicate` - Manual syndication UI + +## Documentation + +See [CLAUDE.md](./CLAUDE.md) for complete technical reference, architecture details, and integration guidance. + +## Debugging + +Enable debug output: + +```bash +DEBUG=indiekit:endpoint-micropub:* npm start +``` + +## License + +MIT - Original work by [Paul Robert Lloyd](https://paulrobertlloyd.com), custom features by [Ricardo Mendes](https://rmendes.net). diff --git a/node_modules/@indiekit/endpoint-micropub/assets/icon.svg b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/assets/icon.svg similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/assets/icon.svg rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/assets/icon.svg diff --git a/node_modules/@indiekit/endpoint-micropub/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/index.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/index.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/config.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/config.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/lib/config.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/config.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/controllers/action.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/controllers/action.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/lib/controllers/action.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/controllers/action.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/jf2.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/jf2.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/lib/jf2.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/jf2.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/markdown.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/markdown.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/lib/markdown.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/markdown.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/media.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/media.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/lib/media.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/media.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/mf2.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/mf2.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/lib/mf2.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/mf2.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/post-content.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/post-content.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/lib/post-content.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/post-content.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/post-data.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/post-data.js similarity index 94% rename from node_modules/@indiekit/endpoint-micropub/lib/post-data.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/post-data.js index 346ad085..6795a5a3 100644 --- a/node_modules/@indiekit/endpoint-micropub/lib/post-data.js +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/post-data.js @@ -150,6 +150,11 @@ export const postData = { const typeConfig = postTypes[type]; properties["post-type"] = type; + // Validate post type configuration exists + if (!typeConfig || !typeConfig.post?.path) { + throw IndiekitError.notImplemented(type); + } + // Post paths const path = await renderPath( typeConfig.post.path, @@ -219,6 +224,11 @@ export const postData = { const type = properties["post-type"]; const typeConfig = postTypes[type]; + // Validate post type configuration exists + if (!typeConfig || !typeConfig.post?.path) { + throw IndiekitError.notImplemented(type); + } + // Post paths const path = await renderPath( typeConfig.post.path, @@ -261,6 +271,11 @@ export const postData = { const type = properties["post-type"]; const typeConfig = postTypes[type]; + // Validate post type configuration exists + if (!typeConfig || !typeConfig.post?.path) { + throw IndiekitError.notImplemented(type); + } + // Post paths const path = await renderPath( typeConfig.post.path, diff --git a/node_modules/@indiekit/endpoint-micropub/lib/post-type-count.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/post-type-count.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/lib/post-type-count.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/post-type-count.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/post-type-discovery.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/post-type-discovery.js similarity index 82% rename from node_modules/@indiekit/endpoint-micropub/lib/post-type-discovery.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/post-type-discovery.js index b0dc8d9e..bb417e78 100644 --- a/node_modules/@indiekit/endpoint-micropub/lib/post-type-discovery.js +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/post-type-discovery.js @@ -8,11 +8,18 @@ export const getPostType = (postTypes, properties) => { const propertiesMap = new Map(Object.entries(properties)); - // If post has the event type, it’s an event + // If post has the event type, it's an event if (properties.type && properties.type === "event") { return properties.type; } + // If post has a custom type (h value) that matches a configured post type + // This allows plugins to use h: "page" or similar for type-based discovery + // instead of requiring a discovery property that survives mf2->JF2 conversion + if (properties.type && properties.type !== "entry" && postTypes[properties.type]) { + return properties.type; + } + const basePostTypes = new Map([ ["rsvp", "rsvp"], ["repost", "repost-of"], diff --git a/node_modules/@indiekit/endpoint-micropub/lib/reserved-properties.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/reserved-properties.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/lib/reserved-properties.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/reserved-properties.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/scope.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/scope.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/lib/scope.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/scope.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/update.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/update.js similarity index 100% rename from node_modules/@indiekit/endpoint-micropub/lib/update.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/update.js diff --git a/node_modules/@indiekit/endpoint-micropub/lib/utils.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/utils.js similarity index 88% rename from node_modules/@indiekit/endpoint-micropub/lib/utils.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/utils.js index f19b9f22..d20c370e 100644 --- a/node_modules/@indiekit/endpoint-micropub/lib/utils.js +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/lib/utils.js @@ -30,9 +30,15 @@ export const decodeQueryParameter = (value) => { export const getPostTemplateProperties = (properties) => { const templateProperties = structuredClone(properties); + // mp- properties to preserve for the template (needed for pre-syndication markup) + // mp-syndicate-to must appear in frontmatter so themes can render u-syndication + // links BEFORE the syndication webmention is sent (required by IndieNews, etc.) + const preserveMpProperties = ["mp-syndicate-to"]; + for (let key in templateProperties) { // Remove server commands from post template properties - if (key.startsWith("mp-")) { + // Exception: preserve mp-syndicate-to for pre-syndication u-syndication links + if (key.startsWith("mp-") && !preserveMpProperties.includes(key)) { delete templateProperties[key]; } diff --git a/node_modules/@indiekit/endpoint-micropub/package.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/package.json similarity index 57% rename from node_modules/@indiekit/endpoint-micropub/package.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/package.json index 7186724e..48e02479 100644 --- a/node_modules/@indiekit/endpoint-micropub/package.json +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/endpoint-micropub/package.json @@ -1,18 +1,24 @@ { - "name": "@indiekit/endpoint-micropub", - "version": "1.0.0-beta.25", - "description": "Micropub endpoint for Indiekit. Enables publishing content to your website using the Micropub protocol.", + "name": "@rmdes/indiekit-endpoint-micropub", + "version": "1.0.0-beta.29", + "description": "Micropub endpoint for Indiekit with custom type-based post discovery. Enables publishing content to your website using the Micropub protocol.", "keywords": [ "indiekit", "indiekit-plugin", "indieweb", "micropub" ], - "homepage": "https://getindiekit.com", + "homepage": "https://github.com/rmdes/indiekit-endpoint-micropub", "author": { "name": "Paul Robert Lloyd", "url": "https://paulrobertlloyd.com" }, + "contributors": [ + { + "name": "Ricardo Mendes", + "url": "https://rmendes.net" + } + ], "license": "MIT", "engines": { "node": ">=20" @@ -25,12 +31,11 @@ "index.js" ], "bugs": { - "url": "https://github.com/getindiekit/indiekit/issues" + "url": "https://github.com/rmdes/indiekit-endpoint-micropub/issues" }, "repository": { "type": "git", - "url": "https://github.com/getindiekit/indiekit.git", - "directory": "packages/endpoint-micropub" + "url": "https://github.com/rmdes/indiekit-endpoint-micropub.git" }, "dependencies": { "@indiekit/error": "^1.0.0-beta.25", @@ -45,6 +50,5 @@ }, "publishConfig": { "access": "public" - }, - "gitHead": "e11c3b682ccf6bc0da262d7b2a55aceea813e682" + } } diff --git a/node_modules/@indiekit/frontend/README.md b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/README.md similarity index 100% rename from node_modules/@indiekit/frontend/README.md rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/README.md diff --git a/node_modules/@indiekit/frontend/assets/app-icon-any.svg b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/assets/app-icon-any.svg similarity index 100% rename from node_modules/@indiekit/frontend/assets/app-icon-any.svg rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/assets/app-icon-any.svg diff --git a/node_modules/@indiekit/frontend/assets/app-icon-maskable.svg b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/assets/app-icon-maskable.svg similarity index 100% rename from node_modules/@indiekit/frontend/assets/app-icon-maskable.svg rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/assets/app-icon-maskable.svg diff --git a/node_modules/@indiekit/frontend/assets/icon.svg b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/assets/icon.svg similarity index 100% rename from node_modules/@indiekit/frontend/assets/icon.svg rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/assets/icon.svg diff --git a/node_modules/@indiekit/frontend/assets/not-found.svg b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/assets/not-found.svg similarity index 100% rename from node_modules/@indiekit/frontend/assets/not-found.svg rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/assets/not-found.svg diff --git a/node_modules/@indiekit/frontend/assets/offline.svg b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/assets/offline.svg similarity index 100% rename from node_modules/@indiekit/frontend/assets/offline.svg rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/assets/offline.svg diff --git a/node_modules/@indiekit/frontend/assets/plug-in.svg b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/assets/plug-in.svg similarity index 100% rename from node_modules/@indiekit/frontend/assets/plug-in.svg rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/assets/plug-in.svg diff --git a/node_modules/@indiekit/frontend/components/actions/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/actions/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/actions/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/actions/macro.njk diff --git a/node_modules/@indiekit/frontend/components/actions/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/actions/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/actions/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/actions/styles.css diff --git a/node_modules/@indiekit/frontend/components/actions/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/actions/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/actions/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/actions/template.njk diff --git a/node_modules/@indiekit/frontend/components/add-another/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/add-another/index.js similarity index 100% rename from node_modules/@indiekit/frontend/components/add-another/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/add-another/index.js diff --git a/node_modules/@indiekit/frontend/components/add-another/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/add-another/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/add-another/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/add-another/macro.njk diff --git a/node_modules/@indiekit/frontend/components/add-another/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/add-another/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/add-another/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/add-another/styles.css diff --git a/node_modules/@indiekit/frontend/components/add-another/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/add-another/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/add-another/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/add-another/template.njk diff --git a/node_modules/@indiekit/frontend/components/app/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/app/styles.css similarity index 66% rename from node_modules/@indiekit/frontend/components/app/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/app/styles.css index f14458d9..6a0fd4d0 100644 --- a/node_modules/@indiekit/frontend/components/app/styles.css +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/app/styles.css @@ -1,14 +1,32 @@ .app { background-color: var(--color-offset); + font-family: var(--font-family-sans); + + /* Mobile: stacked column layout */ display: flex; flex-direction: column; - font-family: var(--font-family-sans); justify-content: space-between; + + /* Desktop: sidebar + content grid */ + @media (width >= 48rem) { + display: grid; + grid-template-columns: 15rem 1fr; + grid-template-rows: 1fr auto; + grid-template-areas: + "sidebar main" + "sidebar footer"; + min-block-size: 100dvh; + } } .app--minimalui { --container-max-inline-size: 36rem; + /* Always flex column for minimalui, never grid */ + display: flex; + flex-direction: column; + justify-content: space-between; + & .header, & .footer { border: 0; diff --git a/node_modules/@indiekit/frontend/components/authorize/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/authorize/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/authorize/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/authorize/macro.njk diff --git a/node_modules/@indiekit/frontend/components/authorize/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/authorize/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/authorize/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/authorize/styles.css diff --git a/node_modules/@indiekit/frontend/components/authorize/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/authorize/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/authorize/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/authorize/template.njk diff --git a/node_modules/@indiekit/frontend/components/avatar/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/avatar/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/avatar/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/avatar/macro.njk diff --git a/node_modules/@indiekit/frontend/components/avatar/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/avatar/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/avatar/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/avatar/styles.css diff --git a/node_modules/@indiekit/frontend/components/avatar/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/avatar/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/avatar/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/avatar/template.njk diff --git a/node_modules/@indiekit/frontend/components/back-link/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/back-link/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/back-link/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/back-link/macro.njk diff --git a/node_modules/@indiekit/frontend/components/back-link/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/back-link/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/back-link/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/back-link/styles.css diff --git a/node_modules/@indiekit/frontend/components/back-link/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/back-link/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/back-link/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/back-link/template.njk diff --git a/node_modules/@indiekit/frontend/components/badge/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/badge/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/badge/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/badge/macro.njk diff --git a/node_modules/@indiekit/frontend/components/badge/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/badge/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/badge/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/badge/styles.css diff --git a/node_modules/@indiekit/frontend/components/badge/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/badge/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/badge/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/badge/template.njk diff --git a/node_modules/@indiekit/frontend/components/bookmarklet/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/bookmarklet/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/bookmarklet/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/bookmarklet/macro.njk diff --git a/node_modules/@indiekit/frontend/components/bookmarklet/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/bookmarklet/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/bookmarklet/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/bookmarklet/styles.css diff --git a/node_modules/@indiekit/frontend/components/bookmarklet/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/bookmarklet/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/bookmarklet/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/bookmarklet/template.njk diff --git a/node_modules/@indiekit/frontend/components/button/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/button/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/button/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/button/macro.njk diff --git a/node_modules/@indiekit/frontend/components/button/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/button/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/button/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/button/styles.css diff --git a/node_modules/@indiekit/frontend/components/button/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/button/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/button/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/button/template.njk diff --git a/node_modules/@indiekit/frontend/components/card-grid/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/card-grid/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/card-grid/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/card-grid/macro.njk diff --git a/node_modules/@indiekit/frontend/components/card-grid/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/card-grid/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/card-grid/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/card-grid/styles.css diff --git a/node_modules/@indiekit/frontend/components/card-grid/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/card-grid/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/card-grid/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/card-grid/template.njk diff --git a/node_modules/@indiekit/frontend/components/card/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/card/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/card/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/card/macro.njk diff --git a/node_modules/@indiekit/frontend/components/card/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/card/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/card/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/card/styles.css diff --git a/node_modules/@indiekit/frontend/components/card/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/card/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/card/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/card/template.njk diff --git a/node_modules/@indiekit/frontend/components/character-count/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/character-count/index.js similarity index 100% rename from node_modules/@indiekit/frontend/components/character-count/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/character-count/index.js diff --git a/node_modules/@indiekit/frontend/components/character-count/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/character-count/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/character-count/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/character-count/macro.njk diff --git a/node_modules/@indiekit/frontend/components/character-count/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/character-count/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/character-count/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/character-count/styles.css diff --git a/node_modules/@indiekit/frontend/components/character-count/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/character-count/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/character-count/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/character-count/template.njk diff --git a/node_modules/@indiekit/frontend/components/checkboxes/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/checkboxes/index.js similarity index 100% rename from node_modules/@indiekit/frontend/components/checkboxes/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/checkboxes/index.js diff --git a/node_modules/@indiekit/frontend/components/checkboxes/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/checkboxes/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/checkboxes/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/checkboxes/macro.njk diff --git a/node_modules/@indiekit/frontend/components/checkboxes/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/checkboxes/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/checkboxes/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/checkboxes/styles.css diff --git a/node_modules/@indiekit/frontend/components/checkboxes/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/checkboxes/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/checkboxes/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/checkboxes/template.njk diff --git a/node_modules/@indiekit/frontend/components/details/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/details/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/details/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/details/macro.njk diff --git a/node_modules/@indiekit/frontend/components/details/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/details/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/details/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/details/styles.css diff --git a/node_modules/@indiekit/frontend/components/details/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/details/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/details/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/details/template.njk diff --git a/node_modules/@indiekit/frontend/components/error-message/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-message/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/error-message/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-message/macro.njk diff --git a/node_modules/@indiekit/frontend/components/error-message/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-message/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/error-message/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-message/styles.css diff --git a/node_modules/@indiekit/frontend/components/error-message/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-message/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/error-message/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-message/template.njk diff --git a/node_modules/@indiekit/frontend/components/error-summary/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-summary/index.js similarity index 100% rename from node_modules/@indiekit/frontend/components/error-summary/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-summary/index.js diff --git a/node_modules/@indiekit/frontend/components/error-summary/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-summary/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/error-summary/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-summary/macro.njk diff --git a/node_modules/@indiekit/frontend/components/error-summary/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-summary/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/error-summary/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-summary/styles.css diff --git a/node_modules/@indiekit/frontend/components/error-summary/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-summary/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/error-summary/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/error-summary/template.njk diff --git a/node_modules/@indiekit/frontend/components/event-duration/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/event-duration/index.js similarity index 100% rename from node_modules/@indiekit/frontend/components/event-duration/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/event-duration/index.js diff --git a/node_modules/@indiekit/frontend/components/event-duration/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/event-duration/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/event-duration/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/event-duration/styles.css diff --git a/node_modules/@indiekit/frontend/components/field/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/field/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/field/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/field/macro.njk diff --git a/node_modules/@indiekit/frontend/components/field/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/field/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/field/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/field/styles.css diff --git a/node_modules/@indiekit/frontend/components/field/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/field/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/field/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/field/template.njk diff --git a/node_modules/@indiekit/frontend/components/fieldset/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/fieldset/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/fieldset/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/fieldset/macro.njk diff --git a/node_modules/@indiekit/frontend/components/fieldset/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/fieldset/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/fieldset/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/fieldset/styles.css diff --git a/node_modules/@indiekit/frontend/components/fieldset/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/fieldset/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/fieldset/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/fieldset/template.njk diff --git a/node_modules/@indiekit/frontend/components/file-input/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/file-input/index.js similarity index 100% rename from node_modules/@indiekit/frontend/components/file-input/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/file-input/index.js diff --git a/node_modules/@indiekit/frontend/components/file-input/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/file-input/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/file-input/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/file-input/macro.njk diff --git a/node_modules/@indiekit/frontend/components/file-input/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/file-input/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/file-input/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/file-input/styles.css diff --git a/node_modules/@indiekit/frontend/components/file-input/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/file-input/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/file-input/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/file-input/template.njk diff --git a/node_modules/@indiekit/frontend/components/footer/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/footer/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/footer/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/footer/macro.njk diff --git a/node_modules/@indiekit/frontend/components/footer/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/footer/styles.css similarity index 77% rename from node_modules/@indiekit/frontend/components/footer/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/footer/styles.css index d8eafdeb..c3757da5 100644 --- a/node_modules/@indiekit/frontend/components/footer/styles.css +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/footer/styles.css @@ -10,6 +10,13 @@ justify-content: flex-end; } +/* Hide footer on desktop when sidebar is active */ +@media (width >= 48rem) { + body:not(.app--minimalui) .footer { + display: none; + } +} + .footer__container { align-items: center; display: flex; diff --git a/node_modules/@indiekit/frontend/components/footer/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/footer/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/footer/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/footer/template.njk diff --git a/node_modules/@indiekit/frontend/components/geo-input/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/geo-input/index.js similarity index 100% rename from node_modules/@indiekit/frontend/components/geo-input/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/geo-input/index.js diff --git a/node_modules/@indiekit/frontend/components/geo-input/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/geo-input/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/geo-input/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/geo-input/macro.njk diff --git a/node_modules/@indiekit/frontend/components/geo-input/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/geo-input/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/geo-input/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/geo-input/styles.css diff --git a/node_modules/@indiekit/frontend/components/geo-input/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/geo-input/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/geo-input/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/geo-input/template.njk diff --git a/node_modules/@indiekit/frontend/components/header/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/header/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/header/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/header/macro.njk diff --git a/node_modules/@indiekit/frontend/components/header/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/header/styles.css similarity index 71% rename from node_modules/@indiekit/frontend/components/header/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/header/styles.css index 5f9b97dd..edc70810 100644 --- a/node_modules/@indiekit/frontend/components/header/styles.css +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/header/styles.css @@ -18,6 +18,13 @@ display: flex; } +/* Hide header on desktop (sidebar replaces it) */ +@media (width >= 48rem) { + body:not(.app--minimalui) .header { + display: none; + } +} + body:not(.app--minimalui) .header { backdrop-filter: blur(16px); background-color: var(--header-background-color); @@ -58,3 +65,24 @@ body:not(.app--minimalui) .header { padding: var(--space-s); } } + +.header__hamburger { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--space-s); + color: var(--color-on-offset); + background: none; + border: 0; + border-radius: var(--border-radius-small); + cursor: pointer; + + &:hover { + color: var(--color-primary-on-background); + background-color: var(--color-offset-variant); + } + + @media (width >= 48rem) { + display: none; + } +} diff --git a/node_modules/@indiekit/frontend/components/header/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/header/template.njk similarity index 51% rename from node_modules/@indiekit/frontend/components/header/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/header/template.njk index d8bdcce3..bfe687f9 100644 --- a/node_modules/@indiekit/frontend/components/header/template.njk +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/header/template.njk @@ -6,6 +6,10 @@ {{ opts.name }} - {{ navigation(opts.navigation) | indent(4) if opts.navigation.items.length > 0 }} + - \ No newline at end of file + diff --git a/node_modules/@indiekit/frontend/components/heading/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/heading/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/heading/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/heading/macro.njk diff --git a/node_modules/@indiekit/frontend/components/heading/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/heading/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/heading/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/heading/styles.css diff --git a/node_modules/@indiekit/frontend/components/heading/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/heading/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/heading/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/heading/template.njk diff --git a/node_modules/@indiekit/frontend/components/hint/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/hint/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/hint/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/hint/macro.njk diff --git a/node_modules/@indiekit/frontend/components/hint/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/hint/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/hint/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/hint/styles.css diff --git a/node_modules/@indiekit/frontend/components/hint/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/hint/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/hint/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/hint/template.njk diff --git a/node_modules/@indiekit/frontend/components/icon/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/icon/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/icon/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/icon/styles.css diff --git a/node_modules/@indiekit/frontend/components/input/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/input/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/input/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/input/macro.njk diff --git a/node_modules/@indiekit/frontend/components/input/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/input/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/input/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/input/styles.css diff --git a/node_modules/@indiekit/frontend/components/input/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/input/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/input/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/input/template.njk diff --git a/node_modules/@indiekit/frontend/components/label/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/label/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/label/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/label/macro.njk diff --git a/node_modules/@indiekit/frontend/components/label/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/label/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/label/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/label/styles.css diff --git a/node_modules/@indiekit/frontend/components/label/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/label/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/label/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/label/template.njk diff --git a/node_modules/@indiekit/frontend/components/logo/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/logo/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/logo/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/logo/macro.njk diff --git a/node_modules/@indiekit/frontend/components/logo/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/logo/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/logo/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/logo/styles.css diff --git a/node_modules/@indiekit/frontend/components/logo/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/logo/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/logo/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/logo/template.njk diff --git a/node_modules/@indiekit/frontend/components/main/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/main/styles.css similarity index 93% rename from node_modules/@indiekit/frontend/components/main/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/main/styles.css index 16d9bef7..5c01f0f5 100644 --- a/node_modules/@indiekit/frontend/components/main/styles.css +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/main/styles.css @@ -3,6 +3,7 @@ background-color: var(--color-background); color: var(--color-on-background); flex: 1; + grid-area: main; } .main__container { diff --git a/node_modules/@indiekit/frontend/components/mention/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/mention/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/mention/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/mention/macro.njk diff --git a/node_modules/@indiekit/frontend/components/mention/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/mention/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/mention/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/mention/styles.css diff --git a/node_modules/@indiekit/frontend/components/mention/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/mention/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/mention/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/mention/template.njk diff --git a/node_modules/@indiekit/frontend/components/navigation/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/navigation/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/navigation/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/navigation/macro.njk diff --git a/node_modules/@indiekit/frontend/components/navigation/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/navigation/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/navigation/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/navigation/styles.css diff --git a/node_modules/@indiekit/frontend/components/navigation/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/navigation/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/navigation/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/navigation/template.njk diff --git a/node_modules/@indiekit/frontend/components/notification-banner/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/notification-banner/index.js similarity index 100% rename from node_modules/@indiekit/frontend/components/notification-banner/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/notification-banner/index.js diff --git a/node_modules/@indiekit/frontend/components/notification-banner/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/notification-banner/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/notification-banner/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/notification-banner/macro.njk diff --git a/node_modules/@indiekit/frontend/components/notification-banner/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/notification-banner/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/notification-banner/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/notification-banner/styles.css diff --git a/node_modules/@indiekit/frontend/components/notification-banner/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/notification-banner/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/notification-banner/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/notification-banner/template.njk diff --git a/node_modules/@indiekit/frontend/components/pagination/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/pagination/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/pagination/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/pagination/macro.njk diff --git a/node_modules/@indiekit/frontend/components/pagination/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/pagination/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/pagination/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/pagination/styles.css diff --git a/node_modules/@indiekit/frontend/components/pagination/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/pagination/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/pagination/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/pagination/template.njk diff --git a/node_modules/@indiekit/frontend/components/progress/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/progress/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/progress/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/progress/macro.njk diff --git a/node_modules/@indiekit/frontend/components/progress/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/progress/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/progress/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/progress/styles.css diff --git a/node_modules/@indiekit/frontend/components/progress/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/progress/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/progress/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/progress/template.njk diff --git a/node_modules/@indiekit/frontend/components/prose/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/prose/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/prose/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/prose/macro.njk diff --git a/node_modules/@indiekit/frontend/components/prose/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/prose/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/prose/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/prose/styles.css diff --git a/node_modules/@indiekit/frontend/components/prose/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/prose/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/prose/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/prose/template.njk diff --git a/node_modules/@indiekit/frontend/components/radios/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/radios/index.js similarity index 100% rename from node_modules/@indiekit/frontend/components/radios/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/radios/index.js diff --git a/node_modules/@indiekit/frontend/components/radios/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/radios/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/radios/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/radios/macro.njk diff --git a/node_modules/@indiekit/frontend/components/radios/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/radios/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/radios/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/radios/styles.css diff --git a/node_modules/@indiekit/frontend/components/radios/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/radios/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/radios/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/radios/template.njk diff --git a/node_modules/@indiekit/frontend/components/section/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/section/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/section/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/section/macro.njk diff --git a/node_modules/@indiekit/frontend/components/section/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/section/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/section/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/section/styles.css diff --git a/node_modules/@indiekit/frontend/components/section/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/section/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/section/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/section/template.njk diff --git a/node_modules/@indiekit/frontend/components/select/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/select/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/select/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/select/macro.njk diff --git a/node_modules/@indiekit/frontend/components/select/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/select/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/select/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/select/styles.css diff --git a/node_modules/@indiekit/frontend/components/select/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/select/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/select/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/select/template.njk diff --git a/node_modules/@indiekit/frontend/components/share-preview/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/share-preview/index.js similarity index 100% rename from node_modules/@indiekit/frontend/components/share-preview/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/share-preview/index.js diff --git a/node_modules/@indiekit/frontend/components/share-preview/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/share-preview/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/share-preview/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/share-preview/macro.njk diff --git a/node_modules/@indiekit/frontend/components/share-preview/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/share-preview/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/share-preview/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/share-preview/styles.css diff --git a/node_modules/@indiekit/frontend/components/share-preview/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/share-preview/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/share-preview/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/share-preview/template.njk diff --git a/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/sidebar/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/sidebar/index.js new file mode 100644 index 00000000..074dfde0 --- /dev/null +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/sidebar/index.js @@ -0,0 +1,57 @@ +export const SidebarComponent = class extends HTMLElement { + connectedCallback() { + this.closeButton = this.querySelector(".sidebar__close"); + this.backdrop = document.querySelector(".sidebar-backdrop"); + this.hamburger = document.querySelector(".header__hamburger"); + + if (this.closeButton) { + this.closeButton.addEventListener("click", () => this.close()); + } + + if (this.backdrop) { + this.backdrop.addEventListener("click", () => this.close()); + } + + if (this.hamburger) { + this.hamburger.addEventListener("click", () => this.open()); + } + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && this.classList.contains("sidebar--open")) { + this.close(); + } + }); + + this.mediaQuery = window.matchMedia("(width >= 48rem)"); + this.mediaQuery.addEventListener("change", (event) => { + if (event.matches && this.classList.contains("sidebar--open")) { + this.close(); + } + }); + } + + open() { + this.classList.add("sidebar--open"); + + if (this.backdrop) { + this.backdrop.classList.add("sidebar-backdrop--visible"); + } + + const firstLink = this.querySelector(".sidebar__list-item a"); + if (firstLink) { + firstLink.focus(); + } + } + + close() { + this.classList.remove("sidebar--open"); + + if (this.backdrop) { + this.backdrop.classList.remove("sidebar-backdrop--visible"); + } + + if (this.hamburger) { + this.hamburger.focus(); + } + } +}; diff --git a/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/sidebar/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/sidebar/macro.njk new file mode 100644 index 00000000..f322f299 --- /dev/null +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/sidebar/macro.njk @@ -0,0 +1,3 @@ +{% macro sidebar(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} diff --git a/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/sidebar/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/sidebar/styles.css new file mode 100644 index 00000000..1c40ebb3 --- /dev/null +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/sidebar/styles.css @@ -0,0 +1,181 @@ +/* Sidebar */ +.sidebar { + --sidebar-inline-size: 15rem; + --anchor-color: var(--color-on-offset); + --anchor-color-hover: var(--color-primary-on-background); + --anchor-decoration-color: transparent; + + background-color: var(--color-offset); + color: var(--color-on-offset); + display: flex; + flex-direction: column; + font: var(--font-caption); +} + +/* Desktop: persistent sidebar */ +@media (width >= 48rem) { + .sidebar { + grid-area: sidebar; + position: sticky; + inset-block-start: 0; + block-size: 100dvh; + border-inline-end: var(--border-hairline); + z-index: 10; + } +} + +/* Mobile: off-canvas drawer */ +@media (width < 48rem) { + .sidebar { + position: fixed; + inset-block: 0; + inset-inline-start: 0; + inline-size: min(var(--sidebar-inline-size), 80vw); + z-index: 200; + transform: translateX(-100%); + transition: transform 0.25s ease; + box-shadow: none; + } + + .sidebar--open { + transform: translateX(0); + box-shadow: 4px 0 16px var(--color-shadow); + } +} + +/* Header: site name + close button */ +.sidebar__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-m) var(--space-l); + border-block-end: var(--border-hairline); + flex-shrink: 0; +} + +.sidebar__title { + font-weight: 600; + font-size: var(--font-size-m); + line-height: var(--line-height-tight); + text-decoration: none; +} + +.sidebar__close { + display: none; + align-items: center; + justify-content: center; + padding: var(--space-xs); + color: var(--color-on-offset); + background: none; + border: 0; + border-radius: var(--border-radius-small); + cursor: pointer; + + &:hover { + color: var(--color-primary-on-background); + background-color: var(--color-offset-variant); + } + + @media (width < 48rem) { + display: inline-flex; + } +} + +/* Nav section: scrollable list */ +.sidebar__nav { + flex: 1; + overflow-y: auto; + padding-block: var(--space-xs); +} + +/* Group sections */ +.sidebar__group { + padding-block-end: var(--space-xs); +} + +.sidebar__group-label { + display: block; + padding: var(--space-xs) var(--space-l); + font-size: var(--font-size-xs); + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--color-outline-variant); + user-select: none; +} + +.sidebar__list { + list-style: none; + margin: 0; + padding: 0; +} + +.sidebar__list-item a { + display: block; + padding: var(--space-xs) var(--space-l); + text-decoration: none; + color: var(--color-on-offset); + border-inline-start: var(--border-width-thickest) solid transparent; + + &:hover { + color: var(--color-primary-on-background); + background-color: var(--color-offset-variant); + } +} + +.sidebar__list-item:has(a[aria-current="true"]) a { + color: var(--color-primary-on-background); + background-color: var(--color-offset-variant); + border-inline-start-color: var(--color-primary-variant); + font-weight: 600; +} + +/* Secondary list in footer */ +.sidebar__list--secondary { + margin: 0; + padding: 0; +} + +/* Footer: compact, not sticky */ +.sidebar__footer { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-s) var(--space-l); + border-block-start: var(--border-hairline); +} + +.sidebar__footer .sidebar__list--secondary { + display: flex; + gap: var(--space-s); +} + +.sidebar__list-item--inline { + display: inline-flex; +} + +.sidebar__list-item--inline a { + padding: 0; + border-inline-start: 0; +} + +.sidebar__logo { + flex-shrink: 0; +} + +/* Backdrop (mobile only) */ +.sidebar-backdrop { + display: none; + position: fixed; + inset: 0; + background-color: hsl(0 0% 0% / 0.4); + z-index: 199; + opacity: 0; + transition: opacity 0.25s ease; +} + +.sidebar-backdrop--visible { + display: block; + opacity: 1; +} diff --git a/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/sidebar/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/sidebar/template.njk new file mode 100644 index 00000000..c2881195 --- /dev/null +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/sidebar/template.njk @@ -0,0 +1,154 @@ +{% from "../logo/macro.njk" import logo with context %} +{% set items = opts.navigation.items %} +{% set renames = { "Reader": "Microsub" } %} + + + + + diff --git a/node_modules/@indiekit/frontend/components/skip-link/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/skip-link/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/skip-link/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/skip-link/macro.njk diff --git a/node_modules/@indiekit/frontend/components/skip-link/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/skip-link/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/skip-link/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/skip-link/styles.css diff --git a/node_modules/@indiekit/frontend/components/skip-link/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/skip-link/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/skip-link/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/skip-link/template.njk diff --git a/node_modules/@indiekit/frontend/components/summary/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/summary/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/summary/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/summary/macro.njk diff --git a/node_modules/@indiekit/frontend/components/summary/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/summary/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/summary/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/summary/styles.css diff --git a/node_modules/@indiekit/frontend/components/summary/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/summary/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/summary/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/summary/template.njk diff --git a/node_modules/@indiekit/frontend/components/tag-input/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag-input/index.js similarity index 100% rename from node_modules/@indiekit/frontend/components/tag-input/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag-input/index.js diff --git a/node_modules/@indiekit/frontend/components/tag-input/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag-input/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/tag-input/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag-input/macro.njk diff --git a/node_modules/@indiekit/frontend/components/tag-input/sanitizer.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag-input/sanitizer.js similarity index 100% rename from node_modules/@indiekit/frontend/components/tag-input/sanitizer.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag-input/sanitizer.js diff --git a/node_modules/@indiekit/frontend/components/tag-input/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag-input/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/tag-input/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag-input/styles.css diff --git a/node_modules/@indiekit/frontend/components/tag-input/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag-input/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/tag-input/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag-input/template.njk diff --git a/node_modules/@indiekit/frontend/components/tag/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/tag/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag/macro.njk diff --git a/node_modules/@indiekit/frontend/components/tag/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/tag/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag/styles.css diff --git a/node_modules/@indiekit/frontend/components/tag/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/tag/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/tag/template.njk diff --git a/node_modules/@indiekit/frontend/components/textarea/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/textarea/index.js similarity index 58% rename from node_modules/@indiekit/frontend/components/textarea/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/textarea/index.js index eee7414f..89d208e4 100644 --- a/node_modules/@indiekit/frontend/components/textarea/index.js +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/textarea/index.js @@ -34,6 +34,37 @@ const getButtonSvg = (name) => { `; }; +/** + * Floating toolbar button definitions + * Maps button name to EasyMDE static action method name + */ +const floatingToolbarButtons = [ + { name: "bold", action: "toggleBold", title: "Bold" }, + { name: "italic", action: "toggleItalic", title: "Italic" }, + { name: "heading", action: "toggleHeadingSmaller", title: "Heading" }, + { name: "code", action: "toggleCodeBlock", title: "Code" }, + { name: "link", action: "drawLink", title: "Link" }, +]; + +/** + * Create an SVG element from a known icon path (safe — no user input) + * @param {string} name - Icon name from paths object + * @returns {SVGElement} SVG element + */ +const createButtonSvgElement = (name) => { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("height", "1em"); + svg.setAttribute("width", "1em"); + svg.setAttribute("viewBox", "0 0 32 32"); + svg.setAttribute("focusable", "false"); + svg.setAttribute("aria-hidden", "true"); + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("fill", "currentColor"); + path.setAttribute("d", paths[name] || ""); + svg.append(path); + return svg; +}; + export const TextareaFieldComponent = class extends HTMLElement { connectedCallback() { this.editor = this.getAttribute("editor"); @@ -139,6 +170,105 @@ export const TextareaFieldComponent = class extends HTMLElement { resizeObserver.observe($editorToolbar); } + + // Floating selection toolbar + this._setupFloatingToolbar(editor); + } + + /** + * Create and manage floating toolbar that appears on text selection + * @param {EasyMDE} editor - EasyMDE instance + */ + _setupFloatingToolbar(editor) { + const cm = editor.codemirror; + + // Create floating toolbar element + const $floating = document.createElement("div"); + $floating.className = "editor-floating-toolbar"; + $floating.setAttribute("role", "toolbar"); + $floating.setAttribute("aria-label", "Formatting"); + + for (const btn of floatingToolbarButtons) { + const button = document.createElement("button"); + button.type = "button"; + button.className = `floating-btn floating-btn--${btn.name}`; + button.title = btn.title; + button.append(createButtonSvgElement(btn.name)); + button.addEventListener("mousedown", (event) => { + // Prevent mousedown from stealing focus/clearing selection + event.preventDefault(); + EasyMDE[btn.action](editor); + }); + $floating.append(button); + } + + this.append($floating); + this._$floatingToolbar = $floating; + + // Track selection changes + cm.on("cursorActivity", () => { + const selection = cm.getSelection(); + if (selection && selection.length > 0) { + this._showFloatingToolbar(cm); + } else { + this._hideFloatingToolbar(); + } + }); + + // Hide on blur + cm.on("blur", () => { + setTimeout(() => this._hideFloatingToolbar(), 150); + }); + + // Reposition on scroll + const $cmScroll = this.querySelector(".CodeMirror-scroll"); + if ($cmScroll) { + $cmScroll.addEventListener("scroll", () => { + if (this._$floatingToolbar.classList.contains("is-visible")) { + const selection = cm.getSelection(); + if (selection && selection.length > 0) { + this._showFloatingToolbar(cm); + } else { + this._hideFloatingToolbar(); + } + } + }); + } + } + + /** + * Position and show the floating toolbar above the current selection + * @param {object} cm - CodeMirror instance + */ + _showFloatingToolbar(cm) { + const $floating = this._$floatingToolbar; + const $cmElement = this.querySelector(".CodeMirror"); + if (!$cmElement) return; + + // Get coordinates of selection start relative to editor + const cursor = cm.getCursor("from"); + const coords = cm.cursorCoords(cursor, "local"); + const cmRect = $cmElement.getBoundingClientRect(); + const containerRect = this.getBoundingClientRect(); + + // Position above the selection line + const top = coords.top + cmRect.top - containerRect.top - 44; + const left = coords.left + cmRect.left - containerRect.left; + + // Clamp within container bounds + const maxLeft = containerRect.width - ($floating.offsetWidth || 200) - 8; + $floating.style.top = `${Math.max(0, top)}px`; + $floating.style.left = `${Math.max(8, Math.min(left, maxLeft))}px`; + $floating.classList.add("is-visible"); + } + + /** + * Hide the floating toolbar + */ + _hideFloatingToolbar() { + if (this._$floatingToolbar) { + this._$floatingToolbar.classList.remove("is-visible"); + } } /** diff --git a/node_modules/@indiekit/frontend/components/textarea/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/textarea/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/textarea/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/textarea/macro.njk diff --git a/node_modules/@indiekit/frontend/components/textarea/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/textarea/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/textarea/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/textarea/styles.css diff --git a/node_modules/@indiekit/frontend/components/textarea/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/textarea/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/textarea/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/textarea/template.njk diff --git a/node_modules/@indiekit/frontend/components/user/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/user/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/user/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/user/macro.njk diff --git a/node_modules/@indiekit/frontend/components/user/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/user/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/user/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/user/styles.css diff --git a/node_modules/@indiekit/frontend/components/user/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/user/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/user/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/user/template.njk diff --git a/node_modules/@indiekit/frontend/components/warning-text/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/warning-text/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/warning-text/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/warning-text/macro.njk diff --git a/node_modules/@indiekit/frontend/components/warning-text/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/warning-text/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/warning-text/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/warning-text/styles.css diff --git a/node_modules/@indiekit/frontend/components/warning-text/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/warning-text/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/warning-text/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/warning-text/template.njk diff --git a/node_modules/@indiekit/frontend/components/widget/macro.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/widget/macro.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/widget/macro.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/widget/macro.njk diff --git a/node_modules/@indiekit/frontend/components/widget/styles.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/widget/styles.css similarity index 100% rename from node_modules/@indiekit/frontend/components/widget/styles.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/widget/styles.css diff --git a/node_modules/@indiekit/frontend/components/widget/template.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/widget/template.njk similarity index 100% rename from node_modules/@indiekit/frontend/components/widget/template.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/components/widget/template.njk diff --git a/node_modules/@indiekit/frontend/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/index.js similarity index 100% rename from node_modules/@indiekit/frontend/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/index.js diff --git a/node_modules/@indiekit/frontend/layouts/default.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/layouts/default.njk similarity index 91% rename from node_modules/@indiekit/frontend/layouts/default.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/layouts/default.njk index c3b5c81e..69b93cad 100644 --- a/node_modules/@indiekit/frontend/layouts/default.njk +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/layouts/default.njk @@ -31,6 +31,7 @@ {% from "textarea/macro.njk" import textarea with context %} {% from "user/macro.njk" import user with context %} {% from "warning-text/macro.njk" import warningText with context %} +{% from "sidebar/macro.njk" import sidebar with context %} {% from "widget/macro.njk" import widget with context %} {% set appClasses = "app" + (" " + appClasses if appClasses) + (" app--minimalui" if minimalui) %} {% set mainClasses = "main" + (" " + mainClasses if mainClasses) %} @@ -65,6 +66,21 @@ {{ skipLink() }} +{% if not minimalui %} +{{ sidebar({ + url: application.url, + name: application.name, + navigation: { + items: application.navigation | rejectattr("secondary") + }, + secondaryNavigation: { + items: application.navigation | selectattr("secondary") + }, + logoSrc: application.url + assetPath | default("/assets") + "/icon.svg" +}) }} + +{% endif %} + {% block header %} {{ header({ url: application.url, diff --git a/node_modules/@indiekit/frontend/layouts/document.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/layouts/document.njk similarity index 100% rename from node_modules/@indiekit/frontend/layouts/document.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/layouts/document.njk diff --git a/node_modules/@indiekit/frontend/layouts/error.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/layouts/error.njk similarity index 100% rename from node_modules/@indiekit/frontend/layouts/error.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/layouts/error.njk diff --git a/node_modules/@indiekit/frontend/layouts/form.njk b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/layouts/form.njk similarity index 100% rename from node_modules/@indiekit/frontend/layouts/form.njk rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/layouts/form.njk diff --git a/node_modules/@indiekit/frontend/lib/esbuild.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/esbuild.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/esbuild.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/esbuild.js diff --git a/node_modules/@indiekit/frontend/lib/filters/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/filters/index.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/filters/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/filters/index.js diff --git a/node_modules/@indiekit/frontend/lib/filters/locale.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/filters/locale.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/filters/locale.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/filters/locale.js diff --git a/node_modules/@indiekit/frontend/lib/filters/string.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/filters/string.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/filters/string.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/filters/string.js diff --git a/node_modules/@indiekit/frontend/lib/filters/url.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/filters/url.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/filters/url.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/filters/url.js diff --git a/node_modules/@indiekit/frontend/lib/globals/attributes.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/attributes.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/globals/attributes.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/attributes.js diff --git a/node_modules/@indiekit/frontend/lib/globals/classes.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/classes.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/globals/classes.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/classes.js diff --git a/node_modules/@indiekit/frontend/lib/globals/error-list.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/error-list.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/globals/error-list.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/error-list.js diff --git a/node_modules/@indiekit/frontend/lib/globals/field-data.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/field-data.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/globals/field-data.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/field-data.js diff --git a/node_modules/@indiekit/frontend/lib/globals/icon.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/icon.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/globals/icon.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/icon.js diff --git a/node_modules/@indiekit/frontend/lib/globals/index.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/index.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/globals/index.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/index.js diff --git a/node_modules/@indiekit/frontend/lib/globals/item-id.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/item-id.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/globals/item-id.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/item-id.js diff --git a/node_modules/@indiekit/frontend/lib/globals/summary-rows.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/summary-rows.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/globals/summary-rows.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/globals/summary-rows.js diff --git a/node_modules/@indiekit/frontend/lib/lightningcss.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/lightningcss.js similarity index 86% rename from node_modules/@indiekit/frontend/lib/lightningcss.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/lightningcss.js index b727af24..cba7ca21 100644 --- a/node_modules/@indiekit/frontend/lib/lightningcss.js +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/lightningcss.js @@ -13,8 +13,10 @@ const require = createRequire(import.meta.url); * @example `@import url("~codemirror/lib/codemirror.css");` */ function resolveModuleFilePath(filePath) { - if (filePath.includes("~")) { - const moduleFilePath = filePath.split("~")[1]; + const modulePathMatch = filePath.match(/(?:^|[\\/])~([^\\/].*)$/); + + if (modulePathMatch?.[1]) { + const moduleFilePath = modulePathMatch[1]; return require.resolve(moduleFilePath); } diff --git a/node_modules/@indiekit/frontend/lib/markdown-it.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/markdown-it.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/markdown-it.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/markdown-it.js diff --git a/node_modules/@indiekit/frontend/lib/nunjucks.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/nunjucks.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/nunjucks.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/nunjucks.js diff --git a/node_modules/@indiekit/frontend/lib/serviceworker.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/serviceworker.js similarity index 78% rename from node_modules/@indiekit/frontend/lib/serviceworker.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/serviceworker.js index 5c5004d4..91f89c43 100644 --- a/node_modules/@indiekit/frontend/lib/serviceworker.js +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/serviceworker.js @@ -151,18 +151,32 @@ self.addEventListener("fetch", (event) => { // NETWORK // Save a copy of page to pages cache clearTimeout(timer); - const copy = responseFromPreloadOrFetch.clone(); - const pagesCache = await caches.open(pagesCacheName); - await pagesCache.put(request, copy); + try { + const copy = responseFromPreloadOrFetch.clone(); + const pagesCache = await caches.open(pagesCacheName); + await pagesCache.put(request, copy); + } catch (cacheError) { + // Cache put failed (e.g., network error response), but continue serving the response + console.error("Failed to cache page:", cacheError); + } return responseFromPreloadOrFetch; } catch (error) { - console.error(error, request); + console.error("Network fetch failed:", error, request.url); // CACHE or OFFLINE PAGE clearTimeout(timer); const responseFromCache = await retrieveFromCache; - return responseFromCache || caches.match("/offline"); + const offlineResponse = await caches.match("/offline"); + return ( + responseFromCache || + offlineResponse || + new Response("Offline", { + status: 503, + statusText: "Service Unavailable", + headers: { "Content-Type": "text/plain" }, + }) + ); } })(), ); @@ -185,15 +199,24 @@ self.addEventListener("fetch", (event) => { // NETWORK // If request is for an image, save a copy to images cache if (/\.(jpe?g|png|gif|svg|webp)/.test(request.url)) { - const copy = responseFromFetch.clone(); - const imagesCache = await caches.open(imageCacheName); - await imagesCache.put(request, copy); + try { + const copy = responseFromFetch.clone(); + const imagesCache = await caches.open(imageCacheName); + await imagesCache.put(request, copy); + } catch (cacheError) { + // Cache put failed (e.g., network error response), but continue serving the response + console.error("Failed to cache image:", cacheError); + } } return responseFromFetch; } } catch (error) { - console.error(error); + console.error( + "Fetch failed for non-HTML resource:", + error, + request.url, + ); // OFFLINE IMAGE if (/\.(jpe?g|png|gif|svg|webp)/.test(request.url)) { @@ -204,6 +227,13 @@ self.addEventListener("fetch", (event) => { }, }); } + + // For other resources, return a network error response + return new Response("Network error", { + status: 503, + statusText: "Service Unavailable", + headers: { "Content-Type": "text/plain" }, + }); } })(), ); diff --git a/node_modules/@indiekit/frontend/lib/sharp.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/sharp.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/sharp.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/sharp.js diff --git a/node_modules/@indiekit/frontend/lib/utils/theme.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/utils/theme.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/utils/theme.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/utils/theme.js diff --git a/node_modules/@indiekit/frontend/lib/utils/wrap-element.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/utils/wrap-element.js similarity index 100% rename from node_modules/@indiekit/frontend/lib/utils/wrap-element.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/lib/utils/wrap-element.js diff --git a/node_modules/@indiekit/frontend/locales/de.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/de.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/de.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/de.json diff --git a/node_modules/@indiekit/frontend/locales/en.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/en.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/en.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/en.json diff --git a/node_modules/@indiekit/frontend/locales/es-419.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/es-419.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/es-419.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/es-419.json diff --git a/node_modules/@indiekit/frontend/locales/es.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/es.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/es.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/es.json diff --git a/node_modules/@indiekit/frontend/locales/fr.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/fr.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/fr.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/fr.json diff --git a/node_modules/@indiekit/frontend/locales/hi.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/hi.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/hi.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/hi.json diff --git a/node_modules/@indiekit/frontend/locales/id.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/id.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/id.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/id.json diff --git a/node_modules/@indiekit/frontend/locales/it.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/it.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/it.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/it.json diff --git a/node_modules/@indiekit/frontend/locales/nl.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/nl.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/nl.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/nl.json diff --git a/node_modules/@indiekit/frontend/locales/pl.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/pl.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/pl.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/pl.json diff --git a/node_modules/@indiekit/frontend/locales/pt-BR.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/pt-BR.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/pt-BR.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/pt-BR.json diff --git a/node_modules/@indiekit/frontend/locales/pt.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/pt.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/pt.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/pt.json diff --git a/node_modules/@indiekit/frontend/locales/sr.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/sr.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/sr.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/sr.json diff --git a/node_modules/@indiekit/frontend/locales/sv.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/sv.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/sv.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/sv.json diff --git a/node_modules/@indiekit/frontend/locales/zh-Hans-CN.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/zh-Hans-CN.json similarity index 100% rename from node_modules/@indiekit/frontend/locales/zh-Hans-CN.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/locales/zh-Hans-CN.json diff --git a/node_modules/@indiekit/frontend/package.json b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/package.json similarity index 69% rename from node_modules/@indiekit/frontend/package.json rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/package.json index ba2f4f23..d6491fd5 100644 --- a/node_modules/@indiekit/frontend/package.json +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/package.json @@ -1,16 +1,21 @@ { - "name": "@indiekit/frontend", - "version": "1.0.0-beta.25", - "description": "Frontend components for Indiekit", + "name": "@rmdes/indiekit-frontend", + "version": "1.0.0-beta.27", + "description": "Frontend components for Indiekit (fork with floating toolbar)", "keywords": [ "express", "indieweb", "micropub" ], - "author": { - "name": "Paul Robert Lloyd", - "url": "https://paulrobertlloyd.com" - }, + "contributors": [ + { + "name": "Paul Robert Lloyd", + "url": "https://paulrobertlloyd.com" + }, + { + "name": "rmdes" + } + ], "license": "MIT", "engines": { "node": ">=20" @@ -28,12 +33,11 @@ "index.js" ], "bugs": { - "url": "https://github.com/getindiekit/indiekit/issues" + "url": "https://github.com/rmdes/indiekit-frontend/issues" }, "repository": { "type": "git", - "url": "https://github.com/getindiekit/indiekit.git", - "directory": "packages/frontend" + "url": "https://github.com/rmdes/indiekit-frontend.git" }, "dependencies": { "@accessible-components/tag-input": "^0.2.0", @@ -56,6 +60,5 @@ }, "publishConfig": { "access": "public" - }, - "gitHead": "e11c3b682ccf6bc0da262d7b2a55aceea813e682" + } } diff --git a/node_modules/@indiekit/frontend/scripts/app.js b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/scripts/app.js similarity index 92% rename from node_modules/@indiekit/frontend/scripts/app.js rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/scripts/app.js index 5d4fd551..a769fc93 100644 --- a/node_modules/@indiekit/frontend/scripts/app.js +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/scripts/app.js @@ -7,6 +7,7 @@ import { GeoInputFieldComponent } from "../components/geo-input/index.js"; import { NotificationBannerComponent } from "../components/notification-banner/index.js"; import { RadiosFieldComponent } from "../components/radios/index.js"; import { SharePreviewComponent } from "../components/share-preview/index.js"; +import { SidebarComponent } from "../components/sidebar/index.js"; import { TagInputFieldComponent } from "../components/tag-input/index.js"; import { TextareaFieldComponent } from "../components/textarea/index.js"; @@ -19,5 +20,6 @@ customElements.define("geo-input-field", GeoInputFieldComponent); customElements.define("notification-banner", NotificationBannerComponent); customElements.define("radios-field", RadiosFieldComponent); customElements.define("share-preview", SharePreviewComponent); +customElements.define("sidebar-nav", SidebarComponent); customElements.define("tag-input-field", TagInputFieldComponent); customElements.define("textarea-field", TextareaFieldComponent); diff --git a/node_modules/@indiekit/frontend/styles/app.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/app.css similarity index 98% rename from node_modules/@indiekit/frontend/styles/app.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/app.css index 48db83f8..2d21b353 100644 --- a/node_modules/@indiekit/frontend/styles/app.css +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/app.css @@ -54,6 +54,7 @@ @import url("../components/section/styles.css"); @import url("../components/select/styles.css"); @import url("../components/share-preview/styles.css"); +@import url("../components/sidebar/styles.css"); @import url("../components/skip-link/styles.css"); @import url("../components/summary/styles.css"); @import url("../components/tag/styles.css"); diff --git a/node_modules/@indiekit/frontend/styles/base/embedded.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/embedded.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/base/embedded.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/embedded.css diff --git a/node_modules/@indiekit/frontend/styles/base/forms.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/forms.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/base/forms.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/forms.css diff --git a/node_modules/@indiekit/frontend/styles/base/grouping.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/grouping.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/base/grouping.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/grouping.css diff --git a/node_modules/@indiekit/frontend/styles/base/interactive.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/interactive.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/base/interactive.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/interactive.css diff --git a/node_modules/@indiekit/frontend/styles/base/sections.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/sections.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/base/sections.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/sections.css diff --git a/node_modules/@indiekit/frontend/styles/base/tables.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/tables.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/base/tables.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/tables.css diff --git a/node_modules/@indiekit/frontend/styles/base/text.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/text.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/base/text.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/base/text.css diff --git a/node_modules/@indiekit/frontend/styles/config/custom-properties.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/config/custom-properties.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/config/custom-properties.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/config/custom-properties.css diff --git a/node_modules/@indiekit/frontend/styles/scopes/flow.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/scopes/flow.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/scopes/flow.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/scopes/flow.css diff --git a/node_modules/@indiekit/frontend/styles/utilities/container.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/utilities/container.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/utilities/container.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/utilities/container.css diff --git a/node_modules/@indiekit/frontend/styles/utilities/visually-hidden.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/utilities/visually-hidden.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/utilities/visually-hidden.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/utilities/visually-hidden.css diff --git a/node_modules/@indiekit/frontend/styles/vendor/codemirror.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/vendor/codemirror.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/vendor/codemirror.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/vendor/codemirror.css diff --git a/node_modules/@indiekit/frontend/styles/vendor/easy-markdown-editor.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/vendor/easy-markdown-editor.css similarity index 70% rename from node_modules/@indiekit/frontend/styles/vendor/easy-markdown-editor.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/vendor/easy-markdown-editor.css index 79162258..491a48e0 100644 --- a/node_modules/@indiekit/frontend/styles/vendor/easy-markdown-editor.css +++ b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/vendor/easy-markdown-editor.css @@ -1,3 +1,9 @@ +/* Positioning context for floating toolbar */ +textarea-field[editor] { + display: block; + position: relative; +} + .EasyMDEContainer { --toolbar-button-size: 44px; --toolbar-padding: var(--space-2xs); @@ -150,3 +156,64 @@ position: fixed; z-index: 1009; } + +/* Floating selection toolbar */ +.editor-floating-toolbar { + background: var(--color-on-background, #1a1a2e); + border-radius: var(--border-radius-small, 6px); + box-shadow: 0 4px 12px rgb(0 0 0 / 25%); + display: flex; + gap: 2px; + opacity: 0; + padding: 4px; + pointer-events: none; + position: absolute; + transform: translateY(4px); + transition: + opacity 0.15s ease, + transform 0.15s ease; + z-index: 1020; + + &.is-visible { + opacity: 1; + pointer-events: auto; + transform: translateY(0); + } + + /* Arrow pointing down */ + &::after { + border-color: var(--color-on-background, #1a1a2e) transparent transparent; + border-style: solid; + border-width: 6px 6px 0; + content: ""; + inset-block-start: 100%; + inset-inline-start: 16px; + position: absolute; + } +} + +.floating-btn { + background: none; + block-size: 32px; + border: 0; + border-radius: 4px; + color: var(--color-background, #fff); + cursor: pointer; + display: flex; + inline-size: 32px; + place-content: center; + place-items: center; + + & svg { + block-size: 14px; + inline-size: 14px; + } + + &:hover { + background: rgb(255 255 255 / 15%); + } + + &:active { + background: rgb(255 255 255 / 25%); + } +} diff --git a/node_modules/@indiekit/frontend/styles/vendor/markdown-it-prism.css b/node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/vendor/markdown-it-prism.css similarity index 100% rename from node_modules/@indiekit/frontend/styles/vendor/markdown-it-prism.css rename to node_modules/@indiekit/endpoint-posts/node_modules/@indiekit/frontend/styles/vendor/markdown-it-prism.css diff --git a/node_modules/@indiekit/endpoint-share/LICENSE b/node_modules/@indiekit/endpoint-share/LICENSE deleted file mode 100644 index a460806a..00000000 --- a/node_modules/@indiekit/endpoint-share/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Paul Robert Lloyd - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/node_modules/@indiekit/endpoint-share/lib/controllers/share.js b/node_modules/@indiekit/endpoint-share/lib/controllers/share.js deleted file mode 100644 index 7428700c..00000000 --- a/node_modules/@indiekit/endpoint-share/lib/controllers/share.js +++ /dev/null @@ -1,69 +0,0 @@ -import { IndiekitError } from "@indiekit/error"; -import { validationResult } from "express-validator"; - -export const shareController = { - /** - * View share page - * @type {import("express").RequestHandler} - */ - get(request, response) { - const { content, name, url, success } = request.query; - - response.render("share", { - title: response.locals.__("share.title"), - data: { content, name, url }, - success, - minimalui: request.params.path === "bookmarklet", - }); - }, - - /** - * Post share content - * @type {import("express").RequestHandler} - */ - async post(request, response) { - const { application } = request.app.locals; - const data = request.body || {}; - data["bookmark-of"] = data.url || data["bookmark-of"]; - - const errors = validationResult(request); - if (!errors.isEmpty()) { - return response.status(422).render("share", { - title: response.locals.__("share.title"), - data, - errors: errors.mapped(), - minimalui: request.params.path === "bookmarklet", - }); - } - - try { - const micropubResponse = await fetch(application.micropubEndpoint, { - method: "POST", - headers: { - accept: "application/json", - "content-type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams(data).toString(), - }); - - if (!micropubResponse.ok) { - throw await IndiekitError.fromFetch(micropubResponse); - } - - /** @type {object} */ - const body = await micropubResponse.json(); - - const message = encodeURIComponent(body.success_description); - - response.redirect(`?success=${message}`); - } catch (error) { - response.status(error.status || 500); - response.render("share", { - title: response.locals.__("share.title"), - data, - error, - minimalui: request.params.path === "bookmarklet", - }); - } - }, -}; diff --git a/node_modules/@indiekit/endpoint-share/lib/middleware/validation.js b/node_modules/@indiekit/endpoint-share/lib/middleware/validation.js deleted file mode 100644 index 5c9b93cb..00000000 --- a/node_modules/@indiekit/endpoint-share/lib/middleware/validation.js +++ /dev/null @@ -1,13 +0,0 @@ -import { check } from "express-validator"; - -export const validate = [ - check("name") - .notEmpty() - .withMessage((value, { req, path }) => req.__(`share.error.${path}.empty`)), - check("bookmark-of") - .exists() - .isURL() - .withMessage((value, { req, path }) => - req.__(`share.error.${path}.empty`, "https://example.org"), - ), -]; diff --git a/node_modules/@indiekit/endpoint-share/locales/en.json b/node_modules/@indiekit/endpoint-share/locales/en.json deleted file mode 100644 index 12293f21..00000000 --- a/node_modules/@indiekit/endpoint-share/locales/en.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "share": { - "error": { - "bookmark-of": { - "empty": "Enter a web address like %s" - }, - "name": { - "empty": "Enter a title" - } - }, - "title": "Share", - "bookmark-of": { - "label": "URL" - }, - "name": { - "label": "Title" - }, - "content": { - "label": "Content" - }, - "submit": "Publish" - }, - "status": { - "bookmarklet": { - "title": "Share bookmarklet", - "label": "Share page", - "guidance": "Drag this link to your bookmarks bar: %s" - } - } -} diff --git a/node_modules/@indiekit/endpoint-share/package.json b/node_modules/@indiekit/endpoint-share/package.json deleted file mode 100644 index 505df6ef..00000000 --- a/node_modules/@indiekit/endpoint-share/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "@indiekit/endpoint-share", - "version": "1.0.0-beta.25", - "description": "Share endpoint for Indiekit. Provides a simple interface for bookmarking websites and publishing them on your website.", - "keywords": [ - "indiekit", - "indiekit-plugin", - "indieweb", - "micropub" - ], - "homepage": "https://getindiekit.com", - "author": { - "name": "Paul Robert Lloyd", - "url": "https://paulrobertlloyd.com" - }, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "type": "module", - "main": "index.js", - "files": [ - "assets", - "includes", - "lib", - "locales", - "views", - "index.js" - ], - "bugs": { - "url": "https://github.com/getindiekit/indiekit/issues" - }, - "repository": { - "type": "git", - "url": "https://github.com/getindiekit/indiekit.git", - "directory": "packages/endpoint-share" - }, - "dependencies": { - "@indiekit/error": "^1.0.0-beta.25", - "express": "^5.0.0", - "express-validator": "^7.0.0" - }, - "publishConfig": { - "access": "public" - }, - "gitHead": "e11c3b682ccf6bc0da262d7b2a55aceea813e682" -} diff --git a/node_modules/@indiekit/endpoint-share/views/share.njk b/node_modules/@indiekit/endpoint-share/views/share.njk deleted file mode 100644 index 2497064f..00000000 --- a/node_modules/@indiekit/endpoint-share/views/share.njk +++ /dev/null @@ -1,70 +0,0 @@ -{% extends "form.njk" %} - -{% from "share-preview/macro.njk" import sharePreview %} - -{% block fieldset %} - {{ sharePreview({ - title: { - for: "name", - value: name, - placeholder: __("share.name.label") - }, - text: { - for: "content", - value: content, - placeholder: __("share.content.label") - }, - url: { - for: "bookmark-of", - value: bookmarkOf or url, - placeholder: __("share.bookmark-of.label") - }, - date: "now", - locale: application.locale - }) }} - - {{ input({ - name: "bookmark-of", - type: "url", - value: fieldData("bookmark-of").value or data.url, - label: __("share.bookmark-of.label"), - attributes: { - "placeholder": "https://" - }, - errorMessage: fieldData("bookmark-of").errorMessage - }) }} - - {{ input({ - name: "name", - value: fieldData("name").value, - label: __("share.name.label"), - errorMessage: fieldData("name").errorMessage - }) }} - - {{ textarea({ - name: "content", - value: content, - optional: true, - label: __("share.content.label") - }) }} - - {{ input({ - name: "access_token", - type: "hidden", - value: token - }) }} - - {{ input({ - name: "h", - type: "hidden", - value: "entry" - }) }} -{% endblock %} - -{% block buttons %} - {{ button({ - text: __("share.submit") - }) if token else warningText({ - text: __("scope.none.label") - }) }} -{% endblock %} \ No newline at end of file diff --git a/node_modules/@indiekit/endpoint-syndicate/LICENSE b/node_modules/@indiekit/endpoint-syndicate/LICENSE deleted file mode 100644 index a460806a..00000000 --- a/node_modules/@indiekit/endpoint-syndicate/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Paul Robert Lloyd - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/node_modules/@indiekit/endpoint-syndicate/README.md b/node_modules/@indiekit/endpoint-syndicate/README.md deleted file mode 100644 index 4fc5578c..00000000 --- a/node_modules/@indiekit/endpoint-syndicate/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# @indiekit/endpoint-syndicate - -Syndication endpoint for Indiekit. Provides an endpoint you can ping to check that recently published posts have been posted to configured syndication targets. - -## Installation - -`npm install @indiekit/endpoint-syndicate` - -> [!NOTE] -> This package is installed alongside `@indiekit/indiekit` - -## Usage - -To customise the behaviour of this plug-in, add `@indiekit/endpoint-syndicate` to your configuration, specifying options as required: - -```jsonc -{ - "@indiekit/endpoint-syndicate": { - "mountPath": "/syndikat", // de-DE - }, -} -``` - -## Options - -| Option | Type | Description | -| :---------- | :------- | :------------------------------------------------------------------ | -| `mountPath` | `string` | Path to syndication endpoint. _Optional_, defaults to `/syndicate`. | - -## Supported endpoint queries - -- URL to syndicate: `/syndicate?source_url=https%3A%2F%2Fwebsite.example%2Fposts%2F1` - -## Authorization - -Authorization is needed to update posts with any syndicated URLs. This can be done in a few different ways: - -### Query string - -Include your server’s access token as the `token` query: - -```http -POST /syndicate?token=[ACCESS_TOKEN] HTTP/1.1 -Host: indiekit.website.example -Accept: application/json -``` - -You can find an access token on your server’s status page. - -### Form body - -Include a value for `access_token` in your form submission: - -```http -POST /syndicate HTTP/1.1 -Host: indiekit.website.example -Content-type: application/x-www-form-urlencoded -Accept: application/json - -access_token=[ACCESS_TOKEN] -``` - -You can find an access token on your server’s status page. - -### Using a webhook secret (Netlify only) - -If you are using [Netlify](https://www.netlify.com) to host your website, you can send a notification to the syndication endpoint once a deployment has been completed. - -First, create an environment variable for your Indiekit server called `WEBHOOK_SECRET` and give it a secret, hard-to-guess value. - -Then on Netlify, in your site’s ‘Build & Deploy’ settings, add an [outgoing webhook](https://docs.netlify.com/site-deploys/notifications/#outgoing-webhooks) with the following values: - -- **Event to listen for:** ‘Deploy succeeded’ -- **URL to notify:** `[YOUR_INDIEKIT_URL]/syndicate` -- **JWS secret token:** The same value you used for `WEBHOOK_SECRET` diff --git a/node_modules/@indiekit/endpoint-syndicate/assets/icon.svg b/node_modules/@indiekit/endpoint-syndicate/assets/icon.svg deleted file mode 100644 index bc0be99c..00000000 --- a/node_modules/@indiekit/endpoint-syndicate/assets/icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/node_modules/@indiekit/endpoint-syndicate/lib/controllers/syndicate.js b/node_modules/@indiekit/endpoint-syndicate/lib/controllers/syndicate.js deleted file mode 100644 index fbb20f6d..00000000 --- a/node_modules/@indiekit/endpoint-syndicate/lib/controllers/syndicate.js +++ /dev/null @@ -1,98 +0,0 @@ -import { IndiekitError } from "@indiekit/error"; - -import { findBearerToken } from "../token.js"; -import { getPostData, syndicateToTargets } from "../utils.js"; - -export const syndicateController = { - async post(request, response, next) { - try { - const { application, publication } = request.app.locals; - const bearerToken = findBearerToken(request); - const sourceUrl = - request.query.source_url || request.body?.syndication?.source_url; - const redirectUri = - request.query.redirect_uri || request.body?.syndication?.redirect_uri; - - const postsCollection = application?.collections?.get("posts"); - if (!postsCollection) { - throw IndiekitError.notImplemented( - response.locals.__("NotImplementedError.database"), - ); - } - - // Get syndication targets - const { syndicationTargets } = publication; - if (syndicationTargets.length === 0) { - return response.json({ - success: "OK", - success_description: "No syndication targets have been configured", - }); - } - - // Get post data - const postData = await getPostData(postsCollection, sourceUrl); - - if (!postData && sourceUrl) { - return response.json({ - success: "OK", - success_description: `No post record available for ${sourceUrl}`, - }); - } - - if (!postData) { - return response.json({ - success: "OK", - success_description: "No posts awaiting syndication", - }); - } - - // Syndicate to targets - const { failedTargets, syndicatedUrls } = await syndicateToTargets( - publication, - postData.properties, - ); - - // Update post with syndicated URL(s) and remaining syndication target(s) - const micropubResponse = await fetch(application.micropubEndpoint, { - method: "POST", - headers: { - accept: "application/json", - authorization: `Bearer ${bearerToken}`, - "content-type": "application/json", - }, - body: JSON.stringify({ - action: "update", - url: postData.properties.url, - ...(!failedTargets && { delete: ["mp-syndicate-to"] }), - replace: { - ...(failedTargets && { "mp-syndicate-to": failedTargets }), - ...(syndicatedUrls && { syndication: syndicatedUrls }), - }, - }), - }); - - if (!micropubResponse.ok) { - throw await IndiekitError.fromFetch(micropubResponse); - } - - /** @type {object} */ - const body = await micropubResponse.json(); - - // Include failed syndication targets in ‘success’ response - if (failedTargets) { - body.success_description += - ". The following target(s) did not return a URL: " + - failedTargets.join(" "); - } - - if (redirectUri && redirectUri.startsWith("/")) { - const message = encodeURIComponent(body.success_description); - return response.redirect(`${redirectUri}?success=${message}`); - } - - return response.status(micropubResponse.status).json(body); - } catch (error) { - next(error); - } - }, -}; diff --git a/node_modules/@indiekit/endpoint-syndicate/lib/utils.js b/node_modules/@indiekit/endpoint-syndicate/lib/utils.js deleted file mode 100644 index b7e1deb2..00000000 --- a/node_modules/@indiekit/endpoint-syndicate/lib/utils.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Get post data - * @param {object} postsCollection - Posts database collection - * @param {string} url - URL of existing post (optional) - * @returns {Promise} Post data for given URL else recently published post - */ -export const getPostData = async (postsCollection, url) => { - let postData = {}; - - if (url) { - // Get item in database which matching URL - postData = await postsCollection.findOne({ - "properties.url": url, - }); - } else { - // Get published posts awaiting syndication and return first item - const items = await postsCollection - .find({ - "properties.mp-syndicate-to": { - $exists: true, - }, - "properties.syndication": { - $exists: false, - }, - "properties.post-status": { - $ne: "draft", - }, - }) - // eslint-disable-next-line unicorn/no-array-sort - .sort({ "properties.published": -1 }) - .limit(1) - .toArray(); - postData = items[0]; - } - - return postData; -}; - -/** - * Check if target already returned a syndication URL - * @param {Array} syndicatedUrls - Syndication URLs - * @param {string} syndicateTo - Syndication target - * @returns {boolean} Target returned a syndication URL - */ -export const hasSyndicationUrl = (syndicatedUrls, syndicateTo) => { - return syndicatedUrls.some((url) => { - const { origin } = new URL(url); - return syndicateTo.includes(origin); - }); -}; - -/** - * Get syndication target for syndication URL - * @param {Array} syndicationTargets - Publication syndication targets - * @param {string} syndicateTo - Syndication URL - * @returns {object|undefined} Publication syndication target - */ -export const getSyndicationTarget = (syndicationTargets, syndicateTo) => { - return syndicationTargets.find((target) => { - if (!target?.info?.uid) { - return; - } - - const targetOrigin = new URL(target.info.uid).origin; - const syndicateToOrigin = new URL(syndicateTo).origin; - return targetOrigin === syndicateToOrigin; - }); -}; - -/** - * Syndicate URLs to configured syndication targets - * @param {object} publication - Publication configuration - * @param {object} properties - JF2 properties - * @returns {Promise} Syndication target - */ -export const syndicateToTargets = async (publication, properties) => { - const { syndicationTargets } = publication; - const syndicateTo = properties["mp-syndicate-to"]; - const syndicateToUrls = Array.isArray ? syndicateTo : [syndicateTo]; - const syndicatedUrls = properties.syndication || []; - const failedTargets = []; - - for (const url of syndicateToUrls) { - const target = getSyndicationTarget(syndicationTargets, url); - const alreadySyndicated = hasSyndicationUrl(syndicatedUrls, url); - - if (target && !alreadySyndicated) { - try { - const syndicatedUrl = await target.syndicate(properties, publication); - - if (syndicatedUrl) { - // Add syndicated URL to list of syndicated URLs - syndicatedUrls.push(syndicatedUrl); - } else { - // Add failed syndication target to list of failed targets - failedTargets.push(target.info.uid); - } - } catch (error) { - // Add failed syndication target to list of failed targets - failedTargets.push(target.info.uid); - console.error(error.message); - } - } - } - - return { - ...(failedTargets.length > 0 && { failedTargets }), - syndicatedUrls, - }; -}; diff --git a/node_modules/@indiekit/endpoint-syndicate/package.json b/node_modules/@indiekit/endpoint-syndicate/package.json deleted file mode 100644 index caf15d69..00000000 --- a/node_modules/@indiekit/endpoint-syndicate/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "@indiekit/endpoint-syndicate", - "version": "1.0.0-beta.25", - "description": "Syndication endpoint for Indiekit. Provides an endpoint you can ping to check that recently published posts have been posted to configured syndication targets.", - "keywords": [ - "indiekit", - "indiekit-plugin", - "indieweb", - "syndication" - ], - "homepage": "https://getindiekit.com", - "author": { - "name": "Paul Robert Lloyd", - "url": "https://paulrobertlloyd.com" - }, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "type": "module", - "main": "index.js", - "files": [ - "assets", - "lib", - "index.js" - ], - "bugs": { - "url": "https://github.com/getindiekit/indiekit/issues" - }, - "repository": { - "type": "git", - "url": "https://github.com/getindiekit/indiekit.git", - "directory": "packages/endpoint-syndicate" - }, - "dependencies": { - "@indiekit/error": "^1.0.0-beta.25", - "express": "^5.0.0", - "jsonwebtoken": "^9.0.0" - }, - "publishConfig": { - "access": "public" - }, - "gitHead": "e11c3b682ccf6bc0da262d7b2a55aceea813e682" -} diff --git a/node_modules/@indiekit/frontend/LICENSE b/node_modules/@indiekit/frontend/LICENSE deleted file mode 100644 index a460806a..00000000 --- a/node_modules/@indiekit/frontend/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Paul Robert Lloyd - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/node_modules/@indiekit/endpoint-files/README.md b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/README.md similarity index 100% rename from node_modules/@indiekit/endpoint-files/README.md rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/README.md diff --git a/node_modules/@indiekit/endpoint-files/assets/icon.svg b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/assets/icon.svg similarity index 100% rename from node_modules/@indiekit/endpoint-files/assets/icon.svg rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/assets/icon.svg diff --git a/node_modules/@indiekit/endpoint-files/includes/@indiekit-endpoint-files-widget.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/includes/@indiekit-endpoint-files-widget.njk similarity index 100% rename from node_modules/@indiekit/endpoint-files/includes/@indiekit-endpoint-files-widget.njk rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/includes/@indiekit-endpoint-files-widget.njk diff --git a/node_modules/@indiekit/endpoint-files/index.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/index.js similarity index 100% rename from node_modules/@indiekit/endpoint-files/index.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/index.js diff --git a/node_modules/@indiekit/endpoint-files/lib/controllers/delete.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/controllers/delete.js similarity index 100% rename from node_modules/@indiekit/endpoint-files/lib/controllers/delete.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/controllers/delete.js diff --git a/node_modules/@indiekit/endpoint-files/lib/controllers/file.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/controllers/file.js similarity index 100% rename from node_modules/@indiekit/endpoint-files/lib/controllers/file.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/controllers/file.js diff --git a/node_modules/@indiekit/endpoint-files/lib/controllers/files.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/controllers/files.js similarity index 100% rename from node_modules/@indiekit/endpoint-files/lib/controllers/files.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/controllers/files.js diff --git a/node_modules/@indiekit/endpoint-files/lib/controllers/form.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/controllers/form.js similarity index 55% rename from node_modules/@indiekit/endpoint-files/lib/controllers/form.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/controllers/form.js index 0373838b..3d438a42 100644 --- a/node_modules/@indiekit/endpoint-files/lib/controllers/form.js +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/controllers/form.js @@ -45,25 +45,39 @@ export const formController = { throw new Error(response.locals.__("files.error.file.empty")); } - const { data, name } = request.files.file; - const formData = new FormData(); - formData.append("file", new Blob([data]), name); + // Normalize to array (supports both single and multi-file upload) + const uploadedFiles = Array.isArray(request.files.file) + ? request.files.file + : [request.files.file]; - try { - const mediaResponse = await endpoint.post( - mediaEndpoint, - accessToken, - formData, - ); - const message = encodeURIComponent(mediaResponse.success_description); + const results = []; + for (const file of uploadedFiles) { + const formData = new FormData(); + formData.append("file", new Blob([file.data]), file.name); - response.redirect(`${request.baseUrl}?success=${message}`); - } catch (error) { - response.status(error.status || 500); - response.render("file-form", { - title: response.locals.__("files.upload.title"), - error, - }); + try { + await endpoint.post(mediaEndpoint, accessToken, formData); + results.push({ name: file.name, success: true }); + } catch { + results.push({ name: file.name, success: false }); + } } + + const successCount = results.filter((r) => r.success).length; + const failCount = results.filter((r) => !r.success).length; + + let message; + if (failCount === 0) { + message = + successCount === 1 + ? `${successCount} file uploaded` + : `${successCount} files uploaded`; + } else { + message = `${successCount} uploaded, ${failCount} failed`; + } + + response.redirect( + `${request.baseUrl}?success=${encodeURIComponent(message)}`, + ); }, }; diff --git a/node_modules/@indiekit/endpoint-files/lib/endpoint.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/endpoint.js similarity index 100% rename from node_modules/@indiekit/endpoint-files/lib/endpoint.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/endpoint.js diff --git a/node_modules/@indiekit/endpoint-files/lib/middleware/file-data.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/middleware/file-data.js similarity index 100% rename from node_modules/@indiekit/endpoint-files/lib/middleware/file-data.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/middleware/file-data.js diff --git a/node_modules/@indiekit/endpoint-files/lib/middleware/validation.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/middleware/validation.js similarity index 100% rename from node_modules/@indiekit/endpoint-files/lib/middleware/validation.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/middleware/validation.js diff --git a/node_modules/@indiekit/endpoint-files/lib/utils.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/utils.js similarity index 100% rename from node_modules/@indiekit/endpoint-files/lib/utils.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/lib/utils.js diff --git a/node_modules/@indiekit/endpoint-files/locales/de.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/de.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/de.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/de.json diff --git a/node_modules/@indiekit/endpoint-files/locales/en.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/en.json similarity index 79% rename from node_modules/@indiekit/endpoint-files/locales/en.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/en.json index b7a31f32..15b31b7f 100644 --- a/node_modules/@indiekit/endpoint-files/locales/en.json +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/en.json @@ -9,7 +9,7 @@ "action": "Delete file", "title": "Are you sure you want to delete this file?", "note": "It may still be accessible or recoverable from your content store (%s).", - "submit": "I’m sure – delete this file", + "submit": "I'm sure – delete this file", "cancel": "No – return to file" }, "file": { @@ -23,7 +23,10 @@ "title": "Files", "upload": { "action": "Upload file", - "title": "Upload a new file" + "title": "Upload files", + "dropText": "Drag files here or", + "browse": "Browse files", + "submitMultiple": "Upload files" }, "form": { "submit": "Upload", diff --git a/node_modules/@indiekit/endpoint-files/locales/es-419.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/es-419.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/es-419.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/es-419.json diff --git a/node_modules/@indiekit/endpoint-files/locales/es.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/es.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/es.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/es.json diff --git a/node_modules/@indiekit/endpoint-files/locales/fr.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/fr.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/fr.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/fr.json diff --git a/node_modules/@indiekit/endpoint-files/locales/hi.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/hi.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/hi.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/hi.json diff --git a/node_modules/@indiekit/endpoint-files/locales/id.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/id.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/id.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/id.json diff --git a/node_modules/@indiekit/endpoint-files/locales/it.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/it.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/it.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/it.json diff --git a/node_modules/@indiekit/endpoint-files/locales/nl.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/nl.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/nl.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/nl.json diff --git a/node_modules/@indiekit/endpoint-files/locales/pl.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/pl.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/pl.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/pl.json diff --git a/node_modules/@indiekit/endpoint-files/locales/pt-BR.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/pt-BR.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/pt-BR.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/pt-BR.json diff --git a/node_modules/@indiekit/endpoint-files/locales/pt.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/pt.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/pt.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/pt.json diff --git a/node_modules/@indiekit/endpoint-files/locales/sr.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/sr.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/sr.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/sr.json diff --git a/node_modules/@indiekit/endpoint-files/locales/sv.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/sv.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/sv.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/sv.json diff --git a/node_modules/@indiekit/endpoint-files/locales/zh-Hans-CN.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/zh-Hans-CN.json similarity index 100% rename from node_modules/@indiekit/endpoint-files/locales/zh-Hans-CN.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales/zh-Hans-CN.json diff --git a/node_modules/@indiekit/endpoint-files/package.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/package.json similarity index 56% rename from node_modules/@indiekit/endpoint-files/package.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/package.json index dc8323e4..3a020d99 100644 --- a/node_modules/@indiekit/endpoint-files/package.json +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/package.json @@ -1,17 +1,22 @@ { - "name": "@indiekit/endpoint-files", - "version": "1.0.0-beta.25", - "description": "File management endpoint for Indiekit. View files uploaded to your media endpoint and upload new files to it.", + "name": "@rmdes/indiekit-endpoint-files", + "version": "1.0.0", + "description": "File management endpoint for Indiekit with multi-file upload support.", "keywords": [ "indiekit", "indiekit-plugin", "indieweb" ], - "homepage": "https://getindiekit.com", + "homepage": "https://github.com/rmdes/indiekit-endpoint-files", "author": { "name": "Paul Robert Lloyd", "url": "https://paulrobertlloyd.com" }, + "contributors": [ + { + "name": "rmdes" + } + ], "license": "MIT", "engines": { "node": ">=20" @@ -27,12 +32,11 @@ "index.js" ], "bugs": { - "url": "https://github.com/getindiekit/indiekit/issues" + "url": "https://github.com/rmdes/indiekit-endpoint-files/issues" }, "repository": { "type": "git", - "url": "https://github.com/getindiekit/indiekit.git", - "directory": "packages/endpoint-files" + "url": "https://github.com/rmdes/indiekit-endpoint-files.git" }, "dependencies": { "@indiekit/error": "^1.0.0-beta.25", @@ -41,6 +45,5 @@ }, "publishConfig": { "access": "public" - }, - "gitHead": "e11c3b682ccf6bc0da262d7b2a55aceea813e682" + } } diff --git a/node_modules/@indiekit/endpoint-files/views/file-delete.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/views/file-delete.njk similarity index 100% rename from node_modules/@indiekit/endpoint-files/views/file-delete.njk rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/views/file-delete.njk diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/views/file-form.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/views/file-form.njk new file mode 100644 index 00000000..f82b96a3 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/views/file-form.njk @@ -0,0 +1,182 @@ +{% extends "form.njk" %} + +{% block form %} + {{ heading({ text: title, parent: parent }) }} + +
+ + {# Drop zone #} +
+

{{ __("files.upload.dropText") }}

+ +
+ + {# File list #} +
    + +
+ + {# Upload button #} + +
+ + {# No-JS fallback #} + + + +{% endblock %} diff --git a/node_modules/@indiekit/endpoint-files/views/file.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/views/file.njk similarity index 100% rename from node_modules/@indiekit/endpoint-files/views/file.njk rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/views/file.njk diff --git a/node_modules/@indiekit/endpoint-files/views/files.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/views/files.njk similarity index 100% rename from node_modules/@indiekit/endpoint-files/views/files.njk rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/views/files.njk diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/README.md b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/README.md new file mode 100644 index 00000000..c629b876 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/README.md @@ -0,0 +1,201 @@ +# @rmdes/indiekit-endpoint-micropub + +Micropub endpoint for Indiekit with custom type-based post discovery and pre-syndication markup support. Enables publishing content to your website using the [Micropub protocol](https://micropub.spec.indieweb.org/). + +## Fork Notice + +This is a fork of `@indiekit/endpoint-micropub` with two custom features: + +### 1. Pre-Syndication Markup Support + +Services like [IndieNews](https://news.indieweb.org/) require a `u-syndication` link in your HTML **before** they receive the syndication webmention. The upstream Micropub endpoint strips all `mp-*` properties (including `mp-syndicate-to`) before passing data to the preset's `postTemplate()`. + +This fork preserves `mp-syndicate-to` so that: +1. The property reaches the preset's `postTemplate()` +2. The preset can include it in frontmatter (as `mpSyndicateTo` in Eleventy) +3. The theme can render the `u-syndication` link +4. IndieNews (and similar services) can find the link when parsing the webmention + +**Technical change** in `lib/utils.js`: + +```javascript +// mp- properties to preserve for the template (needed for pre-syndication markup) +const preserveMpProperties = ["mp-syndicate-to"]; + +for (let key in templateProperties) { + if (key.startsWith("mp-") && !preserveMpProperties.includes(key)) { + delete templateProperties[key]; + } + // ... +} +``` + +### 2. Type-Based Post Type Discovery + +The external `@paulrobertlloyd/mf2tojf2` library only preserves standard microformat properties during mf2→JF2 conversion. Custom discovery properties were being stripped, making it impossible for custom post type plugins to trigger type detection. + +This fork adds a type-based discovery mechanism in `lib/post-type-discovery.js`: + +```javascript +// If post has a custom type (h value) that matches a configured post type +// This allows plugins to use h: "page" or similar for type-based discovery +if (properties.type && properties.type !== "entry" && postTypes[properties.type]) { + return properties.type; +} +``` + +This enables custom post type plugins like `@rmdes/indiekit-post-type-page` to work correctly. + +## Installation + +```bash +npm install @rmdes/indiekit-endpoint-micropub +``` + +### Using npm overrides (recommended) + +Add to your `package.json`: + +```json +{ + "overrides": { + "@indiekit/endpoint-micropub": "npm:@rmdes/indiekit-endpoint-micropub@^1.0.0-beta.28" + } +} +``` + +This replaces the upstream package with this fork without changing your plugin configuration. + +### Direct installation + +```javascript +import MicropubEndpoint from "@rmdes/indiekit-endpoint-micropub"; + +export default { + plugins: [ + new MicropubEndpoint({ + mountPath: "/micropub" // Optional, defaults to /micropub + }) + ] +}; +``` + +## Configuration + +### Options + +| Option | Type | Description | +| :---------- | :------- | :------------------------------------------------------------------------ | +| `mountPath` | `string` | Path to listen to Micropub requests. Optional, defaults to `/micropub`. | + +## Supported Endpoints + +### POST /micropub (Action Endpoint) + +Create, update, delete, or undelete posts using the Micropub protocol. + +**Actions:** +- `create` - Create new post (requires `create` or `post` scope) +- `update` - Update existing post (requires `update` scope) +- `delete` - Delete post (requires `delete` scope) +- `undelete` - Restore deleted post (requires `create` scope) + +**Update operations:** +- `replace` - Replace entire property value +- `add` - Add value to existing property +- `delete` - Delete property or specific values + +### GET /micropub (Query Endpoint) + +Query published posts and configuration. + +**Supported queries:** + +| Query | Description | Example | +|-------|-------------|---------| +| `config` | Full configuration | `/micropub?q=config` | +| `media-endpoint` | Media endpoint URL | `/micropub?q=media-endpoint` | +| `syndicate-to` | Available syndication targets | `/micropub?q=syndicate-to` | +| `post-types` | Supported post types | `/micropub?q=post-types` | +| `category` | Publication categories | `/micropub?q=category` | +| `channel` | Publication channels | `/micropub?q=channel` | +| `source` | Published posts (paginated) | `/micropub?q=source` | +| `source&url=URL` | Single post by URL | `/micropub?q=source&url=https://example.com/post` | + +**Pagination parameters:** +- `filter` - Filter results by string match +- `limit` - Maximum number of results +- `offset` - Skip first N results +- `after` - Cursor for next page +- `before` - Cursor for previous page + +Example: `/micropub?q=source&filter=web&limit=10&offset=10` + +## Features + +### Media Uploads + +Supports file uploads via multipart/form-data for `photo`, `video`, and `audio` properties. Files are uploaded to the media endpoint before post creation. + +### Post Type Discovery + +Implements the [Post Type Discovery](https://ptd.spec.indieweb.org/) algorithm with custom type-based detection: + +1. Event type (`type: "event"`) +2. **Custom h type** (fork feature - matches configured post type names) +3. Standard discovery properties (`rsvp`, `repost-of`, `like-of`, `in-reply-to`, `video`, `photo`) +4. Custom discovery properties (from post type config) +5. Collection (populated `children` array) +6. Article (`name` + `content`) +7. Note (default fallback) + +### Content Normalization + +Automatically converts between Markdown and HTML: +- Plaintext only → generates HTML via markdown-it +- HTML only → generates plaintext via Turndown +- Markdown conversion uses typographer and smart quotes + +### Soft Deletes + +Deleted posts are not removed from the database. They store `_deletedProperties` for restoration via the `undelete` action. + +## Requirements + +This endpoint requires: +- MongoDB database for storing post metadata +- IndieAuth authentication endpoint (`@indiekit/endpoint-auth` or `@rmdes/indiekit-endpoint-auth`) +- Media endpoint (`@indiekit/endpoint-media`) +- At least one post type plugin +- At least one preset plugin (Jekyll, Hugo, Eleventy, etc.) +- At least one store plugin (GitHub, GitLab, Gitea) + +## Related Plugins + +### Works With + +- **Post types:** `@indiekit/post-type-*`, `@rmdes/indiekit-post-type-page` +- **Syndicators:** `@rmdes/indiekit-syndicator-bluesky`, `@rmdes/indiekit-syndicator-mastodon`, `@rmdes/indiekit-syndicator-indienews` +- **Presets:** `@rmdes/indiekit-preset-eleventy`, `@indiekit/preset-jekyll`, `@indiekit/preset-hugo` +- **Stores:** `@indiekit/store-github`, `@indiekit/store-gitlab`, `@indiekit/store-gitea` + +### Optional + +- `@indiekit/endpoint-posts` or `@rmdes/indiekit-endpoint-posts` - Web UI for post management +- `@indiekit/endpoint-syndicate` or `@rmdes/indiekit-endpoint-syndicate` - Manual syndication UI + +## Documentation + +See [CLAUDE.md](./CLAUDE.md) for complete technical reference, architecture details, and integration guidance. + +## Debugging + +Enable debug output: + +```bash +DEBUG=indiekit:endpoint-micropub:* npm start +``` + +## License + +MIT - Original work by [Paul Robert Lloyd](https://paulrobertlloyd.com), custom features by [Ricardo Mendes](https://rmendes.net). diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/assets/icon.svg b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/assets/icon.svg new file mode 100644 index 00000000..5f3464b8 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/assets/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/index.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/index.js new file mode 100644 index 00000000..97c153ff --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/index.js @@ -0,0 +1,33 @@ +import express from "express"; + +import { actionController } from "./lib/controllers/action.js"; +import { queryController } from "./lib/controllers/query.js"; + +const defaults = { mountPath: "/micropub" }; +const router = express.Router(); + +export default class MicropubEndpoint { + name = "Micropub endpoint"; + + constructor(options = {}) { + this.options = { ...defaults, ...options }; + this.mountPath = this.options.mountPath; + } + + get routes() { + router.get("/", queryController); + router.post("/", actionController); + + return router; + } + + init(Indiekit) { + Indiekit.addCollection("posts"); + Indiekit.addEndpoint(this); + + // Only mount if micropub endpoint not already configured + if (!Indiekit.config.application.micropubEndpoint) { + Indiekit.config.application.micropubEndpoint = this.mountPath; + } + } +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/config.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/config.js new file mode 100644 index 00000000..6183ca1f --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/config.js @@ -0,0 +1,82 @@ +/** + * Return queryable publication configuration + * @param {object} application - Application configuration + * @param {object} publication - Publication configuration + * @returns {object} Queryable configuration + */ +export const getConfig = (application, publication) => { + const { mediaEndpoint, url } = application; + const { categories, channels, postTypes, syndicationTargets } = publication; + + // Supported queries + const q = [ + "category", + "channel", + "config", + "media-endpoint", + "post-types", + "source", + "syndicate-to", + ]; + + // Ensure syndication targets use absolute URLs + const syndicateTo = syndicationTargets.map((target) => target.info); + for (const info of syndicateTo) { + if (info.service && info.service.photo) { + info.service.photo = new URL(info.service.photo, url).href; + } + } + + return { + categories, + channels: Object.entries(channels).map(([uid, channel]) => ({ + uid, + name: channel.name, + })), + "media-endpoint": mediaEndpoint, + "post-types": Object.values(postTypes).map((postType) => ({ + type: postType.type, + name: postType.name, + h: postType.h, + properties: postType.properties, + "required-properties": postType["required-properties"], + })), + "syndicate-to": syndicateTo, + q, + }; +}; + +/** + * Query config value + * @param {Array} property - Property to query + * @param {object} options - List options (filter, limit, offset) + * @param {string} [options.filter] - Value to filter items by + * @param {number} [options.limit] - Limit of items to return + * @param {number} [options.offset] - Offset to start limit of items + * @returns {Array} Updated config property + */ +export const queryConfig = (property, options) => { + const { filter, limit } = options; + + if (!Array.isArray(property)) { + return property; + } + + let properties = property || []; + + if (filter) { + properties = properties.filter((item) => { + item = JSON.stringify(item); + item = item.toLowerCase(); + return item.includes(filter); + }); + } + + if (limit) { + const offset = options.offset || 0; + properties = properties.slice(offset, offset + limit); + properties.length = Math.min(properties.length, limit); + } + + return properties; +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/controllers/action.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/controllers/action.js new file mode 100644 index 00000000..c8e2fa1f --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/controllers/action.js @@ -0,0 +1,141 @@ +import { IndiekitError } from "@indiekit/error"; + +import { formEncodedToJf2, mf2ToJf2 } from "../jf2.js"; +import { uploadMedia } from "../media.js"; +import { postContent } from "../post-content.js"; +import { postData } from "../post-data.js"; +import { checkScope } from "../scope.js"; + +/** + * Perform requested post action + * @type {import("express").RequestHandler} + */ +export const actionController = async (request, response, next) => { + const { app, body, files, query, session } = request; + const action = query.action || body?.action || "create"; + const url = query.url || body?.url; + const { application, publication } = app.locals; + + try { + // Check provided scope + const { scope, token } = session; + const hasScope = checkScope(scope, action); + if (!hasScope) { + throw IndiekitError.insufficientScope( + response.locals.__("ForbiddenError.insufficientScope"), + { scope: action }, + ); + } + + // Toggle draft mode if `draft` scope + const draftMode = hasScope === "draft"; + + // Check for URL if not creating a new post + if (action !== "create" && !url) { + throw IndiekitError.badRequest( + response.locals.__("BadRequestError.missingParameter", "url"), + ); + } + + let data; + let jf2; + let content; + switch (action) { + case "create": { + // Create and normalise JF2 data + jf2 = request.is("json") + ? await mf2ToJf2(body, publication.enrichPostData) + : await formEncodedToJf2(body, publication.enrichPostData); + + // Attach files + jf2 = files + ? await uploadMedia(application.mediaEndpoint, token, jf2, files) + : jf2; + + data = await postData.create(application, publication, jf2, draftMode); + content = await postContent.create(publication, data); + break; + } + + case "update": { + // Check for update operations + if (!(body.replace || body.add || body.remove)) { + throw IndiekitError.badRequest( + response.locals.__( + "BadRequestError.missingProperty", + "replace, add or remove operations", + ), + ); + } + + data = await postData.update(application, publication, url, body); + + if (!data) { + content = { + status: 200, + location: url, + json: { + success: "update", + success_description: `Post at ${url} not updated as no properties changed`, + }, + }; + break; + } + + // Draft mode: Only update posts that have `draft` post status + if (draftMode && data.properties["post-status"] !== "draft") { + throw IndiekitError.insufficientScope( + response.locals.__("ForbiddenError.insufficientScope"), + { scope: action }, + ); + } + + content = await postContent.update(publication, data, url); + break; + } + + case "delete": { + data = await postData.delete(application, publication, url); + content = await postContent.delete(publication, data); + break; + } + + case "undelete": { + data = await postData.undelete( + application, + publication, + url, + draftMode, + ); + content = await postContent.undelete(publication, data); + break; + } + + default: + } + + response + .status(content.status) + .location(content.location) + .json(content.json); + } catch (error) { + let nextError = error; + + // Hoist not found error to controller to localise response + if (error.name === "NotFoundError") { + nextError = IndiekitError.notFound( + response.locals.__("NotFoundError.record", error.message), + ); + } + + // Hoist unsupported post type error to controller to localise response + if (error.name === "NotImplementedError") { + nextError = IndiekitError.notImplemented( + response.locals.__("NotImplementedError.postType", error.message), + { uri: "https://getindiekit.com/configuration/post-types" }, + ); + } + + return next(nextError); + } +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js new file mode 100644 index 00000000..176966db --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js @@ -0,0 +1,117 @@ +import { IndiekitError } from "@indiekit/error"; +import { getCursor } from "@indiekit/util"; + +import { getConfig, queryConfig } from "../config.js"; +import { getMf2Properties, jf2ToMf2 } from "../mf2.js"; + +/** + * Query published posts + * @type {import("express").RequestHandler} + */ +export const queryController = async (request, response, next) => { + const { application, publication } = request.app.locals; + const postsCollection = application?.collections?.get("posts"); + + try { + const config = getConfig(application, publication); + const limit = Number(request.query.limit) || 0; + const offset = Number(request.query.offset) || 0; + let { after, before, filter, properties, q, url } = request.query; + + if (!q) { + throw IndiekitError.badRequest( + response.locals.__("BadRequestError.missingParameter", "q"), + ); + } + + // `category` param is used to query `categories` configuration property + q = q === "category" ? "categories" : String(q); + + // `channel` param is used to query `channels` configuration property + q = q === "channel" ? "channels" : String(q); + + switch (q) { + case "config": { + response.json(config); + + break; + } + + case "source": { + if (url) { + // Return mf2 for a given URL (optionally filtered by properties) + let postData; + + if (postsCollection) { + postData = await postsCollection.findOne({ + "properties.url": url, + }); + } + + if (!postData) { + throw IndiekitError.badRequest( + response.locals.__("BadRequestError.missingResource", "post"), + ); + } + + const mf2 = jf2ToMf2(postData); + response.json(getMf2Properties(mf2, properties)); + } else { + // Return mf2 for published posts + let cursor = { + items: [], + hasNext: false, + hasPrev: false, + }; + + if (postsCollection) { + cursor = await getCursor(postsCollection, after, before, limit); + } + + const items = []; + for (let item of cursor.items) { + if (item.properties) { + items.push(jf2ToMf2(item)); + } else { + /** + * @todo Consider better way to handle item with no properties + * - notify user and remove item from database? + * - notify user and don’t delete item from database? + * - fail silently? + */ + console.warn(`Item ignored because it has no properties`, item); + } + } + + response.json({ + items, + paging: { + ...(cursor.hasNext && { after: cursor.lastItem }), + ...(cursor.hasPrev && { before: cursor.firstItem }), + }, + }); + } + + break; + } + + default: { + // Query configuration value (can be filtered, limited and offset) + if (config[q]) { + response.json({ + [q]: queryConfig(config[q], { filter, limit, offset }), + }); + } else { + throw IndiekitError.notImplemented( + response.locals.__("NotImplementedError.query", { + key: "q", + value: q, + }), + ); + } + } + } + } catch (error) { + next(error); + } +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/jf2.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/jf2.js new file mode 100644 index 00000000..65521db9 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/jf2.js @@ -0,0 +1,318 @@ +import { excerpt, getDate, md5, slugify } from "@indiekit/util"; +import { + fetchReferences, + mf2tojf2, + mf2tojf2referenced, +} from "@paulrobertlloyd/mf2tojf2"; + +import { markdownToHtml, htmlToMarkdown } from "./markdown.js"; +import { reservedProperties } from "./reserved-properties.js"; +import { decodeQueryParameter, relativeMediaPath, toArray } from "./utils.js"; + +/** + * Create JF2 object from form-encoded request + * @param {object} body - Form-encoded request body + * @param {boolean} [requestReferences] - Request data for any referenced URLs + * @returns {Promise} Micropub action + */ +export const formEncodedToJf2 = async (body, requestReferences) => { + const jf2 = { + type: body.h || "entry", + }; + + for (const key in body) { + if (Object.prototype.hasOwnProperty.call(body, key)) { + // Delete reserved properties + const isReservedProperty = reservedProperties.includes(key); + if (isReservedProperty) { + delete body[key]; + continue; + } + + // Add decoded string value to JF2 object + jf2[key] = decodeQueryParameter(body[key]); + } + } + + if (requestReferences) { + return fetchReferences(jf2); + } + + return jf2; +}; + +/** + * Convert mf2 to JF2 + * @param {object} body - Form-encoded request body + * @param {boolean} [requestReferences] - Request data for any referenced URLs + * @returns {Promise} Micropub action + */ +export const mf2ToJf2 = async (body, requestReferences) => { + const mf2 = { + items: [body], + }; + + if (requestReferences) { + return mf2tojf2referenced(mf2); + } + + return mf2tojf2(mf2); +}; + +/** + * Normalise JF2 properties + * @param {object} publication - Publication configuration + * @param {object} properties - JF2 properties + * @param {string} timeZone - Application time zone + * @returns {object} Normalised JF2 properties + */ +export const normaliseProperties = (publication, properties, timeZone) => { + const { channels, me, slugSeparator } = publication; + + properties.published = getDate(timeZone, properties.published); + + if (properties.name) { + properties.name = properties.name.trim(); + } + + if (properties.content) { + properties.content = getContentProperty(properties); + } + + if (properties.location) { + properties.location = getLocationProperty(properties); + } + + if (properties.audio) { + properties.audio = getAudioProperty(properties, me); + } + + if (properties.photo) { + properties.photo = getPhotoProperty(properties, me); + } + + if (properties.video) { + properties.video = getVideoProperty(properties, me); + } + + properties.slug = getSlugProperty(properties, slugSeparator); + + const publicationHasChannels = channels && Object.keys(channels).length > 0; + if (publicationHasChannels) { + properties.channel = getChannelProperty(properties, channels); + delete properties["mp-channel"]; + } + + if (properties["mp-syndicate-to"]) { + properties["mp-syndicate-to"] = toArray(properties["mp-syndicate-to"]); + } + + if (properties.syndication) { + properties.syndication = toArray(properties.syndication); + } + + return properties; +}; + +/** + * Get audio property + * @param {object} properties - JF2 properties + * @param {object} me - Publication URL + * @returns {Array} `audio` property + */ +export const getAudioProperty = (properties, me) => { + let { audio } = properties; + audio = Array.isArray(audio) ? audio : [audio]; + + return audio.map((item) => ({ + url: relativeMediaPath(item.url || item, me), + })); +}; + +/** + * Get channel property. + * + * If a publication has configured channels, but no channel has been selected, + * the default channel is used. + * + * If `mp-channel` provides a UID that does not appear in the publication’s + * channels, the default channel is used. + * + * The first item in a publication’s configured channels is considered the + * default channel. + * @param {object} properties - JF2 properties + * @param {object} channels - Publication channels + * @returns {Array} `mp-channel` property + * @see {@link https://github.com/indieweb/micropub-extensions/issues/40} + */ +export const getChannelProperty = (properties, channels) => { + channels = Object.keys(channels); + const mpChannel = properties["mp-channel"]; + const providedChannels = Array.isArray(mpChannel) ? mpChannel : [mpChannel]; + const selectedChannels = new Set(); + + // Only select channels that have been configured + for (const uid of providedChannels) { + if (channels.includes(uid)) { + selectedChannels.add(uid); + } + } + + // If no channels provided, use default channel UID + if (selectedChannels.size === 0) { + const defaultChannel = channels[0]; + selectedChannels.add(defaultChannel); + } + + return toArray([...selectedChannels]); +}; + +/** + * Get content property. + * + * JF2 allows for the provision of both plaintext and HTML representations. + * Use existing values, or add HTML representation if only plaintext provided. + * @param {object} properties - JF2 properties + * @returns {object} `content` property + * @see {@link https://www.w3.org/TR/jf2/#html-content} + */ +export const getContentProperty = (properties) => { + const { content } = properties; + let { html, text } = content; + + // Return existing text and HTML representations, unamended + if (html && text) { + return { html, text }; + } + + // If HTML representation only, add text representation + if (html && !text) { + return { html, text: htmlToMarkdown(html) }; + } + + // If text representation only, add HTML representation + if (!html && text) { + return { html: markdownToHtml(text), text }; + } + + // If content is a string, add `html` and move plaintext to `text. + if (typeof content === "string") { + text = content; + html = markdownToHtml(content); + } + + return { html, text }; +}; + +/** + * Get location property, parsing a Geo URI if provided + * @param {object|string} properties - JF2 properties + * @returns {object} `location` property + */ +export const getLocationProperty = (properties) => { + let { location } = properties; + + if (typeof location === "string" && location.startsWith("geo:")) { + const geoUriRegexp = + /geo:(?[\d+.?-]*),(?[\d+.?-]*)(?:,(?[\d+.?-]*))?/; + const { latitude, longitude, altitude } = + location.match(geoUriRegexp).groups; + + location = { + type: "geo", + latitude, + longitude, + ...(altitude ? { altitude } : {}), + }; + } + + return location; +}; + +/** + * Get photo property (adding text alternatives where provided) + * @param {object} properties - JF2 properties + * @param {object} me - Publication URL + * @returns {Array} `photo` property + */ +export const getPhotoProperty = (properties, me) => { + let { photo } = properties; + photo = Array.isArray(photo) ? photo : [photo]; + + let photoAlt = properties["mp-photo-alt"]; + if (photoAlt) { + photoAlt = Array.isArray(photoAlt) ? photoAlt : [photoAlt]; + } + + const property = photo.map((item, index) => ({ + url: relativeMediaPath(item.url || item, me), + ...(item.alt && { alt: item.alt.trim() }), + ...(photoAlt && { alt: photoAlt[index].trim() }), + })); + delete properties["mp-photo-alt"]; + return property; +}; + +/** + * Get video property + * @param {object} properties - JF2 properties + * @param {object} me - Publication URL + * @returns {Array} `video` property + */ +export const getVideoProperty = (properties, me) => { + let { video } = properties; + video = Array.isArray(video) ? video : [video]; + + return video.map((item) => ({ + url: relativeMediaPath(item.url || item, me), + })); +}; + +/** + * Get slug + * @param {object} properties - JF2 properties + * @param {string} separator - Slug separator + * @returns {string} Array containing slug value + */ +export const getSlugProperty = (properties, separator) => { + const suggested = properties["mp-slug"]; + const { name, published } = properties; + + let string; + if (suggested) { + string = suggested; + } else if (name) { + string = excerpt(name, 5); + } else { + string = md5(published).slice(0, 5); + } + + return slugify(string, { separator }); +}; + +/** + * Get `mp-syndicate-to` property + * @param {object} properties - JF2 properties + * @param {Array} syndicationTargets - Configured syndication targets + * @returns {Array|undefined} Resolved syndication targets + */ +export const getSyndicateToProperty = (properties, syndicationTargets) => { + const property = []; + + if (!syndicationTargets || syndicationTargets.length === 0) { + return; + } + + for (const target of syndicationTargets) { + const { uid } = target.info; + const syndicateTo = properties["mp-syndicate-to"]; + + if (syndicateTo?.includes(uid)) { + property.push(uid); + } + } + + if (property.length > 0) { + return property; + } +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/markdown.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/markdown.js new file mode 100644 index 00000000..44596a6c --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/markdown.js @@ -0,0 +1,56 @@ +import markdownIt from "markdown-it"; +import TurndownService from "turndown"; + +/** + * Convert Markdown to HTML + * @param {string} string - Markdown + * @returns {string} HTML + */ +export const markdownToHtml = (string) => { + const options = { + html: true, + breaks: true, + typographer: true, + }; + + const parser = markdownIt(options); + + const html = parser.render(string).trim(); + + return html; +}; + +/** + * Convert HTML to Markdown + * @param {string} string - String (may be HTML or Markdown) + * @returns {string} Markdown + */ +export const htmlToMarkdown = (string) => { + // Normalise text as HTML before converting to Markdown + string = markdownToHtml(string); + + const options = { + codeBlockStyle: "fenced", + emDelimiter: "*", + headingStyle: "atx", + }; + + const turndownService = new TurndownService(options); + + /** + * Disable escaping of Markdown characters + * @param {string} string - String + * @returns {string} String + * @see {@link https://github.com/mixmark-io/turndown#escaping-markdown-characters} + */ + turndownService.escape = (string) => string; + + /** + * List of inline elements to keep in Markdown + */ + turndownService.keep(["cite", "del", "ins"]); + + const markdown = turndownService.turndown(string); + + return markdown; +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/media.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/media.js new file mode 100644 index 00000000..cf4a2d63 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/media.js @@ -0,0 +1,45 @@ +/** + * Upload attached file(s) via media endpoint + * @param {string} mediaEndpoint - Media endpoint URL + * @param {string} token - Bearer token + * @param {object} properties - JF2 properties + * @param {object} files - Files to upload + * @returns {Promise} Uploaded file locations + */ +export const uploadMedia = async (mediaEndpoint, token, properties, files) => { + for await (let [mediaProperty, media] of Object.entries(files)) { + // Media property may contain one or many media files + media = Array.isArray(media) ? media : [media]; + + for await (const file of media) { + const { data, name } = file; + + // Create multipart/form-data + const formData = new FormData(); + formData.append("file", new Blob([data]), name); + + // Upload file via media endpoint + const response = await fetch(mediaEndpoint, { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + }, + body: formData, + }); + + if (!response.ok) { + /** @type {object} */ + const body = await response.json(); + + const message = body.error_description || response.statusText; + throw new Error(message); + } + + // Update respective media property with location of upload + properties[mediaProperty] = properties[mediaProperty] || []; + properties[mediaProperty].push(response.headers.get("location")); + } + } + + return properties; +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/mf2.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/mf2.js new file mode 100644 index 00000000..ff9de9ef --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/mf2.js @@ -0,0 +1,80 @@ +/** + * Return mf2 properties of a post + * @param {object} mf2 - mf2 object + * @param {Array|string} requestedProperties - mf2 properties to select + * @returns {Promise} mf2 with requested properties + */ +export const getMf2Properties = (mf2, requestedProperties) => { + if (!requestedProperties) { + return mf2; + } + + const item = mf2.items ? mf2.items[0] : mf2; + const { properties } = item; + + // Return requested properties + if (requestedProperties) { + requestedProperties = Array.isArray(requestedProperties) + ? requestedProperties + : [requestedProperties]; + + const selectedProperties = {}; + + for (const key of requestedProperties) { + if (properties[key]) { + selectedProperties[key] = properties[key]; + } + } + + item.properties = selectedProperties; + } + + // Return properties + delete item.type; + return item; +}; + +/** + * Convert JF2 post data to mf2 + * @param {object} postData - Post data + * @param {boolean} [includeObjectId] - Include ObjectID from post data + * @returns {object} mf2 + */ +export const jf2ToMf2 = (postData, includeObjectId = true) => { + const { properties, _id } = postData; + + const mf2 = { + type: [`h-${properties.type}`], + properties: { + ...(includeObjectId && _id && { uid: [_id] }), + }, + }; + + delete properties.type; + + // Move values to property object + for (const key in properties) { + // Convert nested vocabulary to mf2 (i.e. h-card, h-geo, h-adr) + if (Object.prototype.hasOwnProperty.call(properties[key], "type")) { + mf2.properties[key] = [jf2ToMf2({ properties: properties[key] }, false)]; + } + + // Convert values to arrays (i.e. 'a' => ['a']) + else if (Object.prototype.hasOwnProperty.call(properties, key)) { + const value = properties[key]; + mf2.properties[key] = Array.isArray(value) ? value : [value]; + } + } + + // Update key for plaintext content + if ( + mf2.properties.content && + mf2.properties.content[0] && + mf2.properties.content[0].text + ) { + mf2.properties.content[0].value = properties.content.text; + delete mf2.properties.content[0].text; + } + + return mf2; +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-content.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-content.js new file mode 100644 index 00000000..ca45a03a --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-content.js @@ -0,0 +1,147 @@ +import makeDebug from "debug"; + +import { getPostTemplateProperties } from "./utils.js"; + +const debug = makeDebug("indiekit:endpoint-micropub:post-content"); + +export const postContent = { + /** + * Create post + * @param {object} publication - Publication configuration + * @param {object} postData - Post data + * @returns {Promise} Response data + */ + async create(publication, postData) { + debug(`Create %O`, { postData }); + + const { postTemplate, store, storeMessageTemplate } = publication; + const { path, properties } = postData; + const metaData = { + action: "create", + result: "created", + fileType: "post", + postType: properties["post-type"], + }; + const templateProperties = getPostTemplateProperties(properties); + const content = await postTemplate(templateProperties); + const message = storeMessageTemplate(metaData); + + await store.createFile(path, content, { message }); + + return { + location: properties.url, + status: 202, + json: { + success: "create_pending", + success_description: `Post will be created at ${properties.url}`, + }, + }; + }, + + /** + * Update post + * @param {object} publication - Publication configuration + * @param {object} postData - Post data + * @param {string} url - Files attached to request + * @returns {Promise} Response data + */ + async update(publication, postData, url) { + debug(`Update ${url} %O`, { postData }); + + const { postTemplate, store, storeMessageTemplate } = publication; + const { _originalPath, path, properties } = postData; + const metaData = { + action: "update", + result: "updated", + fileType: "post", + postType: properties["post-type"], + }; + const templateProperties = getPostTemplateProperties(properties); + const content = await postTemplate(templateProperties); + const message = storeMessageTemplate(metaData); + const hasUpdatedUrl = url !== properties.url; + + _originalPath === path + ? await store.updateFile(path, content, { message }) + : await store.updateFile(_originalPath, content, { + message, + newPath: path, + }); + + delete postData._originalPath; + + return { + location: properties.url, + status: hasUpdatedUrl ? 201 : 200, + json: { + success: "update", + success_description: hasUpdatedUrl + ? `Post updated and moved to ${properties.url}` + : `Post updated at ${url}`, + }, + }; + }, + + /** + * Delete post + * @param {object} publication - Publication configuration + * @param {object} postData - Post data + * @returns {Promise} Response data + */ + async delete(publication, postData) { + debug(`Delete %O`, { postData }); + + const { store, storeMessageTemplate } = publication; + const { path, properties } = postData; + const metaData = { + action: "delete", + result: "deleted", + fileType: "post", + postType: properties["post-type"], + }; + const message = storeMessageTemplate(metaData); + + await store.deleteFile(path, { message }); + + return { + status: 200, + json: { + success: "delete", + success_description: `Post deleted from ${properties.url}`, + }, + }; + }, + + /** + * Undelete post + * @param {object} publication - Publication configuration + * @param {object} postData - Post data + * @returns {Promise} Response data + */ + async undelete(publication, postData) { + debug(`Undelete %O`, { postData }); + + const { postTemplate, store, storeMessageTemplate } = publication; + const { path, properties } = postData; + const metaData = { + action: "undelete", + result: "undeleted", + fileType: "post", + postType: properties["post-type"], + }; + const templateProperties = getPostTemplateProperties(properties); + const content = await postTemplate(templateProperties); + const message = storeMessageTemplate(metaData); + + await store.createFile(path, content, { message }); + + return { + location: properties.url, + status: 200, + json: { + success: "delete_undelete", + success_description: `Post restored to ${properties.url}`, + }, + }; + }, +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-data.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-data.js new file mode 100644 index 00000000..6795a5a3 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-data.js @@ -0,0 +1,300 @@ +import { isDeepStrictEqual } from "node:util"; + +import { IndiekitError } from "@indiekit/error"; +import { getCanonicalUrl, getDate } from "@indiekit/util"; +import makeDebug from "debug"; + +import { getSyndicateToProperty, normaliseProperties } from "./jf2.js"; +import { getPostType } from "./post-type-discovery.js"; +import * as updateMf2 from "./update.js"; +import { getPostTemplateProperties, renderPath } from "./utils.js"; + +const debug = makeDebug("indiekit:endpoint-micropub:post-data"); + +export const postData = { + /** + * Create post data + * @param {object} application - Application configuration + * @param {object} publication - Publication configuration + * @param {object} properties - JF2 properties + * @param {boolean} [draftMode] - Draft mode + * @returns {Promise} Post data + */ + async create(application, publication, properties, draftMode = false) { + debug(`Create %O`, { draftMode, properties }); + + const { timeZone } = application; + const { me, postTypes, syndicationTargets } = publication; + + // Add syndication targets + const syndicateTo = getSyndicateToProperty(properties, syndicationTargets); + if (syndicateTo) { + properties["mp-syndicate-to"] = syndicateTo; + } + + // Normalise properties + properties = normaliseProperties(publication, properties, timeZone); + + // Post type + const type = getPostType(postTypes, properties); + properties["post-type"] = type; + + // Get post type configuration + const typeConfig = postTypes[type]; + if (!typeConfig || !typeConfig.post?.path) { + throw IndiekitError.notImplemented(type); + } + + // Post paths + const path = await renderPath( + typeConfig.post.path, + properties, + application, + publication, + ); + const url = await renderPath( + typeConfig.post.url || typeConfig.post.path, + properties, + application, + publication, + ); + properties.url = getCanonicalUrl(url, me); + + // Post status + // Draft mode: Only create post with a `draft` post-status + properties["post-status"] = draftMode + ? "draft" + : properties["post-status"] || "published"; + + const postData = { path, properties }; + + // Add data to posts collection (or replace existing if present) + const postsCollection = application?.collections?.get("posts"); + if (postsCollection) { + const query = { "properties.url": properties.url }; + await postsCollection.replaceOne(query, postData, { upsert: true }); + } + + return postData; + }, + + /** + * Read post data + * @param {object} application - Application configuration + * @param {string} url - URL of existing post + * @returns {Promise} Post data + */ + async read(application, url) { + debug(`Read ${url}`); + + const query = { "properties.url": url }; + const postsCollection = application?.collections?.get("posts"); + + const postData = await postsCollection.findOne(query); + if (!postData) { + throw IndiekitError.notFound(url); + } + + return postData; + }, + + /** + * Update post data + * + * Add, delete or replace properties and/or replace property values + * @param {object} application - Application configuration + * @param {object} publication - Publication configuration + * @param {string} url - URL of existing post + * @param {object} operation - Requested operation(s) + * @returns {Promise} Post data + */ + async update(application, publication, url, operation) { + debug(`Update ${url} %O`, { operation }); + + const { timeZone } = application; + const { me, postTypes } = publication; + const postsCollection = application?.collections?.get("posts"); + + // Read properties + let { path: _originalPath, properties } = await this.read(application, url); + + // Save incoming properties for later comparison + let oldProperties = structuredClone(properties); + + // Add properties + if (operation.add) { + properties = updateMf2.addProperties(properties, operation.add); + } + + // Replace property entries + if (operation.replace) { + properties = await updateMf2.replaceEntries( + properties, + operation.replace, + ); + } + + // Remove properties and/or property entries + if (operation.delete) { + properties = Array.isArray(operation.delete) + ? updateMf2.deleteProperties(properties, operation.delete) + : updateMf2.deleteEntries(properties, operation.delete); + } + + // Normalise properties + properties = normaliseProperties(publication, properties, timeZone); + oldProperties = normaliseProperties(publication, oldProperties, timeZone); + + // Post type + const type = getPostType(postTypes, properties); + const typeConfig = postTypes[type]; + properties["post-type"] = type; + + // Validate post type configuration exists + if (!typeConfig || !typeConfig.post?.path) { + throw IndiekitError.notImplemented(type); + } + + // Post paths + const path = await renderPath( + typeConfig.post.path, + properties, + application, + publication, + ); + const updatedUrl = await renderPath( + typeConfig.post.url, + properties, + application, + publication, + ); + properties.url = getCanonicalUrl(updatedUrl, me); + + // Return if no changes to template properties detected + const newProperties = getPostTemplateProperties(properties); + oldProperties = getPostTemplateProperties(oldProperties); + if (isDeepStrictEqual(newProperties, oldProperties)) { + return; + } + + // Add updated date + properties.updated = getDate(timeZone); + + // Update data in posts collection + const postData = { _originalPath, path, properties }; + const query = { "properties.url": url }; + await postsCollection.replaceOne(query, postData); + + return postData; + }, + + /** + * Delete post data + * + * Delete (most) properties, keeping a record of deleted for later retrieval + * @param {object} application - Application configuration + * @param {object} publication - Publication configuration + * @param {string} url - URL of existing post + * @returns {Promise} Post data + */ + async delete(application, publication, url) { + debug(`Delete ${url}`); + + const { timeZone } = application; + const { postTypes } = publication; + const postsCollection = application?.collections?.get("posts"); + + // Read properties + const { properties } = await this.read(application, url); + + // Make a copy of existing properties + const _deletedProperties = structuredClone(properties); + + // Delete all properties, except those required for path creation + for (const key in _deletedProperties) { + if (!["post-type", "published", "slug", "type", "url"].includes(key)) { + delete properties[key]; + } + } + + // Add deleted date + properties.deleted = getDate(timeZone); + + // Post type + const type = properties["post-type"]; + const typeConfig = postTypes[type]; + + // Validate post type configuration exists + if (!typeConfig || !typeConfig.post?.path) { + throw IndiekitError.notImplemented(type); + } + + // Post paths + const path = await renderPath( + typeConfig.post.path, + properties, + application, + publication, + ); + + // Update data in posts collection + const postData = { path, properties, _deletedProperties }; + const query = { "properties.url": url }; + await postsCollection.replaceOne(query, postData); + + return postData; + }, + + /** + * Undelete post data + * + * Restore previously deleted properties + * @param {object} application - Application configuration + * @param {object} publication - Publication configuration + * @param {string} url - URL of existing post + * @param {boolean} [draftMode] - Draft mode + * @returns {Promise} Post data + */ + async undelete(application, publication, url, draftMode) { + debug(`Undelete ${url} %O`, { draftMode }); + + const { postTypes } = publication; + const postsCollection = application?.collections?.get("posts"); + + // Read deleted properties + const { _deletedProperties } = await this.read(application, url); + + // Restore previously deleted properties + const properties = _deletedProperties; + + // Post type + const type = properties["post-type"]; + const typeConfig = postTypes[type]; + + // Validate post type configuration exists + if (!typeConfig || !typeConfig.post?.path) { + throw IndiekitError.notImplemented(type); + } + + // Post paths + const path = await renderPath( + typeConfig.post.path, + properties, + application, + publication, + ); + + // Post status + // Draft mode: Only restore post with a `draft` post-status + properties["post-status"] = draftMode + ? "draft" + : properties["post-status"] || "published"; + + // Update data in posts collection + const postData = { path, properties }; + const query = { "properties.url": url }; + await postsCollection.replaceOne(query, postData); + + return postData; + }, +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-type-count.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-type-count.js new file mode 100644 index 00000000..98e1a146 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-type-count.js @@ -0,0 +1,49 @@ +import { getObjectId } from "@indiekit/util"; + +export const postTypeCount = { + /** + * Count the number of posts of a given type + * @param {object} postsCollection - Posts database collection + * @param {object} properties - JF2 properties + * @returns {Promise} Post count + */ + async get(postsCollection, properties) { + if (!postsCollection || !postsCollection.count()) { + console.warn("No database configuration provided"); + console.info( + "See https://getindiekit.com/configuration/application/#mongodburl", + ); + + return; + } + + // Post type + const postType = properties["post-type"]; + const postUid = properties.uid; + const startDate = new Date(new Date(properties.published).toDateString()); + const endDate = new Date(startDate); + endDate.setDate(endDate.getDate() + 1); + const response = await postsCollection + .aggregate([ + { + $addFields: { + convertedDate: { + $toDate: "$properties.published", + }, + }, + }, + { + $match: { + _id: getObjectId(postUid), + "properties.post-type": postType, + convertedDate: { + $gte: startDate, + $lt: endDate, + }, + }, + }, + ]) + .toArray(); + return response.length; + }, +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-type-discovery.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-type-discovery.js new file mode 100644 index 00000000..bb417e78 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-type-discovery.js @@ -0,0 +1,75 @@ +/** + * Accepts a JF2 object and attempts to determine the post type + * @param {object} postTypes - Configured post types + * @param {object} properties - JF2 properties + * @returns {string|null} The post type or null if unknown + * @see {@link https://ptd.spec.indieweb.org/#algorithm} + */ +export const getPostType = (postTypes, properties) => { + const propertiesMap = new Map(Object.entries(properties)); + + // If post has the event type, it's an event + if (properties.type && properties.type === "event") { + return properties.type; + } + + // If post has a custom type (h value) that matches a configured post type + // This allows plugins to use h: "page" or similar for type-based discovery + // instead of requiring a discovery property that survives mf2->JF2 conversion + if (properties.type && properties.type !== "entry" && postTypes[properties.type]) { + return properties.type; + } + + const basePostTypes = new Map([ + ["rsvp", "rsvp"], + ["repost", "repost-of"], + ["like", "like-of"], + ["reply", "in-reply-to"], + ["video", "video"], + ["photo", "photo"], + ]); + + // Types defined in Post Type Discovery specification + + // Types defined in post type configuration + for (const [type, { discovery }] of Object.entries(postTypes)) { + if (!discovery) { + continue; + } + + basePostTypes.set(type, discovery); + } + + for (const basePostType of basePostTypes) { + if (propertiesMap.has(basePostType[1])) { + return basePostType[0]; + } + } + + // If has `children` property that is populated, is collection type + if ( + properties.children && + Array.isArray(properties.children) && + properties.children.length > 0 + ) { + return "collection"; + } + + // Use summary value for content if no content value + let content; + if (propertiesMap.has("content")) { + content = + properties.content.text || properties.content.html || properties.content; + } else if (propertiesMap.has("summary")) { + content = properties.summary; + } + + // If post has `name` and content, it’s an article + // This is a deviation from the Post Type Algorithm, which identifies a post + // as a note if the content is prefixed with the `name` value. + if (properties.name && content) { + return "article"; + } + + return "note"; +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/reserved-properties.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/reserved-properties.js new file mode 100644 index 00000000..93637cc3 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/reserved-properties.js @@ -0,0 +1,10 @@ +/** + * Reserved body property names + * @see {@link https://micropub.spec.indieweb.org/#reserved-properties} + */ +export const reservedProperties = Object.freeze([ + "access_token", + "h", + "action", + "url", +]); diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/scope.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/scope.js new file mode 100644 index 00000000..2d402763 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/scope.js @@ -0,0 +1,36 @@ +/** + * Check provided scope(s) satisfies required scope + * @param {string} scope - Provided scope (space separated) + * @param {string} [action] - Required action + * @returns {boolean|string} `true` if provided scope includes action, + * `draft` if draft scope, otherwise `false` + */ +export const checkScope = (scope, action = "create") => { + // Default scope request is `create` + if (!scope) { + scope = "create"; + } + + // Undeleting a post is equivalent to creating a post + if (action === "undelete") { + action = "create"; + } + + // Check for scope matching desired action + let hasScope = scope.includes(action); + + // Handle deprecated `post` scope + if (!hasScope && action === "create") { + hasScope = scope.includes("post"); + } + + // Check for draft scope + const draftScope = scope.includes("draft"); + + // Can create/update with `draft` scope, but using draft post status + if (draftScope && (action === "create" || action === "update")) { + hasScope = "draft"; + } + + return hasScope; +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/update.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/update.js new file mode 100644 index 00000000..e70cdf91 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/update.js @@ -0,0 +1,129 @@ +import _ from "lodash"; + +import { mf2ToJf2 } from "./jf2.js"; + +/** + * Add properties to object + * @param {object} object - Object to update + * @param {object} additions - Properties to add (mf2) + * @returns {object|undefined} Updated object + */ +export const addProperties = (object, additions) => { + for (const key in additions) { + if (Object.prototype.hasOwnProperty.call(additions, key)) { + const newValue = additions[key]; + let existingValue = object[key]; + + // If no existing value, add it + if (!existingValue) { + object[key] = newValue; + return object; + } + + // If existing value, add to it + if (existingValue) { + existingValue = Array.isArray(existingValue) + ? existingValue + : [existingValue]; + + const updatedValue = [...existingValue]; + + for (const value of newValue) { + updatedValue.push(value); + } + + object = _.set(object, key, updatedValue); + return object; + } + } + } +}; + +/** + * Replace entries of a property. If property doesn’t exist, create it. + * @param {object} object - Object to update + * @param {object} replacements - Properties to replace (mf2) + * @returns {Promise} Updated object (JF2) + */ +export const replaceEntries = async (object, replacements) => { + for await (const [key, value] of Object.entries(replacements)) { + if (!Array.isArray(value)) { + throw new TypeError("Replacement value should be an array"); + } + + // Replacement given as mf2, but data stored as JF2 + switch (value.length) { + case 0: { + // Array is empty, don’t perform replacement + continue; + } + + case 1: { + // Array contains a single value, save as JF2 + const jf2 = await mf2ToJf2(value[0], false); + object = _.set(object, key, jf2); + break; + } + + default: { + // Array contains multiple values, save as array + object = _.set(object, key, value); + break; + } + } + } + + return object; +}; + +/** + * Delete entries for properties of object + * @param {object} object - Object to update + * @param {object} deletions - Property entries to delete (mf2) + * @returns {object} Updated object + */ +export const deleteEntries = (object, deletions) => { + for (const key in deletions) { + if (Object.prototype.hasOwnProperty.call(deletions, key)) { + const valuesToDelete = deletions[key]; + + if (!Array.isArray(valuesToDelete)) { + throw new TypeError(`${key} should be an array`); + } + + const values = object[key]; + if (!valuesToDelete || !values) { + return object; + } + + for (const value of valuesToDelete) { + const index = values.indexOf(value); + if (index !== -1) { + values.splice(index, 1); + } + + if (values.length === 0) { + delete object[key]; // Delete property if no values remain + } else { + object[key] = values; + } + } + } + } + + return object; +}; + +/** + * Delete properties of object + * @param {object} object - Object to update + * @param {Array} deletions - Properties to delete (mf2) + * @returns {object} Updated object + */ +export const deleteProperties = (object, deletions) => { + for (const key of deletions) { + delete object[key]; + } + + return object; +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/utils.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/utils.js new file mode 100644 index 00000000..d20c370e --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/utils.js @@ -0,0 +1,121 @@ +import { dateTokens, formatDate, isDate, supplant } from "@indiekit/util"; +import newbase60 from "newbase60"; + +import { postTypeCount } from "./post-type-count.js"; + +/** + * Decode form-encoded query parameter + * @param {string} value - Parameter value to decode + * @returns {string} Decoded string, else original parameter value + * @example decodeQueryParameter(['foo', 'bar']) => ['foo', 'bar'] + * @example decodeQueryParameter('2024-02-14T13:24:00+0100') => '2024-02-14T13:24:00+0100' + * @example decodeQueryParameter('https%3A%2F%2Ffoo.bar') => 'https://foo.bar' + * @example decodeQueryParameter('foo+bar') => 'foo bar' + */ +export const decodeQueryParameter = (value) => { + if (typeof value !== "string") { + return value; + } + + return isDate(value) + ? decodeURIComponent(value) + : decodeURIComponent(value.replaceAll("+", " ")); +}; + +/** + * Get post template properties + * @param {object} properties - JF2 properties + * @returns {object} Template properties + */ +export const getPostTemplateProperties = (properties) => { + const templateProperties = structuredClone(properties); + + // mp- properties to preserve for the template (needed for pre-syndication markup) + // mp-syndicate-to must appear in frontmatter so themes can render u-syndication + // links BEFORE the syndication webmention is sent (required by IndieNews, etc.) + const preserveMpProperties = ["mp-syndicate-to"]; + + for (let key in templateProperties) { + // Remove server commands from post template properties + // Exception: preserve mp-syndicate-to for pre-syndication u-syndication links + if (key.startsWith("mp-") && !preserveMpProperties.includes(key)) { + delete templateProperties[key]; + } + + // Remove post-type property, only needed internally + if (key === "post-type") { + delete templateProperties["post-type"]; + } + } + + return templateProperties; +}; + +/** + * Render relative path if URL is on publication + * @param {string} url - External URL + * @param {string} me - Publication URL + * @returns {string} Path + */ +export const relativeMediaPath = (url, me) => + url.includes(me) ? url.replace(me, "") : url; + +/** + * Render path from URI template and properties + * @param {string} path - URI template path + * @param {object} properties - JF2 properties + * @param {object} application - Application configuration + * @param {object} publication - Publication configuration + * @returns {Promise} Path + */ +export const renderPath = async ( + path, + properties, + application, + publication, +) => { + const dateObject = new Date(properties.published); + const { timeZone } = Intl.DateTimeFormat().resolvedOptions(); + const { slugSeparator } = publication; + let tokens = {}; + + // Add date tokens + for (const dateToken of dateTokens) { + tokens[dateToken] = formatDate(properties.published, dateToken, { + locale: application.locale, + timeZone: + application.timeZone === "server" ? timeZone : application.timeZone, + useAdditionalDayOfYearTokens: true, + }); + } + + // Add day of the year (NewBase60) token + tokens.D60 = newbase60.DateToSxg(dateObject); + + // Add count of post-type for the day + const postsCollection = application?.collections?.get("posts"); + const count = await postTypeCount.get(postsCollection, properties); + tokens.n = count + 1; + + // Add slug token + tokens.slug = properties.slug; + + // Add channel token + if (properties.channel) { + tokens.channel = Array.isArray(properties.channel) + ? properties.channel.join(slugSeparator) + : properties.channel; + } + + // Populate URI template path with properties + path = supplant(path, tokens); + + return path; +}; + +/** + * Convert string to array if not already an array + * @param {string|Array} object - String or array to convert + * @returns {Array} Array + */ +export const toArray = (object) => (Array.isArray(object) ? object : [object]); diff --git a/node_modules/@rmdes/indiekit-endpoint-posts/package.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/package.json similarity index 51% rename from node_modules/@rmdes/indiekit-endpoint-posts/package.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/package.json index d69bc13c..48e02479 100644 --- a/node_modules/@rmdes/indiekit-endpoint-posts/package.json +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/package.json @@ -1,13 +1,14 @@ { - "name": "@rmdes/indiekit-endpoint-posts", - "version": "1.0.0-beta.25", - "description": "Post management endpoint for Indiekit with syndicate form fix. View posts published by your Micropub endpoint and publish new posts to it.", + "name": "@rmdes/indiekit-endpoint-micropub", + "version": "1.0.0-beta.29", + "description": "Micropub endpoint for Indiekit with custom type-based post discovery. Enables publishing content to your website using the Micropub protocol.", "keywords": [ "indiekit", "indiekit-plugin", - "indieweb" + "indieweb", + "micropub" ], - "homepage": "https://github.com/rmdes/indiekit-endpoint-posts", + "homepage": "https://github.com/rmdes/indiekit-endpoint-micropub", "author": { "name": "Paul Robert Lloyd", "url": "https://paulrobertlloyd.com" @@ -26,28 +27,26 @@ "main": "index.js", "files": [ "assets", - "includes", "lib", - "locales", - "views", "index.js" ], "bugs": { - "url": "https://github.com/rmdes/indiekit-endpoint-posts/issues" + "url": "https://github.com/rmdes/indiekit-endpoint-micropub/issues" }, "repository": { "type": "git", - "url": "https://github.com/rmdes/indiekit-endpoint-posts.git" + "url": "https://github.com/rmdes/indiekit-endpoint-micropub.git" }, "dependencies": { - "@indiekit/endpoint-micropub": "^1.0.0-beta.25", "@indiekit/error": "^1.0.0-beta.25", - "@indiekit/frontend": "^1.0.0-beta.25", "@indiekit/util": "^1.0.0-beta.25", "@paulrobertlloyd/mf2tojf2": "^3.0.0", + "debug": "^4.3.2", "express": "^5.0.0", - "express-validator": "^7.0.0", - "formatcoords": "^1.1.3" + "lodash": "^4.17.21", + "markdown-it": "^14.0.0", + "newbase60": "^1.3.1", + "turndown": "^7.1.1" }, "publishConfig": { "access": "public" diff --git a/node_modules/@indiekit/endpoint-share/README.md b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/README.md similarity index 100% rename from node_modules/@indiekit/endpoint-share/README.md rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/README.md diff --git a/node_modules/@indiekit/endpoint-share/assets/icon.svg b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/assets/icon.svg similarity index 100% rename from node_modules/@indiekit/endpoint-share/assets/icon.svg rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/assets/icon.svg diff --git a/node_modules/@indiekit/endpoint-share/includes/@indiekit-endpoint-share-widget.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/includes/@indiekit-endpoint-share-widget.njk similarity index 100% rename from node_modules/@indiekit/endpoint-share/includes/@indiekit-endpoint-share-widget.njk rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/includes/@indiekit-endpoint-share-widget.njk diff --git a/node_modules/@indiekit/endpoint-share/index.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/index.js similarity index 100% rename from node_modules/@indiekit/endpoint-share/index.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/index.js diff --git a/node_modules/@rmdes/indiekit-endpoint-share/lib/controllers/share.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/lib/controllers/share.js similarity index 100% rename from node_modules/@rmdes/indiekit-endpoint-share/lib/controllers/share.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/lib/controllers/share.js diff --git a/node_modules/@rmdes/indiekit-endpoint-share/lib/middleware/validation.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/lib/middleware/validation.js similarity index 100% rename from node_modules/@rmdes/indiekit-endpoint-share/lib/middleware/validation.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/lib/middleware/validation.js diff --git a/node_modules/@indiekit/endpoint-share/locales/de.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/de.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/de.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/de.json diff --git a/node_modules/@rmdes/indiekit-endpoint-share/locales/en.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/en.json similarity index 100% rename from node_modules/@rmdes/indiekit-endpoint-share/locales/en.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/en.json diff --git a/node_modules/@indiekit/endpoint-share/locales/es-419.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/es-419.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/es-419.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/es-419.json diff --git a/node_modules/@indiekit/endpoint-share/locales/es.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/es.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/es.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/es.json diff --git a/node_modules/@indiekit/endpoint-share/locales/fr.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/fr.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/fr.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/fr.json diff --git a/node_modules/@indiekit/endpoint-share/locales/hi.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/hi.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/hi.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/hi.json diff --git a/node_modules/@indiekit/endpoint-share/locales/id.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/id.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/id.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/id.json diff --git a/node_modules/@indiekit/endpoint-share/locales/it.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/it.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/it.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/it.json diff --git a/node_modules/@indiekit/endpoint-share/locales/nl.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/nl.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/nl.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/nl.json diff --git a/node_modules/@indiekit/endpoint-share/locales/pl.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/pl.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/pl.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/pl.json diff --git a/node_modules/@indiekit/endpoint-share/locales/pt-BR.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/pt-BR.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/pt-BR.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/pt-BR.json diff --git a/node_modules/@indiekit/endpoint-share/locales/pt.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/pt.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/pt.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/pt.json diff --git a/node_modules/@indiekit/endpoint-share/locales/sr.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/sr.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/sr.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/sr.json diff --git a/node_modules/@indiekit/endpoint-share/locales/sv.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/sv.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/sv.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/sv.json diff --git a/node_modules/@indiekit/endpoint-share/locales/zh-Hans-CN.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/zh-Hans-CN.json similarity index 100% rename from node_modules/@indiekit/endpoint-share/locales/zh-Hans-CN.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/locales/zh-Hans-CN.json diff --git a/node_modules/@rmdes/indiekit-endpoint-share/package.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/package.json similarity index 100% rename from node_modules/@rmdes/indiekit-endpoint-share/package.json rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/package.json diff --git a/node_modules/@rmdes/indiekit-endpoint-share/views/share.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/views/share.njk similarity index 100% rename from node_modules/@rmdes/indiekit-endpoint-share/views/share.njk rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-share/views/share.njk diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/README.md b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/README.md new file mode 100644 index 00000000..1d3b96b7 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/README.md @@ -0,0 +1,383 @@ +# @rmdes/indiekit-endpoint-syndicate + +Syndication endpoint for Indiekit. Fork of @indiekit/endpoint-syndicate with batch syndication support and critical bug fixes. + +## Features + +- **Single Post Syndication** - Syndicate a specific post to configured targets +- **Batch Syndication** - Process all pending posts at once (useful for cron jobs) +- **Webhook Support** - Netlify webhook signature verification +- **Partial Syndication** - Correctly handles posts partially syndicated (fixed upstream bug) +- **Failed Target Retry** - Automatically retries failed syndication targets +- **Rate Limiting** - 2-second delay between posts in batch mode +- **Detailed Results** - Per-post success/failure tracking in batch mode + +## Bug Fixes vs Upstream + +### Critical Fixes + +1. **Array.isArray Bug** - Fixed function reference bug that prevented correct array detection +2. **Partial Syndication** - Removed filter that prevented retry of partially syndicated posts +3. **Enhanced Error Handling** - Per-post error tracking instead of failing entire batch + +### New Features + +- Batch mode for processing all pending posts +- Detailed results array with success/failure per post +- Better console logging for debugging + +## Installation + +```bash +npm install @rmdes/indiekit-endpoint-syndicate +``` + +## Configuration + +Add to your `indiekit.config.js`: + +```javascript +import SyndicateEndpoint from "@rmdes/indiekit-endpoint-syndicate"; +import BlueskyyndicatorSyndicator from "@rmdes/indiekit-syndicator-bluesky"; +import MastodonSyndicator from "@rmdes/indiekit-syndicator-mastodon"; + +export default { + plugins: [ + // Configure syndicators first + new BlueskyyndicatorSyndicator({ + user: "username.bsky.social", + password: process.env.BLUESKY_PASSWORD + }), + new MastodonSyndicator({ + url: "https://mastodon.social", + accessToken: process.env.MASTODON_TOKEN + }), + + // Add syndication endpoint + new SyndicateEndpoint({ + mountPath: "/syndicate" // Default: /syndicate + }) + ] +}; +``` + +### Environment Variables + +```bash +# Required for token signing +SECRET=your-secret-key + +# Required for Netlify webhook verification (optional) +WEBHOOK_SECRET=your-netlify-webhook-secret +``` + +## Usage + +### Single Post Syndication + +**Query Parameters:** +```bash +POST /syndicate?source_url=https://yoursite.com/posts/hello-world&token=YOUR_TOKEN +``` + +**Request Body:** +```bash +curl -X POST https://yoursite.com/syndicate \ + -H "Content-Type: application/json" \ + -d '{ + "syndication": { + "source_url": "https://yoursite.com/posts/hello-world", + "redirect_uri": "/posts" + }, + "access_token": "YOUR_TOKEN" + }' +``` + +### Batch Syndication (All Pending Posts) + +```bash +curl -X POST https://yoursite.com/syndicate?token=YOUR_TOKEN +``` + +This will: +1. Find all posts with `mp-syndicate-to` property +2. Process each post sequentially (2-second delay between posts) +3. Return detailed results for each post + +### Netlify Webhook (Auto-Syndicate After Deploy) + +**Netlify Configuration:** +1. Go to Site Settings → Build & deploy → Deploy notifications +2. Add notification: Outgoing webhook +3. Event: Deploy succeeded +4. URL: `https://yoursite.com/syndicate` +5. Save webhook secret to `WEBHOOK_SECRET` env var + +**How It Works:** +1. Netlify deploys your site +2. Netlify sends webhook with JWT signature +3. Endpoint verifies signature, generates short-lived token +4. Endpoint syndicates all pending posts + +## API Reference + +### POST /syndicate + +Syndicate post(s) to external services. + +**Authentication:** +- Bearer token (header, body, or query) +- OR Netlify webhook signature + +**Request:** + +Query parameters: +- `source_url` (optional) - Specific post URL to syndicate +- `redirect_uri` (optional) - Redirect after success (must start with `/`) +- `token` (optional) - Bearer token + +Body (JSON): +```json +{ + "syndication": { + "source_url": "https://yoursite.com/posts/hello-world", + "redirect_uri": "/posts" + }, + "access_token": "YOUR_TOKEN" +} +``` + +**Response (Single Post):** +```json +{ + "success": "Post updated", + "success_description": "Added syndication https://bsky.app/profile/user/post/123" +} +``` + +**Response (Batch Mode):** +```json +{ + "success": "OK", + "success_description": "Processed 5 post(s): 4 succeeded, 1 failed", + "results": [ + { + "url": "https://yoursite.com/posts/post-1", + "success": true, + "syndicatedUrls": [ + "https://bsky.app/profile/user/post/123", + "https://mastodon.social/@user/456" + ] + }, + { + "url": "https://yoursite.com/posts/post-2", + "success": true, + "syndicatedUrls": ["https://bsky.app/profile/user/post/789"], + "failedTargets": ["https://mastodon.social/@user"] + }, + { + "url": "https://yoursite.com/posts/post-3", + "success": false, + "error": "Micropub update failed: 401 Unauthorized" + } + ] +} +``` + +## How It Works + +### Data Flow + +1. **Post Created** - Micropub endpoint creates post with `mp-syndicate-to` property +2. **Trigger Received** - Webhook, cron, or manual POST to `/syndicate` +3. **Query Posts** - Find posts with `mp-syndicate-to` in MongoDB +4. **Syndicate** - Call syndicator plugins for each target +5. **Update Post** - Send Micropub update with syndicated URLs +6. **Cleanup** - Remove `mp-syndicate-to` if all targets succeeded + +### Post Properties + +**Before Syndication:** +```json +{ + "url": "https://yoursite.com/posts/hello-world", + "mp-syndicate-to": [ + "https://bsky.app/@username", + "https://mastodon.social/@username" + ] +} +``` + +**After Syndication (All Succeeded):** +```json +{ + "url": "https://yoursite.com/posts/hello-world", + "syndication": [ + "https://bsky.app/profile/username/post/abc123", + "https://mastodon.social/@username/456789" + ] +} +``` + +**After Syndication (One Failed):** +```json +{ + "url": "https://yoursite.com/posts/hello-world", + "mp-syndicate-to": [ + "https://mastodon.social/@username" + ], + "syndication": [ + "https://bsky.app/profile/username/post/abc123" + ] +} +``` + +### Syndicator Plugin Interface + +Syndicator plugins must implement: + +```javascript +export default class MySyndicator { + get info() { + return { + uid: "https://example.com/@username", + name: "Example Service" + }; + } + + async syndicate(properties, publication) { + // Post to external service + // Return syndicated URL or null if failed + return "https://example.com/@username/post/123"; + } + + init(Indiekit) { + Indiekit.addSyndicator(this); + } +} +``` + +## Common Use Cases + +### 1. Cron Job (Daily Batch Syndication) + +```bash +#!/bin/bash +# Crontab: 0 9 * * * /path/to/syndicate.sh + +curl -X POST https://yoursite.com/syndicate?token=$INDIEKIT_TOKEN +``` + +### 2. GitHub Actions (Post-Deploy) + +```yaml +name: Deploy and Syndicate + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Deploy site + run: # ... deploy steps ... + + - name: Syndicate posts + run: | + curl -X POST https://yoursite.com/syndicate \ + -H "Authorization: Bearer ${{ secrets.INDIEKIT_TOKEN }}" +``` + +### 3. Manual Trigger from Admin UI + +```html +
+ + + + +
+``` + +## Troubleshooting + +### Posts Not Being Syndicated + +**Check 1: Post has `mp-syndicate-to` property** +```javascript +db.posts.findOne({ "properties.url": "https://yoursite.com/posts/hello-world" }) +// Should have: properties.mp-syndicate-to: ["https://bsky.app/@user", ...] +``` + +**Check 2: Post is not a draft** +```javascript +// properties.post-status should NOT be "draft" +``` + +**Check 3: Syndicators are configured** +```javascript +// In indiekit.config.js, syndicators must be loaded BEFORE syndicate endpoint +``` + +**Check 4: Token is valid** +```bash +# Test with explicit token +curl -X POST https://yoursite.com/syndicate?source_url=URL&token=TOKEN +``` + +### Partial Syndication Not Retrying + +This was a bug in upstream `@indiekit/endpoint-syndicate`. This fork fixes it. + +**Example:** +- Post has `mp-syndicate-to: ["https://bsky.app", "https://mastodon.social"]` +- First run: Bluesky succeeds, Mastodon fails +- Post now has `mp-syndicate-to: ["https://mastodon.social"]` and `syndication: ["https://bsky.app/..."]` +- **Upstream:** Post is skipped (has `syndication` property) +- **This fork:** Post is processed, Mastodon is retried + +### Rate Limiting Errors + +Batch mode waits 2 seconds between posts. If you still hit rate limits: +- Reduce batch size (manually split posts) +- Increase delay (modify `delay(2000)` in `syndicate.js`) +- Syndicate to fewer targets at once + +## Migration from Upstream + +```bash +npm uninstall @indiekit/endpoint-syndicate +npm install @rmdes/indiekit-endpoint-syndicate +``` + +```javascript +// Update import +import SyndicateEndpoint from "@rmdes/indiekit-endpoint-syndicate"; +``` + +No other changes needed - fully backwards compatible. + +## Requirements + +- **Indiekit** >= 1.0.0-beta.25 +- **MongoDB** database (for posts collection) +- **Syndicator plugins** (e.g., `@rmdes/indiekit-syndicator-bluesky`) +- **Node.js** >= 20 + +## License + +MIT + +## Author + +Ricardo Mendes - https://rmendes.net + +## Repository + +https://github.com/rmdes/indiekit-endpoint-syndicate + +## Upstream + +Fork of [@indiekit/endpoint-syndicate](https://github.com/getindiekit/indiekit/tree/main/packages/endpoint-syndicate) by Paul Lloyd. diff --git a/node_modules/@indiekit/endpoint-syndicate/index.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/index.js similarity index 100% rename from node_modules/@indiekit/endpoint-syndicate/index.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/index.js diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/lib/controllers/syndicate.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/lib/controllers/syndicate.js new file mode 100644 index 00000000..0baafbb2 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/lib/controllers/syndicate.js @@ -0,0 +1,207 @@ +import { IndiekitError } from "@indiekit/error"; + +import { findBearerToken } from "../token.js"; +import { + getAllPostData, + getPostData, + syndicateToTargets, +} from "../utils.js"; + +/** + * Delay helper for rate limiting between posts + * @param {number} ms - Milliseconds to wait + * @returns {Promise} + */ +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Syndicate a single post and update it via Micropub + * @param {object} options - Options + * @param {object} options.application - Application config + * @param {object} options.publication - Publication config + * @param {object} options.postData - Post data from database + * @param {string} options.bearerToken - Bearer token for Micropub + * @param {boolean} [options.force] - Force re-syndication (skip dedup) + * @returns {Promise} Result object + */ +const syndicatePost = async ({ + application, + publication, + postData, + bearerToken, + force = false, +}) => { + const { failedTargets, syndicatedUrls } = await syndicateToTargets( + publication, + postData.properties, + { force }, + ); + + // Update post with syndicated URL(s) and remaining syndication target(s) + const micropubResponse = await fetch(application.micropubEndpoint, { + method: "POST", + headers: { + accept: "application/json", + authorization: `Bearer ${bearerToken}`, + "content-type": "application/json", + }, + body: JSON.stringify({ + action: "update", + url: postData.properties.url, + ...(!failedTargets && { delete: ["mp-syndicate-to"] }), + replace: { + ...(failedTargets && { "mp-syndicate-to": failedTargets }), + ...(syndicatedUrls && { syndication: syndicatedUrls }), + }, + }), + }); + + if (!micropubResponse.ok) { + throw await IndiekitError.fromFetch(micropubResponse); + } + + /** @type {object} */ + const body = await micropubResponse.json(); + + return { + url: postData.properties.url, + body, + failedTargets, + syndicatedUrls, + }; +}; + +export const syndicateController = { + async post(request, response, next) { + try { + const { application, publication } = request.app.locals; + const bearerToken = findBearerToken(request); + const sourceUrl = + request.query.source_url || request.body?.syndication?.source_url; + const redirectUri = + request.query.redirect_uri || request.body?.syndication?.redirect_uri; + + const postsCollection = application?.collections?.get("posts"); + if (!postsCollection) { + throw IndiekitError.notImplemented( + response.locals.__("NotImplementedError.database"), + ); + } + + // Get syndication targets + const { syndicationTargets } = publication; + if (syndicationTargets.length === 0) { + return response.json({ + success: "OK", + success_description: "No syndication targets have been configured", + }); + } + + // --- Single post mode (when source_url is provided) --- + if (sourceUrl) { + const postData = await getPostData(postsCollection, sourceUrl); + + if (!postData) { + return response.json({ + success: "OK", + success_description: `No post record available for ${sourceUrl}`, + }); + } + + const result = await syndicatePost({ + application, + publication, + postData, + bearerToken, + force: true, + }); + + // Include failed syndication targets in response + if (result.failedTargets) { + result.body.success_description += + ". The following target(s) did not return a URL: " + + result.failedTargets.join(" "); + } + + if (redirectUri && redirectUri.startsWith("/")) { + const message = encodeURIComponent(result.body.success_description); + return response.redirect(`${redirectUri}?success=${message}`); + } + + return response.json(result.body); + } + + // --- Batch mode (no source_url — process ALL pending posts) --- + const allPostData = await getAllPostData(postsCollection); + + if (!allPostData || allPostData.length === 0) { + return response.json({ + success: "OK", + success_description: "No posts awaiting syndication", + }); + } + + console.log( + `[syndication] Batch processing ${allPostData.length} post(s)`, + ); + + const results = []; + let successCount = 0; + let failCount = 0; + + for (const postData of allPostData) { + try { + const result = await syndicatePost({ + application, + publication, + postData, + bearerToken, + }); + + results.push({ + url: result.url, + success: true, + syndicatedUrls: result.syndicatedUrls, + ...(result.failedTargets && { + failedTargets: result.failedTargets, + }), + }); + + successCount++; + console.log( + `[syndication] Syndicated: ${result.url} (${result.syndicatedUrls.length} target(s))`, + ); + } catch (error) { + results.push({ + url: postData.properties?.url, + success: false, + error: error.message, + }); + + failCount++; + console.error( + `[syndication] Failed: ${postData.properties?.url} - ${error.message}`, + ); + } + + // Rate limit delay between posts (2 seconds) + if (allPostData.indexOf(postData) < allPostData.length - 1) { + await delay(2000); + } + } + + const description = + `Processed ${allPostData.length} post(s): ${successCount} succeeded, ${failCount} failed`; + + console.log(`[syndication] ${description}`); + + return response.json({ + success: "OK", + success_description: description, + results, + }); + } catch (error) { + next(error); + } + }, +}; diff --git a/node_modules/@indiekit/endpoint-syndicate/lib/token.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/lib/token.js similarity index 100% rename from node_modules/@indiekit/endpoint-syndicate/lib/token.js rename to node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/lib/token.js diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/lib/utils.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/lib/utils.js new file mode 100644 index 00000000..2cf1390c --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/lib/utils.js @@ -0,0 +1,219 @@ +/** + * Get post data for a single post + * @param {object} postsCollection - Posts database collection + * @param {string} url - URL of existing post (optional) + * @returns {Promise} Post data for given URL else most recent pending post + */ +export const getPostData = async (postsCollection, url) => { + let postData = {}; + + if (url) { + // Get item in database with matching URL + postData = await postsCollection.findOne({ + "properties.url": url, + }); + } else { + // Get most recent published post awaiting syndication + const items = await postsCollection + .find({ + "properties.mp-syndicate-to": { + $exists: true, + }, + // BUG FIX: Removed "properties.syndication": { $exists: false } + // That filter skipped partially syndicated posts (e.g., posted to + // Mastodon but not yet to Bluesky). syndicateToTargets() already + // calls hasSyndicationUrl() to skip completed targets. + "properties.post-status": { + $ne: "draft", + }, + }) + // eslint-disable-next-line unicorn/no-array-sort + .sort({ "properties.published": -1 }) + .limit(1) + .toArray(); + postData = items[0]; + } + + return postData; +}; + +/** + * Get ALL posts awaiting syndication (batch mode) + * @param {object} postsCollection - Posts database collection + * @returns {Promise} Array of post data objects + */ +export const getAllPostData = async (postsCollection) => { + const items = await postsCollection + .find({ + "properties.mp-syndicate-to": { + $exists: true, + }, + // No syndication filter — let syndicateToTargets() handle dedup + "properties.post-status": { + $ne: "draft", + }, + }) + // eslint-disable-next-line unicorn/no-array-sort + .sort({ "properties.published": -1 }) + .toArray(); + + return items; +}; + +/** + * Check if target already returned a syndication URL + * @param {Array} syndicatedUrls - Syndication URLs + * @param {string} syndicateTo - Syndication target + * @returns {boolean} Target returned a syndication URL + */ +export const hasSyndicationUrl = (syndicatedUrls, syndicateTo) => { + return syndicatedUrls.some((url) => { + try { + const { origin } = new URL(url); + return syndicateTo.includes(origin); + } catch { + return false; + } + }); +}; + +/** + * Get syndication target for syndication URL + * @param {Array} syndicationTargets - Publication syndication targets + * @param {string} syndicateTo - Syndication URL + * @returns {object|undefined} Publication syndication target + */ +export const getSyndicationTarget = (syndicationTargets, syndicateTo) => { + return syndicationTargets.find((target) => { + if (!target?.info?.uid) { + return; + } + + try { + const targetOrigin = new URL(target.info.uid).origin; + const syndicateToOrigin = new URL(syndicateTo).origin; + return targetOrigin === syndicateToOrigin; + } catch { + // syndicateTo or target uid is not a valid URL + return false; + } + }); +}; + +/** + * Syndicate URLs to configured syndication targets + * @param {object} publication - Publication configuration + * @param {object} properties - JF2 properties + * @param {object} [options] - Options + * @param {boolean} [options.force] - Force re-syndication (skip dedup check) + * @returns {Promise} Syndication target + */ +export const syndicateToTargets = async ( + publication, + properties, + { force = false } = {}, +) => { + const { syndicationTargets } = publication; + let syndicateTo = properties["mp-syndicate-to"]; + + // In force mode with no mp-syndicate-to, re-syndicate only to targets that + // were previously used (matched by origin against existing syndication URLs). + // This prevents re-syndication from sending to ALL targets when the user + // clicks "Syndicate" on an already-syndicated post. + if (!syndicateTo && force) { + const existingSyndication = properties.syndication || []; + if (existingSyndication.length > 0) { + // Extract origins from existing syndication URLs and match against targets + const existingOrigins = new Set( + existingSyndication.flatMap((url) => { + try { + return [new URL(url).origin]; + } catch { + return []; + } + }), + ); + syndicateTo = syndicationTargets + .filter((t) => { + try { + return t?.info?.uid && existingOrigins.has(new URL(t.info.uid).origin); + } catch { + return false; + } + }) + .map((t) => t.info.uid); + } + // If no existing syndication URLs, don't default to all — leave empty + } + + // BUG FIX: Was `Array.isArray` (always truthy, it's a function reference) + // Now correctly passes syndicateTo as the argument + const syndicateToUrls = Array.isArray(syndicateTo) + ? syndicateTo + : syndicateTo + ? [syndicateTo] + : []; + + // In force mode, remove old syndication URLs for targets being re-syndicated + // but keep URLs for other targets (e.g. keep Mastodon URL when re-syndicating Bluesky) + let syndicatedUrls = [...(properties.syndication || [])]; + if (force) { + syndicatedUrls = syndicatedUrls.filter((existingUrl) => { + try { + const existingOrigin = new URL(existingUrl).origin; + // Keep URL if its origin doesn't match any target being re-syndicated + return !syndicateToUrls.some((targetUrl) => { + try { + return targetUrl.includes(existingOrigin); + } catch { + return false; + } + }); + } catch { + return true; + } + }); + } + const failedTargets = []; + + console.info( + `[syndication] syndicateToTargets:`, + JSON.stringify({ + syndicateToUrls, + configuredTargets: syndicationTargets.map((t) => t.info?.uid), + postUrl: properties.url, + }), + ); + + for (const url of syndicateToUrls) { + const target = getSyndicationTarget(syndicationTargets, url); + const alreadySyndicated = !force && hasSyndicationUrl(syndicatedUrls, url); + + console.info( + `[syndication] Target ${url}: found=${!!target}, alreadySyndicated=${alreadySyndicated}`, + ); + + if (target && !alreadySyndicated) { + try { + const syndicatedUrl = await target.syndicate(properties, publication); + + if (syndicatedUrl) { + // Add syndicated URL to list of syndicated URLs + syndicatedUrls.push(syndicatedUrl); + } else { + // Add failed syndication target to list of failed targets + failedTargets.push(target.info.uid); + } + } catch (error) { + // Add failed syndication target to list of failed targets + failedTargets.push(target.info.uid); + console.error(error.message); + } + } + } + + return { + ...(failedTargets.length > 0 && { failedTargets }), + syndicatedUrls, + }; +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/package.json b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/package.json new file mode 100644 index 00000000..1d8020db --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/package.json @@ -0,0 +1,41 @@ +{ + "name": "@rmdes/indiekit-endpoint-syndicate", + "version": "1.0.0-beta.34", + "description": "Syndication endpoint for Indiekit. Fork of @indiekit/endpoint-syndicate with batch syndication support and bug fixes.", + "keywords": [ + "indiekit", + "indiekit-plugin", + "indieweb", + "syndication" + ], + "homepage": "https://github.com/rmdes/indiekit-endpoint-syndicate", + "author": { + "name": "Ricardo Mendes", + "url": "https://rmendes.net" + }, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "type": "module", + "main": "index.js", + "files": [ + "lib", + "index.js" + ], + "repository": { + "type": "git", + "url": "https://github.com/rmdes/indiekit-endpoint-syndicate.git" + }, + "dependencies": { + "@indiekit/error": "^1.0.0-beta.25", + "express": "^5.0.0", + "jsonwebtoken": "^9.0.0" + }, + "peerDependencies": { + "@indiekit/indiekit": ">=1.0.0-beta.25" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/README.md b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/README.md new file mode 100644 index 00000000..e6fc93c3 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/README.md @@ -0,0 +1,10 @@ +# @indiekit/frontend + +Frontend components for Indiekit. + +## Installation + +`npm install @indiekit/frontend` + +> [!NOTE] +> This package is installed alongside `@indiekit/indiekit` diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/app-icon-any.svg b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/app-icon-any.svg new file mode 100644 index 00000000..fd43ebc5 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/app-icon-any.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/app-icon-maskable.svg b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/app-icon-maskable.svg new file mode 100644 index 00000000..0e8f4d8c --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/app-icon-maskable.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/icon.svg b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/icon.svg new file mode 100644 index 00000000..ab1ec4c9 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/not-found.svg b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/not-found.svg new file mode 100644 index 00000000..71aa4989 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/not-found.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/offline.svg b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/offline.svg new file mode 100644 index 00000000..bf8a7fea --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/offline.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/plug-in.svg b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/plug-in.svg new file mode 100644 index 00000000..b856e847 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/assets/plug-in.svg @@ -0,0 +1,4 @@ + + + + diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/actions/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/actions/macro.njk new file mode 100644 index 00000000..548c5da4 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/actions/macro.njk @@ -0,0 +1,3 @@ +{% macro actions(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/actions/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/actions/styles.css new file mode 100644 index 00000000..a02f1831 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/actions/styles.css @@ -0,0 +1,19 @@ +.actions { + --anchor-decoration-color: transparent; + --icon-margin: var(--space-2xs); + display: flex; + flex-wrap: wrap; + font: var(--font-body); + gap: var(--space-2xs) var(--space-l); +} + +.actions__link { + margin: calc(var(--space-s) * -1); + padding: var(--space-s); + white-space: nowrap; +} + +.actions__link--warning { + --anchor-color: var(--color-error); + --anchor-color-hover: var(--color-error-variant); +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/actions/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/actions/template.njk new file mode 100644 index 00000000..2d0144a9 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/actions/template.njk @@ -0,0 +1,19 @@ +{% macro _actionLink(action) %} + + {{- icon(action.icon) if action.icon -}} + {{- action.text | safe -}} + +{% endmacro %} +{%- if opts.items.length === 1 %} +
+ {{ _actionLink(opts.items[0]) | trim }} +
+{%- else %} +
    + {% for item in opts.items %}{% if item.text %} +
  • + {{ _actionLink(item) | trim }} +
  • + {% endif %}{% endfor %} +
+{%- endif %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/add-another/index.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/add-another/index.js new file mode 100644 index 00000000..3e978e5a --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/add-another/index.js @@ -0,0 +1,170 @@ +const focusableSelector = `button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]`; + +export const AddAnotherComponent = class extends HTMLElement { + connectedCallback() { + this.$addButtonTemplate = this.querySelector("#add-button"); + this.$deleteButtonTemplate = this.querySelector("#delete-button"); + this.$$fields = this.querySelectorAll(".field"); + this.$list = this.querySelector(".add-another__list"); + + this.updateItems(); + this.createAddButton(); + } + + /** + * Add item to list + * @param {Event} event - Add button event + */ + add(event) { + event.preventDefault(); + const $newItem = this.createItem(); + this.$list.append($newItem); + this.updateItems(); + $newItem.querySelector(focusableSelector).focus(); + } + + /** + * Delete item from list + * @param {Event} event - Delete button event + */ + delete(event) { + event.preventDefault(); + event.target.closest("li").remove(); + this.updateItems(); + this.focusHeading(); + } + + /** + * Get heading + * @returns {HTMLLegendElement} - Group legend + */ + getHeading() { + return this.querySelector("legend"); + } + + /** + * Focus heading + */ + focusHeading() { + const $heading = this.getHeading(); + + $heading.setAttribute("tabindex", "-1"); + $heading.focus(); + } + + /** + * Create add button + */ + createAddButton() { + let $addButton = this.$addButtonTemplate.content.cloneNode(true); + + this.append($addButton); + + $addButton = this.querySelector(".add-another__add"); + $addButton.addEventListener("click", (event) => this.add(event)); + } + + /** + * Get delete button + * @param {HTMLElement} element - Containing element + * @returns {HTMLButtonElement} - Delete button + */ + getDeleteButton(element) { + return element.querySelector(".add-another__delete"); + } + + /** + * Create delete button + * @param {HTMLElement} element - Containing element + */ + createDeleteButton(element) { + const $deleteButton = + this.$deleteButtonTemplate.content.firstElementChild.cloneNode(true); + + element.append($deleteButton); + } + + /** + * Update delete button + * @param {HTMLElement} element - Containing element + */ + updateDeleteButton(element) { + const $deleteButton = this.getDeleteButton(element); + $deleteButton.setAttribute("aria-labelledby", `delete-title ${element.id}`); + $deleteButton.addEventListener("click", (event) => this.delete(event)); + } + + /** + * Create new item by cloning first item in list and updating its attributes + * @returns {HTMLLIElement} - List item containing form field(s) + */ + createItem() { + const $$items = this.querySelectorAll(".add-another__list-item"); + const $item = $$items[0].cloneNode(true); + const uid = Date.now().toString(); + + const $$fields = $item.querySelectorAll(".field--error"); + for (const $field of $$fields) { + $field.classList.remove("field--error"); + } + + const $$errorMessages = $item.querySelectorAll(".error-message"); + for (const $errorMessage of $$errorMessages) { + $errorMessage.remove(); + } + + const $$inputs = $item.querySelectorAll("input, select, textarea"); + for (const $input of $$inputs) { + $input.id = $input.id.replace("-0", `-${uid}`); + $input.name = $input.name.replace("[0]", `[${uid}]`); + $input.value = ""; + $input.classList.remove( + "input--error", + "select--error", + "textarea--error", + ); + } + + const $$labels = $item.querySelectorAll("label"); + for (const $label of $$labels) { + const forAttribute = $label.getAttribute("for"); + $label.setAttribute("for", forAttribute.replace("-0", `-${uid}`)); + } + + $item.id = `${this.id}-${uid}`; + + return $item; + } + + /** + * Update all items + * - Update ID’s to use for labelling remove button + * - Update ARIA label to reference item’s position in list + * - Add remove buttons (or remove if only one item remaining in list) + */ + updateItems() { + const $$items = this.querySelectorAll(".add-another__list-item"); + + for (const [index, $item] of $$items.entries()) { + $item.id = $item.id || `${this.id}-${index}`; + $item.setAttribute("aria-label", `Item ${index + 1}`); + + // If no delete button, add one (if more than 1 item in list) + // Used when initializing + if (!this.getDeleteButton($item) && $$items.length > 1) { + this.createDeleteButton($item); + } + + // If has delete button + if (this.getDeleteButton($item)) { + if ($$items.length === 1) { + // If only 1 item in list, remove button + this.getDeleteButton($item).remove(); + } else { + // Else update button attributes + this.updateDeleteButton($item); + } + } + } + } +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/add-another/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/add-another/macro.njk new file mode 100644 index 00000000..9ed37e9e --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/add-another/macro.njk @@ -0,0 +1,3 @@ +{% macro addAnother(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/add-another/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/add-another/styles.css new file mode 100644 index 00000000..038f7223 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/add-another/styles.css @@ -0,0 +1,63 @@ +add-another { + --counter-size: 1.66667em; + display: block; +} + +.add-another__list { + counter-reset: items; +} + +.add-another__list-item { + --label-font: var(--font-fieldset-label); + margin-block-end: var(--space-m); + padding-block: var(--space-xs); + padding-inline-start: calc(var(--counter-size) + var(--space-s)); + position: relative; + + & .fieldset--group { + margin-block-start: 0; + } + + &::before { + align-items: center; + background-color: var(--color-offset); + block-size: var(--counter-size); + border-radius: var(--border-radius-small); + color: var(--color-on-offset); + content: counter(items); + counter-increment: items; + display: flex; + font: var(--font-label); + inline-size: var(--counter-size); + inset: 0; + justify-content: center; + position: absolute; + } + + &:not([hidden]) { + display: flex; + } + + & > :first-child { + flex: 1; + } +} + +.add-another__add.button { + display: flex; + inline-size: calc(100% - var(--counter-size) - var(--space-s)); + margin-block-start: 0; + margin-inline-start: calc(var(--counter-size) + var(--space-s)); +} + +.add-another__delete.button { + --button-padding: 0; + --icon-size: 0.875em; + + block-size: var(--counter-size); + display: flex; + inline-size: var(--counter-size); + inset-block-start: var(--counter-size); + inset-inline-start: 0; + position: absolute; +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/add-another/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/add-another/template.njk new file mode 100644 index 00000000..fa352efb --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/add-another/template.njk @@ -0,0 +1,39 @@ +{% from "../button/macro.njk" import button with context %} +{% from "../field/macro.njk" import field with context %} +{% from "../fieldset/macro.njk" import fieldset with context %} +{% set id = opts.id or opts.name | slugify({ decamelize: true }) %} +{# `fieldset` is false by default #} +{%- set hasFieldset = true if opts.fieldset else false %} +{# Capture the HTML so we can optionally nest it within a fieldset #} +{%- set innerHtml %}{{ caller() if caller }}{% endset %} +{% call field({ + element: "add-another", + classes: opts.field.classes, + attributes: { + id: id + } +}) %} +{% if opts.fieldset %} + {% call fieldset({ + describedBy: describedBy, + classes: opts.fieldset.classes, + attributes: opts.fieldset.attributes, + legend: opts.fieldset.legend + }) %}{{ innerHtml | trim | safe }}{% endcall %} +{% else %} + {{ innerHtml | trim | safe }} +{% endif %} + + +{% endcall %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/app/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/app/styles.css new file mode 100644 index 00000000..6a0fd4d0 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/app/styles.css @@ -0,0 +1,63 @@ +.app { + background-color: var(--color-offset); + font-family: var(--font-family-sans); + + /* Mobile: stacked column layout */ + display: flex; + flex-direction: column; + justify-content: space-between; + + /* Desktop: sidebar + content grid */ + @media (width >= 48rem) { + display: grid; + grid-template-columns: 15rem 1fr; + grid-template-rows: 1fr auto; + grid-template-areas: + "sidebar main" + "sidebar footer"; + min-block-size: 100dvh; + } +} + +.app--minimalui { + --container-max-inline-size: 36rem; + + /* Always flex column for minimalui, never grid */ + display: flex; + flex-direction: column; + justify-content: space-between; + + & .header, + & .footer { + border: 0; + } + + & .header__container, + & .footer__container { + align-items: center; + inline-size: auto; + } + + & .main { + @media (width > 36rem) { + border-radius: var(--border-radius-large); + flex: 0; + margin: auto; + overflow: hidden; + } + } + + & .main__container { + padding-block: var(--space-l); + } + + & .notification { + border-start-end-radius: var(--border-radius-large); + border-start-start-radius: var(--border-radius-large); + } + + & .notification + .main__container { + border-start-end-radius: 0; + border-start-start-radius: 0; + } +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/authorize/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/authorize/macro.njk new file mode 100644 index 00000000..af3dd141 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/authorize/macro.njk @@ -0,0 +1,3 @@ +{% macro authorize(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/authorize/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/authorize/styles.css new file mode 100644 index 00000000..c71db984 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/authorize/styles.css @@ -0,0 +1,23 @@ +.authorize { + --authorize-client-icon-size: 4rem; + line-height: var(--line-height-tight); + + &:has(.authorize__client-icon) { + min-block-size: var(--authorize-client-icon-size); + } + + & a { + font-weight: 600; + } +} + +.authorize__client-icon { + background-color: var(--color-neutral90); /* Ignore color-scheme */ + block-size: var(--authorize-client-icon-size); + border-radius: var(--border-radius-small); + box-shadow: inset 0 0 0 1px var(--color-shadow); + float: inline-start; + inline-size: var(--authorize-client-icon-size); + margin-inline-end: var(--space-s); + padding: var(--space-2xs); +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/authorize/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/authorize/template.njk new file mode 100644 index 00000000..03095e29 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/authorize/template.njk @@ -0,0 +1,14 @@ +{% set clientLink = opts.client.name | friendlyUrl | linkTo(opts.client.url) %} +{% set meLink = opts.me | friendlyUrl | replace("/", "/") | linkTo(opts.me) %} +

+ {% if opts.client.logo %} + + + + {% endif %} + {{ __(opts.key, { + client: "__client__", + me: "__me__" + }) | replace("__client__", clientLink) | replace("__me__", meLink) | markdown("inline") | safe }} +

\ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/avatar/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/avatar/macro.njk new file mode 100644 index 00000000..e1363014 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/avatar/macro.njk @@ -0,0 +1,3 @@ +{% macro avatar(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/avatar/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/avatar/styles.css new file mode 100644 index 00000000..550d01f9 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/avatar/styles.css @@ -0,0 +1,23 @@ +.avatar { + background: + radial-gradient( + circle, + var(--color-offset-variant-darker) 50%, + transparent 50% + ) + 50% 20%/75% 75% no-repeat, + radial-gradient( + circle, + var(--color-offset-variant-darker) 50%, + transparent 50% + ) + 50% -50%/100% 160% no-repeat, + var(--color-offset); + block-size: var(--avatar-size, 3rem); + border-radius: var(--border-radius-small); + inline-size: var(--avatar-size, 3rem); + max-inline-size: none; + object-fit: cover; + outline: var(--border-width-thin) solid var(--color-shadow); + outline-offset: calc(var(--border-width-thin) * -1); +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/avatar/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/avatar/template.njk new file mode 100644 index 00000000..2cbaacfc --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/avatar/template.njk @@ -0,0 +1 @@ +{{ opts.alt }} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/back-link/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/back-link/macro.njk new file mode 100644 index 00000000..5c587f64 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/back-link/macro.njk @@ -0,0 +1,3 @@ +{% macro backLink(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/back-link/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/back-link/styles.css new file mode 100644 index 00000000..db8ab87b --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/back-link/styles.css @@ -0,0 +1,46 @@ +.back-link { + --anchor-decoration-color: transparent; + --back-link-padding: var(--space-s); + --back-link-chevron-rotation: -45deg; + --back-link-chevron-size: 0.5em; + --back-link-chevron-thickness: var(--text-thickness); + + display: inline-block; + font: var(--font-caption); + margin-inline: calc(var(--back-link-padding) * -1); + padding: var(--back-link-padding); + padding-block-start: var(--back-link-padding); + padding-inline-start: calc(var(--back-link-padding) * 2); + position: relative; + + &::before { + block-size: var(--back-link-chevron-size); + border: 0 solid currentcolor; + border-block-start-width: var(--back-link-chevron-thickness); + border-inline-start-width: var(--back-link-chevron-thickness); + content: ""; + display: block; + inline-size: var(--back-link-chevron-size); + inset-block-start: calc( + var(--back-link-padding) + var(--back-link-chevron-size) + ); + inset-inline-start: var(--back-link-padding); + position: absolute; + transform: rotate(var(--back-link-chevron-rotation)); + } + + &:focus-visible::before, + &:hover::before { + --back-link-chevron-thickness: calc(var(--text-thickness) * 2); + } + + &::after { + content: ""; + inset: 0; + position: absolute; + } +} + +:root[dir="rtl"] .back-link { + --back-link-chevron-rotation: 45deg; +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/back-link/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/back-link/template.njk new file mode 100644 index 00000000..0fe9fe5f --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/back-link/template.njk @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/badge/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/badge/macro.njk new file mode 100644 index 00000000..a549e62e --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/badge/macro.njk @@ -0,0 +1,3 @@ +{% macro badge(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/badge/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/badge/styles.css new file mode 100644 index 00000000..0ce47427 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/badge/styles.css @@ -0,0 +1,85 @@ +.badge { + /* + * 1. When a user customises their colours, often the background is removed. + * Adding an outline ensures that badge still keeps it’s meaning. + * + * @link https://accessibility.blog.gov.uk/2017/03/27/how-users-change-colours-on-websites/ + * + * 2. Adjust baseline to visually center text within pill shape. + */ + --icon-margin: var(--space-xs); + align-items: center; + background-color: var(--color-primary); + border-radius: var(--space-l); + color: var(--color-on-primary); + display: inline flow-root; + font: var(--font-caption); + font-weight: 500; + outline: var(--border-width-thick) solid transparent; /* 1 */ + outline-offset: calc(var(--border-width-thick) * -1); /* 1 */ + padding-block-end: calc(var(--space-2xs) - 0.0625rem); /* 2 */ + padding-block-start: calc(var(--space-2xs) + 0.0625rem); /* 2 */ + padding-inline: var(--space-m); + white-space: nowrap; + + & + .badge { + margin-inline-start: var(--space-2xs); + } + + & .icon { + inset-block-start: -0.125em; + } +} + +.badge--small { + font-size: 0.75rem; + padding-block-end: calc(var(--space-2xs) / 2 - 0.0625rem); + padding-block-start: calc(var(--space-2xs) / 2 + 0.0625rem); + padding-inline: var(--space-s); +} + +.badge--green { + background: var(--color-green50); + color: var(--color-green10); +} + +.badge--purple { + background: var(--color-purple45); + color: var(--color-neutral99); +} + +.badge--red { + background: var(--color-red45); + color: var(--color-neutral99); +} + +.badge--yellow { + background: var(--color-yellow50); + color: var(--color-neutral10); +} + +.badge--offset { + background: var(--color-offset); + box-shadow: inset 0 0 0 1px var(--color-shadow); + color: var(--color-on-offset); +} + +.badge--offset-green { + background: var(--color-green90); + color: var(--color-green10); +} + +.badge--offset-purple { + background: var(--color-purple90); + color: var(--color-purple10); +} + +.badge--offset-red { + background: var(--color-red90); + color: var(--color-red10); +} + +.badge--offset-yellow { + background: var(--color-yellow90); + color: var(--color-yellow10); +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/badge/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/badge/template.njk new file mode 100644 index 00000000..8d0d89cb --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/badge/template.njk @@ -0,0 +1,7 @@ + + {{- icon(opts.icon) if opts.icon -}} + {{- opts.text | safe -}} + \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/bookmarklet/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/bookmarklet/macro.njk new file mode 100644 index 00000000..0f0cdb50 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/bookmarklet/macro.njk @@ -0,0 +1,3 @@ +{% macro bookmarklet(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/bookmarklet/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/bookmarklet/styles.css new file mode 100644 index 00000000..ea08ddf3 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/bookmarklet/styles.css @@ -0,0 +1,18 @@ +.bookmarklet { + --anchor-color: var(--color-on-offset); + --anchor-decoration-line: none; + background-color: var(--color-offset); + border: var(--border-width-thin) solid var(--color-shadow); + border-radius: var(--border-radius-small); + display: inline-block; + font: var(--font-caption); + margin-block: var(--space-xs); + padding-block: calc(var(--space-xs) / 2); + padding-inline: var(--space-xs); + white-space: nowrap; + + &:hover { + background-color: var(--color-offset-variant); + border-color: currentcolor; + } +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/bookmarklet/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/bookmarklet/template.njk new file mode 100644 index 00000000..0104abe9 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/bookmarklet/template.njk @@ -0,0 +1 @@ +{{ opts.text }} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/button/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/button/macro.njk new file mode 100644 index 00000000..f7d28e63 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/button/macro.njk @@ -0,0 +1,3 @@ +{% macro button(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/button/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/button/styles.css new file mode 100644 index 00000000..04df9c65 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/button/styles.css @@ -0,0 +1,108 @@ +.button, +::file-selector-button { + --anchor-color-hover: var(--button-color, var(--color-on-primary)); + --anchor-decoration-line: none; + --icon-size: 1.5em; + align-items: center; + background-color: var(--button-background-color, var(--color-primary)); + border: var(--input-border-width) solid transparent; + border-block-end-color: var(--color-shadow); + border-radius: var(--border-radius-small); + color: var(--button-color, var(--color-on-primary)); + display: inline-block; + font: var(--button-font, var(--font-label)); + font-weight: var(--button-font-weight, 500); + justify-content: center; + line-height: var(--line-height-loose); + margin-block-end: var(--input-border-width-focus-offset); + padding-block: calc( + calc(var(--button-padding, var(--space-m)) / 2) - var(--input-border-width) + ); + padding-inline: var(--button-padding, var(--space-m)); +} + +.button--secondary, +::file-selector-button { + --button-background-color: var(--color-offset-variant); + --button-background-hover-color: var(--color-offset-variant-darker); + --button-color: var(--color-on-offset); + --button-font-weight: normal; +} + +/* Icon button with text moved to SVG title */ +.button:has(title) { + --icon-margin: 0; + font: unset; + padding: calc(var(--button-padding, var(--space-m)) / 2); +} + +.button:disabled { + --button-background-color: var(--color-offset); + --button-color: var(--color-on-offset); + border-block-end-color: transparent; +} + +.button:not(:disabled):hover, +::file-selector-button:hover { + background-color: var( + --button-background-hover-color, + var(--color-primary-variant) + ); +} + +.button--secondary-on-offset { + --button-background-color: var(--color-background); + --button-background-hover-color: var(--color-offset-variant); + --button-color: var(--color-on-background); +} + +.button--warning { + --button-color: var(--color-on-error); + background-color: var(--color-error); + + &:not(:disabled):hover { + background-color: var(--color-error-variant); + } +} + +.button--small { + --button-font: var(--font-caption); + --button-font-weight: 500; + --button-padding: var(--space-s); + --icon-size: 1em; +} + +.button--block { + display: flex; + inline-size: 100%; +} + +.button--inline { + --icon-margin: 0; + --icon-size: 1.5em; + display: inline flex; + flex-direction: column; + gap: var(--space-3xs); + padding-block-end: calc(var(--space-m) * 0.75); + padding-block-start: var(--space-m); +} + +.button-grid { + display: grid; + gap: var(--space-xs); + grid-template-columns: repeat(auto-fill, minmax(6em, 1fr)); +} + +.button-group { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: var(--space-m); + text-align: center; + + @media (width < 30rem) { + & > * { + inline-size: 100%; + } + } +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/button/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/button/template.njk new file mode 100644 index 00000000..9f3bbffa --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/button/template.njk @@ -0,0 +1,17 @@ +{%- if opts.href %} + + {{- icon(opts.icon, opts.iconText) if opts.icon -}} + {{- opts.text | safe -}} + +{%- else %} + +{%- endif %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card-grid/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card-grid/macro.njk new file mode 100644 index 00000000..a748e649 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card-grid/macro.njk @@ -0,0 +1,3 @@ +{% macro cardGrid(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card-grid/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card-grid/styles.css new file mode 100644 index 00000000..6d52d838 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card-grid/styles.css @@ -0,0 +1,9 @@ +.card-grid { + display: grid; + gap: var(--space-l); + grid-template-columns: repeat(auto-fill, minmax(var(--min-card-size), 1fr)); +} + +.card-grid__item { + display: flex; +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card-grid/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card-grid/template.njk new file mode 100644 index 00000000..241f7441 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card-grid/template.njk @@ -0,0 +1,8 @@ +{% from "../card/macro.njk" import card with context %} +
    + {% for item in opts.items %} +
  1. + {{ card(item) | indent(4) }} +
  2. + {% endfor %} +
\ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card/macro.njk new file mode 100644 index 00000000..a07a6460 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card/macro.njk @@ -0,0 +1,3 @@ +{% macro card(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card/styles.css new file mode 100644 index 00000000..23662162 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card/styles.css @@ -0,0 +1,93 @@ +.card { + --anchor-decoration-color: transparent; + --icon-margin: var(--space-2xs); + --icon-size: 1.125em; + background-color: var(--color-offset); + border-radius: var(--border-radius-small); + display: flex; + flex-grow: 1; + flex-wrap: wrap; + overflow: hidden; + position: relative; + + &:hover { + background-color: var(--color-offset-variant); + } + + &:has(:focus-visible) { + overflow: visible; + } +} + +.card__photo { + flex-basis: var(--card-photo-size, 8em); + flex-grow: 1; + + & :is(img, video) { + aspect-ratio: 1; + background-color: var(--color-offset); + block-size: 100%; + display: block; + inline-size: 100%; + object-fit: cover; + } +} + +.card__body { + display: flex; + flex-basis: 0; + flex-direction: column; + flex-grow: 999; + gap: calc(var(--card-space, var(--space-s)) / 2); + margin-block-start: 0; + min-inline-size: 67%; + padding: var(--card-space, var(--space-s)); + + a:not([rel="bookmark"]) { + position: relative; + z-index: 1; + } +} + +.card__title { + font: var(--card-title-font, var(--font-subhead)); +} + +.card a[rel="bookmark"] { + &::before { + border-radius: var(--border-radius-small); + content: ""; + display: block; + inset: 0; + position: absolute; + } + + &:focus-visible { + box-shadow: none; + position: initial; + } + + &:focus-visible::before { + box-shadow: 0 0 0 var(--focus-width) var(--color-focus); + } +} + +.card__meta { + --flow-line-measure: 40rem; + --prose-font: var(--font-caption); + margin-block-start: 0; +} + +.card__footer { + align-items: start; + display: flex; + flex-direction: column; + gap: var(--space-xs); + margin-block-start: auto; + padding-block-start: var(--space-2xs); + + & time { + color: var(--color-on-offset); + font-size: var(--font-size-xs); + } +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card/template.njk new file mode 100644 index 00000000..7eb93acd --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/card/template.njk @@ -0,0 +1,46 @@ +{% from "../badge/macro.njk" import badge with context %} +{% from "../prose/macro.njk" import prose with context %} +{% set srcOnError = opts.photo.srcOnError or "/assets/not-found.svg" %} +{% set headingLevel = opts.headingLevel or 2 %} +
+ {% if opts.photo %} +
+ {{ opts.photo.alt }} +
+ {% endif %} +
+ {% if opts.title %} + + {% if opts.url and not opts.permalink %} + + {% endif %} + {{- icon(opts.icon) if opts.icon -}} + {{- opts.title | safe -}} + {% if opts.url and not opts.permalink %} + + {% endif %} + + {% endif %} + {{ prose({ + classes: "card__meta", + text: opts.description.text, + html: opts.description.html + }) | indent(4) if opts.description }} + {% if opts.published or opts.badges %} + + {% endif %} +
+
\ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/character-count/index.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/character-count/index.js new file mode 100644 index 00000000..3dc3a662 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/character-count/index.js @@ -0,0 +1,185 @@ +/** + * Based on the Character count component from the GOV.UK Design System. + * Provides a visible, real-time character and word count, and visually + * hidden counter that announces updates to screen readers periodically. + * + * This component provides a character and word count, and doesn’t inform or + * enforce any arbitrary content limits. + * @see {@link https://design-system.service.gov.uk/components/character-count} + * @see {@link https://dav-idc.com/making-a-character-count-component-more-accessible} + */ +export const CharacterCountComponent = class extends HTMLElement { + constructor() { + super(); + + this.lastInputTimestamp = undefined; + this.lastInputValue = ""; + this.valueChecker = undefined; + + this.$screenReaderCountMessage = document.createElement("p"); + this.$screenReaderCountMessage.className = "-!-visually-hidden"; + this.$screenReaderCountMessage.setAttribute("aria-live", "polite"); + this.$textareaDescription.insertAdjacentElement( + "afterend", + this.$screenReaderCountMessage, + ); + + this.$visibleCountMessage = document.createElement("p"); + this.$visibleCountMessage.className = this.$textareaDescription.className; + this.$visibleCountMessage.setAttribute("aria-hidden", "true"); + } + + connectedCallback() { + this.i18nChar = this.getAttribute("i18n-char") || `%s character`; + this.i18nChars = this.getAttribute("i18n-chars") || `%s characters`; + this.i18nWord = this.getAttribute("i18n-word") || `%s word`; + this.i18nWords = this.getAttribute("i18n-words") || `%s words`; + + this.$textarea = this.querySelector("textarea"); + this.$textarea.addEventListener("keyup", this.#handleKeyUp.bind(this)); + this.$textarea.addEventListener("focus", this.#handleFocus.bind(this)); + this.$textarea.addEventListener("blur", this.#handleBlur.bind(this)); + window.addEventListener("pageshow", this.#updateCountMessages.bind(this)); + this.#updateCountMessages(); + + this.$textareaDescription = this.querySelector( + `#${this.$textarea.id}-info`, + ); + this.$textareaDescription.classList.add("-!-visually-hidden"); + this.$textareaDescription.insertAdjacentElement( + "afterend", + this.$visibleCountMessage, + ); + } + + /** + * Update visible character counter and keep track of when the last update + * happened for each keypress + * @access private + */ + #handleKeyUp() { + this.#updateVisibleCountMessage(); + this.lastInputTimestamp = Date.now(); + } + + /** + * Handle focus event + * + * Speech recognition software such as Dragon NaturallySpeaking will modify + * a field by directly changing its `value`. These changes don’t trigger + * events in JavaScript, so we need to poll to handle when and if they occur. + * + * Once the keyup event hasn’t been detected for at least 1000 ms (1s), check + * if textarea value has changed and update count message if it has. + * + * This is so that update triggered by manual comparison doesn’t conflict + * with debounced KeyboardEvent updates. + * @access private + */ + #handleFocus() { + this.valueChecker = globalThis.setInterval(() => { + if ( + !this.lastInputTimestamp || + Date.now() - 500 >= this.lastInputTimestamp + ) { + this.#updateIfValueChanged(); + } + }, 1000); + } + + /** + * Stop checking textarea value once it no longer has focus + * @access private + */ + #handleBlur() { + if (this.valueChecker) { + globalThis.clearInterval(this.valueChecker); + } + } + + /** + * Update count message if textarea value has changed + * @access private + */ + #updateIfValueChanged() { + if (this.$textarea.value !== this.lastInputValue) { + this.lastInputValue = this.$textarea.value; + this.#updateCountMessages(); + } + } + + /** + * Helper method to update both visible and screen reader-specific counters + * simultaneously (e.g. on init) + * @access private + */ + #updateCountMessages() { + this.#updateVisibleCountMessage(); + this.#updateScreenReaderCountMessage(); + } + + /** + * Update visible count message + * @access private + */ + #updateVisibleCountMessage() { + this.$visibleCountMessage.textContent = this.#countMessage; + } + + /** + * Update screen reader count message + * @access private + */ + #updateScreenReaderCountMessage() { + this.$screenReaderCountMessage.textContent = this.#countMessage; + } + + /** + * Count number of characters (or words) in given string + * @access private + * @param {string} string - The text to count the characters of + * @param {boolean} [countWords] - Count words instead of characters + * @returns {number} the number of characters (or words) in the text + */ + #count(string, countWords = false) { + if (countWords) { + // Matches consecutive non-whitespace characters up to a word boundary + const tokens = string.match(/\S+\b/g) ?? []; + return tokens.length; + } + + return string.length; + } + + /** + * Get count message + * @access private + * @returns {string} Status message + */ + get #countMessage() { + return this.#formatCountMessage(this.$textarea.value); + } + + /** + * Format and localise count shown to users + * @access private + * @param {string} string - Number of characters remaining + * @returns {string} Status message + */ + #formatCountMessage(string) { + const characters = this.#count(string); + const words = this.#count(string, true); + + let characterCount = this.i18nChars.replace("%s", String(characters)); + if (characters === 1) { + characterCount = this.i18nChar.replace("%s", String(characters)); + } + + let wordCount = this.i18nWords.replace("%s", String(words)); + if (words === 1) { + wordCount = this.i18nWord.replace("%s", String(words)); + } + + return `${characterCount}, ${wordCount}`; + } +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/character-count/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/character-count/macro.njk new file mode 100644 index 00000000..c27d5764 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/character-count/macro.njk @@ -0,0 +1,3 @@ +{% macro characterCount(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/character-count/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/character-count/styles.css new file mode 100644 index 00000000..dd948539 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/character-count/styles.css @@ -0,0 +1,3 @@ +character-count { + display: block; +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/character-count/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/character-count/template.njk new file mode 100644 index 00000000..dcabd6fb --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/character-count/template.njk @@ -0,0 +1,14 @@ +{% from "../hint/macro.njk" import hint with context %} +{% from "../textarea/macro.njk" import textarea with context %} +{%- set id = opts.id or opts.name | slugify({ decamelize: true }) -%} + + {{- textarea(opts) }} + {{ hint({ + text: " ", + id: id + "-info" + }) | trim | indent(2) }} + \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/checkboxes/index.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/checkboxes/index.js new file mode 100644 index 00000000..e8d1b274 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/checkboxes/index.js @@ -0,0 +1,130 @@ +/** + * Based by the checkboxes component provided by GOV.UK Frontend + * @see {@link https://github.com/alphagov/govuk-frontend/blob/main/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs} + */ +export const CheckboxesFieldComponent = class extends HTMLElement { + connectedCallback() { + this.$$inputTargets = this.querySelectorAll("input"); + for (const $input of this.$$inputTargets) { + const targetId = $input.dataset.ariaControls; + const $conditional = document.querySelector(`#${targetId}`); + + // Promote `data-aria-controls` attribute to an `aria-controls` attribute + // so that relationship is exposed in the AOM + if (targetId && $conditional) { + $input.setAttribute("aria-controls", targetId); + delete $input.dataset.ariaControls; + } + + $input.addEventListener("input", (event) => + this.toggleConditional(event), + ); + } + + this.syncAllConditionalReveals(); + } + + /** + * Input event handler + * + * Sync state of conditional reveal with checkbox state + * @param {Event} event - Event + */ + toggleConditional(event) { + const $target = event.target; + + // If the checkbox conditionally-reveals content, sync the state + if ($target.hasAttribute("aria-controls")) { + this.syncConditionalRevealWithInputState($target); + } + + // No further behaviour needed for unchecking + if (!$target.checked) { + return; + } + + // Handle ‘exclusive’ checkbox behaviour (ie “None of these”) + if ($target.dataset.behaviour === "exclusive") { + this.unCheckAllInputsExcept($target); + } else { + this.unCheckExclusiveInputs($target); + } + } + + /** + * Sync conditional reveal states for all checkboxes in this checkbox group + */ + syncAllConditionalReveals() { + for (const $input of this.$$inputTargets) { + this.syncConditionalRevealWithInputState($input); + } + } + + /** + * Synchronise visibility of conditional reveal, and its accessible + * state, with the input’s checked state. + * @param {HTMLInputElement} input - Checkbox input + */ + syncConditionalRevealWithInputState(input) { + const targetId = input.getAttribute("aria-controls"); + + if (!targetId) { + return; + } + + const $target = document.querySelector(`#${targetId}`); + + if ($target && $target.classList.contains("checkboxes__conditional")) { + const inputIsChecked = input.checked; + + input.setAttribute("aria-expanded", inputIsChecked.toString()); + $target.classList.toggle( + "checkboxes__conditional--hidden", + !inputIsChecked, + ); + } + } + + /** + * Uncheck other checkboxes + * + * Find any other checkbox inputs with same name value, and uncheck them. + * Useful when a “None of these” checkbox is checked. + * @param {HTMLInputElement} $input - Checkbox input + */ + unCheckAllInputsExcept($input) { + const $$inputsWithSameName = document.querySelectorAll( + `input[type="checkbox"][name="${$input.name}"]`, + ); + + for (const $inputWithSameName of $$inputsWithSameName) { + const hasSameFormOwner = $input.form === $inputWithSameName.form; + if (hasSameFormOwner && $inputWithSameName !== $input) { + $inputWithSameName.checked = false; + this.syncConditionalRevealWithInputState($inputWithSameName); + } + } + } + + /** + * Uncheck exclusive checkboxes + * + * Find any checkbox inputs with same name value and has the ‘exclusive’ + * behaviour, and uncheck them. This helps prevent someone checking both a + * regular checkbox and a “None of these” checkbox in the same fieldset. + * @param {HTMLInputElement} $input - Checkbox input + */ + unCheckExclusiveInputs($input) { + const $$inputsWithSameNameAndExclusiveBehaviour = document.querySelectorAll( + `input[data-behaviour="exclusive"][type="checkbox"][name="${$input.name}"]`, + ); + + for (const $exclusiveInput of $$inputsWithSameNameAndExclusiveBehaviour) { + const hasSameFormOwner = $input.form === $exclusiveInput.form; + if (hasSameFormOwner) { + $exclusiveInput.checked = false; + this.syncConditionalRevealWithInputState($exclusiveInput); + } + } + } +}; diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/checkboxes/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/checkboxes/macro.njk new file mode 100644 index 00000000..d97051ac --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/checkboxes/macro.njk @@ -0,0 +1,3 @@ +{% macro checkboxes(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/checkboxes/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/checkboxes/styles.css new file mode 100644 index 00000000..958356ff --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/checkboxes/styles.css @@ -0,0 +1,96 @@ +checkboxes-field { + --checkbox-size: 1.7778em; + --checkbox-space: var(--space-s); + --checkbox-padding-inline: calc(var(--checkbox-size) + var(--checkbox-space)); + display: block; +} + +.checkboxes { + display: flex; + flex-direction: column; +} + +.checkboxes__item { + margin-block: var(--space-2xs); + padding-block: var(--space-xs); + padding-inline-start: var(--checkbox-padding-inline); + position: relative; +} + +.checkboxes__input { + appearance: none; + background: var(--color-background); + block-size: var(--checkbox-size); + border: var(--input-border-width) solid var(--color-outline-variant); + border-radius: var(--border-radius-small); + cursor: pointer; + inline-size: var(--checkbox-size); + inset: 0; + position: absolute; + z-index: 1; + + &:checked { + background: var(--color-background) + url('data:image/svg+xml;utf8,') + center center / 100% auto no-repeat; + + @media (prefers-color-scheme: dark) { + background-image: url('data:image/svg+xml;utf8,'); + } + } + + &:focus-visible { + border-color: var(--color-on-background); + border-width: var(--input-border-width-focus); + } + + &:disabled, + &[readonly] { + background-color: var(--color-offset); + border-color: var(--color-offset); + color: var(--color-on-offset); + cursor: default; + + & + .checkboxes__label { + cursor: default; + } + } +} + +.checkboxes__label.label { + --label-font: var(--font-fieldset-label); + cursor: pointer; + margin: 0; + touch-action: manipulation; /* remove 300ms pause on mobile */ +} + +.checkboxes__hint { + inline-size: 100%; +} + +/* Divider (‘or’) */ +.checkboxes__divider { + inline-size: var(--checkbox-size); + text-align: center; +} + +/* Conditonal reveals */ +.checkboxes__conditional { + --label-font: var(--font-fieldset-label); + padding-inline-start: var(--checkbox-padding-inline); + position: relative; + + &::before { + border-inline-start: var(--input-border-width) solid + var(--color-outline-variant); + content: ""; + display: block; + inset-block: 0; + inset-inline-start: calc(var(--checkbox-size) / 2); + position: absolute; + } + + .js-enabled &.checkboxes__conditional--hidden { + display: none; + } +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/checkboxes/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/checkboxes/template.njk new file mode 100644 index 00000000..66f4196c --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/checkboxes/template.njk @@ -0,0 +1,96 @@ +{% from "../error-message/macro.njk" import errorMessage with context %} +{% from "../fieldset/macro.njk" import fieldset with context %} +{% from "../field/macro.njk" import field with context %} +{% from "../hint/macro.njk" import hint with context %} +{% from "../label/macro.njk" import label with context %} +{# If `idPrefix` not passed, fallback to using name attribute instead. + We need this for error messages and hints. #} +{%- set idPrefix = opts.idPrefix if opts.idPrefix else opts.name %} +{# A record of other elements we need to associate with the input using + `aria-describedby`, for example error messages or hints. #} +{%- set describedBy = opts.describedBy if opts.describedBy else "" %} +{%- if opts.fieldset.describedBy %} + {%- set describedBy = opts.fieldset.describedBy %} +{%- endif %} +{# `fieldset` is false by default #} +{%- set hasFieldset = true if opts.fieldset else false %} +{# Capture the HTML so we can optionally nest it within a fieldset #} +{%- set innerHtml %} +{% if opts.hint %} + {% set hintId = idPrefix + "-hint" %} + {% set describedBy = describedBy + " " + hintId if describedBy else hintId %} + {{ hint({ + id: hintId, + text: opts.hint + }) | trim }} +{% endif %} +{% if opts.errorMessage %} + {% set errorId = idPrefix + "-error" %} + {% set describedBy = describedBy + " " + errorId if describedBy else errorId %} + {{ errorMessage({ + id: errorId, + classes: opts.errorMessage.classes, + attributes: opts.errorMessage.attributes, + text: opts.errorMessage.text, + visuallyHiddenText: opts.errorMessage.visuallyHiddenText + }) | indent(2) | trim }} +{%- endif %} +
+{% for item in opts.items %}{% if item %} + {% set id = itemId(item.id, idPrefix, loop) %} + {% set name = item.name if item.name else opts.name %} + {% set conditionalId = "conditional-" + id %} + {% if item.divider %} +
{{ item.divider }}
+ {% else %} + {% set isChecked = item.checked | default(opts.values and item.value in opts.values) %} + {% set itemHintId = id + "-item-hint" if item.hint else "" %} + {% set itemDescribedBy = describedBy if not hasFieldset else "" %} + {% set itemDescribedBy = (itemDescribedBy + " " + itemHintId) | trim %} +
+ + {{ label({ + classes: "checkboxes__label", + for: id, + text: item.label + }) }} + {{ hint({ + classes: "checkboxes__hint", + id: itemHintId, + text: item.hint + }) if item.hint }} +
+ {% if item.conditional %} +
+ {{ item.conditional | safe }} +
+ {% endif %} + {% endif %} +{% endif %}{% endfor %} +
+{% endset %} +{% if opts.items %} +{% call field({ + element: "checkboxes-field", + classes: opts.field.classes, + attributes: opts.field.attributes +}) %} +{% if opts.fieldset %} + {% call fieldset({ + describedBy: describedBy, + classes: opts.fieldset.classes, + attributes: opts.fieldset.attributes, + legend: opts.fieldset.legend, + optional: opts.fieldset.optional + }) %}{{ innerHtml | trim | safe }}{% endcall %} +{% else %} + {{ innerHtml | trim | safe }} +{% endif %} +{% endcall %} +{% endif %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/details/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/details/macro.njk new file mode 100644 index 00000000..326e7abd --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/details/macro.njk @@ -0,0 +1,3 @@ +{% macro details(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/details/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/details/styles.css new file mode 100644 index 00000000..9acb1f3e --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/details/styles.css @@ -0,0 +1,24 @@ +.details { + background-color: var(--color-offset); + border-radius: var(--border-radius-large); + padding-block: var(--space-m); + padding-inline: var(--space-l); +} + +.details__summary { + cursor: pointer; + font: var(--font-label); + margin-block: calc(var(--space-xs) * -1); + margin-inline: calc(var(--space-m) * -1); + padding-block: var(--space-xs); + padding-block: calc(calc(var(--space-m) / 2) - var(--input-border-width)); + padding-inline: var(--space-m); + + &:focus-visible { + color: currentcolor; + } +} + +.details__main { + margin-block: var(--space-s); +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/details/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/details/template.njk new file mode 100644 index 00000000..d7a37b8b --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/details/template.njk @@ -0,0 +1,12 @@ +{% from "../prose/macro.njk" import prose with context %} +
+ + {{ opts.summary | safe }} + +
+ {{ caller() if caller else prose({ text: opts.text }) }} +
+
\ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/error-message/macro.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/error-message/macro.njk new file mode 100644 index 00000000..fa612692 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/error-message/macro.njk @@ -0,0 +1,3 @@ +{% macro errorMessage(opts) %} + {%- include "./template.njk" -%} +{% endmacro %} \ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/error-message/styles.css b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/error-message/styles.css new file mode 100644 index 00000000..054667fb --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/error-message/styles.css @@ -0,0 +1,5 @@ +.error-message { + color: var(--color-error); + font: var(--font-caption); + font-weight: 600; +} diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/error-message/template.njk b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/error-message/template.njk new file mode 100644 index 00000000..1cec9cd9 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/error-message/template.njk @@ -0,0 +1,9 @@ +

+ + {{- opts.visuallyHiddenText | default(__("error")) + ":" -}} + + + {{- opts.text | safe -}} + +

\ No newline at end of file diff --git a/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/error-summary/index.js b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/error-summary/index.js new file mode 100644 index 00000000..39316b42 --- /dev/null +++ b/node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/error-summary/index.js @@ -0,0 +1,147 @@ +/** + * Based by the error summary component provided by GOV.UK Frontend + * @see {@link https://github.com/alphagov/govuk-frontend/blob/main/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.mjs} + */ +export const ErrorSummaryComponent = class extends HTMLElement { + connectedCallback() { + this.setFocus(); + this.addEventListener("click", this.handleClick.bind(this)); + } + + /** + * Focus error summary + */ + setFocus() { + if (this.getAttribute("disable-auto-focus") === "true") { + return; + } + + // Set tabindex to -1 to make the element programmatically focusable… + this.setAttribute("tabindex", "-1"); + + // …and remove it on blur as error summary doesn’t need to be focused again + this.addEventListener("blur", function (event) { + event.target.removeAttribute("tabindex"); + }); + + this.focus(); + } + + /** + * Click event handler + * @param {object} event - Click event + */ + handleClick(event) { + if (this.focusTarget(event.target)) { + event.preventDefault(); + } + } + + /** + * Focus target element + * + * By default, the browser will scroll the target into view. Because our + * labels and legends appear above their respective inputs, this means users + * will be presented with inputs without any context, with the label or + * legend will off the top of the screen. + * + * Manually handling the click event, scrolling the question into view and + * then focussing the element solves this. + * + * This also results in the label and/or legend being announced correctly in + * NVDA - without this only the field type is announced (e.g. "Edit, has + * autocomplete"). + * @param {HTMLAnchorElement} $target - Event target + * @returns {boolean} True if the target was able to be focussed + */ + focusTarget($target) { + // If the element that was clicked was not a link, return early + if (!($target instanceof HTMLAnchorElement)) { + return false; + } + + const fragment = this.getFragmentFromUrl($target.href); + + /** @satisfies {HTMLInputElement} */ + const $input = document.querySelector(`#${fragment}`); + if (!$input) { + return false; + } + + const legendOrLabel = this.getAssociatedLegendOrLabel($input); + if (!legendOrLabel) { + return false; + } + + // Scroll the legend or label into view _before_ calling focus on the input + // to avoid extra scrolling in browsers that don’t support `preventScroll` + legendOrLabel.scrollIntoView(); + $input.focus({ preventScroll: true }); + + return true; + } + + /** + * Get fragment name from a URL + * @param {string} url - URL + * @returns {string|boolean} Fragment name (without the hash) + */ + getFragmentFromUrl(url) { + return url.startsWith("#") ? false : url.split("#").pop(); + } + + /** + * Get associated legend or label + * + * Returns first element that exists from this list: + * + * - The `` associated with the closest `
` ancestor, as long + * as the top of it is no more than half a viewport height away from the + * bottom of the input + * - The first `