docs: improve workshop and homepage

This commit is contained in:
ajnart committed 2026-08-11 22:38:43 +02:00
1 parent 874432becf
commit 47c349bcd4
21 files changed
+758 -348

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{
"label": "Advanced",
"collapsed": true,
"position": 4,
"position": 5,
"link": {
"type": "generated-index",
"title": "Advanced Concepts or features"
@@ -57,50 +57,8 @@ Homarr becomes the instance administrator.
Use `pnpm docker:dev:up` instead when you only need Redis in the background. Running the migration also seeds the
default data; use `pnpm db:seed` to seed it explicitly later.
### Run PocketBase and documentation
Start PocketBase in another terminal:
```sh
cp apps/workshop/.env.example apps/workshop/.env
docker compose --env-file apps/workshop/.env -f apps/workshop/docker-compose.yml up --build workshop
```
Then start Docusaurus in a third terminal:
```sh
WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs
```
PocketBase runs on port 8090 and Docusaurus on port 3003. Stopping the services preserves the PocketBase volume.
Open `http://127.0.0.1:8090/_/` and create the first PocketBase superuser. The Workshop UI is available at
`http://127.0.0.1:3003/workshop`.
Point Homarr at the local services with:
```dotenv
HOMARR_WEBSITE_URL=http://127.0.0.1:3003
WORKSHOP_API_URL=http://127.0.0.1:8090
WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop
```
Restart `pnpm dev` after changing `.env`. GitHub OAuth is optional locally. See
[`apps/workshop/README.md`](https://github.com/homarr-labs/homarr/blob/HEAD/apps/workshop/README.md) for OAuth,
Workshop moderator promotion, logs, tests, backup, restore, stopping, and intentional volume reset.
### URL testing
Docusaurus reads `HOMARR_WEBSITE_URL` and `WORKSHOP_API_URL` when it starts or builds. Homarr also reads
`WORKSHOP_WEB_URL` when its server starts, so these values do not rely on `NEXT_PUBLIC_*` build variables. Test a
separate API origin before release:
```sh
HOMARR_WEBSITE_URL=http://127.0.0.1:3003 \
WORKSHOP_API_URL=http://127.0.0.1:8090 \
WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop \
pnpm dev:docs
```
Workshop contributors can set up PocketBase and Docusaurus with the dedicated
[Workshop development guide](../../workshop/development).
### Useful commands
@@ -112,7 +70,6 @@ pnpm dev:docs
- `pnpm dev:cli -- rebuild <name>` — rebuild an image from its recorded checkout or pull request.
- `pnpm dev:cli:install` — optionally install the developer CLI as a `homarr` binary.
- `pnpm dev:docs` — Docusaurus only on port 3003.
- `pnpm docker:workshop` — local PocketBase through Docker Compose.
- `pnpm docker:dev:up` — Redis development service in the background.
- `pnpm cli` — Homarr operations CLI.
- `pnpm db:migration:sqlite:run` — create or update a SQLite database.
@@ -122,7 +79,6 @@ pnpm dev:docs
- `pnpm lint` / `pnpm lint:fix` — check or apply oxlint.
- `pnpm typecheck` — typecheck all workspaces.
- `pnpm test` — unit and contract suites.
- `pnpm test:workshop` — disposable Workshop integration suite.
- `pnpm build` — production monorepo build.
- `docker build -t homarr .` — build a production Docker image.
- `docker run -p 7575:7575 -e SECRET_ENCRYPTION_KEY='your_64_character_hex_string' homarr:<name>` — run it.
@@ -25,19 +25,20 @@ This is especially useful for [docker secrets](https://docs.docker.com/engine/sw
Using the `PUID` and `PGID` will require you to set the correct permissions on the mounted volumes and if used the docker socket. See more in the [Running as a different user](/docs/advanced/running-as-different-user) documentation.
:::
| Environment Variable | Description | Possible values | Default |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ------------------------------ |
| `PUID` | User ID to run the container as | Any valid user ID | `0` |
| `PGID` | Group ID to run the container as | Any valid group ID | `0` |
| `LOG_LEVEL` | Log level to use | `debug` / `info` / `warn` / `error` | `info` |
| `NO_EXTERNAL_CONNECTION` | Disables Homarr's internet-backed requests, including update checks, weather, and RSS feeds | `true` or `false` | `false` |
| `ENABLE_DNS_CACHING` | Enables dns caching. Enabled by default, but if you experience issues with IPv6 or static IPs, see [#4006](https://github.com/homarr-labs/homarr/issues/4006) or disable this flag | `true` or `false` | `true` |
| `HOMARR_DISABLE_IPV6` | Disables the IPv6 listener of the internal nginx proxy, even when the host supports IPv6. IPv6 is normally disabled automatically when the host has no IPv6 configured. See [#4596](https://github.com/homarr-labs/homarr/issues/4596) | `true` or `false` | `false` |
| `HOMARR_WEBSITE_URL` | Main Homarr documentation/site base used by runtime links. Must be an absolute HTTP(S) URL. | HTTP(S) URL | `https://homarr.dev` |
| `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone documentation. | HTTP(S) URL | `HOMARR_WEBSITE_URL` |
| `WORKSHOP_WEB_URL` | Public Workshop page opened from Homarr. | HTTP(S) URL | `HOMARR_WEBSITE_URL/workshop` |
| `HOMARR_AI_DAILY_REQUEST_LIMIT` | Initial Homarr provider request allowance stored for each Workshop user. Existing per-user limits are retained. | Integer from `1` to `100000` | `50` |
| `HOMARR_AI_OPENROUTER_BASE_URL` | OpenRouter-compatible upstream used by the Workshop Homarr provider proxy. | HTTP(S) URL | `https://openrouter.ai/api/v1` |
| Environment Variable | Description | Possible values | Default |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------- |
| `PUID` | User ID to run the container as | Any valid user ID | `0` |
| `PGID` | Group ID to run the container as | Any valid group ID | `0` |
| `LOG_LEVEL` | Log level to use | `debug` / `info` / `warn` / `error` | `info` |
| `NO_EXTERNAL_CONNECTION` | Disables Homarr's internet-backed requests, including update checks, weather, and RSS feeds | `true` or `false` | `false` |
| `ENABLE_DNS_CACHING` | Enables dns caching. Enabled by default, but if you experience issues with IPv6 or static IPs, see [#4006](https://github.com/homarr-labs/homarr/issues/4006) or disable this flag | `true` or `false` | `true` |
| `HOMARR_DISABLE_IPV6` | Disables the IPv6 listener of the internal nginx proxy, even when the host supports IPv6. IPv6 is normally disabled automatically when the host has no IPv6 configured. See [#4596](https://github.com/homarr-labs/homarr/issues/4596) | `true` or `false` | `false` |
| `HOMARR_WEBSITE_URL` | Main Homarr documentation/site base used by runtime links. Must be an absolute HTTP(S) URL. | HTTP(S) URL | `https://homarr.dev` |
| `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone documentation. | HTTP(S) URL | `HOMARR_WEBSITE_URL` |
| `WORKSHOP_WEB_URL` | Public Workshop page opened from Homarr. | HTTP(S) URL | `HOMARR_WEBSITE_URL/workshop` |
Workshop service operators can find its server-only configuration in the
[Workshop operator guide](../../workshop/operator#environment-variables).
## Authentication
@@ -111,13 +112,9 @@ External redis is currently not supported with Proxmox Community Scripts
The advanced deployments environment variables should only be used if you know what you are doing.
:::
| Environment Variable | Description |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts. |
| `WORKSHOP_PUBLIC_ORIGIN` | Origin used in PocketBase-generated links and email. It must be set explicitly in production and must match the externally reachable Workshop origin. |
| `PB_ALLOWED_ORIGINS` | Browser origins accepted by PocketBase. Use `*` for the central public community so arbitrary self-hosted Homarr instances can use the API and OAuth; private staging may use a comma-separated origin allowlist. |
| `OPENROUTER_API_KEY` | Server-only OpenRouter key used by the Workshop Homarr provider proxy. The provider is unavailable when empty. |
| Environment Variable | Description |
| ------------------------ | --------------------------------------------------- |
| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts. |
All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize
trailing slashes when Homarr or Docusaurus starts. The Docusaurus-only `WORKSHOP_URL` name is accepted as a deprecated alias for
`WORKSHOP_API_URL` for one release. `WORKSHOP_API_URL` takes precedence.
trailing slashes when Homarr or Docusaurus starts.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"label": "Community",
"position": 5,
"position": 6,
"collapsed": true,
"link": {
"type": "generated-index"
+2 -1
View File
@@ -55,7 +55,8 @@ preset connects any other service that implements the OpenAI Chat Completions AP
The **Homarr provider** offers one community-funded model, DeepSeek V4 Flash Latest, without an administrator API key.
Users authorize it with their existing Community Workshop GitHub session. A compact battery beside the model selector
shows their remaining daily request allowance and the UTC reset time. See [Homarr provider](./homarr-provider) for its
shows their remaining daily request allowance and the UTC reset time. See
[Homarr provider](../workshop/homarr-provider) for its
quota, privacy, live activity, and funding details.
The management page presents setup as three compact sections: provider and endpoint, credentials, then
@@ -1,72 +0,0 @@
---
title: Homarr provider
sidebar_position: 7
tags:
- Assistant
- AI
- Privacy
- Community
---
import { AssistantProviderActivity } from "@site/src/components/assistant-provider/assistant-provider-activity";
# Homarr provider
The Homarr provider gives signed-in community members a small daily allowance for **DeepSeek V4 Flash Latest**.
It is an OpenAI-compatible proxy operated alongside the [Community Workshop](./workshop). Homarr administrators can
select it without adding an API key to their own instance. Each user signs in to the Workshop with GitHub, and that
existing PocketBase session authorizes only their requests.
<AssistantProviderActivity />
## Allowance
- The default allowance is **50 requests per user per UTC day**.
- Every model request consumes one request. Every completed Homarr tool call consumes one additional request.
- The allowance resets at midnight UTC.
- The battery beside the assistant model selector shows the remaining count. Select it to see the limit, reset time,
Workshop identity, and current status.
- A future account plan can change the stored per-user limit without changing other users.
## Privacy
The proxy streams the model response without storing prompts, messages, attachments, tool inputs, tool outputs, or
response text. Private accounting records contain only the Workshop user ID, status, model identifier, request-unit
count, token totals, duration, cost, and timestamps. The public live feed above is a separate read-only collection
with the same operational metrics and **no user or GitHub field**.
The upstream OpenRouter key exists only in the Workshop server environment. It is never sent to Homarr instances or
browsers. A user's Workshop token is forwarded by their Homarr server only to the configured Workshop origin.
## Fund free access
Inference is paid from the Homarr community budget. If this provider is useful, help keep the daily allowance
available through the [Homarr OpenCollective](https://opencollective.com/homarr).
<a className="button button--primary" href="https://opencollective.com/homarr">
Fund free Homarr AI
</a>
## Share what you build
The provider is intended to help people create useful dashboards and Custom Widgets without first configuring a
commercial model account. Publish successful Custom Widgets in the [Community Workshop](pathname:///workshop), and
share the prompt, API source, and screenshots so other Homarr users can improve them.
## Run the proxy
The open-source proxy is part of the Workshop image. Operators configure it with server-only environment variables:
| Variable | Purpose | Default |
| ------------------------------- | -------------------------------------------- | ------------------------------ |
| `OPENROUTER_API_KEY` | Upstream key used only by the Workshop proxy | Disabled when empty |
| `HOMARR_AI_DAILY_REQUEST_LIMIT` | Initial daily limit stored for each new user | `50` |
| `HOMARR_AI_OPENROUTER_BASE_URL` | OpenRouter-compatible upstream base URL | `https://openrouter.ai/api/v1` |
Changing the global default affects new quota records. Existing per-user limits remain unchanged so administrators can
support future account plans. Run `pnpm test:workshop` to verify authentication, streaming, UTC reset, atomic quota
enforcement, tool-call accounting, private records, anonymous public activity, realtime delivery, and migration rollback.
The Custom Widget AI evaluation suites can use this proxy by setting `AI_PROVIDER_BASE_URL` to this service's
`/api/ai/v1` endpoint and `AI_PROVIDER_API_KEY` to a disposable Workshop user token. See
[Agent authoring](./custom-widgets/agent-authoring#evaluate-the-copied-prompt) for the complete command.
@@ -1,172 +0,0 @@
---
tags:
- Workshop
- Custom Widgets
- Custom CSS
---
# Community Workshop
:::warning Experimental feature
This feature is experimental and will probably change. If you encounter bugs, need more capabilities, or find something confusing, tell us in the [official discussion thread](https://github.com/homarr-labs/homarr/discussions/categories/custom-widgets). Your feedback directly shapes what comes next. Happy hacking!
:::
The [Community Workshop](pathname:///workshop) is where Homarr users share and discover
**custom CSS themes** and **custom widgets**. You can browse submissions, upvote the ones
you like, report abuse, and install entries directly into your own Homarr instance.
The central `homarr.dev` Workshop is a public community. Browsing it on the website is public. Browsing, installing,
publishing, or importing Workshop content **inside a Homarr instance is restricted to Homarr administrators**.
:::warning
The Workshop contains community-made content. Moderation is not a security boundary, and PocketBase does not interpret
or validate submission content. Always review widget capabilities, network origins, actions, and CSS before installing.
Homarr's installation-time validator is definitive and can reject content that the public service accepted earlier.
Back up your instance before installing untrusted content.
:::
## Browsing and voting
- Filter by **Widgets** or **CSS**, show or hide outdated entries, and sort by popularity, creation date, recent updates, or discussion activity.
- Sign in with **GitHub** to upvote/downvote, submit your own creations, or report a submission.
- Each submission shows its title, description, linked GitHub author, revision, optional screenshots, comments, and full content. Widget pages also summarize API sources, actions, options, network access, and required credentials before installation.
- A new submission receives one real upvote owned by its author. Votes remain unique per user and submission.
- Open report counts are public on submission cards so visitors can make an informed installation decision. The submission author and Workshop moderators can read each report category and explanation; reporter identities remain moderator-only.
- Signed-in moderators see a **Moderation** button in the Workshop header. The moderation queue is sorted by open report count and lets moderators inspect every report, dismiss it, or delete the submission. Reports do not automatically remove a submission.
- The manual **Outdated** badge warns visitors that a submission may need an update.
## Comments
Comments are publicly readable and appear in chronological conversation bubbles with the author's avatar and GitHub profile. Signed-in users can post; authors can edit or delete their own comments, and Workshop moderators can delete any comment. Failed posts keep the text you typed. When SMTP and `WORKSHOP_PUBLIC_ORIGIN` are configured, the submission author receives one email for a comment from another user. Self-comments, edits, and deletions do not send mail.
## Submitting
1. Sign in with GitHub.
2. Click **Share yours** and choose the type:
- **Custom widget** — paste the JSON exported from **Manage → Custom Widgets → Export**.
It must match the `homarr-custom-widget-v2` schema (validated before submission).
- **Custom CSS** — paste your stylesheet (max 16384 characters).
3. Review the widget JSON or CSS in the syntax-aware editor, then add a title, description, optional screenshots, and changelog.
4. Submit the entry.
Custom widget JSON is normalized to readable multi-line formatting when it is published, and the Workshop detail page
also formats older compact submissions before displaying them.
Community members can report submissions as outdated or no longer working, malicious, spam, copyright-infringing,
inappropriate, or another concern. An outdated report does not automatically mark or remove the submission.
Submissions are published immediately. Reported entries are reviewed and may be removed.
Authors can edit a submission later to publish a new revision. The edit form can keep or remove existing screenshots and add new PNG, JPG, or WebP images without changing the Workshop URL.
When SMTP and `WORKSHOP_PUBLIC_ORIGIN` are configured, every current Workshop moderator receives an email when a report is submitted. If a moderator deletes the reported submission, its author receives a separate removal email. Author self-deletion does not send a moderation email.
:::info Separate roles
A Workshop moderator is a user with PocketBase's internal `isAdmin` field. This role moderates the central community
and is unrelated to a Homarr instance administrator.
:::
## Submission guidelines
Submissions must be useful to Homarr users and safe to install. Do not submit content that is
malicious, deceptive, illegal, hateful, sexually explicit, gore, gambling-focused, spam, or content
that primarily promotes unrelated products or services.
## Installing into Homarr
You can install workshop entries without leaving Homarr:
- **Custom widgets (Homarr administrators only)** — go to **Manage → Custom Widgets**, or use **Import from Workshop** in the
board widget picker. Choose **Install widget**, review the complete source and capabilities, then confirm API sources and any local secrets in the second modal step.
- **Custom CSS from Workshop (Homarr administrators only)** — open a board's **Settings → Custom CSS**, click **Import from Workshop**, choose a theme, review its full CSS in the second same-size modal step, and click **Import CSS** once. The board setting is still saved separately.
Alternatively, open a submission in the Workshop and use **Download widget JSON** or **Copy CSS**. The submission page shows the matching manual installation steps before the source code.
Custom Widget imports detect required bearer, basic, header, or query credentials. You can enter them during import or explicitly defer them; secret values are never stored in Workshop content. Missing credentials remain visible on the local Custom Widget afterward.
## Workshop deployment
Workshop is powered by [PocketBase](https://pocketbase.io). The production image compiles the current Docusaurus site and serves it together with the API from one origin. The deployment lives in `apps/workshop/` with a Dockerfile, Compose configuration, version-controlled migrations, hooks, a smoke test, and ACT-local workflow instructions.
Submission links include Open Graph and X/Twitter metadata for the submission title, description, and type. The first uploaded screenshot is used as the social preview image; submissions without screenshots use the Homarr logo.
Run `pnpm docker:workshop` for PocketBase on port 8090, then run
`WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs` for Docusaurus on port 3003. `pnpm docker:docs` builds the
combined production layout. Normal start and stop commands retain data; `down --volumes` is an intentional,
irreversible development reset.
GitHub needs only the central callback `https://<workshop-host>/api/oauth2-redirect`. Keep PocketBase origins unrestricted
unless every self-hosted Homarr caller is explicitly listed. Configure both `GITHUB_CLIENT_ID` and
`GITHUB_CLIENT_SECRET`; supplying only one is a startup error. Set `WORKSHOP_PUBLIC_ORIGIN` in production so generated
links and email use the public host. Workshop stores only the OAuth provider username in `users.name`, and
users cannot rename it. Map **OAuth2 username** to `name`; leave OAuth2 id, full name, and avatar mappings
empty. Profile and avatar URLs are derived from `https://github.com/<username>` and
`https://github.com/<username>.png`, so PocketBase does not store duplicate profile metadata or avatar files. Configure
SMTP for comment, report, and removal notices.
The public URL contract is:
| Variable | Purpose | Default |
| ------------------------------- | ------------------------------------------------- | ------------------------------ |
| `HOMARR_WEBSITE_URL` | Documentation/site base used by Homarr links | `https://homarr.dev` |
| `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone docs | `HOMARR_WEBSITE_URL` |
| `WORKSHOP_WEB_URL` | Public Workshop page linked from Homarr | `HOMARR_WEBSITE_URL/workshop` |
| `WORKSHOP_PUBLIC_ORIGIN` | PocketBase-generated links and email | Required in production |
| `PB_ALLOWED_ORIGINS` | PocketBase browser origins | `*` for the central community |
| `OPENROUTER_API_KEY` | Server-only key for the Homarr provider proxy | Provider disabled when empty |
| `HOMARR_AI_DAILY_REQUEST_LIMIT` | Initial per-user daily provider allowance | `50` |
| `HOMARR_AI_OPENROUTER_BASE_URL` | OpenRouter-compatible proxy upstream | `https://openrouter.ai/api/v1` |
The deprecated docs-only `WORKSHOP_URL` alias remains for one release; `WORKSHOP_API_URL` takes precedence. The
combined Workshop image writes these public URLs into its browser runtime configuration whenever the container starts;
the Docker build arguments remain fallbacks. Restart the container after changing these values, without rebuilding it.
### Staging and demo deployment
Use `apps/workshop/docker-compose.staging.yml` with the published `ghcr.io/homarr-labs/workshop:v2` image for a v2
preview. Copy `apps/workshop/.env.staging.example` to `.env.staging`; its default public contract is:
```dotenv
HOMARR_WEBSITE_URL=https://v2.preview.homarr.dev
WORKSHOP_API_URL=https://v2.preview.homarr.dev
WORKSHOP_WEB_URL=https://v2.preview.homarr.dev/workshop
WORKSHOP_PUBLIC_ORIGIN=https://v2.preview.homarr.dev
PB_ALLOWED_ORIGINS=*
```
Start or update it from the repository root:
```sh
docker compose --env-file apps/workshop/.env.staging \
-f apps/workshop/docker-compose.staging.yml pull
docker compose --env-file apps/workshop/.env.staging \
-f apps/workshop/docker-compose.staging.yml up --detach
curl --fail http://127.0.0.1:8090/api/health
```
The service binds to `127.0.0.1:8090`. Terminate TLS at a reverse proxy and forward the **entire hostname**, including
`/api`, `/_/`, and `/workshop-runtime-config.js`; forwarding only `/workshop` breaks PocketBase and runtime URL loading.
This combined image serves both the documentation site and PocketBase, so use a dedicated Workshop hostname if the
chosen hostname already serves Homarr itself. In that case, replace the hostname in all four URL values and keep
`/workshop` only on `WORKSHOP_WEB_URL`.
The Compose volume preserves PocketBase data across pulls and container recreation. The mutable `v2` tag is suitable
for previews; use the `sha-<commit>` tag for promotion. For the public central Workshop, `PB_ALLOWED_ORIGINS=*` allows
self-hosted Homarr origins to use the API and OAuth. A private demo may use a comma-separated origin allowlist instead.
GitHub OAuth requires both credentials and the callback
`https://v2.preview.homarr.dev/api/oauth2-redirect`. Restart after changing environment values, then verify the health
endpoint, `/workshop`, a public listing request, GitHub sign-in, and an installation from the Homarr preview.
Back up application and PocketBase data before deployment. Publish the combined image by immutable digest, test the
exact digest in staging, rehearse application rollback and PocketBase restore, then promote that same digest.
## Client architecture
Workshop access is centralized in `@homarr/workshop/backend`. It exports the typed `WorkshopBackend`, its typed PocketBase instance, stable TanStack Query option factories, and the `useWorkshop…` query and mutation hooks used by Homarr. The documentation site uses the same backend rather than maintaining a second API contract.
Listing fields derived by the `workshop_listings` view—such as submission type, score, and counts—are normalized by the shared client. Healthy deployments keep filtering, sorting, and pagination in PocketBase. If a persistent development volume still has an older view that rejects those fields, the client retries without them and applies the same operations locally; it can also fall back to the base submissions collection. Production deployments should nevertheless apply the current migration so the view exposes the complete metadata.
+9
View File
@@ -0,0 +1,9 @@
{
"label": "Workshop",
"collapsed": true,
"position": 4,
"link": {
"type": "doc",
"id": "workshop/index"
}
}
+61
View File
@@ -0,0 +1,61 @@
---
title: Workshop development
sidebar_position: 4
tags:
- Workshop
- Developer
- Docusaurus
- PocketBase
---
# Workshop development
Install the repository dependencies and make sure Docker is available before starting the Workshop services. See the
[main development guide](../advanced/development/getting-started) for the repository prerequisites.
## Run PocketBase and documentation
Start PocketBase in one terminal:
```sh
cp apps/workshop/.env.example apps/workshop/.env
docker compose --env-file apps/workshop/.env -f apps/workshop/docker-compose.yml up --build workshop
```
Then start Docusaurus in another terminal:
```sh
WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs
```
PocketBase runs on port 8090 and Docusaurus on port 3003. Stopping the services preserves the PocketBase volume.
Open `http://127.0.0.1:8090/_/` and create the first PocketBase superuser. The Workshop UI is available at
`http://127.0.0.1:3003/workshop`.
Point Homarr at the local services with:
```dotenv
HOMARR_WEBSITE_URL=http://127.0.0.1:3003
WORKSHOP_API_URL=http://127.0.0.1:8090
WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop
```
Restart `pnpm dev` after changing `.env`. GitHub OAuth is optional locally. See
[`apps/workshop/README.md`](https://github.com/homarr-labs/homarr/blob/HEAD/apps/workshop/README.md) for OAuth,
Workshop moderator promotion, logs, tests, backup, restore, stopping, and intentional volume reset.
## URL testing
Docusaurus reads `HOMARR_WEBSITE_URL` and `WORKSHOP_API_URL` when it starts or builds. Homarr also reads
`WORKSHOP_WEB_URL` when its server starts, so these values do not rely on `NEXT_PUBLIC_*` build variables. Test a
separate API origin before release:
```sh
HOMARR_WEBSITE_URL=http://127.0.0.1:3003 \
WORKSHOP_API_URL=http://127.0.0.1:8090 \
WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop \
pnpm dev:docs
```
The [Workshop operator guide](./operator) covers production environment variables, staging, and service validation.
@@ -0,0 +1,81 @@
---
title: Homarr provider
sidebar_position: 2
tags:
- Assistant
- AI
- Privacy
- Community
---
import { AssistantProviderActivity } from "@site/src/components/assistant-provider/assistant-provider-activity";
import { OpenCollectiveLink } from "@site/src/components/assistant-provider/open-collective-link";
# Homarr provider
The Homarr provider gives Workshop users access to **DeepSeek V4 Flash Latest**. It uses an OpenAI-compatible proxy in
the [Community Workshop](/docs/workshop). A Homarr administrator can select this provider without an API key. Each user
signs in to the Workshop with GitHub. The Workshop session authorizes the user's requests.
<AssistantProviderActivity />
## Allowance
- Each user gets **50 request units per UTC day** by default.
- A model request uses one unit.
- Each completed Homarr tool call uses one additional unit.
- A request that reaches the upstream uses its reserved units even when the upstream fails. Invalid requests rejected
by the Workshop before forwarding do not use units.
- The limit resets at 00:00 UTC.
- The battery icon beside the model selector shows the remaining units. Select it to see the limit, reset time,
Workshop account, and status.
- The server stores a separate limit for each user. A change to the global default applies only to new users.
The remaining count shown in Homarr is informational. The Workshop server authenticates the user and reserves request
units in a database transaction before it contacts OpenRouter. Concurrent or direct API requests cannot spend more
than the stored allowance.
## Security and cost controls
The proxy owns the upstream model and routing configuration. It removes client-supplied fallback models, providers,
routes, plugins, transforms, and user identifiers before forwarding a request. It also forces one completion, disables
parallel tool calls, caps OpenRouter web search, and limits every response to **32,768 output tokens**.
Input uses a conservative **256k token ceiling**. The Workshop enforces this with a 256 KiB serialized request limit,
because a byte can encode at most one input token. This deliberately rejects some large attachments before they reach
the theoretical token limit. It prevents a browser or direct API client from hiding an oversized prompt behind an
optimistic client-side token estimate.
Only OpenAI function tools and Homarr's bounded `openrouter:web_search` server tool are accepted. The response stream
has an independent byte limit in case an upstream ignores the requested output ceiling. Upstream error bodies are not
forwarded to users.
## Privacy
The proxy does not store prompts, messages, attachments, tool inputs, tool outputs, or response text. Private records
store only the Workshop user ID, status, model ID, request units, token totals, duration, cost, and timestamps. The
public feed is read-only. It contains operational data but does not contain a user ID or GitHub account.
The Workshop server stores the OpenRouter API key. It does not send this key to a Homarr instance or browser. A Homarr
server sends the user's Workshop token only to the configured Workshop origin.
Prompts, attachments, tool definitions, and tool results are forwarded to the configured OpenRouter-compatible
upstream so it can generate the response. That upstream's data-handling policy applies. The Workshop proxy does not
log or persist this content.
The open-source proxy can limit authenticated use, but it cannot prevent one person from creating several eligible
Workshop accounts. Operators should also set a hard OpenRouter credit limit and monitor the anonymous activity feed.
## Costs
The Homarr community pays for provider requests. Donations help cover these costs.
<OpenCollectiveLink />
## Share your results
Publish useful Custom Widgets in the [Community Workshop](pathname:///workshop). Include the prompt, API source, and
screenshots. This information helps other users test and improve the widget.
Workshop operators can configure the proxy, its upstream, and its default allowance in the
[Workshop operator guide](./operator#homarr-provider-proxy).
+95
View File
@@ -0,0 +1,95 @@
---
title: Community Workshop
sidebar_label: Overview
tags:
- Workshop
- Custom Widgets
- Custom CSS
---
# Community Workshop
:::warning Experimental feature
This feature is experimental and will probably change. If you encounter bugs, need more capabilities, or find something confusing, tell us in the [official discussion thread](https://github.com/homarr-labs/homarr/discussions/categories/custom-widgets). Your feedback directly shapes what comes next. Happy hacking!
:::
The [Community Workshop](pathname:///workshop) is where Homarr users share and discover
**custom CSS themes** and **custom widgets**. You can browse submissions, upvote the ones
you like, report abuse, and install entries directly into your own Homarr instance.
The central `homarr.dev` Workshop is a public community. Browsing it on the website is public. Browsing, installing,
publishing, or importing Workshop content **inside a Homarr instance is restricted to Homarr administrators**.
:::warning
The Workshop contains community-made content. Moderation is not a security boundary, and PocketBase does not interpret
or validate submission content. Always review widget capabilities, network origins, actions, and CSS before installing.
Homarr's installation-time validator is definitive and can reject content that the public service accepted earlier.
Back up your instance before installing untrusted content.
:::
## Browsing and voting
- Filter by **Widgets** or **CSS**, show or hide outdated entries, and sort by popularity, creation date, recent updates, or discussion activity.
- Sign in with **GitHub** to upvote/downvote, submit your own creations, or report a submission.
- Each submission shows its title, description, linked GitHub author, revision, optional screenshots, comments, and full content. Widget pages also summarize API sources, actions, options, network access, and required credentials before installation.
- A new submission receives one real upvote owned by its author. Votes remain unique per user and submission.
- Open report counts are public on submission cards so visitors can make an informed installation decision. The submission author and Workshop moderators can read each report category and explanation; reporter identities remain moderator-only.
- Signed-in moderators see a **Moderation** button in the Workshop header. The moderation queue is sorted by open report count and lets moderators inspect every report, dismiss it, or delete the submission. Reports do not automatically remove a submission.
- The manual **Outdated** badge warns visitors that a submission may need an update.
## Comments
Comments are publicly readable and appear in chronological conversation bubbles with the author's avatar and GitHub profile. Signed-in users can post; authors can edit or delete their own comments, and Workshop moderators can delete any comment. Failed posts keep the text you typed. When SMTP and `WORKSHOP_PUBLIC_ORIGIN` are configured, the submission author receives one email for a comment from another user. Self-comments, edits, and deletions do not send mail.
## Submitting
1. Sign in with GitHub.
2. Click **Share yours** and choose the type:
- **Custom widget** — paste the JSON exported from **Manage → Custom Widgets → Export**.
It must match the `homarr-custom-widget-v2` schema (validated before submission).
- **Custom CSS** — paste your stylesheet (max 16384 characters).
3. Review the widget JSON or CSS in the syntax-aware editor, then add a title, description, optional screenshots, and changelog.
4. Submit the entry.
Custom widget JSON is normalized to readable multi-line formatting when it is published, and the Workshop detail page
also formats older compact submissions before displaying them.
Community members can report submissions as outdated or no longer working, malicious, spam, copyright-infringing,
inappropriate, or another concern. An outdated report does not automatically mark or remove the submission.
Submissions are published immediately. Reported entries are reviewed and may be removed.
Authors can edit a submission later to publish a new revision. The edit form can keep or remove existing screenshots and add new PNG, JPG, or WebP images without changing the Workshop URL.
When SMTP and `WORKSHOP_PUBLIC_ORIGIN` are configured, every current Workshop moderator receives an email when a report is submitted. If a moderator deletes the reported submission, its author receives a separate removal email. Author self-deletion does not send a moderation email.
:::info Separate roles
A Workshop moderator is a user with PocketBase's internal `isAdmin` field. This role moderates the central community
and is unrelated to a Homarr instance administrator.
:::
## Submission guidelines
Submissions must be useful to Homarr users and safe to install. Do not submit content that is
malicious, deceptive, illegal, hateful, sexually explicit, gore, gambling-focused, spam, or content
that primarily promotes unrelated products or services.
## Installing into Homarr
You can install workshop entries without leaving Homarr:
- **Custom widgets (Homarr administrators only)** — go to **Manage → Custom Widgets**, or use **Import from Workshop** in the
board widget picker. Choose **Install widget**, review the complete source and capabilities, then confirm API sources and any local secrets in the second modal step.
- **Custom CSS from Workshop (Homarr administrators only)** — open a board's **Settings → Custom CSS**, click **Import from Workshop**, choose a theme, review its full CSS in the second same-size modal step, and click **Import CSS** once. The board setting is still saved separately.
Alternatively, open a submission in the Workshop and use **Download widget JSON** or **Copy CSS**. The submission page shows the matching manual installation steps before the source code.
Custom Widget imports detect required bearer, basic, header, or query credentials. You can enter them during import or explicitly defer them; secret values are never stored in Workshop content. Missing credentials remain visible on the local Custom Widget afterward.
Deployment, environment variables, backups, and service architecture are covered in the
[Workshop operator guide](./operator).
+121
View File
@@ -0,0 +1,121 @@
---
title: Workshop operator guide
sidebar_label: Operator guide
sidebar_position: 3
tags:
- Workshop
- Deployment
- Environment variables
- PocketBase
---
# Workshop operator guide
Workshop is powered by [PocketBase](https://pocketbase.io). The production image compiles the current Docusaurus site
and serves it together with the API from one origin. The deployment lives in `apps/workshop/` with a Dockerfile,
Compose configuration, version-controlled migrations, hooks, a smoke test, and ACT-local workflow instructions.
Submission links include Open Graph and X/Twitter metadata for the submission title, description, and type. The first
uploaded screenshot is used as the social preview image; submissions without screenshots use the Homarr logo.
Run `pnpm docker:workshop` for PocketBase on port 8090, then run
`WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs` for Docusaurus on port 3003. `pnpm docker:docs` builds the
combined production layout. Normal start and stop commands retain data; `down --volumes` is an intentional,
irreversible development reset. See [Workshop development](./development) for the complete local setup.
## Authentication and notifications
GitHub needs only the central callback `https://<workshop-host>/api/oauth2-redirect`. Keep PocketBase origins
unrestricted unless every self-hosted Homarr caller is explicitly listed. Configure both `GITHUB_CLIENT_ID` and
`GITHUB_CLIENT_SECRET`; supplying only one is a startup error. Set `WORKSHOP_PUBLIC_ORIGIN` in production so generated
links and email use the public host. Workshop stores only the OAuth provider username in `users.name`, and users cannot
rename it. Map **OAuth2 username** to `name`; leave OAuth2 id, full name, and avatar mappings empty. Profile and avatar
URLs are derived from `https://github.com/<username>` and `https://github.com/<username>.png`, so PocketBase does not
store duplicate profile metadata or avatar files. Configure SMTP for comment, report, and removal notices.
## Environment variables
The public URL contract and Workshop-only server configuration are:
| Variable | Purpose | Default |
| ------------------------------- | ------------------------------------------------- | ------------------------------ |
| `HOMARR_WEBSITE_URL` | Documentation/site base used by Homarr links | `https://homarr.dev` |
| `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone docs | `HOMARR_WEBSITE_URL` |
| `WORKSHOP_WEB_URL` | Public Workshop page linked from Homarr | `HOMARR_WEBSITE_URL/workshop` |
| `WORKSHOP_PUBLIC_ORIGIN` | PocketBase-generated links and email | Required in production |
| `PB_ALLOWED_ORIGINS` | PocketBase browser origins | `*` for the central community |
| `OPENROUTER_API_KEY` | Server-only key for the Homarr provider proxy | Provider disabled when empty |
| `HOMARR_AI_DAILY_REQUEST_LIMIT` | Initial per-user daily provider allowance | `50` |
| `HOMARR_AI_OPENROUTER_BASE_URL` | OpenRouter-compatible proxy upstream | `https://openrouter.ai/api/v1` |
All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize
trailing slashes when Homarr or Docusaurus starts. The deprecated docs-only `WORKSHOP_URL` alias remains for one
release; `WORKSHOP_API_URL` takes precedence. The combined Workshop image writes the public URLs into its browser
runtime configuration whenever the container starts; the Docker build arguments remain fallbacks. Restart the
container after changing these values, without rebuilding it.
### Homarr provider proxy
The [Homarr provider](./homarr-provider) proxy is part of the Workshop image. `OPENROUTER_API_KEY` enables the proxy,
`HOMARR_AI_OPENROUTER_BASE_URL` selects its OpenRouter-compatible upstream, and `HOMARR_AI_DAILY_REQUEST_LIMIT` sets the
initial limit stored for each new user. Existing users keep their assigned limits. The upstream URL cannot contain
embedded credentials, a query, or a fragment. Use HTTPS for any upstream outside a trusted private network.
Run `pnpm test:workshop` to test authentication, streaming, UTC reset, atomic quota enforcement, failed-request
accounting, input and output controls, tool-call accounting, private records, public activity, realtime delivery, and
migration rollback.
The Custom Widget AI tests can use this proxy. Set `AI_PROVIDER_BASE_URL` to the `/api/ai/v1` endpoint and
`AI_PROVIDER_API_KEY` to a temporary Workshop user token. See
[Agent authoring](../management/custom-widgets/agent-authoring#evaluate-the-copied-prompt) for the command.
## Staging and demo deployment
Use `apps/workshop/docker-compose.staging.yml` with the published `ghcr.io/homarr-labs/workshop:v2` image for a v2
preview. Copy `apps/workshop/.env.staging.example` to `.env.staging`; its default public contract is:
```dotenv
HOMARR_WEBSITE_URL=https://v2.preview.homarr.dev
WORKSHOP_API_URL=https://v2.preview.homarr.dev
WORKSHOP_WEB_URL=https://v2.preview.homarr.dev/workshop
WORKSHOP_PUBLIC_ORIGIN=https://v2.preview.homarr.dev
PB_ALLOWED_ORIGINS=*
```
Start or update it from the repository root:
```sh
docker compose --env-file apps/workshop/.env.staging \
-f apps/workshop/docker-compose.staging.yml pull
docker compose --env-file apps/workshop/.env.staging \
-f apps/workshop/docker-compose.staging.yml up --detach
curl --fail http://127.0.0.1:8090/api/health
```
The service binds to `127.0.0.1:8090`. Terminate TLS at a reverse proxy and forward the **entire hostname**, including
`/api`, `/_/`, and `/workshop-runtime-config.js`; forwarding only `/workshop` breaks PocketBase and runtime URL loading.
This combined image serves both the documentation site and PocketBase, so use a dedicated Workshop hostname if the
chosen hostname already serves Homarr itself. In that case, replace the hostname in all four URL values and keep
`/workshop` only on `WORKSHOP_WEB_URL`.
The Compose volume preserves PocketBase data across pulls and container recreation. The mutable `v2` tag is suitable
for previews; use the `sha-<commit>` tag for promotion. For the public central Workshop, `PB_ALLOWED_ORIGINS=*` allows
self-hosted Homarr origins to use the API and OAuth. A private demo may use a comma-separated origin allowlist instead.
GitHub OAuth requires both credentials and the callback `https://v2.preview.homarr.dev/api/oauth2-redirect`. Restart
after changing environment values, then verify the health endpoint, `/workshop`, a public listing request, GitHub
sign-in, and an installation from the Homarr preview.
Back up application and PocketBase data before deployment. Publish the combined image by immutable digest, test the
exact digest in staging, rehearse application rollback and PocketBase restore, then promote that same digest.
## Client architecture
Workshop access is centralized in `@homarr/workshop/backend`. It exports the typed `WorkshopBackend`, its typed
PocketBase instance, stable TanStack Query option factories, and the `useWorkshop…` query and mutation hooks used by
Homarr. The documentation site uses the same backend rather than maintaining a second API contract.
Listing fields derived by the `workshop_listings` view—such as submission type, score, and counts—are normalized by the
shared client. Healthy deployments keep filtering, sorting, and pagination in PocketBase. If a persistent development
volume still has an older view that rejects those fields, the client retries without them and applies the same
operations locally; it can also fall back to the base submissions collection. Production deployments should
nevertheless apply the current migration so the view exposes the complete metadata.
@@ -0,0 +1,41 @@
.link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.7rem;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 0.5rem;
background: var(--ifm-background-surface-color);
color: var(--ifm-font-color-base);
font-size: 0.875rem;
font-weight: 600;
line-height: 1.25;
text-decoration: none;
transition:
background-color 150ms ease,
border-color 150ms ease;
}
.link:hover {
border-color: var(--ifm-color-emphasis-500);
background: var(--ifm-color-emphasis-100);
color: var(--ifm-font-color-base);
text-decoration: none;
}
.link:focus-visible {
outline: 2px solid var(--ifm-color-primary);
outline-offset: 2px;
}
.logo {
width: 1.1rem;
height: 1.1rem;
flex: 0 0 auto;
}
@media (prefers-reduced-motion: reduce) {
.link {
transition: none;
}
}
@@ -0,0 +1,8 @@
import styles from "./open-collective-link.module.css";
export const OpenCollectiveLink = () => (
<a className={styles.link} href="https://opencollective.com/homarr">
<img className={styles.logo} src="/img/opencollective.svg" alt="" aria-hidden />
Donate to Homarr on OpenCollective
</a>
);
@@ -1,22 +1,79 @@
import { IconBrandDocker, IconCpu, IconDeviceDesktopAnalytics } from "@tabler/icons-react";
import styles from "../../../../pages/index.module.css";
import { AppWidget } from "./widgets/app-widget";
import { ClockWidget } from "./widgets/clock-widget";
import { AssistantProviderWidget } from "./widgets/assistant-provider-widget";
import { DownloadsWidget } from "./widgets/downloads-widget";
import { WeatherWidget } from "./widgets/weather-widget";
import { StockWidget } from "./widgets/stock-widget";
import { EntityStateWidget } from "./widgets/entity-state-widget";
import { WidgetCard } from "./widgets/card";
export const HeroCards = () => {
return (
<div className="argos-ignore hero-cards flex flex-wrap max-w-[504px] gap-y-4 gap-x-3 text-gray-700 dark:text-gray-300">
<StockWidget />
<AppWidget />
<AppWidget className="hidden 3xl:block" />
<div className={`argos-ignore ${styles.heroCards}`}>
<SystemResourcesWidget />
<WeatherWidget />
<ClockWidget />
<DownloadsWidget />
<AssistantProviderWidget />
<DockerWidget />
<EntityStateWidget />
<AppWidget />
<ClockWidget />
</div>
);
};
const SystemResourcesWidget = () => (
<WidgetCard width={2} className={styles.resourcesWidget}>
<div className={styles.widgetHeading}>
<span className={styles.widgetTitle}>
<IconDeviceDesktopAnalytics size={17} aria-hidden="true" />
System resources
</span>
<span className={styles.onlineStatus}>Online</span>
</div>
<ResourceMeter icon={<IconCpu size={15} aria-hidden="true" />} label="CPU" value={34} />
<ResourceMeter label="Memory" value={62} />
</WidgetCard>
);
const ResourceMeter = ({ icon, label, value }: { icon?: React.ReactNode; label: string; value: number }) => (
<div className={styles.resourceMeter}>
<div className={styles.resourceLabel}>
<span>
{icon}
{label}
</span>
<span>{value}%</span>
</div>
<progress className={styles.resourceTrack} aria-label={`${label} usage`} max={100} value={value} />
</div>
);
const DockerWidget = () => (
<WidgetCard width={2} className={styles.dockerWidget}>
<div className={styles.widgetHeading}>
<span className={styles.widgetTitle}>
<IconBrandDocker size={18} aria-hidden="true" />
Docker
</span>
<span className={styles.dockerCount}>3 running</span>
</div>
<div className={styles.containerList}>
{[
["immich", "Healthy"],
["jellyfin", "Running"],
["home-assistant", "Running"],
].map(([name, status]) => (
<div className={styles.containerRow} key={name}>
<span className={styles.containerName}>{name}</span>
<span className={styles.containerStatus}>
<span className={styles.statusDot} aria-hidden="true" />
{status}
</span>
</div>
))}
</div>
</WidgetCard>
);
@@ -7,9 +7,9 @@ import { HeroCards } from "./hero-cards";
export default function HomeHero() {
return (
<header className={clsx("hero hero--primary", styles.heroBanner, styles.hero)}>
<div className="flex">
<div className="ps-0 lg:ps-52">
<h1 className="font-extrabold lg:text-7xl text-3xl">
<div className={styles.heroContent}>
<div className={styles.heroCopy}>
<h1 className={clsx("font-extrabold lg:text-7xl text-3xl", styles.heroTitle)}>
A simple, yet
<br />
powerful dashboard
@@ -22,7 +22,7 @@ export default function HomeHero() {
information.
</p>
<div className={"flex flex-nowrap gap-2"}>
<div className={styles.heroActions}>
<Link
data-attr="Install button"
className={"button button--secondary button--lg rounded-3xl dark:border-zinc-600 dark:bg-zinc-800"}
@@ -57,7 +57,7 @@ export default function HomeHero() {
</Link>
</div>
</div>
<div className="flex-grow-1 w-full lg:block hidden hover-animation">
<div className={styles.heroPreview} aria-label="Example Homarr dashboard">
<HeroCards />
</div>
</div>
@@ -1,5 +1,6 @@
import clsx from "clsx";
import { ReactNode } from "react";
import styles from "../../../../../pages/index.module.css";
export interface CommonWidgetProps {
className?: string;
@@ -16,15 +17,13 @@ export const WidgetCard = ({ width, children, className, onClick }: WidgetCardPr
return (
<Component
className={clsx(
"rounded-xl flex flex-col justify-center items-center p-2 relative border-none bg-[var(--ifm-navbar-background-color)]",
styles.widgetCard,
width === 2 ? styles.widgetCardWide : styles.widgetCardSingle,
"flex flex-col justify-center items-center p-2 relative border-none bg-[var(--ifm-navbar-background-color)]",
className,
onClick ? "cursor-pointer" : undefined,
)}
style={{
width: (128 * width + (width === 2 ? 16 : 0)) * 0.9,
height: 128,
background: "rgb(from var(--ifm-navbar-background-color) r g b / 80%)",
}}
type={onClick ? "button" : undefined}
onClick={onClick}
>
{children}
@@ -3,24 +3,29 @@ import { useEffect, useState } from "react";
import { CommonWidgetProps, WidgetCard } from "./card";
import clsx from "clsx";
import { generateSlug } from "random-word-slugs";
import styles from "../../../../../pages/index.module.css";
export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
const [downloads, setDownloads] = useState<Download[]>([]);
const [downloads, setDownloads] = useState<Download[]>([
{ filename: "photos-backup.zip", progress: 72 },
{ filename: "media-library.mkv", progress: 38 },
]);
useEffect(() => {
const intervalId = setInterval(() => {
setDownloads((prev: Download[]) => {
const filteredPrev = prev.filter((download) => download.progress !== 100);
filteredPrev.forEach((download, index) => {
download.progress = Math.min(100, download.progress + getRndInteger(0, index < 2 ? 16 : 8));
});
const progressedDownloads = filteredPrev.map((download, index) => ({
...download,
progress: Math.min(100, download.progress + getRndInteger(0, index < 2 ? 16 : 8)),
}));
if (filteredPrev.length < 3 && filteredPrev.length === prev.length) {
filteredPrev.push(generateRandomDownload());
if (progressedDownloads.length < 3 && progressedDownloads.length === prev.length) {
progressedDownloads.push(generateRandomDownload());
}
return [...filteredPrev];
return progressedDownloads;
});
}, 500);
@@ -39,14 +44,17 @@ export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
</tr>
</thead>
<tbody>
{downloads.map((file: Download, index: number) => {
{downloads.map((file: Download) => {
return (
<tr key={index}>
<tr key={file.filename}>
<td className="border-none text-nowrap p-2">{file.filename}</td>
<td className="border-none p-2 w-full">
<div className="overflow-hidden rounded-xl h-2 w-full bg-zinc-600">
<div className="bg-green-600 h-full animated-width" style={{ width: `${file.progress}%` }}></div>
</div>
<td className="border-none p-2 w-full" aria-label={`${file.progress}% complete`}>
<progress
className={styles.downloadProgress}
value={file.progress}
max={100}
aria-label={`${file.filename} download progress`}
/>
</td>
</tr>
);
@@ -758,7 +758,7 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => {
</div>
</div>
<Link
to="/docs/management/workshop/"
to="/docs/workshop/"
className="mt-4 inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
>
Installation guide <IconExternalLink size={13} />
+215 -2
View File
@@ -28,13 +28,226 @@ section {
}
.heroBanner {
padding: 4rem;
padding: clamp(3rem, 6vw, 6rem) clamp(1.25rem, 4vw, 4rem);
position: relative;
overflow: hidden;
}
.heroContent {
display: grid;
grid-template-columns: minmax(24rem, 1fr) minmax(31.25rem, 0.9fr);
align-items: center;
gap: clamp(2rem, 5vw, 5rem);
max-width: 90rem;
margin-inline: auto;
}
.heroCopy {
max-width: 52rem;
}
.heroTitle {
text-wrap: balance;
}
.heroActions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.heroPreview {
min-width: 0;
}
.heroCards {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.75rem;
width: min(100%, 31.25rem);
margin-inline: auto;
color: #343a40;
}
:global([data-theme="dark"]) .heroCards {
color: #dee2e6;
}
.widgetCard {
height: 8rem;
min-width: 0;
border-radius: 0.75rem;
background: color-mix(in srgb, var(--ifm-navbar-background-color) 88%, transparent) !important;
box-shadow: 0 4px 8px rgb(31 41 55 / 12%);
}
.widgetCardSingle {
grid-column: span 1;
}
.widgetCardWide {
grid-column: span 2;
}
.resourcesWidget,
.dockerWidget {
align-items: stretch !important;
justify-content: flex-start !important;
gap: 0.5rem;
padding: 0.75rem !important;
}
.widgetHeading,
.resourceLabel,
.containerRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.widgetTitle {
display: inline-flex;
align-items: center;
gap: 0.375rem;
min-width: 0;
font-size: 0.8125rem;
font-weight: 700;
}
.onlineStatus,
.dockerCount {
flex: none;
font-size: 0.6875rem;
font-weight: 600;
}
.onlineStatus {
color: #087f5b;
}
:global([data-theme="dark"]) .onlineStatus {
color: #69dbb1;
}
.resourceMeter {
display: grid;
gap: 0.25rem;
}
.resourceLabel {
font-size: 0.6875rem;
}
.resourceLabel > span:first-child {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.resourceTrack {
display: block;
appearance: none;
overflow: hidden;
width: 100%;
height: 0.375rem;
border: 0;
border-radius: 999px;
background: color-mix(in srgb, currentColor 16%, transparent);
}
.resourceTrack::-webkit-progress-bar {
background: transparent;
}
.resourceTrack::-webkit-progress-value,
.resourceTrack::-moz-progress-bar {
height: 100%;
border-radius: inherit;
background: var(--ifm-color-primary);
}
.downloadProgress {
display: block;
appearance: none;
overflow: hidden;
width: 100%;
height: 0.5rem;
border: 0;
border-radius: 999px;
background: color-mix(in srgb, currentColor 18%, transparent);
}
.downloadProgress::-webkit-progress-bar {
background: transparent;
}
.downloadProgress::-webkit-progress-value,
.downloadProgress::-moz-progress-bar {
border-radius: inherit;
background: #2f9e44;
}
.containerList {
display: grid;
gap: 0.25rem;
}
.containerRow {
font-size: 0.6875rem;
line-height: 1.25;
}
.containerName {
overflow: hidden;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.containerStatus {
display: inline-flex;
align-items: center;
gap: 0.25rem;
color: color-mix(in srgb, currentColor 82%, transparent);
}
.statusDot {
width: 0.375rem;
height: 0.375rem;
border-radius: 999px;
background: #12b886;
}
@media screen and (max-width: 996px) {
.heroBanner {
padding: 2rem;
padding: 3rem 1.5rem;
}
.heroContent {
grid-template-columns: minmax(0, 1fr);
gap: 2.5rem;
}
.heroCopy {
max-width: 46rem;
}
.heroCards {
width: min(100%, 36rem);
}
}
@media screen and (max-width: 640px) {
.heroBanner {
padding: 2.5rem 1rem;
}
.heroPreview {
display: none;
}
.heroActions > :global(*) {
flex: 1 1 auto;
}
}
+7
View File
@@ -0,0 +1,7 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>OpenCollective</title>
<path
fill="#7FADF2"
d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12c2.54 0 4.894-.79 6.834-2.135l-3.107-3.109a7.715 7.715 0 1 1 0-13.512l3.107-3.109A11.943 11.943 0 0 0 12 0zm9.865 5.166-3.109 3.107A7.67 7.67 0 0 1 19.715 12a7.682 7.682 0 0 1-.959 3.727l3.109 3.107A11.943 11.943 0 0 0 24 12c0-2.54-.79-4.894-2.135-6.834z"
/>
</svg>

After

Width:  |  Height:  |  Size: 447 B