diff --git a/astro.config.mjs b/astro.config.mjs
index 4a9622f..7d5c233 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -11,13 +11,13 @@ export default defineConfig({
vite: {
ssr: {
noExternal: ["@patternfly/*", "react-dropzone"],
- external: ["node:fs", "node:path"]
+ external: ["node:fs", "node:path", "fs/promises", "path"]
},
server: {
fs: {
allow: ['./']
}
- }
+ },
},
adapter: cloudflare()
});
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 9bc293c..eb6c533 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -46,6 +46,8 @@
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.26.0",
"@eslint/js": "^9.16.0",
+ "@patternfly/react-data-view": "^6.0.0",
+ "@patternfly/react-user-feedback": "^6.0.0",
"@semantic-release/git": "^10.0.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
@@ -1517,6 +1519,23 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz",
+ "integrity": "sha512-uxJqm/sqwXw3YPA5GXX365OBcJGFtxUVkB6WyezqFHlNe9jqUWH5ur2O2M8dGBz61kn1g3ZBlzUunFQXQIClhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "0.7.1"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.1.tgz",
+ "integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
@@ -4065,22 +4084,60 @@
"react-dom": "^17 || ^18"
}
},
+ "node_modules/@patternfly/react-component-groups": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-6.4.0.tgz",
+ "integrity": "sha512-vg0761nQ/7hfggbp6+XowRcQQSd9oIToh77+4lmsyrs41MkA5ppQIPBCZ4lUZW87kmEPhkHqglpJcVfsrrIM/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@patternfly/react-core": "^6.0.0",
+ "@patternfly/react-icons": "^6.0.0",
+ "@patternfly/react-styles": "^6.0.0",
+ "@patternfly/react-table": "^6.0.0",
+ "react-jss": "^10.10.0"
+ },
+ "peerDependencies": {
+ "@patternfly/react-drag-drop": "^6.0.0",
+ "react": "^17 || ^18 || ^19",
+ "react-dom": "^17 || ^18 || ^19"
+ }
+ },
"node_modules/@patternfly/react-core": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.2.2.tgz",
- "integrity": "sha512-JUrZ57JQ4bkmed1kxaciXb0ZpIVYyCHc2HjtzoKQ5UNRlx204zR2isATSHjdw2GFcWvwpkC5/fU2BR+oT3opbg==",
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.4.0.tgz",
+ "integrity": "sha512-zMgJmcFohp2FqgAoZHg7EXZS7gnaFESquk0qIavemYI0FsqspVlzV2/PUru7w+86+jXfqebRhgubPRsv1eJwEg==",
"license": "MIT",
"dependencies": {
- "@patternfly/react-icons": "^6.2.2",
- "@patternfly/react-styles": "^6.2.2",
- "@patternfly/react-tokens": "^6.2.2",
+ "@patternfly/react-icons": "^6.4.0",
+ "@patternfly/react-styles": "^6.4.0",
+ "@patternfly/react-tokens": "^6.4.0",
"focus-trap": "7.6.4",
"react-dropzone": "^14.3.5",
"tslib": "^2.8.1"
},
"peerDependencies": {
- "react": "^17 || ^18",
- "react-dom": "^17 || ^18"
+ "react": "^17 || ^18 || ^19",
+ "react-dom": "^17 || ^18 || ^19"
+ }
+ },
+ "node_modules/@patternfly/react-data-view": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-data-view/-/react-data-view-6.4.0.tgz",
+ "integrity": "sha512-AYIJvWLSoZaf3askvBjyyFQEvSCiquw5PFzEOiTsNoM2pDYkRagzppjclpI+MRJr44ZrfpljC6ZKE4f5Ni2p+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@patternfly/react-component-groups": "^6.1.0",
+ "@patternfly/react-core": "^6.4.0",
+ "@patternfly/react-icons": "^6.4.0",
+ "@patternfly/react-table": "^6.4.0",
+ "clsx": "^2.1.1",
+ "react-jss": "^10.10.0"
+ },
+ "peerDependencies": {
+ "react": "^17 || ^18 || ^19",
+ "react-dom": "^17 || ^18 || ^19"
}
},
"node_modules/@patternfly/react-drag-drop": {
@@ -4103,45 +4160,60 @@
}
},
"node_modules/@patternfly/react-icons": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.2.2.tgz",
- "integrity": "sha512-XkBwzuV/uiolX+T6QgB3RIqphM1m+vAZjAe3McYtyY22j1rsOdlWDE4RtRrJ1q7EoIZwyZHj0h8T9vMfUsLn4Q==",
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz",
+ "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==",
"license": "MIT",
"peerDependencies": {
- "react": "^17 || ^18",
- "react-dom": "^17 || ^18"
+ "react": "^17 || ^18 || ^19",
+ "react-dom": "^17 || ^18 || ^19"
}
},
"node_modules/@patternfly/react-styles": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.2.2.tgz",
- "integrity": "sha512-rncRDq66H8VnLyb9DrHHlZtPddlpNL9+W0XuQC0L7F6p78hOwSZmoGTW2Vq8/wJplDj8h/61qRpfRF9VEYPW0g==",
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.4.0.tgz",
+ "integrity": "sha512-EXmHA67s5sy+Wy/0uxWoUQ52jr9lsH2wV3QcgtvVc5zxpyBX89gShpqv4jfVqaowznHGDoL6fVBBrSe9BYOliQ==",
"license": "MIT"
},
"node_modules/@patternfly/react-table": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.2.2.tgz",
- "integrity": "sha512-7CxVKhnpA+f8dLJ0hVvzZOe4Djx/nE+w70ipeAHf4Yi5JwfDWmbK97YvjYPfamp/bsXTLtPcK2n4AoY5DQX6Pg==",
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.4.0.tgz",
+ "integrity": "sha512-yv0sFOLGts8a2q9C1xUegjp50ayYyVRe0wKjMf+aMSNIK8sVYu8qu0yfBsCDybsUCldue7+qsYKRLFZosTllWQ==",
"license": "MIT",
"dependencies": {
- "@patternfly/react-core": "^6.2.2",
- "@patternfly/react-icons": "^6.2.2",
- "@patternfly/react-styles": "^6.2.2",
- "@patternfly/react-tokens": "^6.2.2",
+ "@patternfly/react-core": "^6.4.0",
+ "@patternfly/react-icons": "^6.4.0",
+ "@patternfly/react-styles": "^6.4.0",
+ "@patternfly/react-tokens": "^6.4.0",
"lodash": "^4.17.21",
"tslib": "^2.8.1"
},
"peerDependencies": {
- "react": "^17 || ^18",
- "react-dom": "^17 || ^18"
+ "react": "^17 || ^18 || ^19",
+ "react-dom": "^17 || ^18 || ^19"
}
},
"node_modules/@patternfly/react-tokens": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.2.2.tgz",
- "integrity": "sha512-2GRWDPBTrcTlGNFc5NPJjrjEVU90RpgcGX/CIe2MplLgM32tpVIkeUtqIoJPLRk5GrbhyFuHJYRU+O93gU4o3Q==",
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.4.0.tgz",
+ "integrity": "sha512-iZthBoXSGQ/+PfGTdPFJVulaJZI3rwE+7A/whOXPGp3Jyq3k6X52pr1+5nlO6WHasbZ9FyeZGqXf4fazUZNjbw==",
"license": "MIT"
},
+ "node_modules/@patternfly/react-user-feedback": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-user-feedback/-/react-user-feedback-6.2.0.tgz",
+ "integrity": "sha512-grhaZQwcESNZD2ifpbPoodCJ/NRTt4B24jVNYgDi23EVvQ0oGpyIXAvbUhX3siSkLjdqdYLsY9o5wNRJM78VkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@patternfly/react-core": "^6.0.0",
+ "@patternfly/react-icons": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": "^17 || ^18 || ^19",
+ "react-dom": "^17 || ^18 || ^19"
+ }
+ },
"node_modules/@pkgr/core": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
@@ -8993,6 +9065,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/css-jss": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/css-jss/-/css-jss-10.10.0.tgz",
+ "integrity": "sha512-YyMIS/LsSKEGXEaVJdjonWe18p4vXLo8CMA4FrW/kcaEyqdIGKCFXao31gbJddXEdIxSXFFURWrenBJPlKTgAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "^10.10.0",
+ "jss-preset-default": "^10.10.0"
+ }
+ },
"node_modules/css-tree": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
@@ -9006,6 +9090,17 @@
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
+ "node_modules/css-vendor": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz",
+ "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.3",
+ "is-in-browser": "^1.0.2"
+ }
+ },
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
@@ -9529,9 +9624,9 @@
}
},
"node_modules/dompurify": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
- "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
+ "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -12244,6 +12339,16 @@
"@babel/runtime": "^7.7.6"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
"node_modules/homedir-polyfill": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -12383,6 +12488,13 @@
"node": ">=10.17.0"
}
},
+ "node_modules/hyphenate-style-name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
+ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -13009,6 +13121,13 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/is-in-browser": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
+ "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
@@ -15461,6 +15580,186 @@
"graceful-fs": "^4.1.6"
}
},
+ "node_modules/jss": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz",
+ "integrity": "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "csstype": "^3.0.2",
+ "is-in-browser": "^1.1.3",
+ "tiny-warning": "^1.0.2"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/jss"
+ }
+ },
+ "node_modules/jss-plugin-camel-case": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz",
+ "integrity": "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "hyphenate-style-name": "^1.0.3",
+ "jss": "10.10.0"
+ }
+ },
+ "node_modules/jss-plugin-compose": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-plugin-compose/-/jss-plugin-compose-10.10.0.tgz",
+ "integrity": "sha512-F5kgtWpI2XfZ3Z8eP78tZEYFdgTIbpA/TMuX3a8vwrNolYtN1N4qJR/Ob0LAsqIwCMLojtxN7c7Oo/+Vz6THow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.10.0",
+ "tiny-warning": "^1.0.2"
+ }
+ },
+ "node_modules/jss-plugin-default-unit": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz",
+ "integrity": "sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.10.0"
+ }
+ },
+ "node_modules/jss-plugin-expand": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-plugin-expand/-/jss-plugin-expand-10.10.0.tgz",
+ "integrity": "sha512-ymT62W2OyDxBxr7A6JR87vVX9vTq2ep5jZLIdUSusfBIEENLdkkc0lL/Xaq8W9s3opUq7R0sZQpzRWELrfVYzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.10.0"
+ }
+ },
+ "node_modules/jss-plugin-extend": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-plugin-extend/-/jss-plugin-extend-10.10.0.tgz",
+ "integrity": "sha512-sKYrcMfr4xxigmIwqTjxNcHwXJIfvhvjTNxF+Tbc1NmNdyspGW47Ey6sGH8BcQ4FFQhLXctpWCQSpDwdNmXSwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.10.0",
+ "tiny-warning": "^1.0.2"
+ }
+ },
+ "node_modules/jss-plugin-global": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz",
+ "integrity": "sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.10.0"
+ }
+ },
+ "node_modules/jss-plugin-nested": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz",
+ "integrity": "sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.10.0",
+ "tiny-warning": "^1.0.2"
+ }
+ },
+ "node_modules/jss-plugin-props-sort": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz",
+ "integrity": "sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.10.0"
+ }
+ },
+ "node_modules/jss-plugin-rule-value-function": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz",
+ "integrity": "sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.10.0",
+ "tiny-warning": "^1.0.2"
+ }
+ },
+ "node_modules/jss-plugin-rule-value-observable": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-observable/-/jss-plugin-rule-value-observable-10.10.0.tgz",
+ "integrity": "sha512-ZLMaYrR3QE+vD7nl3oNXuj79VZl9Kp8/u6A1IbTPDcuOu8b56cFdWRZNZ0vNr8jHewooEeq2doy8Oxtymr2ZPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.10.0",
+ "symbol-observable": "^1.2.0"
+ }
+ },
+ "node_modules/jss-plugin-template": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-plugin-template/-/jss-plugin-template-10.10.0.tgz",
+ "integrity": "sha512-ocXZBIOJOA+jISPdsgkTs8wwpK6UbsvtZK5JI7VUggTD6LWKbtoxUzadd2TpfF+lEtlhUmMsCkTRNkITdPKa6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.10.0",
+ "tiny-warning": "^1.0.2"
+ }
+ },
+ "node_modules/jss-plugin-vendor-prefixer": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz",
+ "integrity": "sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "css-vendor": "^2.0.8",
+ "jss": "10.10.0"
+ }
+ },
+ "node_modules/jss-preset-default": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/jss-preset-default/-/jss-preset-default-10.10.0.tgz",
+ "integrity": "sha512-GL175Wt2FGhjE+f+Y3aWh+JioL06/QWFgZp53CbNNq6ZkVU0TDplD8Bxm9KnkotAYn3FlplNqoW5CjyLXcoJ7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.10.0",
+ "jss-plugin-camel-case": "10.10.0",
+ "jss-plugin-compose": "10.10.0",
+ "jss-plugin-default-unit": "10.10.0",
+ "jss-plugin-expand": "10.10.0",
+ "jss-plugin-extend": "10.10.0",
+ "jss-plugin-global": "10.10.0",
+ "jss-plugin-nested": "10.10.0",
+ "jss-plugin-props-sort": "10.10.0",
+ "jss-plugin-rule-value-function": "10.10.0",
+ "jss-plugin-rule-value-observable": "10.10.0",
+ "jss-plugin-template": "10.10.0",
+ "jss-plugin-vendor-prefixer": "10.10.0"
+ }
+ },
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -17204,12 +17503,36 @@
}
},
"node_modules/monaco-editor": {
- "version": "0.52.2",
- "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
- "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
+ "version": "0.54.0",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz",
+ "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==",
"license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "dompurify": "3.1.7",
+ "marked": "14.0.0"
+ }
+ },
+ "node_modules/monaco-editor/node_modules/dompurify": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
+ "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
"peer": true
},
+ "node_modules/monaco-editor/node_modules/marked": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
+ "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -21482,6 +21805,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-display-name": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/react-display-name/-/react-display-name-0.2.5.tgz",
+ "integrity": "sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/react-docgen": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.1.1.tgz",
@@ -21578,6 +21908,29 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
+ "node_modules/react-jss": {
+ "version": "10.10.0",
+ "resolved": "https://registry.npmjs.org/react-jss/-/react-jss-10.10.0.tgz",
+ "integrity": "sha512-WLiq84UYWqNBF6579/uprcIUnM1TSywYq6AIjKTTTG5ziJl9Uy+pwuvpN3apuyVwflMbD60PraeTKT7uWH9XEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "@emotion/is-prop-valid": "^0.7.3",
+ "css-jss": "10.10.0",
+ "hoist-non-react-statics": "^3.2.0",
+ "is-in-browser": "^1.1.3",
+ "jss": "10.10.0",
+ "jss-preset-default": "10.10.0",
+ "prop-types": "^15.6.0",
+ "shallow-equal": "^1.2.0",
+ "theming": "^3.3.0",
+ "tiny-warning": "^1.0.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.6"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -22742,6 +23095,13 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
+ "node_modules/shallow-equal": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
+ "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/sharp": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
@@ -23514,6 +23874,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/symbol-observable": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -23632,6 +24002,25 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/theming": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/theming/-/theming-3.3.0.tgz",
+ "integrity": "sha512-u6l4qTJRDaWZsqa8JugaNt7Xd8PPl9+gonZaIe28vAhqgHMIG/DOyFPqiKN/gQLQYj05tHv+YQdNILL4zoiAVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hoist-non-react-statics": "^3.3.0",
+ "prop-types": "^15.5.8",
+ "react-display-name": "^0.2.4",
+ "tiny-warning": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "react": ">=16.3"
+ }
+ },
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -23693,6 +24082,13 @@
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
+ "node_modules/tiny-warning": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tinyexec": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
diff --git a/package.json b/package.json
index bfe7cf6..cf3fc79 100644
--- a/package.json
+++ b/package.json
@@ -110,7 +110,9 @@
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript-eslint": "^8.15.0",
- "wrangler": "^4.20.0"
+ "wrangler": "^4.20.0",
+ "@patternfly/react-user-feedback": "^6.0.0",
+ "@patternfly/react-data-view": "^6.0.0"
},
"config": {
"commitizen": {
diff --git a/pf-docs.config.mjs b/pf-docs.config.mjs
index 02fde7a..22760e7 100644
--- a/pf-docs.config.mjs
+++ b/pf-docs.config.mjs
@@ -25,6 +25,8 @@ export const config = {
pattern: '*/patternfly-docs/**/*.md',
name: 'quickstarts-docs',
},
+ { packageName: "@patternfly/react-user-feedback", pattern: "patternfly-docs/**/*.md", name: "user-feedback-docs" },
+ { packageName: "@patternfly/react-data-view", pattern: "patternfly-docs/**/*.md", name: "data-view-docs" },
],
outputDir: './dist',
propsGlobs: [
diff --git a/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts
index de85fde..ab544bb 100644
--- a/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts
+++ b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts
@@ -146,6 +146,7 @@ jest.mock('../../../../../../../utils/apiIndex/fetch', () => ({
tabs: {
'v6::components::alert': ['react', 'html', 'react-demos'],
},
+ examples: {},
}),
}))
diff --git a/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap b/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap
index d7076eb..2550ce9 100644
--- a/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap
@@ -197,6 +197,7 @@ exports[`matches snapshot 1`] = `
aria-labelledby="nav-section-section-two"
class="pf-v6-c-nav__subnav"
hidden=""
+ inert=""
>
{
try {
const index = await fetchApiIndex(url)
+
+ // Get pages for this section (includes underscore-separated subsection pages like "forms-checkbox")
const key = createIndexKey(version, section)
const pages = index.pages[key]
@@ -26,6 +28,7 @@ export const GET: APIRoute = async ({ params, url }) => {
)
}
+ // Return pages array (already sorted, contains both regular and underscore-separated subsection pages)
return createJsonResponse(pages)
} catch (error) {
const details = error instanceof Error ? error.message : String(error)
diff --git a/src/pages/api/[version]/[section]/[page].ts b/src/pages/api/[version]/[section]/[page].ts
index 9ccbf6e..1364181 100644
--- a/src/pages/api/[version]/[section]/[page].ts
+++ b/src/pages/api/[version]/[section]/[page].ts
@@ -16,8 +16,10 @@ export const GET: APIRoute = async ({ params, url }) => {
try {
const index = await fetchApiIndex(url)
- const key = createIndexKey(version, section, page)
- const tabs = index.tabs[key]
+
+ // Return tabs for this page (page may be underscore-separated like "forms_checkbox")
+ const pageKey = createIndexKey(version, section, page)
+ const tabs = index.tabs[pageKey]
if (!tabs) {
return createJsonResponse(
diff --git a/src/pages/api/[version]/[section]/[page]/[tab].ts b/src/pages/api/[version]/[section]/[page]/[tab].ts
index 0a92e5f..f92e3a2 100644
--- a/src/pages/api/[version]/[section]/[page]/[tab].ts
+++ b/src/pages/api/[version]/[section]/[page]/[tab].ts
@@ -23,16 +23,19 @@ export const GET: APIRoute = async ({ params, redirect, url }) => {
}
// Check if section exists for this version
- const sectionKey = createIndexKey(version, section)
- if (!index.sections[version]?.includes(section)) {
+ const sections = index.sections[version] || []
+
+ if (!sections.includes(section)) {
return createJsonResponse(
{ error: `Section '${section}' not found for version '${version}'` },
404,
)
}
- // Check if page exists for this section
+ const sectionKey = createIndexKey(version, section)
const pageKey = createIndexKey(version, section, page)
+
+ // Check if page exists for this section (page may be underscore-separated like "forms_checkbox")
if (!index.pages[sectionKey]?.includes(page)) {
return createJsonResponse(
{
diff --git a/src/pages/api/[version]/[section]/[page]/[tab]/examples.ts b/src/pages/api/[version]/[section]/[page]/[tab]/examples.ts
index 5d8db83..cbb9cf2 100644
--- a/src/pages/api/[version]/[section]/[page]/[tab]/examples.ts
+++ b/src/pages/api/[version]/[section]/[page]/[tab]/examples.ts
@@ -17,9 +17,11 @@ export const GET: APIRoute = async ({ params, url }) => {
// Get examples with titles directly from the index
try {
const index = await fetchApiIndex(url)
- const tabKey = createIndexKey(version, section, page, tab)
- const examples = index.examples[tabKey] || []
+ // Example key is version::section::page::tab (page may be underscore-separated like "forms_checkbox")
+ const exampleKey = createIndexKey(version, section, page, tab)
+
+ const examples = index.examples[exampleKey] || []
return createJsonResponse(examples)
} catch (error) {
const details = error instanceof Error ? error.message : String(error)
diff --git a/src/pages/api/[version]/[section]/[page]/[tab]/examples/[example].ts b/src/pages/api/[version]/[section]/[page]/[tab]/examples/[example].ts
index 4c13c09..67cd4ae 100644
--- a/src/pages/api/[version]/[section]/[page]/[tab]/examples/[example].ts
+++ b/src/pages/api/[version]/[section]/[page]/[tab]/examples/[example].ts
@@ -1,14 +1,12 @@
/* eslint-disable no-console */
import type { APIRoute, GetStaticPaths } from 'astro'
-import type { CollectionEntry, CollectionKey } from 'astro:content'
-import { getCollection } from 'astro:content'
import { readFile } from 'fs/promises'
import { resolve } from 'path'
-import { content } from '../../../../../../../content'
-import { kebabCase, addDemosOrDeprecated } from '../../../../../../../utils'
-import { getDefaultTabForApi } from '../../../../../../../utils/packageUtils'
-import { createJsonResponse, createTextResponse, createIndexKey } from '../../../../../../../utils/apiHelpers'
+import { createJsonResponse, createTextResponse } from '../../../../../../../utils/apiHelpers'
import { generateAndWriteApiIndex } from '../../../../../../../utils/apiIndex/generate'
+import { getEnrichedCollections } from '../../../../../../../utils/apiRoutes/collections'
+import { findContentEntryFilePath } from '../../../../../../../utils/apiRoutes/contentMatching'
+import { extractImports, extractExampleFilePath } from '../../../../../../../utils/apiRoutes/exampleParsing'
export const prerender = true
@@ -26,29 +24,23 @@ export const getStaticPaths: GetStaticPaths = async () => {
}
}[] = []
- // Build paths from index structure
- for (const version of index.versions) {
- for (const section of index.sections[version] || []) {
- const sectionKey = createIndexKey(version, section)
- for (const page of index.pages[sectionKey] || []) {
- const pageKey = createIndexKey(version, section, page)
- for (const tab of index.tabs[pageKey] || []) {
- const tabKey = createIndexKey(version, section, page, tab)
-
- // Get all examples for this tab
- const examples = index.examples[tabKey] || []
- for (const example of examples) {
- paths.push({
- params: {
- version,
- section,
- page,
- tab,
- example: example.exampleName,
- },
- })
- }
- }
+ // Build paths from index structure by iterating over examples
+ // All examples are keyed by version::section::page::tab (page may be underscore-separated like "forms_checkbox")
+ for (const [exampleKey, examples] of Object.entries(index.examples)) {
+ const parts = exampleKey.split('::')
+
+ if (parts.length === 4) {
+ const [version, section, page, tab] = parts
+ for (const example of examples) {
+ paths.push({
+ params: {
+ version,
+ section,
+ page,
+ tab,
+ example: example.exampleName,
+ },
+ })
}
}
}
@@ -56,114 +48,6 @@ export const getStaticPaths: GetStaticPaths = async () => {
return paths
}
-/**
- * Extracts import statements from file content
- * Matches import statements with relative paths (starting with ./ or ../)
- *
- * @param fileContent - The file content to parse
- * @returns Array of import statements or null if none found
- */
-function getImports(fileContent: string): string[] | null {
- // Match import statements with relative paths
- // Supports: import X from './path', import X from "../path/file.tsx"
- const importRegex = /import\s+.*\s+from\s+['"]\.{1,2}\/[^'"]+['"]/gm
- const matches = fileContent.match(importRegex)
- return matches
-}
-
-/**
- * Extracts the file path for a specific example from import statements
- * Looks for imports that reference the example name
- *
- * @param imports - Array of import statements
- * @param exampleName - Name of the example to find
- * @returns Relative file path without quotes (including query params like ?raw), or null if not found
- */
-function getExampleFilePath(imports: string[], exampleName: string): string | null {
- const exampleImport = imports.find((imp) => imp.includes(exampleName))
- if (!exampleImport) {
- console.error('No import path found for example', exampleName)
- return null
- }
- // Extract path from import statement, handling query parameters like ?raw
- // Matches: "./path" or "../path" with optional file extensions and query params
- const match = exampleImport.match(/['"](\.[^'"]+)['"]/i)
- if (!match || !match[1]) {
- return null
- }
- return match[1]
-}
-
-/**
- * Fetches all content collections for a specific version
- * Enriches entries with default tab information if not specified
- *
- * @param version - The documentation version (e.g., 'v6')
- * @returns Promise resolving to array of collection entries with metadata
- */
-async function getCollections(version: string) {
- const collectionsToFetch = content
- .filter((entry) => entry.version === version)
- .map((entry) => entry.name as CollectionKey)
- const collections = await Promise.all(
- collectionsToFetch.map(async (name) => await getCollection(name)),
- )
- return collections.flat().map(({ data, filePath, ...rest }) => ({
- filePath,
- ...rest,
- data: {
- ...data,
- tab: data.tab || data.source || getDefaultTabForApi(filePath),
- },
- }))
-}
-
-/**
- * Finds the file path for a content entry matching the given parameters
- * Prefers .mdx files over .md files when both exist, since .mdx files
- * contain the LiveExample components and example imports
- *
- * @param collections - Array of collection entries to search
- * @param section - The section name (e.g., 'components')
- * @param page - The page slug (e.g., 'alert')
- * @param tab - The tab name (e.g., 'react')
- * @returns Promise resolving to the file path, or null if not found
- */
-async function getContentEntryFilePath(
- collections: CollectionEntry<'core-docs' | 'quickstarts-docs' | 'react-component-docs'>[],
- section: string,
- page: string,
- tab: string
-): Promise {
- // Find all matching entries
- const matchingEntries = collections.filter((entry) => {
- const entryTab = addDemosOrDeprecated(entry.data.tab, entry.filePath)
- return (
- entry.data.section === section &&
- kebabCase(entry.data.id) === page &&
- entryTab === tab
- )
- })
-
- if (matchingEntries.length === 0) {
- console.error('No content entry found for section', section, 'page', page, 'tab', tab)
- return null
- }
-
- // Prefer .mdx files over .md files (mdx files have LiveExample components)
- const mdxEntry = matchingEntries.find((entry) =>
- typeof entry.filePath === 'string' && entry.filePath.endsWith('.mdx')
- )
- const contentEntry = mdxEntry || matchingEntries[0]
-
- if (typeof contentEntry.filePath !== 'string') {
- console.error('No file path found for content entry', contentEntry.id)
- return null
- }
-
- return contentEntry.filePath
-}
-
/**
* GET handler for retrieving example source code
* Returns the raw source code for a specific example
@@ -175,14 +59,18 @@ export const GET: APIRoute = async ({ params }) => {
const { version, section, page, tab, example } = params
if (!version || !section || !page || !tab || !example) {
return createJsonResponse(
- { error: 'Version, section, page, tab, and example parameters are required' },
+ { error: 'All parameters are required' },
400
)
}
try {
- const collections = await getCollections(version)
- const contentEntryFilePath = await getContentEntryFilePath(collections, section, page, tab)
+ const collections = await getEnrichedCollections(version)
+ const contentEntryFilePath = findContentEntryFilePath(collections, {
+ section,
+ page,
+ tab
+ })
if (!contentEntryFilePath) {
return createJsonResponse(
@@ -203,7 +91,7 @@ export const GET: APIRoute = async ({ params }) => {
)
}
- const contentEntryImports = getImports(contentEntryFileContent)
+ const contentEntryImports = extractImports(contentEntryFileContent)
if (!contentEntryImports) {
return createJsonResponse(
{ error: 'No imports found in content entry' },
@@ -211,7 +99,7 @@ export const GET: APIRoute = async ({ params }) => {
)
}
- const relativeExampleFilePath = getExampleFilePath(contentEntryImports, example)
+ const relativeExampleFilePath = extractExampleFilePath(contentEntryImports, example)
if (!relativeExampleFilePath) {
return createJsonResponse(
{ error: `Example "${example}" not found in imports` },
diff --git a/src/pages/api/[version]/[section]/[page]/[tab]/text.ts b/src/pages/api/[version]/[section]/[page]/[tab]/text.ts
index 24736f9..219dd2e 100644
--- a/src/pages/api/[version]/[section]/[page]/[tab]/text.ts
+++ b/src/pages/api/[version]/[section]/[page]/[tab]/text.ts
@@ -1,27 +1,16 @@
/* eslint-disable no-console */
import type { APIRoute, GetStaticPaths } from 'astro'
-import type { CollectionEntry, CollectionKey } from 'astro:content'
-import { getCollection } from 'astro:content'
-import { content } from '../../../../../../content'
-import {
- kebabCase,
- addDemosOrDeprecated,
- getDefaultTabForApi,
-} from '../../../../../../utils'
import { generateAndWriteApiIndex } from '../../../../../../utils/apiIndex/generate'
import { getApiIndex } from '../../../../../../utils/apiIndex/get'
import {
createJsonResponse,
createTextResponse,
- createIndexKey,
} from '../../../../../../utils/apiHelpers'
+import { getEnrichedCollections } from '../../../../../../utils/apiRoutes/collections'
+import { findContentEntry } from '../../../../../../utils/apiRoutes/contentMatching'
export const prerender = true
-type ContentEntry = CollectionEntry<
- 'core-docs' | 'quickstarts-docs' | 'react-component-docs'
->
-
export const getStaticPaths: GetStaticPaths = async () => {
// Generate index file for server-side routes to use
// This runs once during build when getCollection() is available
@@ -32,14 +21,21 @@ export const getStaticPaths: GetStaticPaths = async () => {
}[] = []
// Build paths from index structure
- for (const version of index.versions) {
- for (const section of index.sections[version] || []) {
- const sectionKey = createIndexKey(version, section)
- for (const page of index.pages[sectionKey] || []) {
- const pageKey = createIndexKey(version, section, page)
- for (const tab of index.tabs[pageKey] || []) {
- paths.push({ params: { version, section, page, tab } })
- }
+ // All tab keys are now 3-part: version::section::page (page may be underscore-separated for subsections)
+ for (const [tabKey, tabs] of Object.entries(index.tabs)) {
+ const parts = tabKey.split('::')
+
+ if (parts.length === 3) {
+ const [version, section, page] = parts
+ for (const tab of tabs) {
+ paths.push({
+ params: {
+ version,
+ section,
+ page,
+ tab,
+ },
+ })
}
}
}
@@ -74,61 +70,23 @@ export const GET: APIRoute = async ({ params }) => {
}
// Check if section exists for this version
- const sectionKey = createIndexKey(version, section)
- if (!index.sections[version]?.includes(section)) {
- return createJsonResponse(
- { error: `Section '${section}' not found for version '${version}'` },
- 404,
- )
- }
+ const sections = index.sections[version] || []
- // Check if page exists for this section
- const pageKey = createIndexKey(version, section, page)
- if (!index.pages[sectionKey]?.includes(page)) {
+ if (!sections.includes(section)) {
return createJsonResponse(
- {
- error: `Page '${page}' not found in section '${section}' for version '${version}'`,
- },
- 404,
- )
- }
-
- // Check if tab exists for this page
- if (!index.tabs[pageKey]?.includes(tab)) {
- return createJsonResponse(
- {
- error: `Tab '${tab}' not found for page '${page}' in section '${section}' for version '${version}'`,
- },
+ { error: `Section '${section}' not found for version '${version}'` },
404,
)
}
// Path is valid, now fetch the actual content
- const collectionsToFetch = content
- .filter((entry) => entry.version === version)
- .map((entry) => entry.name as CollectionKey)
-
- const collections = await Promise.all(
- collectionsToFetch.map((name) => getCollection(name)),
- )
-
- const flatEntries = collections.flat().map(({ data, filePath, ...rest }) => ({
- filePath,
- ...rest,
- data: {
- ...data,
- tab: data.tab || data.source || getDefaultTabForApi(filePath),
- },
- }))
-
- // Find the matching entry
- const matchingEntry = flatEntries.find((entry: ContentEntry) => {
- const entryTab = addDemosOrDeprecated(entry.data.tab, entry.filePath)
- return (
- entry.data.section === section &&
- kebabCase(entry.data.id) === page &&
- entryTab === tab
- )
+ const flatEntries = await getEnrichedCollections(version)
+
+ // Find the matching entry (page may be underscore-separated for subsections like "forms_checkbox")
+ const matchingEntry = findContentEntry(flatEntries, {
+ section,
+ page,
+ tab,
})
// This shouldn't happen since we validated with index, but handle it anyway
diff --git a/src/pages/api/index.ts b/src/pages/api/index.ts
index cead58f..e04811c 100644
--- a/src/pages/api/index.ts
+++ b/src/pages/api/index.ts
@@ -6,6 +6,7 @@
* - Runtime: Routes fetch /apiIndex.json (prerendered static file) via HTTP
* - Workers-compatible: No Node.js filesystem APIs in SSR handlers
* - Optimized bundle: Runtime handlers don't bundle the 500KB+ index data
+ * - Flattened structure: Subsections are encoded into page names with underscores (e.g., "forms_checkbox")
*
* Data Flow:
* 1. Build: generateApiIndex() → data.ts + apiIndex.json
@@ -68,13 +69,13 @@ export const GET: APIRoute = async () =>
returns: {
type: 'array',
items: 'string',
- example: ['components', 'layouts', 'utilities'],
+ example: ['components', 'layouts', 'patterns', 'utility-classes'],
},
},
{
path: '/api/{version}/{section}',
method: 'GET',
- description: 'List available pages within a section',
+ description: 'List available pages within a section (includes underscore-separated subsection pages)',
parameters: [
{
name: 'version',
@@ -94,14 +95,14 @@ export const GET: APIRoute = async () =>
returns: {
type: 'array',
items: 'string',
- description: 'Array of kebab-cased page IDs',
- example: ['alert', 'button', 'card'],
+ description: 'Array of page IDs. Subsection pages use underscore-separated format: subsection_page',
+ example: ['alert', 'button', 'forms_checkbox', 'forms_radio', 'menus_dropdown'],
},
},
{
path: '/api/{version}/{section}/{page}',
method: 'GET',
- description: 'List available tabs for a page',
+ description: 'List available tabs for a page (page may be underscore-separated for subsection pages)',
parameters: [
{
name: 'version',
@@ -122,20 +123,21 @@ export const GET: APIRoute = async () =>
in: 'path',
required: true,
type: 'string',
- example: 'alert',
+ description: 'Page ID (may be underscore-separated for subsection pages like "forms_checkbox")',
+ example: 'alert or forms_checkbox',
},
],
returns: {
type: 'array',
items: 'string',
- description: 'Array of tab slugs',
+ description: 'Array of tab slugs available for this page',
example: ['react', 'react-demos', 'html'],
},
},
{
path: '/api/{version}/{section}/{page}/{tab}',
method: 'GET',
- description: 'Redirects to /text endpoint after validation',
+ description: 'Redirects to /text endpoint for the specified tab',
parameters: [
{
name: 'version',
@@ -156,7 +158,8 @@ export const GET: APIRoute = async () =>
in: 'path',
required: true,
type: 'string',
- example: 'alert',
+ description: 'Page ID (may be underscore-separated for subsection pages)',
+ example: 'alert or forms_checkbox',
},
{
name: 'tab',
@@ -168,7 +171,7 @@ export const GET: APIRoute = async () =>
],
returns: {
type: 'redirect',
- description: 'Redirects to /{version}/{section}/{page}/{tab}/text',
+ description: '302 redirect to /{version}/{section}/{page}/{tab}/text',
},
},
{
@@ -195,7 +198,8 @@ export const GET: APIRoute = async () =>
in: 'path',
required: true,
type: 'string',
- example: 'alert',
+ description: 'Page ID (may be underscore-separated for subsection pages)',
+ example: 'alert or forms_checkbox',
},
{
name: 'tab',
@@ -235,7 +239,8 @@ export const GET: APIRoute = async () =>
in: 'path',
required: true,
type: 'string',
- example: 'alert',
+ description: 'Page ID (may be underscore-separated for subsection pages)',
+ example: 'alert or forms_checkbox',
},
{
name: 'tab',
@@ -279,7 +284,8 @@ export const GET: APIRoute = async () =>
in: 'path',
required: true,
type: 'string',
- example: 'alert',
+ description: 'Page ID (may be underscore-separated for subsection pages)',
+ example: 'alert or forms_checkbox',
},
{
name: 'tab',
@@ -304,21 +310,31 @@ export const GET: APIRoute = async () =>
},
],
usage: {
- description: 'Navigate the API hierarchically to discover and retrieve documentation',
- exampleFlow: [
+ description: 'Navigate the API hierarchically to discover and retrieve documentation. Subsections are flattened into page names using underscore separators (e.g., "forms_checkbox" represents the checkbox page in the forms subsection).',
+ exampleFlowRegularPage: [
'GET /api/versions → ["v6"]',
'GET /api/v6 → ["components", "layouts", ...]',
- 'GET /api/v6/components → ["alert", "button", ...]',
+ 'GET /api/v6/components → ["alert", "button", "forms_checkbox", ...]',
'GET /api/v6/components/alert → ["react", "html", ...]',
'GET /api/v6/components/alert/react → 302 redirect to /text',
'GET /api/v6/components/alert/react/text → (markdown content)',
'GET /api/v6/components/alert/react/examples → [{exampleName, title}, ...]',
'GET /api/v6/components/alert/react/examples/AlertBasic → (example code)',
],
+ exampleFlowSubsectionPage: [
+ 'GET /api/v6 → ["components", "layouts", ...]',
+ 'GET /api/v6/components → ["alert", "forms_checkbox", "forms_radio", ...]',
+ 'GET /api/v6/components/forms_checkbox → ["react", "html", ...]',
+ 'GET /api/v6/components/forms_checkbox/react → 302 redirect to /text',
+ 'GET /api/v6/components/forms_checkbox/react/text → (markdown content)',
+ 'GET /api/v6/components/forms_checkbox/react/examples → [{exampleName, title}, ...]',
+ 'GET /api/v6/components/forms_checkbox/react/examples/CheckboxBasic → (example code)',
+ ],
architecture: {
buildTime: 'Static index generated to data.ts and apiIndex.json',
runtime: 'SSR routes fetch /apiIndex.json to avoid bundling data into Workers',
optimization: 'Workers bundle is ~110K instead of 500KB+ with embedded data',
+ flattenedStructure: 'Subsections encoded into page names with underscores (e.g., "forms_checkbox")',
},
},
- })
\ No newline at end of file
+ })
diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts
index 0262f0d..af903e0 100644
--- a/src/pages/api/openapi.json.ts
+++ b/src/pages/api/openapi.json.ts
@@ -7,6 +7,10 @@
* - Fetch /apiIndex.json (prerendered static file) at runtime
* - Keep Workers bundle small (~110K vs 500KB+)
* - Add only ~5-10ms latency from CDN fetch
+ *
+ * Structure Note:
+ * All routes use a flattened structure where subsections are encoded
+ * into page names with underscores (e.g., "forms_checkbox")
*/
import type { APIRoute } from 'astro'
import { fetchApiIndex } from '../../utils/apiIndex/fetch'
@@ -32,7 +36,7 @@ export const GET: APIRoute = async ({ url }) => {
info: {
title: 'PatternFly Documentation API',
description:
- 'Machine-readable documentation API for LLM agents and MCP servers. Provides hierarchical access to PatternFly documentation content.',
+ 'Machine-readable documentation API for LLM agents and MCP servers. Provides hierarchical access to PatternFly documentation content. Uses a flattened structure where subsections are encoded into page names with underscores (e.g., "forms_checkbox").',
version: '1.0.0',
contact: {
name: 'PatternFly',
@@ -150,7 +154,7 @@ export const GET: APIRoute = async ({ url }) => {
type: 'string',
},
},
- example: ['components', 'layouts', 'utilities'],
+ example: ['components', 'layouts', 'patterns', 'utility-classes'],
},
},
},
@@ -176,7 +180,7 @@ export const GET: APIRoute = async ({ url }) => {
get: {
summary: 'List pages in a section',
description:
- 'Returns an array of page IDs within the specified section',
+ 'Returns an array of page IDs within the specified section. Subsection pages use underscore-separated format (subsection_page, e.g., "forms_checkbox").',
operationId: 'getPages',
parameters: [
{
@@ -203,7 +207,7 @@ export const GET: APIRoute = async ({ url }) => {
],
responses: {
'200': {
- description: 'List of page IDs (kebab-cased)',
+ description: 'List of page IDs (kebab-cased, with underscore-separated subsection pages)',
content: {
'application/json': {
schema: {
@@ -212,7 +216,7 @@ export const GET: APIRoute = async ({ url }) => {
type: 'string',
},
},
- example: ['alert', 'button', 'card'],
+ example: ['alert', 'button', 'forms_checkbox', 'forms_radio', 'menus_dropdown'],
},
},
},
@@ -238,7 +242,7 @@ export const GET: APIRoute = async ({ url }) => {
get: {
summary: 'List tabs for a page',
description:
- 'Returns an array of available tab slugs for the specified page',
+ 'Returns an array of tab slugs available for the specified page. Page parameter may be underscore-separated for subsection pages (e.g., "forms_checkbox").',
operationId: 'getTabs',
parameters: [
{
@@ -266,16 +270,16 @@ export const GET: APIRoute = async ({ url }) => {
name: 'page',
in: 'path',
required: true,
- description: 'Page ID (kebab-cased)',
+ description: 'Page ID (kebab-cased, may be underscore-separated for subsection pages like "forms_checkbox")',
schema: {
type: 'string',
},
- example: 'alert',
+ example: 'alert or forms_checkbox',
},
],
responses: {
'200': {
- description: 'List of available tab slugs',
+ description: 'List of tab slugs',
content: {
'application/json': {
schema: {
@@ -310,7 +314,7 @@ export const GET: APIRoute = async ({ url }) => {
get: {
summary: 'Validate and redirect to text endpoint',
description:
- 'Validates the path parameters and redirects to the /text endpoint',
+ 'Validates the path parameters and redirects to the /text endpoint. Page parameter may be underscore-separated for subsection pages.',
operationId: 'validateTab',
parameters: [
{
@@ -338,11 +342,11 @@ export const GET: APIRoute = async ({ url }) => {
name: 'page',
in: 'path',
required: true,
- description: 'Page ID (kebab-cased)',
+ description: 'Page ID (kebab-cased, may be underscore-separated for subsection pages)',
schema: {
type: 'string',
},
- example: 'alert',
+ example: 'alert or forms_checkbox',
},
{
name: 'tab',
@@ -381,7 +385,7 @@ export const GET: APIRoute = async ({ url }) => {
get: {
summary: 'Get tab content',
description:
- 'Returns the raw markdown/MDX documentation content for the specified tab',
+ 'Returns the raw markdown/MDX documentation content for the specified tab. Page parameter may be underscore-separated for subsection pages.',
operationId: 'getContent',
parameters: [
{
@@ -409,11 +413,11 @@ export const GET: APIRoute = async ({ url }) => {
name: 'page',
in: 'path',
required: true,
- description: 'Page ID (kebab-cased)',
+ description: 'Page ID (kebab-cased, may be underscore-separated for subsection pages)',
schema: {
type: 'string',
},
- example: 'alert',
+ example: 'alert or forms_checkbox',
},
{
name: 'tab',
@@ -461,7 +465,7 @@ export const GET: APIRoute = async ({ url }) => {
get: {
summary: 'List available examples',
description:
- 'Returns an array of available examples with their names and titles',
+ 'Returns an array of available examples with their names and titles. Page parameter may be underscore-separated for subsection pages.',
operationId: 'getExamples',
parameters: [
{
@@ -489,11 +493,11 @@ export const GET: APIRoute = async ({ url }) => {
name: 'page',
in: 'path',
required: true,
- description: 'Page ID (kebab-cased)',
+ description: 'Page ID (kebab-cased, may be underscore-separated for subsection pages)',
schema: {
type: 'string',
},
- example: 'alert',
+ example: 'alert or forms_checkbox',
},
{
name: 'tab',
@@ -555,7 +559,7 @@ export const GET: APIRoute = async ({ url }) => {
get: {
summary: 'Get example code',
description:
- 'Returns the raw source code for a specific example',
+ 'Returns the raw source code for a specific example. Page parameter may be underscore-separated for subsection pages.',
operationId: 'getExampleCode',
parameters: [
{
@@ -583,11 +587,11 @@ export const GET: APIRoute = async ({ url }) => {
name: 'page',
in: 'path',
required: true,
- description: 'Page ID (kebab-cased)',
+ description: 'Page ID (kebab-cased, may be underscore-separated for subsection pages)',
schema: {
type: 'string',
},
- example: 'alert',
+ example: 'alert or forms_checkbox',
},
{
name: 'tab',
diff --git a/src/utils/__tests__/apiIndex.test.ts b/src/utils/__tests__/apiIndex.test.ts
index df044fe..e55f37e 100644
--- a/src/utils/__tests__/apiIndex.test.ts
+++ b/src/utils/__tests__/apiIndex.test.ts
@@ -24,10 +24,20 @@ describe('getApiIndex (build-time)', () => {
expect(index.versions.length).toBeGreaterThan(0)
})
- it('sections is an object with version keys', async () => {
+ it('sections is an object with version keys containing string arrays', async () => {
const index = await getApiIndex()
expect(typeof index.sections).toBe('object')
expect(Object.keys(index.sections).length).toBeGreaterThan(0)
+
+ // Validate structure of sections array
+ const sectionsArray = index.sections['v6']
+ expect(Array.isArray(sectionsArray)).toBe(true)
+ expect(sectionsArray.length).toBeGreaterThan(0)
+
+ // All items should be strings (subsections are flattened into page names)
+ sectionsArray.forEach(item => {
+ expect(typeof item).toBe('string')
+ })
})
it('pages is an object with composite keys', async () => {
@@ -43,9 +53,9 @@ describe('getApiIndex (build-time)', () => {
const index = await getApiIndex()
expect(typeof index.tabs).toBe('object')
expect(Object.keys(index.tabs).length).toBeGreaterThan(0)
- // Keys should be in format "version::section::page"
+ // Keys should be in format "version::section::page" (always 3 parts, page may be hyphenated)
const firstKey = Object.keys(index.tabs)[0]
- expect(firstKey.split('::').length).toBeGreaterThanOrEqual(3)
+ expect(firstKey.split('::').length).toBe(3)
})
it('examples is an object with composite keys and example arrays', async () => {
@@ -53,9 +63,10 @@ describe('getApiIndex (build-time)', () => {
expect(typeof index.examples).toBe('object')
const exampleKeys = Object.keys(index.examples)
expect(exampleKeys.length).toBeGreaterThan(0)
- // Keys should be in format "version::section::page::tab"
+ // Keys are always "version::section::page::tab" (4 parts, page may be hyphenated)
const firstKey = exampleKeys[0]
- expect(firstKey.split('::').length).toBe(4)
+ const parts = firstKey.split('::').length
+ expect(parts).toBe(4)
// Values should be arrays of example objects
const examples = index.examples[firstKey]
expect(Array.isArray(examples)).toBe(true)
@@ -98,6 +109,8 @@ describe('getSections', () => {
})
})
+
+
describe('getPages', () => {
it('returns array of pages for valid version and section', async () => {
const pages = await getPages('v6', 'components')
@@ -105,10 +118,13 @@ describe('getPages', () => {
expect(pages.length).toBeGreaterThan(0)
})
- it('includes expected pages', async () => {
+ it('includes expected pages (including underscore-separated subsection pages)', async () => {
const pages = await getPages('v6', 'components')
expect(pages).toContain('alert')
expect(pages).toContain('button')
+ // Check for at least one underscore-separated page (subsection page like "forms_checkbox")
+ const hasUnderscorePage = pages.some(page => page.includes('_'))
+ expect(hasUnderscorePage).toBe(true)
})
it('returns empty array for invalid section', async () => {
@@ -198,6 +214,60 @@ describe('getExamples', () => {
})
})
+describe('Flattened subsection support', () => {
+ it('subsections are flattened into page names with underscores', async () => {
+ const index = await getApiIndex()
+ const pages = index.pages['v6::components']
+ expect(pages).toBeDefined()
+ // Should contain underscore-separated page names like "forms_checkbox"
+ const formsCheckbox = pages?.find(p => p === 'forms_checkbox')
+ expect(formsCheckbox).toBeDefined()
+ })
+
+ it('flattened pages have tabs with 3-part keys', async () => {
+ const index = await getApiIndex()
+ // Underscore-separated page name (former subsection page)
+ const formsCheckboxKey = createIndexKey('v6', 'components', 'forms_checkbox')
+ expect(index.tabs[formsCheckboxKey]).toBeDefined()
+ expect(index.tabs[formsCheckboxKey].length).toBeGreaterThan(0)
+ expect(formsCheckboxKey.split('::').length).toBe(3)
+ })
+
+ it('flattened pages have examples with 4-part keys', async () => {
+ const index = await getApiIndex()
+ // Check if forms_checkbox has react examples
+ const formsCheckboxReactKey = createIndexKey('v6', 'components', 'forms_checkbox', 'react')
+ // May or may not have examples, just check structure if it exists
+ if (index.examples[formsCheckboxReactKey]) {
+ expect(Array.isArray(index.examples[formsCheckboxReactKey])).toBe(true)
+ expect(formsCheckboxReactKey.split('::').length).toBe(4)
+ }
+ })
+
+ it('regular pages continue to work with 3-part tab keys', async () => {
+ const index = await getApiIndex()
+ const alertKey = createIndexKey('v6', 'components', 'alert')
+ expect(index.tabs[alertKey]).toBeDefined()
+ expect(alertKey.split('::').length).toBe(3)
+ })
+
+ it('all tab keys are 3-part (no 4-part keys)', async () => {
+ const index = await getApiIndex()
+ const tabKeys = Object.keys(index.tabs)
+ // All tab keys should be 3-part (version::section::page)
+ const allThreePart = tabKeys.every(key => key.split('::').length === 3)
+ expect(allThreePart).toBe(true)
+ })
+
+ it('all example keys are 4-part (no 5-part keys)', async () => {
+ const index = await getApiIndex()
+ const exampleKeys = Object.keys(index.examples)
+ // All example keys should be 4-part (version::section::page::tab)
+ const allFourPart = exampleKeys.every(key => key.split('::').length === 4)
+ expect(allFourPart).toBe(true)
+ })
+})
+
describe('API Index architecture', () => {
it('data structure supports hierarchical navigation', async () => {
const index = await getApiIndex()
@@ -206,13 +276,13 @@ describe('API Index architecture', () => {
const version = index.versions[0]
expect(version).toBeTruthy()
- // Get sections for version
+ // Get sections for version (all sections are flat strings)
const sections = index.sections[version]
expect(sections).toBeDefined()
expect(sections!.length).toBeGreaterThan(0)
+ const section = sections![0]
// Get pages for first section
- const section = sections![0]
const sectionKey = createIndexKey(version, section)
const pages = index.pages[sectionKey]
expect(pages).toBeDefined()
diff --git a/src/utils/apiIndex/generate.ts b/src/utils/apiIndex/generate.ts
index 4a92211..e0e553d 100644
--- a/src/utils/apiIndex/generate.ts
+++ b/src/utils/apiIndex/generate.ts
@@ -30,20 +30,34 @@ const sortSources = (s1: string, s2: string) => {
return s1Index > s2Index ? 1 : -1
}
+/**
+ * Section item is always a string (subsections are flattened into page names)
+ */
+export type SectionItem = string
+
/**
* Structure of the API index used for routing and navigation
* Keys in sections/pages/tabs use '::' separator (e.g., 'v6::components::alert')
+ * Subsections are flattened into page names with underscores (e.g., 'forms_checkbox')
*/
export interface ApiIndex {
/** Available documentation versions (e.g., ['v5', 'v6']) */
versions: string[]
- /** Sections by version (e.g., { 'v6': ['components', 'layouts'] }) */
- sections: Record
- /** Pages by version::section (e.g., { 'v6::components': ['alert', 'button'] }) */
+ /** Sections by version - flat array of section names
+ * (e.g., { 'v6': ['components', 'layouts', 'extensions'] })
+ */
+ sections: Record
+ /** Pages by version::section with subsections flattened using underscores
+ * (e.g., { 'v6::components': ['alert', 'button', 'forms_checkbox', 'menus_select'] })
+ */
pages: Record
- /** Tabs by version::section::page (e.g., { 'v6::components::alert': ['react', 'html'] }) */
+ /** Tabs by version::section::page where page may contain underscore-separated subsection
+ * (e.g., { 'v6::components::alert': ['react'], 'v6::components::forms_checkbox': ['react'] })
+ */
tabs: Record
- /** Examples by version::section::page::tab with titles (e.g., { 'v6::components::alert::react': [{exampleName: 'AlertDefault', title: 'Default alert'}] }) */
+ /** Examples by version::section::page::tab with titles
+ * (e.g., { 'v6::components::alert::react': [{exampleName: 'AlertDefault', title: 'Default alert'}] })
+ */
examples: Record
}
@@ -132,42 +146,48 @@ export async function generateApiIndex(): Promise {
const tabExamples: Record = {}
flatEntries.forEach((entry: any) => {
- if (!entry.data.section) {
+ const { section, subsection, id } = entry.data
+ if (!section) {
return
}
- const section = entry.data.section
- const page = kebabCase(entry.data.id)
- const sectionKey = `${version}::${section}`
- const pageKey = `${version}::${section}::${page}`
+ // Flatten subsections into page name using underscores
+ // e.g., subsection="forms", id="checkbox" becomes page="forms_checkbox"
+ const basePage = kebabCase(id)
+ const page = subsection ? `${subsection}_${basePage}` : basePage
+
+ // All pages are keyed by version::section (no subsection in key)
+ const pageKey = `${version}::${section}`
+ const tabKey = `${version}::${section}::${page}`
// Collect section
sections.add(section)
- // Collect page
- if (!sectionPages[sectionKey]) {
- sectionPages[sectionKey] = new Set()
+ // Collect page under section key
+ if (!sectionPages[pageKey]) {
+ sectionPages[pageKey] = new Set()
}
- sectionPages[sectionKey].add(page)
+ sectionPages[pageKey].add(page)
// Collect tab
const entryTab =
entry.data.tab || entry.data.source || getDefaultTabForApi(entry.filePath)
const tab = addDemosOrDeprecated(entryTab, entry.filePath)
- if (!pageTabs[pageKey]) {
- pageTabs[pageKey] = new Set()
+ if (!pageTabs[tabKey]) {
+ pageTabs[tabKey] = new Set()
}
- pageTabs[pageKey].add(tab)
+ pageTabs[tabKey].add(tab)
// Collect examples for this tab
- const tabKey = `${version}::${section}::${page}::${tab}`
+ const exampleKey = `${tabKey}::${tab}`
const examplesWithTitles = extractExamplesWithTitles(entry.body || '')
if (examplesWithTitles.length > 0) {
- tabExamples[tabKey] = examplesWithTitles
+ tabExamples[exampleKey] = examplesWithTitles
}
})
// Convert sets to sorted arrays
+ // Sections are now always flat strings (subsections are in page names)
index.sections[version] = Array.from(sections).sort()
Object.entries(sectionPages).forEach(([key, pages]) => {
diff --git a/src/utils/apiRoutes/collections.ts b/src/utils/apiRoutes/collections.ts
new file mode 100644
index 0000000..d0f6e94
--- /dev/null
+++ b/src/utils/apiRoutes/collections.ts
@@ -0,0 +1,39 @@
+import type { CollectionKey } from 'astro:content'
+import { getCollection } from 'astro:content'
+import { content } from '../../content'
+import { getDefaultTabForApi } from '../packageUtils'
+
+export type EnrichedContentEntry = {
+ filePath: string
+ data: {
+ tab: string
+ [key: string]: any
+ }
+ [key: string]: any
+}
+
+/**
+ * Fetches and enriches all content collections for a specific version
+ * Enriches entries with default tab information if not specified
+ *
+ * @param version - The documentation version (e.g., 'v6')
+ * @returns Promise resolving to array of collection entries with enriched metadata
+ */
+export async function getEnrichedCollections(version: string): Promise {
+ const collectionsToFetch = content
+ .filter((entry) => entry.version === version)
+ .map((entry) => entry.name as CollectionKey)
+
+ const collections = await Promise.all(
+ collectionsToFetch.map((name) => getCollection(name))
+ )
+
+ return collections.flat().map(({ data, filePath, ...rest }) => ({
+ filePath,
+ ...rest,
+ data: {
+ ...data,
+ tab: data.tab || data.source || getDefaultTabForApi(filePath),
+ },
+ }))
+}
diff --git a/src/utils/apiRoutes/contentMatching.ts b/src/utils/apiRoutes/contentMatching.ts
new file mode 100644
index 0000000..38c1bf0
--- /dev/null
+++ b/src/utils/apiRoutes/contentMatching.ts
@@ -0,0 +1,88 @@
+import type { CollectionEntry } from 'astro:content'
+import { kebabCase, addDemosOrDeprecated } from '../index'
+import type { EnrichedContentEntry } from './collections'
+
+export type ContentEntry = CollectionEntry<
+ 'core-docs' | 'quickstarts-docs' | 'react-component-docs'
+>
+
+export interface ContentMatchParams {
+ section: string
+ page: string
+ tab: string
+}
+
+/**
+ * Checks if a content entry matches the specified parameters
+ * Handles both regular pages and subsection pages (with flattened underscore-separated names)
+ *
+ * @param entry - Content entry to check
+ * @param params - Parameters to match against (section, page, tab)
+ * @returns True if entry matches all parameters
+ */
+function matchesParams(entry: EnrichedContentEntry, params: ContentMatchParams): boolean {
+ const { section, page, tab } = params
+
+ // Match section
+ if (entry.data.section !== section) {
+ return false
+ }
+
+ // Match tab (with demos/deprecated suffix handling)
+ const entryTab = addDemosOrDeprecated(entry.data.tab, entry.filePath)
+ if (entryTab !== tab) {
+ return false
+ }
+
+ // Match page (handling flattened subsection names with underscores)
+ const entryId = kebabCase(entry.data.id)
+ const entryPage = entry.data.subsection
+ ? `${entry.data.subsection}_${entryId}`
+ : entryId
+
+ return entryPage === page
+}
+
+/**
+ * Finds a content entry matching the specified parameters
+ * Handles both regular pages and subsection pages (with flattened underscore-separated names)
+ *
+ * @param entries - Array of enriched content entries to search
+ * @param params - Parameters to match against (section, page, tab)
+ * - page may be underscore-separated for subsection pages (e.g., "forms_checkbox")
+ * @returns Matching entry or null if not found
+ */
+export function findContentEntry(
+ entries: EnrichedContentEntry[],
+ params: ContentMatchParams
+): EnrichedContentEntry | null {
+ return entries.find((entry) => matchesParams(entry, params)) || null
+}
+
+/**
+ * Finds the file path for a content entry matching the given parameters
+ * Prefers .mdx files over .md files when both exist, since .mdx files
+ * contain the LiveExample components and example imports
+ *
+ * @param entries - Array of enriched content entries to search
+ * @param params - Parameters to match against (section, page, tab)
+ * - page may be underscore-separated for subsection pages (e.g., "forms_checkbox")
+ * @returns The file path, or null if not found
+ */
+export function findContentEntryFilePath(
+ entries: EnrichedContentEntry[],
+ params: ContentMatchParams
+): string | null {
+ // Find all matching entries using shared matching logic
+ const matchingEntries = entries.filter((entry) => matchesParams(entry, params))
+
+ if (matchingEntries.length === 0) {
+ return null
+ }
+
+ // Prefer .mdx over .md (mdx files contain LiveExample components)
+ const mdxEntry = matchingEntries.find((entry) => entry.filePath.endsWith('.mdx'))
+ const selectedEntry = mdxEntry || matchingEntries[0]
+
+ return selectedEntry.filePath
+}
diff --git a/src/utils/apiRoutes/exampleParsing.ts b/src/utils/apiRoutes/exampleParsing.ts
new file mode 100644
index 0000000..613def1
--- /dev/null
+++ b/src/utils/apiRoutes/exampleParsing.ts
@@ -0,0 +1,43 @@
+/**
+ * Extracts import statements from file content
+ * Matches import statements with relative paths (starting with ./ or ../)
+ *
+ * @param fileContent - The file content to parse
+ * @returns Array of import statements or null if none found
+ */
+export function extractImports(fileContent: string): string[] | null {
+ // Match import statements with relative paths
+ // Supports: import X from './path', import X from "../path/file.tsx"
+ const importRegex = /import\s+.*\s+from\s+['"]\.{1,2}\/[^'"]+['"]/gm
+ const matches = fileContent.match(importRegex)
+ return matches
+}
+
+/**
+ * Extracts the file path for a specific example from import statements
+ * Looks for imports that reference the example name using word boundaries to avoid substring matches
+ *
+ * @param imports - Array of import statements
+ * @param exampleName - Name of the example to find (must be a valid React component name)
+ * @returns Relative file path without quotes (including query params like ?raw), or null if not found
+ */
+export function extractExampleFilePath(imports: string[], exampleName: string): string | null {
+ // Use word boundaries to match exact example names (e.g., "AlertBasic" won't match "AlertBasicExpanded")
+ // No escaping needed - React component names can only contain [A-Za-z0-9_]
+ const wordRegex = new RegExp(`\\b${exampleName}\\b`)
+ const exampleImport = imports.find((imp) => wordRegex.test(imp))
+
+ if (!exampleImport) {
+ // eslint-disable-next-line no-console
+ console.error('No import path found for example', exampleName)
+ return null
+ }
+
+ // Extract path from import statement, handling query parameters like ?raw
+ // Matches: "./path" or "../path" with optional file extensions and query params
+ const match = exampleImport.match(/['"](\.[^'"]+)['"]/i)
+ if (!match || !match[1]) {
+ return null
+ }
+ return match[1]
+}