mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-09 15:05:26 +03:00
Compare commits
3 Commits
d447e6206f
...
5212abae21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5212abae21 | ||
|
|
882b3a8ffd | ||
|
|
b80a8bbf2b |
5
.gitignore
vendored
5
.gitignore
vendored
@ -22,3 +22,8 @@ core
|
|||||||
!/web/**/*.ts
|
!/web/**/*.ts
|
||||||
.idea/*
|
.idea/*
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# Auto-generated Docker Compose Generator config files
|
||||||
|
docs/src/components/DockerComposeGenerator/config/devices.ts
|
||||||
|
docs/src/components/DockerComposeGenerator/config/hardware.ts
|
||||||
|
docs/src/components/DockerComposeGenerator/config/ports.ts
|
||||||
|
|||||||
@ -4,6 +4,9 @@ title: Installation
|
|||||||
---
|
---
|
||||||
|
|
||||||
import ShmCalculator from '@site/src/components/ShmCalculator'
|
import ShmCalculator from '@site/src/components/ShmCalculator'
|
||||||
|
import DockerComposeGenerator from '@site/src/components/DockerComposeGenerator'
|
||||||
|
import Tabs from '@theme/Tabs';
|
||||||
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
Frigate is a Docker container that can be run on any Docker host including as a [Home Assistant App](https://www.home-assistant.io/apps/). Note that the Home Assistant App is **not** the same thing as the integration. The [integration](/integrations/home-assistant) is required to integrate Frigate into Home Assistant, whether you are running Frigate as a standalone Docker container or as a Home Assistant App.
|
Frigate is a Docker container that can be run on any Docker host including as a [Home Assistant App](https://www.home-assistant.io/apps/). Note that the Home Assistant App is **not** the same thing as the integration. The [integration](/integrations/home-assistant) is required to integrate Frigate into Home Assistant, whether you are running Frigate as a standalone Docker container or as a Home Assistant App.
|
||||||
|
|
||||||
@ -474,6 +477,16 @@ Finally, configure [hardware object detection](/configuration/object_detectors#a
|
|||||||
|
|
||||||
Running through Docker with Docker Compose is the recommended install method.
|
Running through Docker with Docker Compose is the recommended install method.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="domestic" label="Docker Compose Generator" default>
|
||||||
|
|
||||||
|
Generate a Frigate Docker Compose configuration based on your hardware and requirements.
|
||||||
|
|
||||||
|
<DockerComposeGenerator/>
|
||||||
|
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="original" label="Example Docker Compose File">
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
@ -507,6 +520,10 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
FRIGATE_RTSP_PASSWORD: "password"
|
FRIGATE_RTSP_PASSWORD: "password"
|
||||||
```
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
**Docker CLI**
|
||||||
|
|
||||||
If you can't use Docker Compose, you can run the container with something similar to this:
|
If you can't use Docker Compose, you can run the container with something similar to this:
|
||||||
|
|
||||||
|
|||||||
545
docs/package-lock.json
generated
545
docs/package-lock.json
generated
@ -9,14 +9,16 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "^3.7.0",
|
"@docusaurus/core": "^3.7.0",
|
||||||
"@docusaurus/plugin-content-docs": "^3.7.0",
|
"@docusaurus/plugin-content-docs": "^3.10.1",
|
||||||
"@docusaurus/preset-classic": "^3.7.0",
|
"@docusaurus/preset-classic": "^3.7.0",
|
||||||
"@docusaurus/theme-mermaid": "^3.7.0",
|
"@docusaurus/theme-mermaid": "^3.7.0",
|
||||||
"@inkeep/docusaurus": "^2.0.16",
|
"@inkeep/docusaurus": "^2.0.16",
|
||||||
"@mdx-js/react": "^3.1.0",
|
"@mdx-js/react": "^3.1.0",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"docusaurus-plugin-openapi-docs": "^4.5.1",
|
"docusaurus-plugin-openapi-docs": "^4.5.1",
|
||||||
"docusaurus-theme-openapi-docs": "^4.5.1",
|
"docusaurus-theme-openapi-docs": "^4.5.1",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
"prism-react-renderer": "^2.4.1",
|
"prism-react-renderer": "^2.4.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@ -3708,20 +3710,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@docusaurus/plugin-content-docs": {
|
"node_modules/@docusaurus/plugin-content-docs": {
|
||||||
"version": "3.9.2",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.1.tgz",
|
||||||
"integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==",
|
"integrity": "sha512-2jRVrtzjf8LClGTHQlwlwuD3wQXRx3WEoF7XUarJ8Ou+0onV+SLtejsyfY9JLpfUh9hPhXM4pbBGkyAY4Bi3HQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "3.9.2",
|
"@docusaurus/core": "3.10.1",
|
||||||
"@docusaurus/logger": "3.9.2",
|
"@docusaurus/logger": "3.10.1",
|
||||||
"@docusaurus/mdx-loader": "3.9.2",
|
"@docusaurus/mdx-loader": "3.10.1",
|
||||||
"@docusaurus/module-type-aliases": "3.9.2",
|
"@docusaurus/module-type-aliases": "3.10.1",
|
||||||
"@docusaurus/theme-common": "3.9.2",
|
"@docusaurus/theme-common": "3.10.1",
|
||||||
"@docusaurus/types": "3.9.2",
|
"@docusaurus/types": "3.10.1",
|
||||||
"@docusaurus/utils": "3.9.2",
|
"@docusaurus/utils": "3.10.1",
|
||||||
"@docusaurus/utils-common": "3.9.2",
|
"@docusaurus/utils-common": "3.10.1",
|
||||||
"@docusaurus/utils-validation": "3.9.2",
|
"@docusaurus/utils-validation": "3.10.1",
|
||||||
"@types/react-router-config": "^5.0.7",
|
"@types/react-router-config": "^5.0.7",
|
||||||
"combine-promises": "^1.1.0",
|
"combine-promises": "^1.1.0",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
@ -3740,6 +3742,382 @@
|
|||||||
"react-dom": "^18.0.0 || ^19.0.0"
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/babel": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-DZzFO1K3v/GoEt1fx1DiYHF4en+PuhtQf1AkQJa5zu3CoeKSpr5cpQRUlz3jr0m44wyzmSXu9bVpfir+N4+8bg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.25.9",
|
||||||
|
"@babel/generator": "^7.25.9",
|
||||||
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.25.9",
|
||||||
|
"@babel/preset-env": "^7.25.9",
|
||||||
|
"@babel/preset-react": "^7.25.9",
|
||||||
|
"@babel/preset-typescript": "^7.25.9",
|
||||||
|
"@babel/runtime": "^7.25.9",
|
||||||
|
"@babel/traverse": "^7.25.9",
|
||||||
|
"@docusaurus/logger": "3.10.1",
|
||||||
|
"@docusaurus/utils": "3.10.1",
|
||||||
|
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"tslib": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/bundler": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-HIqQPvbqnnQRe4NsBd1774KRarjXqS6wHsWELtyuSs1gCfvixJO2jUGH/OEBtr1Gvzpw+ze5CjGMvSJ8UE1KUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.25.9",
|
||||||
|
"@docusaurus/babel": "3.10.1",
|
||||||
|
"@docusaurus/cssnano-preset": "3.10.1",
|
||||||
|
"@docusaurus/logger": "3.10.1",
|
||||||
|
"@docusaurus/types": "3.10.1",
|
||||||
|
"@docusaurus/utils": "3.10.1",
|
||||||
|
"babel-loader": "^9.2.1",
|
||||||
|
"clean-css": "^5.3.3",
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
"css-loader": "^6.11.0",
|
||||||
|
"css-minimizer-webpack-plugin": "^5.0.1",
|
||||||
|
"cssnano": "^6.1.2",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"html-minifier-terser": "^7.2.0",
|
||||||
|
"mini-css-extract-plugin": "^2.9.2",
|
||||||
|
"null-loader": "^4.0.1",
|
||||||
|
"postcss": "^8.5.4",
|
||||||
|
"postcss-loader": "^7.3.4",
|
||||||
|
"postcss-preset-env": "^10.2.1",
|
||||||
|
"terser-webpack-plugin": "^5.3.9",
|
||||||
|
"tslib": "^2.6.0",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"webpack": "^5.95.0",
|
||||||
|
"webpackbar": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@docusaurus/faster": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@docusaurus/faster": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/core": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-3pf2fXXw0eVk8WnC3T4LIigRDupcpvngpKo9Vy7mYyBhuddc0klDUuZAIfzMoK6z05pdlk6EFC/vBSX43+1O5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/babel": "3.10.1",
|
||||||
|
"@docusaurus/bundler": "3.10.1",
|
||||||
|
"@docusaurus/logger": "3.10.1",
|
||||||
|
"@docusaurus/mdx-loader": "3.10.1",
|
||||||
|
"@docusaurus/utils": "3.10.1",
|
||||||
|
"@docusaurus/utils-common": "3.10.1",
|
||||||
|
"@docusaurus/utils-validation": "3.10.1",
|
||||||
|
"boxen": "^6.2.1",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"chokidar": "^3.5.3",
|
||||||
|
"cli-table3": "^0.6.3",
|
||||||
|
"combine-promises": "^1.1.0",
|
||||||
|
"commander": "^5.1.0",
|
||||||
|
"core-js": "^3.31.1",
|
||||||
|
"detect-port": "^1.5.1",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"eta": "^2.2.0",
|
||||||
|
"eval": "^0.1.8",
|
||||||
|
"execa": "^5.1.1",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"html-tags": "^3.3.1",
|
||||||
|
"html-webpack-plugin": "^5.6.0",
|
||||||
|
"leven": "^3.1.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"open": "^8.4.0",
|
||||||
|
"p-map": "^4.0.0",
|
||||||
|
"prompts": "^2.4.2",
|
||||||
|
"react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0",
|
||||||
|
"react-loadable": "npm:@docusaurus/react-loadable@6.0.0",
|
||||||
|
"react-loadable-ssr-addon-v5-slorber": "^1.0.3",
|
||||||
|
"react-router": "^5.3.4",
|
||||||
|
"react-router-config": "^5.1.1",
|
||||||
|
"react-router-dom": "^5.3.4",
|
||||||
|
"semver": "^7.5.4",
|
||||||
|
"serve-handler": "^6.1.7",
|
||||||
|
"tinypool": "^1.0.2",
|
||||||
|
"tslib": "^2.6.0",
|
||||||
|
"update-notifier": "^6.0.2",
|
||||||
|
"webpack": "^5.95.0",
|
||||||
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
|
"webpack-dev-server": "^5.2.2",
|
||||||
|
"webpack-merge": "^6.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"docusaurus": "bin/docusaurus.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@docusaurus/faster": "*",
|
||||||
|
"@mdx-js/react": "^3.0.0",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@docusaurus/faster": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/cssnano-preset": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-eNfHGcTKCSq6xmcavAkX3RRclHaE2xRCMParlDXLdXVP01/a2e/jKXMj/0ULnLFQSNwwuI62L0Ge8J+nZsR7UQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cssnano-preset-advanced": "^6.1.2",
|
||||||
|
"postcss": "^8.5.4",
|
||||||
|
"postcss-sort-media-queries": "^5.2.0",
|
||||||
|
"tslib": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/logger": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-oPjNFnfJsRCkePVjkGrxWGq4MvJKRQT0r9jOP0eRBTZ7Wr9FAbzdP/Gjs0I2Ss6YRkPoEgygKG112OkE6skvJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"tslib": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/mdx-loader": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-GRmeb/wQ+iXRrFwcHBfgQhrJxGElgCsoTWZYDhccjsZVne1p8MK/EpQVIloXttz76TCe78kKD5AEG9n1xc1oxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/logger": "3.10.1",
|
||||||
|
"@docusaurus/utils": "3.10.1",
|
||||||
|
"@docusaurus/utils-validation": "3.10.1",
|
||||||
|
"@mdx-js/mdx": "^3.0.0",
|
||||||
|
"@slorber/remark-comment": "^1.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"estree-util-value-to-estree": "^3.0.1",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"image-size": "^2.0.2",
|
||||||
|
"mdast-util-mdx": "^3.0.0",
|
||||||
|
"mdast-util-to-string": "^4.0.0",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
|
"remark-directive": "^3.0.0",
|
||||||
|
"remark-emoji": "^4.0.0",
|
||||||
|
"remark-frontmatter": "^5.0.0",
|
||||||
|
"remark-gfm": "^4.0.0",
|
||||||
|
"stringify-object": "^3.3.0",
|
||||||
|
"tslib": "^2.6.0",
|
||||||
|
"unified": "^11.0.3",
|
||||||
|
"unist-util-visit": "^5.0.0",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"vfile": "^6.0.1",
|
||||||
|
"webpack": "^5.88.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/module-type-aliases": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-YoOZKUdGlp8xSYhuAkGdSo5Ydkbq4V4eK3sD8v0a2hloxCWdQbNBhkc+Ko9QyjpESc0BYcIGM5iHVAy5hdFV6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/types": "3.10.1",
|
||||||
|
"@types/history": "^4.7.11",
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-router-config": "*",
|
||||||
|
"@types/react-router-dom": "*",
|
||||||
|
"react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0",
|
||||||
|
"react-loadable": "npm:@docusaurus/react-loadable@6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-dom": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/theme-common": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-0YtmIeoNo1fIw65LO8+/1dPgmDV86UmhMkow37gzjytuiCSQm9xob6PJy0L4kuQEMTLfUOGvkXvZr7GPrHquMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/mdx-loader": "3.10.1",
|
||||||
|
"@docusaurus/module-type-aliases": "3.10.1",
|
||||||
|
"@docusaurus/utils": "3.10.1",
|
||||||
|
"@docusaurus/utils-common": "3.10.1",
|
||||||
|
"@types/history": "^4.7.11",
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-router-config": "*",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"parse-numeric-range": "^1.3.0",
|
||||||
|
"prism-react-renderer": "^2.3.0",
|
||||||
|
"tslib": "^2.6.0",
|
||||||
|
"utility-types": "^3.10.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@docusaurus/plugin-content-docs": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/types": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-XYMK8k1szDCFMw2V+Xyen0g7Kee1sP3dtFnl7vkGkZOkeAJ/oPDQPL8iz4HBKOo/cwU8QeV6onVjMqtP+tFzsw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@mdx-js/mdx": "^3.0.0",
|
||||||
|
"@types/history": "^4.7.11",
|
||||||
|
"@types/mdast": "^4.0.2",
|
||||||
|
"@types/react": "*",
|
||||||
|
"commander": "^5.1.0",
|
||||||
|
"joi": "^17.9.2",
|
||||||
|
"react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0",
|
||||||
|
"utility-types": "^3.10.0",
|
||||||
|
"webpack": "^5.95.0",
|
||||||
|
"webpack-merge": "^5.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/types/node_modules/webpack-merge": {
|
||||||
|
"version": "5.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
|
||||||
|
"integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"clone-deep": "^4.0.1",
|
||||||
|
"flat": "^5.0.2",
|
||||||
|
"wildcard": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/utils": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-3ojeJry9xBYdJO6qoyyzqeJFSJBVx2mXhyDzSdjwL2+URFQMf+h25gG38iswGImicK0ELjTd1EL2xzk8hf3QPw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/logger": "3.10.1",
|
||||||
|
"@docusaurus/types": "3.10.1",
|
||||||
|
"@docusaurus/utils-common": "3.10.1",
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"execa": "^5.1.1",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"github-slugger": "^1.5.0",
|
||||||
|
"globby": "^11.1.0",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"jiti": "^1.20.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"micromatch": "^4.0.5",
|
||||||
|
"p-queue": "^6.6.2",
|
||||||
|
"prompts": "^2.4.2",
|
||||||
|
"resolve-pathname": "^3.0.0",
|
||||||
|
"tslib": "^2.6.0",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"utility-types": "^3.10.0",
|
||||||
|
"webpack": "^5.88.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/utils-common": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-5mFSgEADtnFxFH7RLw02QA5MpU5JVUCj0MPeIvi/aF4Fi45tQRIuTwXoXDqJ+1VfQJuYJGz3SI63wmGz4HvXzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/types": "3.10.1",
|
||||||
|
"tslib": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/utils-validation": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-cRv1X69jwaWv47waglllgZVWzeBFLhl53XT/XED/83BerVBTC5FTP8WTcVl8Z6sZOegDSwitu/wpCSPCDOT6lg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/logger": "3.10.1",
|
||||||
|
"@docusaurus/utils": "3.10.1",
|
||||||
|
"@docusaurus/utils-common": "3.10.1",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
|
"joi": "^17.9.2",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"tslib": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@docusaurus/plugin-content-docs/node_modules/webpackbar": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-aS9soqSO2iCHgqHoCrj4LbfGQUboDCYJPSFOAchEK+9psIjNrfSWW4Y0YEz67MKURNvMmfo0ycOg9d/+OOf9/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansis": "^3.2.0",
|
||||||
|
"consola": "^3.2.3",
|
||||||
|
"pretty-time": "^1.1.0",
|
||||||
|
"std-env": "^3.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@rspack/core": "*",
|
||||||
|
"webpack": "3 || 4 || 5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@rspack/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"webpack": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@docusaurus/plugin-content-pages": {
|
"node_modules/@docusaurus/plugin-content-pages": {
|
||||||
"version": "3.9.2",
|
"version": "3.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.9.2.tgz",
|
||||||
@ -3935,6 +4313,39 @@
|
|||||||
"react-dom": "^18.0.0 || ^19.0.0"
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/plugin-content-docs": {
|
||||||
|
"version": "3.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz",
|
||||||
|
"integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/core": "3.9.2",
|
||||||
|
"@docusaurus/logger": "3.9.2",
|
||||||
|
"@docusaurus/mdx-loader": "3.9.2",
|
||||||
|
"@docusaurus/module-type-aliases": "3.9.2",
|
||||||
|
"@docusaurus/theme-common": "3.9.2",
|
||||||
|
"@docusaurus/types": "3.9.2",
|
||||||
|
"@docusaurus/utils": "3.9.2",
|
||||||
|
"@docusaurus/utils-common": "3.9.2",
|
||||||
|
"@docusaurus/utils-validation": "3.9.2",
|
||||||
|
"@types/react-router-config": "^5.0.7",
|
||||||
|
"combine-promises": "^1.1.0",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"schema-dts": "^1.1.2",
|
||||||
|
"tslib": "^2.6.0",
|
||||||
|
"utility-types": "^3.10.0",
|
||||||
|
"webpack": "^5.88.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@docusaurus/theme-classic": {
|
"node_modules/@docusaurus/theme-classic": {
|
||||||
"version": "3.9.2",
|
"version": "3.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.9.2.tgz",
|
||||||
@ -3975,6 +4386,39 @@
|
|||||||
"react-dom": "^18.0.0 || ^19.0.0"
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/plugin-content-docs": {
|
||||||
|
"version": "3.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz",
|
||||||
|
"integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/core": "3.9.2",
|
||||||
|
"@docusaurus/logger": "3.9.2",
|
||||||
|
"@docusaurus/mdx-loader": "3.9.2",
|
||||||
|
"@docusaurus/module-type-aliases": "3.9.2",
|
||||||
|
"@docusaurus/theme-common": "3.9.2",
|
||||||
|
"@docusaurus/types": "3.9.2",
|
||||||
|
"@docusaurus/utils": "3.9.2",
|
||||||
|
"@docusaurus/utils-common": "3.9.2",
|
||||||
|
"@docusaurus/utils-validation": "3.9.2",
|
||||||
|
"@types/react-router-config": "^5.0.7",
|
||||||
|
"combine-promises": "^1.1.0",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"schema-dts": "^1.1.2",
|
||||||
|
"tslib": "^2.6.0",
|
||||||
|
"utility-types": "^3.10.0",
|
||||||
|
"webpack": "^5.88.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@docusaurus/theme-common": {
|
"node_modules/@docusaurus/theme-common": {
|
||||||
"version": "3.9.2",
|
"version": "3.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz",
|
||||||
@ -4062,6 +4506,39 @@
|
|||||||
"react-dom": "^18.0.0 || ^19.0.0"
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@docusaurus/theme-search-algolia/node_modules/@docusaurus/plugin-content-docs": {
|
||||||
|
"version": "3.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz",
|
||||||
|
"integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/core": "3.9.2",
|
||||||
|
"@docusaurus/logger": "3.9.2",
|
||||||
|
"@docusaurus/mdx-loader": "3.9.2",
|
||||||
|
"@docusaurus/module-type-aliases": "3.9.2",
|
||||||
|
"@docusaurus/theme-common": "3.9.2",
|
||||||
|
"@docusaurus/types": "3.9.2",
|
||||||
|
"@docusaurus/utils": "3.9.2",
|
||||||
|
"@docusaurus/utils-common": "3.9.2",
|
||||||
|
"@docusaurus/utils-validation": "3.9.2",
|
||||||
|
"@types/react-router-config": "^5.0.7",
|
||||||
|
"combine-promises": "^1.1.0",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"schema-dts": "^1.1.2",
|
||||||
|
"tslib": "^2.6.0",
|
||||||
|
"utility-types": "^3.10.0",
|
||||||
|
"webpack": "^5.88.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@docusaurus/theme-translations": {
|
"node_modules/@docusaurus/theme-translations": {
|
||||||
"version": "3.9.2",
|
"version": "3.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.9.2.tgz",
|
||||||
@ -5747,6 +6224,11 @@
|
|||||||
"@types/istanbul-lib-report": "*"
|
"@types/istanbul-lib-report": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/js-yaml": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@ -6468,6 +6950,15 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansis": {
|
||||||
|
"version": "3.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz",
|
||||||
|
"integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/any-promise": {
|
"node_modules/any-promise": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||||
@ -12883,7 +13374,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -18811,9 +19302,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-loadable-ssr-addon-v5-slorber": {
|
"node_modules/react-loadable-ssr-addon-v5-slorber": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.3.tgz",
|
||||||
"integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==",
|
"integrity": "sha512-GXfh9VLwB5ERaCsU6RULh7tkemeX15aNh6wuMEBtfdyMa7fFG8TXrhXlx1SoEK2Ty/l6XIkzzYIQmyaWW3JgdQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.10.3"
|
"@babel/runtime": "^7.10.3"
|
||||||
@ -20594,24 +21085,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/serve-handler": {
|
"node_modules/serve-handler": {
|
||||||
"version": "6.1.6",
|
"version": "6.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz",
|
||||||
"integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
|
"integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.0.0",
|
"bytes": "3.0.0",
|
||||||
"content-disposition": "0.5.2",
|
"content-disposition": "0.5.2",
|
||||||
"mime-types": "2.1.18",
|
"mime-types": "2.1.18",
|
||||||
"minimatch": "3.1.2",
|
"minimatch": "3.1.5",
|
||||||
"path-is-inside": "1.0.2",
|
"path-is-inside": "1.0.2",
|
||||||
"path-to-regexp": "3.3.0",
|
"path-to-regexp": "3.3.0",
|
||||||
"range-parser": "1.2.0"
|
"range-parser": "1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/serve-handler/node_modules/brace-expansion": {
|
"node_modules/serve-handler/node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
@ -20640,9 +21131,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/serve-handler/node_modules/minimatch": {
|
"node_modules/serve-handler/node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
|
|||||||
@ -3,9 +3,10 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build:config": "node scripts/build-config.mjs",
|
||||||
"docusaurus": "docusaurus",
|
"docusaurus": "docusaurus",
|
||||||
"start": "npm run regen-docs && docusaurus start --host 0.0.0.0",
|
"start": "npm run build:config && npm run regen-docs && docusaurus start --host 0.0.0.0",
|
||||||
"build": "npm run regen-docs && docusaurus build",
|
"build": "npm run build:config && npm run regen-docs && docusaurus build",
|
||||||
"swizzle": "docusaurus swizzle",
|
"swizzle": "docusaurus swizzle",
|
||||||
"deploy": "docusaurus deploy",
|
"deploy": "docusaurus deploy",
|
||||||
"clear": "docusaurus clear",
|
"clear": "docusaurus clear",
|
||||||
@ -18,14 +19,16 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "^3.7.0",
|
"@docusaurus/core": "^3.7.0",
|
||||||
"@docusaurus/plugin-content-docs": "^3.7.0",
|
"@docusaurus/plugin-content-docs": "^3.10.1",
|
||||||
"@docusaurus/preset-classic": "^3.7.0",
|
"@docusaurus/preset-classic": "^3.7.0",
|
||||||
"@docusaurus/theme-mermaid": "^3.7.0",
|
"@docusaurus/theme-mermaid": "^3.7.0",
|
||||||
"@inkeep/docusaurus": "^2.0.16",
|
"@inkeep/docusaurus": "^2.0.16",
|
||||||
"@mdx-js/react": "^3.1.0",
|
"@mdx-js/react": "^3.1.0",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"docusaurus-plugin-openapi-docs": "^4.5.1",
|
"docusaurus-plugin-openapi-docs": "^4.5.1",
|
||||||
"docusaurus-theme-openapi-docs": "^4.5.1",
|
"docusaurus-theme-openapi-docs": "^4.5.1",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
"prism-react-renderer": "^2.4.1",
|
"prism-react-renderer": "^2.4.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
64
docs/scripts/build-config.mjs
Normal file
64
docs/scripts/build-config.mjs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build script: reads config.yaml and generates TypeScript files
|
||||||
|
* for the Docker Compose Generator.
|
||||||
|
*
|
||||||
|
* Usage: node scripts/build-config.mjs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const CONFIG_DIR = path.resolve(__dirname, "../src/components/DockerComposeGenerator/config");
|
||||||
|
const YAML_PATH = path.join(CONFIG_DIR, "config.yaml");
|
||||||
|
|
||||||
|
// Read & parse YAML
|
||||||
|
const raw = fs.readFileSync(YAML_PATH, "utf8");
|
||||||
|
const config = yaml.load(raw);
|
||||||
|
|
||||||
|
if (!config.devices || !config.hardware || !config.ports) {
|
||||||
|
console.error("config.yaml must contain 'devices', 'hardware', and 'ports' sections.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a .ts file from a section of the YAML config.
|
||||||
|
*/
|
||||||
|
function generateTsFile(sectionName, items, typeName, varName, mapVarName, yamlFilename) {
|
||||||
|
const jsonItems = JSON.stringify(items, null, 2);
|
||||||
|
// Indent JSON to fit inside the array literal
|
||||||
|
const indented = jsonItems
|
||||||
|
.split("\n")
|
||||||
|
.map((line, i) => (i === 0 ? line : " " + line))
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const content = `/**
|
||||||
|
* AUTO-GENERATED FILE — do not edit directly.
|
||||||
|
* Source: ${yamlFilename}
|
||||||
|
* To update, edit the YAML file and run: npm run build:config
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ${typeName} } from "./types";
|
||||||
|
|
||||||
|
export const ${varName}: ${typeName}[] = ${indented};
|
||||||
|
|
||||||
|
/** Lookup map for quick access by ID */
|
||||||
|
export const ${mapVarName}: Map<string, ${typeName}> = new Map(${varName}.map((item) => [item.id, item]));
|
||||||
|
`;
|
||||||
|
|
||||||
|
const outPath = path.join(CONFIG_DIR, `${sectionName}.ts`);
|
||||||
|
fs.writeFileSync(outPath, content, "utf8");
|
||||||
|
console.log(` ✓ Generated ${sectionName}.ts (${items.length} items)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Building config from config.yaml...");
|
||||||
|
|
||||||
|
generateTsFile("devices", config.devices, "DeviceConfig", "devices", "deviceMap", "config.yaml");
|
||||||
|
generateTsFile("hardware", config.hardware, "HardwareOption", "hardwareOptions", "hardwareMap", "config.yaml");
|
||||||
|
generateTsFile("ports", config.ports, "PortConfig", "ports", "portMap", "config.yaml");
|
||||||
|
|
||||||
|
console.log("Done!");
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Admonition from "@theme/Admonition";
|
||||||
|
import DeviceSelector from "./components/DeviceSelector";
|
||||||
|
import HardwareOptions from "./components/HardwareOptions";
|
||||||
|
import PortConfigSection from "./components/PortConfig";
|
||||||
|
import StoragePaths from "./components/StoragePaths";
|
||||||
|
import NvidiaGpuConfig from "./components/NvidiaGpuConfig";
|
||||||
|
import OtherOptions from "./components/OtherOptions";
|
||||||
|
import GeneratedOutput from "./components/GeneratedOutput";
|
||||||
|
import { useConfigGenerator } from "./hooks/useConfigGenerator";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple markdown-link-to-React renderer for help text.
|
||||||
|
* Only supports [text](url) syntax — no nested brackets.
|
||||||
|
*/
|
||||||
|
function renderHelpText(text: string): React.ReactNode {
|
||||||
|
const parts = text.split(/(\[[^\]]+\]\([^)]+\))/g);
|
||||||
|
return parts.map((part, i) => {
|
||||||
|
const match = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
||||||
|
if (match) {
|
||||||
|
return (
|
||||||
|
<a key={i} href={match[2]}>
|
||||||
|
{match[1]}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <React.Fragment key={i}>{part}</React.Fragment>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DockerComposeGenerator() {
|
||||||
|
const {
|
||||||
|
deviceId, device, hardwareEnabled,
|
||||||
|
portEnabled,
|
||||||
|
nvidiaGpuCount, nvidiaGpuDeviceId,
|
||||||
|
configPath, mediaPath, rtspPassword, timezone, shmSize,
|
||||||
|
shmSizeError, gpuDeviceIdError, configPathError, mediaPathError,
|
||||||
|
hasAnyHardware, generatedYaml,
|
||||||
|
selectDevice, toggleHardware, togglePort,
|
||||||
|
handleShmSizeChange, handleConfigPathChange, handleMediaPathChange,
|
||||||
|
handleNvidiaGpuCountChange, handleNvidiaGpuDeviceIdChange,
|
||||||
|
setRtspPassword, setTimezone, isHardwareDisabled,
|
||||||
|
} = useConfigGenerator();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.generator}>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<DeviceSelector selectedId={deviceId} onSelect={selectDevice} />
|
||||||
|
|
||||||
|
{device.helpText && (
|
||||||
|
<Admonition type={device.helpType || "info"}>
|
||||||
|
{renderHelpText(device.helpText)}
|
||||||
|
</Admonition>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{device.needsNvidiaConfig && (
|
||||||
|
<NvidiaGpuConfig
|
||||||
|
gpuCount={nvidiaGpuCount}
|
||||||
|
gpuDeviceId={nvidiaGpuDeviceId}
|
||||||
|
gpuDeviceIdError={gpuDeviceIdError}
|
||||||
|
onGpuCountChange={handleNvidiaGpuCountChange}
|
||||||
|
onGpuDeviceIdChange={handleNvidiaGpuDeviceIdChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<HardwareOptions
|
||||||
|
deviceId={deviceId}
|
||||||
|
hardwareEnabled={hardwareEnabled}
|
||||||
|
onToggle={toggleHardware}
|
||||||
|
isDisabled={isHardwareDisabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StoragePaths
|
||||||
|
configPath={configPath}
|
||||||
|
mediaPath={mediaPath}
|
||||||
|
configPathError={configPathError}
|
||||||
|
mediaPathError={mediaPathError}
|
||||||
|
onConfigPathChange={handleConfigPathChange}
|
||||||
|
onMediaPathChange={handleMediaPathChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PortConfigSection
|
||||||
|
portEnabled={portEnabled}
|
||||||
|
onTogglePort={togglePort}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OtherOptions
|
||||||
|
rtspPassword={rtspPassword}
|
||||||
|
timezone={timezone}
|
||||||
|
shmSize={shmSize}
|
||||||
|
shmSizeError={shmSizeError}
|
||||||
|
onRtspPasswordChange={setRtspPassword}
|
||||||
|
onTimezoneChange={setTimezone}
|
||||||
|
onShmSizeChange={handleShmSizeChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GeneratedOutput
|
||||||
|
yaml={generatedYaml}
|
||||||
|
configPath={configPath}
|
||||||
|
mediaPath={mediaPath}
|
||||||
|
hasAnyHardware={hasAnyHardware}
|
||||||
|
deviceId={deviceId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,147 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useColorMode } from "@docusaurus/theme-common";
|
||||||
|
import { devices } from "../config";
|
||||||
|
import type { DeviceConfig } from "../config";
|
||||||
|
import styles from "../styles.module.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedId: string;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the icon type from the icon string:
|
||||||
|
* - Starts with "<svg" → inline SVG
|
||||||
|
* - Starts with "/" or "http" → image URL/path
|
||||||
|
* - Otherwise → emoji text
|
||||||
|
*/
|
||||||
|
function getIconType(icon: string): "svg" | "image" | "emoji" {
|
||||||
|
const trimmed = icon.trim();
|
||||||
|
if (trimmed.startsWith("<svg")) return "svg";
|
||||||
|
if (trimmed.startsWith("/") || trimmed.startsWith("http://") || trimmed.startsWith("https://")) return "image";
|
||||||
|
return "emoji";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the style object contains background-* properties,
|
||||||
|
* indicating the image should be rendered as a CSS background-image
|
||||||
|
* rather than an <img> tag.
|
||||||
|
*/
|
||||||
|
function hasBackgroundProps(style: React.CSSProperties | undefined): boolean {
|
||||||
|
if (!style) return false;
|
||||||
|
return Object.keys(style).some((key) => {
|
||||||
|
const k = key.toLowerCase().replace(/-/g, "");
|
||||||
|
return k === "backgroundsize" || k === "backgroundposition" || k === "backgroundrepeat" || k === "backgroundimage";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a style object to CSS custom properties (e.g. { width: "24px" } → { "--svg-width": "24px" })
|
||||||
|
* so they can be consumed by CSS rules targeting child elements like <svg>.
|
||||||
|
*/
|
||||||
|
function toCssVars(style: React.CSSProperties | undefined, prefix: string): React.CSSProperties {
|
||||||
|
if (!style) return {};
|
||||||
|
const vars: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(style)) {
|
||||||
|
const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
||||||
|
vars[`--${prefix}-${cssKey}`] = value;
|
||||||
|
}
|
||||||
|
return vars as React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeviceIcon({ device }: { device: DeviceConfig }) {
|
||||||
|
const { isDarkTheme } = useColorMode();
|
||||||
|
const iconStr = isDarkTheme && device.iconDark ? device.iconDark : device.icon;
|
||||||
|
const iconStyle = (isDarkTheme && device.iconDarkStyle
|
||||||
|
? device.iconDarkStyle
|
||||||
|
: device.iconStyle) as React.CSSProperties | undefined;
|
||||||
|
const svgStyle = (isDarkTheme && device.svgDarkStyle
|
||||||
|
? device.svgDarkStyle
|
||||||
|
: device.svgStyle) as React.CSSProperties | undefined;
|
||||||
|
|
||||||
|
const iconType = getIconType(iconStr);
|
||||||
|
|
||||||
|
if (iconType === "svg") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.deviceIconSvg}
|
||||||
|
style={{ ...iconStyle, ...toCssVars(svgStyle, "svg") }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: iconStr }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iconType === "image") {
|
||||||
|
// When iconStyle contains background-* properties, render as background-image
|
||||||
|
// on the container div instead of an <img> tag, enabling background-size/position control.
|
||||||
|
if (hasBackgroundProps(iconStyle)) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.deviceIconImage}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${iconStr})`,
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
backgroundSize: "contain",
|
||||||
|
...iconStyle,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.deviceIconImage}>
|
||||||
|
<img src={iconStr} alt={device.name} style={iconStyle} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.deviceIcon} style={iconStyle}>
|
||||||
|
{iconStr}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeviceCard({
|
||||||
|
device,
|
||||||
|
active,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
device: DeviceConfig;
|
||||||
|
active: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.deviceCard} ${active ? styles.deviceCardActive : ""}`}
|
||||||
|
onClick={onClick}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") onClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeviceIcon device={device} />
|
||||||
|
<div className={styles.deviceName}>{device.name}</div>
|
||||||
|
<div className={styles.deviceDesc}>{device.description}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeviceSelector({ selectedId, onSelect }: Props) {
|
||||||
|
return (
|
||||||
|
<div className={styles.formSection}>
|
||||||
|
<h4>Device Type</h4>
|
||||||
|
<div className={styles.deviceGrid}>
|
||||||
|
{devices.map((d) => (
|
||||||
|
<DeviceCard
|
||||||
|
key={d.id}
|
||||||
|
device={d}
|
||||||
|
active={selectedId === d.id}
|
||||||
|
onClick={() => onSelect(d.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import React, { useState, useCallback } from "react";
|
||||||
|
import CodeBlock from "@theme/CodeBlock";
|
||||||
|
import Admonition from "@theme/Admonition";
|
||||||
|
import styles from "../styles.module.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
yaml: string;
|
||||||
|
configPath: string;
|
||||||
|
mediaPath: string;
|
||||||
|
hasAnyHardware: boolean;
|
||||||
|
deviceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GeneratedOutput({
|
||||||
|
yaml,
|
||||||
|
configPath,
|
||||||
|
mediaPath,
|
||||||
|
hasAnyHardware,
|
||||||
|
deviceId,
|
||||||
|
}: Props) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = useCallback(() => {
|
||||||
|
navigator.clipboard.writeText(yaml).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
});
|
||||||
|
}, [yaml]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.resultSection}>
|
||||||
|
<div className={styles.resultHeader}>
|
||||||
|
<h4>Generated Configuration</h4>
|
||||||
|
<button className="button button--primary button--sm" onClick={handleCopy}>
|
||||||
|
{copied ? "Copied!" : "Copy"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!configPath && (
|
||||||
|
<Admonition type="tip">
|
||||||
|
<p>You haven't specified a config file directory. You may want to modify the default path.</p>
|
||||||
|
</Admonition>
|
||||||
|
)}
|
||||||
|
{!mediaPath && (
|
||||||
|
<Admonition type="tip">
|
||||||
|
<p>You haven't specified a recording storage directory. You may want to modify the default path.</p>
|
||||||
|
</Admonition>
|
||||||
|
)}
|
||||||
|
{deviceId === "stable" && !hasAnyHardware && (
|
||||||
|
<Admonition type="warning">
|
||||||
|
<p>You haven't selected any hardware acceleration. Please check if you have supported hardware available.</p>
|
||||||
|
</Admonition>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CodeBlock language="yaml" title="docker-compose.yml">
|
||||||
|
{yaml}
|
||||||
|
</CodeBlock>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { hardwareOptions } from "../config";
|
||||||
|
import type { HardwareOption } from "../config";
|
||||||
|
import styles from "../styles.module.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deviceId: string;
|
||||||
|
hardwareEnabled: Record<string, boolean>;
|
||||||
|
onToggle: (hwId: string) => void;
|
||||||
|
isDisabled: (hwId: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDescription(text: string): React.ReactNode {
|
||||||
|
const parts = text.split(/(\[[^\]]+\]\([^)]+\))/g);
|
||||||
|
return parts.map((part, i) => {
|
||||||
|
const match = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
||||||
|
if (match) {
|
||||||
|
return <a key={i} href={match[2]}>{match[1]}</a>;
|
||||||
|
}
|
||||||
|
return <React.Fragment key={i}>{part}</React.Fragment>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function HardwareCheckbox({
|
||||||
|
hw, disabled, checked, onToggle,
|
||||||
|
}: {
|
||||||
|
hw: HardwareOption; disabled: boolean; checked: boolean; onToggle: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={styles.hardwareItem}>
|
||||||
|
<label className={`${styles.checkboxLabel} ${disabled ? styles.checkboxDisabled : ""}`}>
|
||||||
|
<input type="checkbox" checked={checked} onChange={onToggle} disabled={disabled} />
|
||||||
|
<span>{hw.label}</span>
|
||||||
|
</label>
|
||||||
|
{checked && hw.description && (
|
||||||
|
<div className={styles.hardwareDescription}>{renderDescription(hw.description)}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HardwareOptions({ deviceId, hardwareEnabled, onToggle, isDisabled }: Props) {
|
||||||
|
return (
|
||||||
|
<div className={styles.formSection}>
|
||||||
|
<h4>Generic Hardware Devices</h4>
|
||||||
|
{deviceId !== "stable" && (
|
||||||
|
<p className={styles.helpText}>
|
||||||
|
Some options have been auto-configured based on your device type.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className={styles.checkboxGrid}>
|
||||||
|
{hardwareOptions.map((hw) => {
|
||||||
|
const disabled = isDisabled(hw.id);
|
||||||
|
const checked = disabled ? false : !!hardwareEnabled[hw.id];
|
||||||
|
return (
|
||||||
|
<HardwareCheckbox key={hw.id} hw={hw} disabled={disabled} checked={checked} onToggle={() => onToggle(hw.id)} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "../styles.module.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
gpuCount: string;
|
||||||
|
gpuDeviceId: string;
|
||||||
|
gpuDeviceIdError: boolean;
|
||||||
|
onGpuCountChange: (value: string) => void;
|
||||||
|
onGpuDeviceIdChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NvidiaGpuConfig({
|
||||||
|
gpuCount,
|
||||||
|
gpuDeviceId,
|
||||||
|
gpuDeviceIdError,
|
||||||
|
onGpuCountChange,
|
||||||
|
onGpuDeviceIdChange,
|
||||||
|
}: Props) {
|
||||||
|
const showDeviceId = gpuCount !== "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.nvidiaConfig}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label htmlFor="dcg-gpu-count" className={styles.label}>
|
||||||
|
GPU count:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="dcg-gpu-count"
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
className={styles.input}
|
||||||
|
value={gpuCount}
|
||||||
|
placeholder="all"
|
||||||
|
onChange={(e) => onGpuCountChange(e.target.value.replace(/\D/g, ""))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{showDeviceId && (
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label htmlFor="dcg-gpu-device-id" className={styles.label}>
|
||||||
|
GPU device IDs (required, comma-separated):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="dcg-gpu-device-id"
|
||||||
|
type="text"
|
||||||
|
className={`${styles.input} ${gpuDeviceIdError ? styles.inputError : ""}`}
|
||||||
|
value={gpuDeviceId}
|
||||||
|
placeholder="0"
|
||||||
|
onChange={(e) => onGpuDeviceIdChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
{gpuDeviceIdError ? (
|
||||||
|
<p className={styles.helpText}>
|
||||||
|
⚠️ GPU device IDs are required when GPU count is a number
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className={styles.helpText}>
|
||||||
|
Single GPU: 0 | Multiple GPUs: 0,1,2
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import CodeInline from "@theme/CodeInline";
|
||||||
|
import styles from "../styles.module.css";
|
||||||
|
|
||||||
|
const AUTO_TIMEZONE_VALUE = "__auto__";
|
||||||
|
|
||||||
|
function getTimezoneList(): string[] {
|
||||||
|
if (typeof Intl !== "undefined") {
|
||||||
|
const intl = Intl as typeof Intl & {
|
||||||
|
supportedValuesOf?: (key: string) => string[];
|
||||||
|
};
|
||||||
|
const supported = intl.supportedValuesOf?.("timeZone");
|
||||||
|
if (supported && supported.length > 0) {
|
||||||
|
return [...supported].sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
return fallback ? [fallback] : ["UTC"];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
rtspPassword: string;
|
||||||
|
timezone: string;
|
||||||
|
shmSize: string;
|
||||||
|
shmSizeError: boolean;
|
||||||
|
onRtspPasswordChange: (value: string) => void;
|
||||||
|
onTimezoneChange: (value: string) => void;
|
||||||
|
onShmSizeChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OtherOptions({
|
||||||
|
rtspPassword,
|
||||||
|
timezone,
|
||||||
|
shmSize,
|
||||||
|
shmSizeError,
|
||||||
|
onRtspPasswordChange,
|
||||||
|
onTimezoneChange,
|
||||||
|
onShmSizeChange,
|
||||||
|
}: Props) {
|
||||||
|
const timezones = useMemo(() => getTimezoneList(), []);
|
||||||
|
const systemTimezone =
|
||||||
|
Intl.DateTimeFormat().resolvedOptions().timeZone || "Etc/UTC";
|
||||||
|
const selectedValue = timezone || AUTO_TIMEZONE_VALUE;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.formSection}>
|
||||||
|
<h4>Other Options</h4>
|
||||||
|
<div className={styles.formGrid}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label htmlFor="dcg-timezone" className={styles.label}>
|
||||||
|
Timezone:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="dcg-timezone"
|
||||||
|
className={`${styles.input} ${styles.select}`}
|
||||||
|
value={selectedValue}
|
||||||
|
onChange={(e) =>
|
||||||
|
onTimezoneChange(
|
||||||
|
e.target.value === AUTO_TIMEZONE_VALUE ? "" : e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value={AUTO_TIMEZONE_VALUE}>
|
||||||
|
Use browser timezone ({systemTimezone})
|
||||||
|
</option>
|
||||||
|
{timezones.map((tz) => (
|
||||||
|
<option key={tz} value={tz}>
|
||||||
|
{tz}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label htmlFor="dcg-shm-size" className={styles.label}>
|
||||||
|
Shared memory (SHM):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="dcg-shm-size"
|
||||||
|
type="text"
|
||||||
|
className={`${styles.input} ${shmSizeError ? styles.inputError : ""}`}
|
||||||
|
value={shmSize}
|
||||||
|
placeholder="512mb"
|
||||||
|
onChange={(e) => onShmSizeChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
{shmSizeError ? (
|
||||||
|
<p className={styles.helpText}>
|
||||||
|
⚠️ Invalid format. Use a number followed by a unit (e.g. 512mb, 1gb)
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className={styles.helpText}>
|
||||||
|
See{" "}
|
||||||
|
<a href="/frigate/installation#calculating-required-shm-size">
|
||||||
|
calculating required SHM size
|
||||||
|
</a>{" "}
|
||||||
|
for the correct value.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label htmlFor="dcg-rtsp-password" className={styles.label}>
|
||||||
|
RTSP password:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="dcg-rtsp-password"
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={rtspPassword}
|
||||||
|
placeholder="password"
|
||||||
|
onChange={(e) => onRtspPasswordChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className={styles.helpText}>
|
||||||
|
Optional. You can specify{" "}
|
||||||
|
<CodeInline>{"{FRIGATE_RTSP_PASSWORD}"}</CodeInline>{" "}
|
||||||
|
in the config file to reference camera stream passwords. This is NOT
|
||||||
|
the Frigate login password.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Admonition from "@theme/Admonition";
|
||||||
|
import { ports } from "../config";
|
||||||
|
import styles from "../styles.module.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
portEnabled: Record<string, boolean>;
|
||||||
|
onTogglePort: (portId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PortItem({
|
||||||
|
port,
|
||||||
|
enabled,
|
||||||
|
onToggle,
|
||||||
|
}: {
|
||||||
|
port: typeof ports[number];
|
||||||
|
enabled: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
}) {
|
||||||
|
const showWarning = port.warningContent && (
|
||||||
|
port.warningWhen === "checked" ? enabled :
|
||||||
|
port.warningWhen === "unchecked" ? !enabled : enabled
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hardwareItem}>
|
||||||
|
<label className={`${styles.checkboxLabel} ${port.locked ? styles.checkboxDisabled : ""}`}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={onToggle}
|
||||||
|
disabled={port.locked}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{port.locked && "🔒 "}
|
||||||
|
Port {port.host}
|
||||||
|
{port.protocol !== "tcp" && `/${port.protocol}`}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{port.description && (
|
||||||
|
<div className={styles.hardwareDescription}>{port.description}</div>
|
||||||
|
)}
|
||||||
|
{showWarning && (
|
||||||
|
<Admonition type={port.warningType || "warning"}>
|
||||||
|
{port.warningContent}
|
||||||
|
</Admonition>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PortConfigSection({
|
||||||
|
portEnabled,
|
||||||
|
onTogglePort,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<div className={styles.formSection}>
|
||||||
|
<h4>Port Configuration</h4>
|
||||||
|
<div className={styles.checkboxGrid}>
|
||||||
|
{ports.map((port) => (
|
||||||
|
<PortItem
|
||||||
|
key={port.id}
|
||||||
|
port={port}
|
||||||
|
enabled={!!portEnabled[port.id]}
|
||||||
|
onToggle={() => onTogglePort(port.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "../styles.module.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
configPath: string;
|
||||||
|
mediaPath: string;
|
||||||
|
configPathError: boolean;
|
||||||
|
mediaPathError: boolean;
|
||||||
|
onConfigPathChange: (value: string) => void;
|
||||||
|
onMediaPathChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StoragePaths({
|
||||||
|
configPath,
|
||||||
|
mediaPath,
|
||||||
|
configPathError,
|
||||||
|
mediaPathError,
|
||||||
|
onConfigPathChange,
|
||||||
|
onMediaPathChange,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<div className={styles.formSection}>
|
||||||
|
<h4>Storage Paths</h4>
|
||||||
|
<div className={styles.formGrid}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label htmlFor="dcg-config-path" className={styles.label}>
|
||||||
|
Config / DB / model cache directory (on your host):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="dcg-config-path"
|
||||||
|
type="text"
|
||||||
|
className={`${styles.input} ${configPathError ? styles.inputError : ""}`}
|
||||||
|
value={configPath}
|
||||||
|
placeholder="/path/to/your/config"
|
||||||
|
onChange={(e) => onConfigPathChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
{configPathError && (
|
||||||
|
<p className={styles.helpText}>
|
||||||
|
⚠️ Path contains invalid characters. Only letters, numbers,
|
||||||
|
underscores, hyphens, slashes, and dots are allowed.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label htmlFor="dcg-media-path" className={styles.label}>
|
||||||
|
Recording storage directory (on your host):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="dcg-media-path"
|
||||||
|
type="text"
|
||||||
|
className={`${styles.input} ${mediaPathError ? styles.inputError : ""}`}
|
||||||
|
value={mediaPath}
|
||||||
|
placeholder="/path/to/your/storage"
|
||||||
|
onChange={(e) => onMediaPathChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
{mediaPathError && (
|
||||||
|
<p className={styles.helpText}>
|
||||||
|
⚠️ Path contains invalid characters. Only letters, numbers,
|
||||||
|
underscores, hyphens, slashes, and dots are allowed.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
297
docs/src/components/DockerComposeGenerator/config/config.yaml
Normal file
297
docs/src/components/DockerComposeGenerator/config/config.yaml
Normal file
File diff suppressed because one or more lines are too long
12
docs/src/components/DockerComposeGenerator/config/index.ts
Normal file
12
docs/src/components/DockerComposeGenerator/config/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export { devices, deviceMap } from "./devices";
|
||||||
|
export { hardwareOptions, hardwareMap } from "./hardware";
|
||||||
|
export { ports, portMap } from "./ports";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
DeviceConfig,
|
||||||
|
DeviceMapping,
|
||||||
|
VolumeMapping,
|
||||||
|
HardwareOption,
|
||||||
|
PortConfig,
|
||||||
|
NvidiaDeployConfig,
|
||||||
|
} from "./types";
|
||||||
154
docs/src/components/DockerComposeGenerator/config/types.ts
Normal file
154
docs/src/components/DockerComposeGenerator/config/types.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* Type definitions for the Docker Compose Generator configuration.
|
||||||
|
* All device, hardware, and port options are declaratively defined
|
||||||
|
* so that adding a new device only requires editing config files.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** A single device mapping entry (e.g. /dev/dri:/dev/dri) */
|
||||||
|
export interface DeviceMapping {
|
||||||
|
/** Host device path */
|
||||||
|
host: string;
|
||||||
|
/** Container device path (defaults to host if omitted) */
|
||||||
|
container?: string;
|
||||||
|
/** Inline comment for this device line */
|
||||||
|
comment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A single volume mapping entry */
|
||||||
|
export interface VolumeMapping {
|
||||||
|
/** Host path */
|
||||||
|
host: string;
|
||||||
|
/** Container path */
|
||||||
|
container: string;
|
||||||
|
/** Whether the mount is read-only */
|
||||||
|
readOnly?: boolean;
|
||||||
|
/** Inline comment */
|
||||||
|
comment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** NVIDIA deploy configuration for docker-compose */
|
||||||
|
export interface NvidiaDeployConfig {
|
||||||
|
/** "all" or a specific number */
|
||||||
|
count: string;
|
||||||
|
/** Specific GPU device IDs (when count is a number) */
|
||||||
|
deviceIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full device type definition */
|
||||||
|
export interface DeviceConfig {
|
||||||
|
/** Unique identifier, e.g. "intel" */
|
||||||
|
id: string;
|
||||||
|
/** Display name, e.g. "Intel GPU" */
|
||||||
|
name: string;
|
||||||
|
/** Short description */
|
||||||
|
description: string;
|
||||||
|
/**
|
||||||
|
* Icon for the device card. Supports:
|
||||||
|
* - Emoji string (e.g. "🖥️")
|
||||||
|
* - Image URL or static path (e.g. "/img/intel.svg", "https://example.com/icon.png")
|
||||||
|
* - Inline SVG markup (e.g. "<svg>...</svg>")
|
||||||
|
*/
|
||||||
|
icon: string;
|
||||||
|
/**
|
||||||
|
* Additional CSS properties applied to the icon element.
|
||||||
|
* - For image-type icons: if any `background-*` property (e.g. `background-size`,
|
||||||
|
* `background-position`) is present, the image is rendered as a CSS `background-image`
|
||||||
|
* on the container div, enabling full background positioning control.
|
||||||
|
* Otherwise the image is rendered as an `<img>` tag and styles apply to it.
|
||||||
|
* - For emoji/SVG icons: styles apply to the container div.
|
||||||
|
*/
|
||||||
|
iconStyle?: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Additional CSS properties applied directly to the inner `<svg>` element
|
||||||
|
* when the icon is an inline SVG. Use this to override the default
|
||||||
|
* `width: 100%; height: 100%` or set `fill`, `transform`, etc.
|
||||||
|
* Ignored for emoji and image-type icons.
|
||||||
|
*/
|
||||||
|
svgStyle?: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Icon for dark mode. Same format as `icon`. When provided, this icon
|
||||||
|
* replaces `icon` when the user is in dark mode.
|
||||||
|
*/
|
||||||
|
iconDark?: string;
|
||||||
|
/** Additional CSS properties for the dark mode icon container */
|
||||||
|
iconDarkStyle?: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* SVG-specific styles for dark mode. Same as `svgStyle` but applied
|
||||||
|
* when dark mode is active. Merged over `svgStyle` in dark mode.
|
||||||
|
*/
|
||||||
|
svgDarkStyle?: Record<string, string>;
|
||||||
|
/** Docker image tag, e.g. "stable" */
|
||||||
|
imageTag: string;
|
||||||
|
/**
|
||||||
|
* Image tag suffix appended to the base tag.
|
||||||
|
* e.g. "-standard-arm64" produces "stable-standard-arm64"
|
||||||
|
*/
|
||||||
|
imageTagSuffix?: string;
|
||||||
|
/** Hardware option IDs to auto-enable when this device is selected */
|
||||||
|
autoHardware: string[];
|
||||||
|
/** Help text shown as an admonition when this device is selected */
|
||||||
|
helpText?: string;
|
||||||
|
/** Admonition type for help text */
|
||||||
|
helpType?: "info" | "warning" | "danger";
|
||||||
|
/** Device mappings always added for this device type */
|
||||||
|
devices?: DeviceMapping[];
|
||||||
|
/** Volume mappings always added for this device type */
|
||||||
|
volumes?: VolumeMapping[];
|
||||||
|
/** Extra environment variables for this device type */
|
||||||
|
env?: Record<string, string>;
|
||||||
|
/** NVIDIA deploy config (only for tensorrt) */
|
||||||
|
nvidiaDeploy?: NvidiaDeployConfig;
|
||||||
|
/** Runtime setting, e.g. "nvidia" for Jetson */
|
||||||
|
runtime?: string;
|
||||||
|
/** Extra hosts entries, e.g. "host.docker.internal:host-gateway" */
|
||||||
|
extraHosts?: string[];
|
||||||
|
/** Security options, e.g. ["apparmor=unconfined"] */
|
||||||
|
securityOpt?: string[];
|
||||||
|
/** Whether this device type needs the NVIDIA GPU config UI */
|
||||||
|
needsNvidiaConfig?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generic hardware acceleration option definition */
|
||||||
|
export interface HardwareOption {
|
||||||
|
/** Unique identifier, e.g. "usbCoral" */
|
||||||
|
id: string;
|
||||||
|
/** Display label */
|
||||||
|
label: string;
|
||||||
|
/**
|
||||||
|
* Description shown below the checkbox when this option is enabled.
|
||||||
|
* Supports markdown link syntax: [text](url)
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
/** Device IDs that disable this option */
|
||||||
|
disabledWhen?: string[];
|
||||||
|
/** Device mappings added when this option is enabled */
|
||||||
|
devices?: DeviceMapping[];
|
||||||
|
/** Volume mappings added when this option is enabled */
|
||||||
|
volumes?: VolumeMapping[];
|
||||||
|
/** Extra environment variables */
|
||||||
|
env?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Port definition */
|
||||||
|
export interface PortConfig {
|
||||||
|
/** Unique identifier (also the default host port as string) */
|
||||||
|
id: string;
|
||||||
|
/** Host port number */
|
||||||
|
host: number;
|
||||||
|
/** Container port number */
|
||||||
|
container: number;
|
||||||
|
/** Protocol */
|
||||||
|
protocol?: "tcp" | "udp";
|
||||||
|
/** Description of the port's purpose */
|
||||||
|
description: string;
|
||||||
|
/** Whether enabled by default */
|
||||||
|
defaultEnabled: boolean;
|
||||||
|
/** Whether this port is locked (always enabled, cannot be toggled off) */
|
||||||
|
locked?: boolean;
|
||||||
|
/** Admonition type for the warning */
|
||||||
|
warningType?: "warning" | "danger";
|
||||||
|
/** Warning content (markdown) */
|
||||||
|
warningContent?: string;
|
||||||
|
/** When to show the warning: when the port is checked or unchecked */
|
||||||
|
warningWhen?: "checked" | "unchecked";
|
||||||
|
}
|
||||||
250
docs/src/components/DockerComposeGenerator/generator/index.ts
Normal file
250
docs/src/components/DockerComposeGenerator/generator/index.ts
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import type {
|
||||||
|
DeviceConfig,
|
||||||
|
DeviceMapping,
|
||||||
|
VolumeMapping,
|
||||||
|
} from "../config/types";
|
||||||
|
import { hardwareMap } from "../config";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Input type
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface GeneratorInput {
|
||||||
|
device: DeviceConfig;
|
||||||
|
selectedHardware: string[];
|
||||||
|
enabledPorts: string[];
|
||||||
|
configPath: string;
|
||||||
|
mediaPath: string;
|
||||||
|
rtspPassword?: string;
|
||||||
|
timezone: string;
|
||||||
|
shmSize: string;
|
||||||
|
nvidiaGpuCount?: string;
|
||||||
|
nvidiaGpuDeviceId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function deviceLine(dm: DeviceMapping): string {
|
||||||
|
const host = dm.host;
|
||||||
|
const container = dm.container ?? dm.host;
|
||||||
|
const mapping = host === container ? host : `${host}:${container}`;
|
||||||
|
const comment = dm.comment ? ` # ${dm.comment}` : "";
|
||||||
|
return ` - ${mapping}${comment}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function volumeLine(vm: VolumeMapping): string {
|
||||||
|
const ro = vm.readOnly ? ":ro" : "";
|
||||||
|
const comment = vm.comment ? ` # ${vm.comment}` : "";
|
||||||
|
return ` - ${vm.host}:${vm.container}${ro}${comment}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// YAML builder — each section returns an array of lines
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function buildImage(device: DeviceConfig): string[] {
|
||||||
|
const tag = device.imageTagSuffix
|
||||||
|
? `${device.imageTag}${device.imageTagSuffix}`
|
||||||
|
: device.imageTag;
|
||||||
|
return [` image: ghcr.io/blakeblackshear/frigate:${tag}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDevices(
|
||||||
|
device: DeviceConfig,
|
||||||
|
hwDevices: DeviceMapping[]
|
||||||
|
): string[] {
|
||||||
|
const all: DeviceMapping[] = [
|
||||||
|
...(device.devices ?? []),
|
||||||
|
...hwDevices,
|
||||||
|
];
|
||||||
|
if (all.length === 0) return [];
|
||||||
|
return [
|
||||||
|
" devices:",
|
||||||
|
...all.map(deviceLine),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVolumes(
|
||||||
|
device: DeviceConfig,
|
||||||
|
hwVolumes: VolumeMapping[],
|
||||||
|
configPath: string,
|
||||||
|
mediaPath: string
|
||||||
|
): string[] {
|
||||||
|
const all: VolumeMapping[] = [
|
||||||
|
...(device.volumes ?? []),
|
||||||
|
...hwVolumes,
|
||||||
|
];
|
||||||
|
return [
|
||||||
|
" volumes:",
|
||||||
|
" - /etc/localtime:/etc/localtime:ro # Sync host time",
|
||||||
|
` - ${configPath}:/config # Config file directory`,
|
||||||
|
` - ${mediaPath}:/media/frigate # Recording storage directory`,
|
||||||
|
" - type: tmpfs # 1GB in-memory filesystem for recording segment storage",
|
||||||
|
" target: /tmp/cache",
|
||||||
|
" tmpfs:",
|
||||||
|
" size: 1000000000",
|
||||||
|
...all.map(volumeLine),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPorts(enabledPorts: string[]): string[] {
|
||||||
|
return [
|
||||||
|
" ports:",
|
||||||
|
...enabledPorts,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEnvironment(
|
||||||
|
device: DeviceConfig,
|
||||||
|
hwEnv: Record<string, string>,
|
||||||
|
rtspPassword: string | undefined,
|
||||||
|
timezone: string
|
||||||
|
): string[] {
|
||||||
|
const allEnv: Record<string, string> = {
|
||||||
|
...hwEnv,
|
||||||
|
...(device.env ?? {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const lines: string[] = [" environment:"];
|
||||||
|
|
||||||
|
if (rtspPassword) {
|
||||||
|
lines.push(
|
||||||
|
` FRIGATE_RTSP_PASSWORD: "${rtspPassword}" # RTSP password — change to your own`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(` TZ: "${timezone}" # Timezone`);
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(allEnv)) {
|
||||||
|
lines.push(` ${key}: "${value}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDeploy(device: DeviceConfig, input: GeneratorInput): string[] {
|
||||||
|
if (device.id === "stable-tensorrt") {
|
||||||
|
const count = input.nvidiaGpuCount || "all";
|
||||||
|
const isAll = count === "all";
|
||||||
|
const deviceId = input.nvidiaGpuDeviceId?.trim();
|
||||||
|
|
||||||
|
if (isAll) {
|
||||||
|
return [
|
||||||
|
" deploy:",
|
||||||
|
" resources:",
|
||||||
|
" reservations:",
|
||||||
|
" devices:",
|
||||||
|
" - driver: nvidia",
|
||||||
|
" count: all # Use all GPUs",
|
||||||
|
" capabilities: [gpu]",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceId) {
|
||||||
|
const ids = deviceId
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((s) => `'${s}'`)
|
||||||
|
.join(", ");
|
||||||
|
return [
|
||||||
|
" deploy:",
|
||||||
|
" resources:",
|
||||||
|
" reservations:",
|
||||||
|
" devices:",
|
||||||
|
" - driver: nvidia",
|
||||||
|
` device_ids: [${ids}] # GPU device IDs`,
|
||||||
|
` count: ${count} # GPU count`,
|
||||||
|
" capabilities: [gpu]",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
" deploy:",
|
||||||
|
" resources:",
|
||||||
|
" reservations:",
|
||||||
|
" devices:",
|
||||||
|
" - driver: nvidia",
|
||||||
|
` count: ${count} # GPU count`,
|
||||||
|
" capabilities: [gpu]",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRuntime(device: DeviceConfig): string[] {
|
||||||
|
if (device.runtime) {
|
||||||
|
return [` runtime: ${device.runtime}`];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExtraHosts(device: DeviceConfig): string[] {
|
||||||
|
if (!device.extraHosts?.length) return [];
|
||||||
|
return [
|
||||||
|
" extra_hosts:",
|
||||||
|
...device.extraHosts.map(
|
||||||
|
(h, i) =>
|
||||||
|
` - "${h}"${i === 0 ? " # Required to talk to the NPU detector" : ""}`
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSecurityOpt(device: DeviceConfig): string[] {
|
||||||
|
if (!device.securityOpt?.length) return [];
|
||||||
|
return [
|
||||||
|
" security_opt:",
|
||||||
|
...device.securityOpt.map((s) => ` - ${s}`),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public API
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a docker-compose YAML string from the given input.
|
||||||
|
* The output is pure YAML with inline comments (no Shiki annotations).
|
||||||
|
*/
|
||||||
|
export function generateDockerCompose(input: GeneratorInput): string {
|
||||||
|
const { device } = input;
|
||||||
|
|
||||||
|
// Collect hardware-level devices, volumes, and env
|
||||||
|
const hwDevices: DeviceMapping[] = [];
|
||||||
|
const hwVolumes: VolumeMapping[] = [];
|
||||||
|
const hwEnv: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const hwId of input.selectedHardware) {
|
||||||
|
const hw = hardwareMap.get(hwId);
|
||||||
|
if (!hw) continue;
|
||||||
|
// Skip GPU device mapping for tensorrt images (it uses deploy instead)
|
||||||
|
if (hw.id === "gpu" && device.imageTag === "stable-tensorrt") continue;
|
||||||
|
hwDevices.push(...(hw.devices ?? []));
|
||||||
|
hwVolumes.push(...(hw.volumes ?? []));
|
||||||
|
Object.assign(hwEnv, hw.env ?? {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: string[] = [
|
||||||
|
"services:",
|
||||||
|
" frigate:",
|
||||||
|
" container_name: frigate",
|
||||||
|
" privileged: true # This may not be necessary for all setups",
|
||||||
|
" restart: unless-stopped",
|
||||||
|
" stop_grace_period: 30s # Allow enough time to shut down the various services",
|
||||||
|
...buildImage(device),
|
||||||
|
` shm_size: "${input.shmSize || "512mb"}" # Update for your cameras based on SHM calculation`,
|
||||||
|
...buildRuntime(device),
|
||||||
|
...buildDeploy(device, input),
|
||||||
|
...buildExtraHosts(device),
|
||||||
|
...buildSecurityOpt(device),
|
||||||
|
...buildDevices(device, hwDevices),
|
||||||
|
...buildVolumes(device, hwVolumes, input.configPath, input.mediaPath),
|
||||||
|
...buildPorts(input.enabledPorts),
|
||||||
|
...buildEnvironment(device, hwEnv, input.rtspPassword, input.timezone),
|
||||||
|
];
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
@ -0,0 +1,195 @@
|
|||||||
|
import { useState, useCallback, useMemo } from "react";
|
||||||
|
import { deviceMap, hardwareMap, portMap } from "../config";
|
||||||
|
import { generateDockerCompose } from "../generator";
|
||||||
|
import type { GeneratorInput } from "../generator";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main hook that holds all form state and generates the Docker Compose output.
|
||||||
|
* Configuration is loaded synchronously from build-time generated .ts files.
|
||||||
|
*/
|
||||||
|
export function useConfigGenerator() {
|
||||||
|
const [deviceId, setDeviceId] = useState("stable");
|
||||||
|
|
||||||
|
const [hardwareEnabled, setHardwareEnabled] = useState<Record<string, boolean>>(() => {
|
||||||
|
const defaultDevice = deviceMap.get("stable");
|
||||||
|
const initial: Record<string, boolean> = {};
|
||||||
|
if (defaultDevice) {
|
||||||
|
for (const hwId of defaultDevice.autoHardware) {
|
||||||
|
initial[hwId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return initial;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [portEnabled, setPortEnabled] = useState<Record<string, boolean>>(() => {
|
||||||
|
const initial: Record<string, boolean> = {};
|
||||||
|
for (const p of portMap.values()) {
|
||||||
|
initial[p.id] = p.defaultEnabled;
|
||||||
|
}
|
||||||
|
return initial;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [nvidiaGpuCount, setNvidiaGpuCount] = useState("");
|
||||||
|
const [nvidiaGpuDeviceId, setNvidiaGpuDeviceId] = useState("");
|
||||||
|
const [configPath, setConfigPath] = useState("");
|
||||||
|
const [mediaPath, setMediaPath] = useState("");
|
||||||
|
const [rtspPassword, setRtspPassword] = useState("");
|
||||||
|
const [timezone, setTimezone] = useState("");
|
||||||
|
const [shmSize, setShmSize] = useState("512mb");
|
||||||
|
const [shmSizeError, setShmSizeError] = useState(false);
|
||||||
|
const [gpuDeviceIdError, setGpuDeviceIdError] = useState(false);
|
||||||
|
const [configPathError, setConfigPathError] = useState(false);
|
||||||
|
const [mediaPathError, setMediaPathError] = useState(false);
|
||||||
|
|
||||||
|
const device = useMemo(() => deviceMap.get(deviceId)!, [deviceId]);
|
||||||
|
|
||||||
|
const selectDevice = useCallback((id: string) => {
|
||||||
|
const newDevice = deviceMap.get(id);
|
||||||
|
if (!newDevice) return;
|
||||||
|
setDeviceId(id);
|
||||||
|
setHardwareEnabled(() => {
|
||||||
|
const next: Record<string, boolean> = {};
|
||||||
|
for (const hwId of newDevice.autoHardware) {
|
||||||
|
next[hwId] = true;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setNvidiaGpuCount("");
|
||||||
|
setNvidiaGpuDeviceId("");
|
||||||
|
setGpuDeviceIdError(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleHardware = useCallback((hwId: string) => {
|
||||||
|
setHardwareEnabled((prev) => ({ ...prev, [hwId]: !prev[hwId] }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const togglePort = useCallback((portId: string) => {
|
||||||
|
const port = portMap.get(portId);
|
||||||
|
if (port?.locked) return;
|
||||||
|
setPortEnabled((prev) => ({ ...prev, [portId]: !prev[portId] }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isHardwareDisabled = useCallback(
|
||||||
|
(hwId: string): boolean => {
|
||||||
|
const hw = hardwareMap.get(hwId);
|
||||||
|
if (!hw) return false;
|
||||||
|
return hw.disabledWhen?.includes(deviceId) ?? false;
|
||||||
|
},
|
||||||
|
[deviceId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const validateShmSize = useCallback((value: string): boolean => {
|
||||||
|
if (!value) return true;
|
||||||
|
return /^\d+(\.\d+)?[bkmgBKMG]{1,2}$/.test(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const validatePath = useCallback((value: string): boolean => {
|
||||||
|
if (!value) return true;
|
||||||
|
return /^[a-zA-Z0-9_\-/./]+$/.test(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleShmSizeChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const filtered = value.replace(/[^0-9.bkmgBKMG]/g, "");
|
||||||
|
const valid = validateShmSize(filtered);
|
||||||
|
setShmSize(filtered);
|
||||||
|
setShmSizeError(!valid && filtered !== "");
|
||||||
|
},
|
||||||
|
[validateShmSize]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfigPathChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const filtered = value.replace(/[^a-zA-Z0-9_\-/./]/g, "");
|
||||||
|
const valid = validatePath(filtered);
|
||||||
|
setConfigPath(filtered);
|
||||||
|
setConfigPathError(!valid && filtered !== "");
|
||||||
|
},
|
||||||
|
[validatePath]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMediaPathChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const filtered = value.replace(/[^a-zA-Z0-9_\-/./]/g, "");
|
||||||
|
const valid = validatePath(filtered);
|
||||||
|
setMediaPath(filtered);
|
||||||
|
setMediaPathError(!valid && filtered !== "");
|
||||||
|
},
|
||||||
|
[validatePath]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNvidiaGpuCountChange = useCallback((value: string) => {
|
||||||
|
// Only allow digits
|
||||||
|
setNvidiaGpuCount(value);
|
||||||
|
if (value === "") {
|
||||||
|
setNvidiaGpuDeviceId("");
|
||||||
|
setGpuDeviceIdError(false);
|
||||||
|
} else {
|
||||||
|
setGpuDeviceIdError(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleNvidiaGpuDeviceIdChange = useCallback((value: string) => {
|
||||||
|
setNvidiaGpuDeviceId(value.trim());
|
||||||
|
setGpuDeviceIdError(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const enabledPortLines = useMemo(() => {
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const [id, enabled] of Object.entries(portEnabled)) {
|
||||||
|
if (!enabled) continue;
|
||||||
|
const p = portMap.get(id);
|
||||||
|
if (!p) continue;
|
||||||
|
const proto = p.protocol && p.protocol !== "tcp" ? `/${p.protocol}` : "";
|
||||||
|
const comment = p.description ? ` # ${p.description}` : "";
|
||||||
|
lines.push(` - "${p.host}:${p.container}${proto}"${comment}`);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}, [portEnabled]);
|
||||||
|
|
||||||
|
const selectedHardwareIds = useMemo(() => {
|
||||||
|
return Object.entries(hardwareEnabled)
|
||||||
|
.filter(([id, enabled]) => {
|
||||||
|
if (!enabled) return false;
|
||||||
|
const hw = hardwareMap.get(id);
|
||||||
|
if (!hw) return false;
|
||||||
|
if (hw.disabledWhen?.includes(deviceId)) return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(([id]) => id);
|
||||||
|
}, [hardwareEnabled, deviceId]);
|
||||||
|
|
||||||
|
const generatedYaml = useMemo(() => {
|
||||||
|
const input: GeneratorInput = {
|
||||||
|
device,
|
||||||
|
selectedHardware: selectedHardwareIds,
|
||||||
|
enabledPorts: enabledPortLines,
|
||||||
|
configPath: configPath || "/path/to/your/config",
|
||||||
|
mediaPath: mediaPath || "/path/to/your/storage",
|
||||||
|
rtspPassword,
|
||||||
|
timezone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "Etc/UTC",
|
||||||
|
shmSize: shmSize || "512mb",
|
||||||
|
nvidiaGpuCount,
|
||||||
|
nvidiaGpuDeviceId,
|
||||||
|
};
|
||||||
|
return generateDockerCompose(input);
|
||||||
|
}, [
|
||||||
|
device, selectedHardwareIds, enabledPortLines,
|
||||||
|
configPath, mediaPath, rtspPassword, timezone, shmSize,
|
||||||
|
nvidiaGpuCount, nvidiaGpuDeviceId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const hasAnyHardware = selectedHardwareIds.length > 0 || !!device?.devices?.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
deviceId, device, hardwareEnabled, portEnabled,
|
||||||
|
nvidiaGpuCount, nvidiaGpuDeviceId,
|
||||||
|
configPath, mediaPath, rtspPassword, timezone, shmSize,
|
||||||
|
shmSizeError, gpuDeviceIdError, configPathError, mediaPathError,
|
||||||
|
hasAnyHardware, generatedYaml,
|
||||||
|
selectDevice, toggleHardware, togglePort,
|
||||||
|
handleShmSizeChange, handleConfigPathChange, handleMediaPathChange,
|
||||||
|
handleNvidiaGpuCountChange, handleNvidiaGpuDeviceIdChange,
|
||||||
|
setRtspPassword, setTimezone, isHardwareDisabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
1
docs/src/components/DockerComposeGenerator/index.ts
Normal file
1
docs/src/components/DockerComposeGenerator/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./DockerComposeGenerator";
|
||||||
381
docs/src/components/DockerComposeGenerator/styles.module.css
Normal file
381
docs/src/components/DockerComposeGenerator/styles.module.css
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
/* ===================================================================
|
||||||
|
Docker Compose Generator — styles
|
||||||
|
Uses Docusaurus / Infima CSS variables for theme compatibility.
|
||||||
|
=================================================================== */
|
||||||
|
|
||||||
|
.generator {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--ifm-background-surface-color);
|
||||||
|
border: 1px solid var(--ifm-color-emphasis-400);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: var(--ifm-global-shadow-lw);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .card {
|
||||||
|
background: var(--ifm-color-emphasis-100);
|
||||||
|
border: 1px solid var(--ifm-color-emphasis-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Form sections --- */
|
||||||
|
|
||||||
|
.formSection {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--ifm-color-emphasis-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection h4 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: var(--ifm-font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Form controls --- */
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
font-weight: var(--ifm-font-weight-semibold);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid var(--ifm-color-emphasis-400);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--ifm-background-color);
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .input {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d0d7de;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--ifm-color-primary);
|
||||||
|
box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .input {
|
||||||
|
border-color: var(--ifm-color-emphasis-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputError {
|
||||||
|
border-color: #e74c3c;
|
||||||
|
animation: shake 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Select dropdown --- */
|
||||||
|
|
||||||
|
.select {
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background: var(--ifm-background-color)
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E")
|
||||||
|
no-repeat right 0.75rem center / 12px 12px;
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .select {
|
||||||
|
background: #fff
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23555' d='M6 8L1 3h10z'/%3E%3C/svg%3E")
|
||||||
|
no-repeat right 0.75rem center / 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpText {
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--ifm-font-color-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpText a {
|
||||||
|
color: var(--ifm-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Device grid --- */
|
||||||
|
|
||||||
|
.deviceGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceCard {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 2px solid var(--ifm-color-emphasis-400);
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--ifm-background-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .deviceCard {
|
||||||
|
border: 2px solid #d0d7de;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceCard:hover {
|
||||||
|
border-color: var(--ifm-color-primary);
|
||||||
|
background: var(--ifm-color-emphasis-100);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceCardActive {
|
||||||
|
border-color: var(--ifm-color-primary);
|
||||||
|
background: var(--ifm-color-primary-lightest);
|
||||||
|
box-shadow: 0 0 0 1px var(--ifm-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .deviceCardActive {
|
||||||
|
background: color-mix(in srgb, var(--ifm-color-primary) 12%, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .deviceCardActive {
|
||||||
|
background: color-mix(in srgb, var(--ifm-color-primary) 25%, #1b1b1b);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .deviceCardActive .deviceName {
|
||||||
|
color: var(--ifm-color-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .deviceCardActive .deviceDesc {
|
||||||
|
color: var(--ifm-color-primary-light);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceIcon {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
height: 40px;
|
||||||
|
width: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceIconSvg {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
height: 40px;
|
||||||
|
width: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: visible;
|
||||||
|
/* Allow iconStyle width/height to override */
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceIconSvg svg {
|
||||||
|
width: var(--svg-width, 100%);
|
||||||
|
height: var(--svg-height, 100%);
|
||||||
|
fill: var(--svg-fill, currentColor);
|
||||||
|
transform: var(--svg-transform, none);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceIconImage {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
height: 40px;
|
||||||
|
width: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceIconImage img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceName {
|
||||||
|
font-weight: var(--ifm-font-weight-semibold);
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
margin-bottom: 0.15rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceDesc {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--ifm-font-color-secondary);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Checkbox grid --- */
|
||||||
|
|
||||||
|
.checkboxGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.checkboxGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hardwareItem {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hardwareDescription {
|
||||||
|
margin: 0.15rem 0 0.4rem 1.6rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--ifm-font-color-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hardwareDescription a {
|
||||||
|
color: var(--ifm-color-primary);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxLabel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.4rem 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxLabel:hover {
|
||||||
|
background: var(--ifm-color-emphasis-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxLabel input[type="checkbox"] {
|
||||||
|
width: 1.1rem;
|
||||||
|
height: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxLabel span {
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxDisabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxDisabled:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxDisabled input[type="checkbox"] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Form grid (side-by-side) --- */
|
||||||
|
|
||||||
|
.formGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.formGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGrid .formGroup {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Port section --- */
|
||||||
|
|
||||||
|
.portSection {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warningBadge {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #e67e22;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- NVIDIA config --- */
|
||||||
|
|
||||||
|
.nvidiaConfig {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--ifm-background-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 3px solid var(--ifm-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .nvidiaConfig {
|
||||||
|
background: #f6f8fa;
|
||||||
|
border-left: 3px solid var(--ifm-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Result section --- */
|
||||||
|
|
||||||
|
.resultSection {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultHeader h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user