docs: streamline release v2 documentation (#6760)

This commit is contained in:
Thomas authored and GitHub committed 2026-09-03 17:03:10 +02:00
1 parent ac62e5bfd6
commit b3aa6155cc
163 files changed
+2908 -4208

No files matched your search

@@ -0,0 +1,245 @@
.diagram {
--diagram-ink: var(--ifm-font-color-base);
--diagram-muted: var(--ifm-color-emphasis-700);
--diagram-panel: var(--ifm-background-surface-color);
--diagram-line: var(--ifm-color-emphasis-300);
margin: 0;
padding: 1rem;
border: 1px solid var(--diagram-line);
border-radius: 0.75rem;
color: var(--diagram-ink);
background: var(--diagram-panel);
}
.lane {
display: grid;
grid-template-columns: minmax(8.5rem, 1.1fr) 2.75rem minmax(8.5rem, 1fr) 2.75rem minmax(8.5rem, 1fr) 2.75rem minmax(
8.5rem,
1fr
);
align-items: stretch;
gap: 0.35rem;
}
.stage {
display: grid;
grid-template-rows: auto 1fr;
gap: 0.75rem;
min-width: 0;
padding: 0.7rem;
border: 1px solid var(--diagram-line);
border-radius: 0.55rem;
margin: 0;
background: var(--ifm-background-color);
}
.stageHeading {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--diagram-ink);
text-decoration: none;
}
a.stageHeading:hover {
color: var(--ifm-color-primary);
text-decoration: none;
}
.stageHeading > span:last-child {
display: grid;
min-width: 0;
}
.stageHeading strong {
font-size: 0.78rem;
}
.stageHeading small {
color: var(--diagram-muted);
font-size: 0.62rem;
line-height: 1.3;
}
.stageNumber {
display: grid;
flex: none;
place-items: center;
width: 1.45rem;
height: 1.45rem;
border: 1px solid var(--diagram-line);
border-radius: 50%;
color: var(--diagram-muted);
font-family: var(--font-mono);
font-size: 0.64rem;
font-weight: 700;
}
.serviceList,
.widgetList {
display: grid;
align-content: center;
gap: 0.35rem;
}
.service,
.widget {
display: flex;
align-items: center;
gap: 0.45rem;
min-width: 0;
padding: 0.4rem;
border: 1px solid var(--diagram-line);
border-radius: 0.4rem;
color: var(--diagram-ink);
background: var(--diagram-panel);
text-decoration: none;
}
.service:hover,
.widget:hover {
border-color: var(--ifm-color-primary);
color: var(--diagram-ink);
text-decoration: none;
}
.service img {
flex: none;
width: 1.25rem;
height: 1.25rem;
}
.widget svg {
flex: none;
color: var(--diagram-muted);
}
.service > span {
display: grid;
min-width: 0;
}
.service strong,
.widget span {
overflow: hidden;
font-size: 0.67rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.service code,
.integration code {
padding: 0;
border: 0;
color: var(--diagram-muted);
background: transparent;
font-size: 0.56rem;
white-space: nowrap;
}
.integration {
display: grid;
align-content: center;
justify-items: center;
gap: 0.35rem;
min-height: 5rem;
text-align: center;
}
.integration > svg {
color: var(--diagram-muted);
}
.integration > span {
color: var(--diagram-muted);
font-size: 0.61rem;
line-height: 1.35;
}
.boardCalendar {
display: grid;
align-content: center;
gap: 0.28rem;
min-height: 5rem;
padding: 0.42rem;
border: 1px solid var(--diagram-line);
border-radius: 0.4rem;
background: var(--diagram-panel);
}
.calendarHeader {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.3rem;
}
.calendarHeader strong {
font-size: 0.58rem;
}
.calendarHeader span {
color: var(--diagram-muted);
font-size: 0.44rem;
}
.calendarWeekdays,
.calendarDays {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 0.12rem;
}
.calendarWeekdays span {
color: var(--diagram-muted);
font-family: var(--font-mono);
font-size: 0.38rem;
text-align: center;
}
.calendarDays span {
position: relative;
display: grid;
place-items: center;
min-width: 0;
aspect-ratio: 1;
border-radius: 0.18rem;
color: var(--diagram-muted);
background: color-mix(in srgb, var(--diagram-line) 28%, transparent);
font-family: var(--font-mono);
font-size: 0.42rem;
}
.calendarDays span[data-event] {
color: var(--diagram-ink);
background: color-mix(in srgb, var(--diagram-line) 62%, transparent);
font-weight: 700;
}
.calendarDays span[data-event]::after {
position: absolute;
bottom: 0.08rem;
width: 0.12rem;
height: 0.12rem;
border-radius: 50%;
background: var(--diagram-muted);
content: "";
}
@media (max-width: 900px) {
.lane {
grid-template-columns: minmax(0, 1fr);
}
.serviceList,
.widgetList {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 430px) {
.serviceList,
.widgetList {
grid-template-columns: minmax(0, 1fr);
}
}
@@ -0,0 +1,154 @@
import type { ReactNode } from "react";
import Link from "@docusaurus/Link";
import { IconCalendar, IconDownload, IconPlugConnected } from "@tabler/icons-react";
import { qBittorentIntegration } from "@site/docs/integrations/q-bittorent";
import { sonarrIntegration } from "@site/docs/integrations/sonarr";
import { FlowConnector } from "@site/src/components/ui/flow-connector";
import styles from "./concept-flow.module.css";
const services = [
{
name: sonarrIntegration.name,
iconUrl: getIconUrl(sonarrIntegration.iconUrl),
endpoint: "sonarr:8989",
href: "/docs/integrations/sonarr",
},
{
name: qBittorentIntegration.name,
iconUrl: getIconUrl(qBittorentIntegration.iconUrl),
endpoint: "qbittorrent:8080",
href: "/docs/integrations/q-bittorent",
},
];
const widgets = [
{
name: "Calendar",
icon: IconCalendar,
href: "/docs/widgets/calendar",
},
{
name: "Downloads",
icon: IconDownload,
href: "/docs/widgets/downloads",
},
];
const calendarDays = Array.from({ length: 14 }, (_, index) => index + 1);
const calendarEventDays = new Set([3, 8, 12]);
function getIconUrl(iconUrl: typeof sonarrIntegration.iconUrl) {
if (typeof iconUrl === "string") return iconUrl;
return iconUrl.light;
}
export function ConceptFlow() {
return (
<figure className={styles.diagram} aria-label="How Homarr connects services to a board">
<div className={styles.lane}>
<Stage number="1" title="Service" detail="API endpoint">
<div className={styles.serviceList}>
{services.map((service) => (
<Link className={styles.service} to={service.href} key={service.name}>
<img src={service.iconUrl} alt="" />
<span>
<strong>{service.name}</strong>
<code>{service.endpoint}</code>
</span>
</Link>
))}
</div>
</Stage>
<FlowConnector />
<Stage number="2" title="Integration" detail="Server-side connection" href="/docs/management/integrations">
<div className={styles.integration}>
<IconPlugConnected size={26} aria-hidden="true" />
<code>URL + credentials</code>
<span>Fetch data and run actions</span>
</div>
</Stage>
<FlowConnector animationDelay={350} />
<Stage number="3" title="Widget" detail="Data and actions" href="/docs/category/widgets">
<div className={styles.widgetList}>
{widgets.map((widget) => {
const Icon = widget.icon;
return (
<Link className={styles.widget} to={widget.href} key={widget.name}>
<Icon size={17} aria-hidden="true" />
<span>{widget.name}</span>
</Link>
);
})}
</div>
</Stage>
<FlowConnector animationDelay={700} />
<Stage number="4" title="Board" detail="Placed in the grid" href="/docs/management/boards">
<div className={styles.boardCalendar} aria-hidden="true">
<div className={styles.calendarHeader}>
<strong>September</strong>
<span>3 events</span>
</div>
<div className={styles.calendarWeekdays}>
{["M", "T", "W", "T", "F", "S", "S"].map((day, index) => (
<span key={`${day}-${index}`}>{day}</span>
))}
</div>
<div className={styles.calendarDays}>
{calendarDays.map((day) => (
<span data-event={calendarEventDays.has(day) || undefined} key={day}>
{day}
</span>
))}
</div>
</div>
</Stage>
</div>
</figure>
);
}
function Stage({
number,
title,
detail,
href,
children,
}: {
number: string;
title: string;
detail: string;
href?: string;
children?: ReactNode;
}) {
const heading = (
<>
<span className={styles.stageNumber}>{number}</span>
<span>
<strong>{title}</strong>
<small>{detail}</small>
</span>
</>
);
return (
<section className={styles.stage}>
{href ? (
<Link className={styles.stageHeading} to={href}>
{heading}
</Link>
) : (
<div className={styles.stageHeading}>{heading}</div>
)}
{children}
</section>
);
}
@@ -0,0 +1,232 @@
.overview {
--start-ink: var(--ifm-font-color-base);
--start-muted: var(--ifm-color-emphasis-700);
--start-panel: var(--ifm-background-surface-color);
--start-line: var(--ifm-color-emphasis-300);
--start-coral: #fa5252;
display: grid;
gap: 1.5rem;
margin: 1.5rem 0 3rem;
}
.intro {
padding-left: 1rem;
border-left: 3px solid var(--ifm-color-primary);
}
.intro h2 {
margin: 0 0 0.4rem;
font-size: 1.5rem;
}
.intro p {
max-width: 48rem;
margin: 0;
color: var(--start-muted);
line-height: 1.55;
}
.editor {
display: grid;
gap: 0.9rem;
padding-top: 0.25rem;
}
.editorHeading {
display: flex;
align-items: end;
justify-content: space-between;
gap: 1rem;
}
.editorHeading h3 {
margin: 0 0 0.25rem;
font-size: 1.1rem;
}
.editorHeading p {
margin: 0;
color: var(--start-muted);
font-size: 0.84rem;
}
.editorHeading > a {
display: inline-flex;
flex: none;
align-items: center;
gap: 0.25rem;
font-size: 0.78rem;
font-weight: 700;
}
.editorSteps {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.65rem;
margin: 0;
padding: 0;
list-style: none;
}
.editorStep {
overflow: hidden;
display: grid;
grid-template-rows: auto 1fr;
min-width: 0;
border: 1px solid var(--start-line);
border-radius: 0.65rem;
background: var(--start-panel);
}
.stepCopy {
display: flex;
align-items: center;
gap: 0.55rem;
padding: 0.65rem;
}
.stepCopy > span {
display: grid;
flex: none;
place-items: center;
width: 1.4rem;
height: 1.4rem;
border: 1px solid var(--start-line);
border-radius: 50%;
color: var(--start-muted);
font-family: var(--font-mono);
font-size: 0.62rem;
font-weight: 700;
}
.stepCopy > div {
display: grid;
min-width: 0;
}
.stepCopy strong {
font-size: 0.78rem;
}
.stepCopy small {
color: var(--start-muted);
font-size: 0.64rem;
line-height: 1.35;
}
.stepMedia {
display: grid;
place-items: center;
min-height: 9rem;
padding: 0.4rem;
border-top: 1px solid var(--start-line);
background: #202020;
}
.stepMedia img {
display: block;
width: 100%;
max-height: 12rem;
object-fit: contain;
}
.stepMedia img[data-fit="cover"] {
height: 100%;
object-fit: cover;
object-position: top;
}
.installPaths {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.8rem;
margin: 1rem 0 1.5rem;
}
.installPath {
position: relative;
display: grid;
gap: 0.45rem;
min-height: 10rem;
padding: 1.1rem;
border: 1px solid var(--ifm-color-emphasis-400);
border-radius: 0.9rem;
color: var(--start-ink);
background: var(--start-panel);
box-shadow: 0 3px 10px rgb(20 25 35 / 8%);
text-decoration: none;
transition:
border-color 160ms ease,
transform 160ms ease,
box-shadow 160ms ease;
}
.installPath:hover {
border-color: rgb(250 82 82 / 55%);
color: var(--start-ink);
text-decoration: none;
transform: translateY(-2px);
box-shadow: 0 12px 28px rgb(20 25 35 / 8%);
}
.installPath > span {
color: #c92f2f;
font-family: var(--font-mono);
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
:global([data-theme="dark"]) .installPath > span {
color: var(--ifm-color-primary-light);
}
.installPath strong {
font-size: 1rem;
}
.installPath small {
color: var(--start-muted);
line-height: 1.45;
}
.installPath > svg {
position: absolute;
right: 0.9rem;
bottom: 0.9rem;
color: var(--start-coral);
}
.recommendedPath {
border-color: var(--ifm-color-primary);
background: var(--start-panel);
}
@media (max-width: 800px) {
.editorHeading {
align-items: start;
}
.editorSteps {
grid-template-columns: minmax(0, 1fr);
}
.stepMedia img {
max-height: 18rem;
}
.installPaths {
grid-template-columns: 1fr;
}
.installPath {
min-height: 8rem;
}
}
@media (prefers-reduced-motion: reduce) {
.installPath {
transition: none;
}
}
@@ -0,0 +1,119 @@
import Link from "@docusaurus/Link";
import { IconArrowRight } from "@tabler/icons-react";
import addItem from "@site/docs/getting-started/img/manage-board-header-choose-item.png";
import moveItem from "@site/docs/getting-started/img/move-item.gif";
import resizeItem from "@site/docs/getting-started/img/resize-item.gif";
import { ConceptFlow } from "./concept-flow";
import styles from "./getting-started-overview.module.css";
const editingSteps = [
{
title: "Add",
description: "Choose an app, widget, or container.",
image: addItem,
alt: "Add item menu in board edit mode",
fit: "cover",
},
{
title: "Move",
description: "Drag the item to another grid position.",
image: moveItem,
alt: "Dragging an app tile on a board",
fit: "contain",
},
{
title: "Resize",
description: "Drag the resize handle to change its grid area.",
image: resizeItem,
alt: "Resizing an app tile on a board",
fit: "contain",
},
];
export function GettingStartedOverview() {
return (
<section className={styles.overview} aria-labelledby="getting-started-overview-title">
<header className={styles.intro}>
<h2 id="getting-started-overview-title">How Homarr works</h2>
<p>Services expose data, integrations connect to them, widgets use the data, and boards arrange the widgets.</p>
</header>
<ConceptFlow />
<section className={styles.editor} aria-labelledby="board-editor-title">
<div className={styles.editorHeading}>
<div>
<h3 id="board-editor-title">Board edit mode</h3>
<p>Add items, move them, and resize them on the grid. Each viewport can have its own layout.</p>
</div>
<Link to="/docs/management/boards">
Board docs <IconArrowRight size={15} aria-hidden="true" />
</Link>
</div>
<ol className={styles.editorSteps}>
{editingSteps.map((step, index) => (
<li className={styles.editorStep} key={step.title}>
<div className={styles.stepCopy}>
<span>{index + 1}</span>
<div>
<strong>{step.title}</strong>
<small>{step.description}</small>
</div>
</div>
<div className={styles.stepMedia}>
<img src={step.image} alt={step.alt} data-fit={step.fit} loading="lazy" />
</div>
</li>
))}
</ol>
</section>
</section>
);
}
const installationPaths = [
{
eyebrow: "Recommended",
title: "Docker Compose",
description: "General-purpose deployment with a persistent appdata volume.",
href: "/docs/getting-started/installation/docker",
recommended: true,
},
{
eyebrow: "Kubernetes",
title: "Helm",
description: "Deploy Homarr into an existing cluster.",
href: "/docs/getting-started/installation/helm",
recommended: false,
},
{
eyebrow: "Platforms",
title: "NAS and hosting guides",
description: "Unraid, TrueNAS, Synology, Portainer, Proxmox, and more.",
href: "/docs/category/installation-1",
recommended: false,
},
];
export function InstallationPaths() {
return (
<div className={styles.installPaths}>
{installationPaths.map((path) => {
let className = styles.installPath;
if (path.recommended) className = `${className} ${styles.recommendedPath}`;
return (
<Link className={className} to={path.href} key={path.href}>
<span>{path.eyebrow}</span>
<strong>{path.title}</strong>
<small>{path.description}</small>
<IconArrowRight size={16} aria-hidden="true" />
</Link>
);
})}
</div>
);
}