docs: streamline release v2 documentation (#6760)
This commit is contained in:
1 parent
ac62e5bfd6
commit
b3aa6155cc
163 files changed
+2908
-4208
No files matched your search
@@ -1,41 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import Translate from "@docusaurus/Translate";
|
||||
|
||||
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 />
|
||||
<Translate id="workshop.provider.donate">Donate to Homarr on OpenCollective</Translate>
|
||||
</a>
|
||||
);
|
||||
@@ -1,8 +1,8 @@
|
||||
export const AddingIntegration = () => {
|
||||
return (
|
||||
<p>
|
||||
You can find how to add the integration on the <a href="/docs/management/integrations">Integrations Management</a>{" "}
|
||||
documentation page.
|
||||
Create the connection under <strong>Management → Integrations</strong>. See{" "}
|
||||
<a href="/docs/management/integrations">Managing integrations</a>.
|
||||
</p>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,400 @@
|
||||
.diagram {
|
||||
--diagram-ink: var(--ifm-font-color-base);
|
||||
--diagram-muted: var(--ifm-color-emphasis-700);
|
||||
--diagram-panel: var(--ifm-background-surface-color);
|
||||
--diagram-surface: var(--ifm-background-color);
|
||||
--diagram-line: var(--ifm-color-emphasis-300);
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
margin: 1.25rem 0 1.75rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--diagram-line);
|
||||
border-radius: 0.75rem;
|
||||
color: var(--diagram-ink);
|
||||
background: var(--diagram-panel);
|
||||
}
|
||||
|
||||
.routeBlock {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.routeBlock + .routeBlock {
|
||||
padding-top: 0.85rem;
|
||||
border-top: 1px solid var(--diagram-line);
|
||||
}
|
||||
|
||||
.routeHeading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.routeHeading > span {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
.routeHeading strong {
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
.routeHeading small {
|
||||
color: var(--diagram-muted);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.58rem;
|
||||
}
|
||||
|
||||
.threeStepLane,
|
||||
.fourStepLane {
|
||||
display: grid;
|
||||
align-items: stretch;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.threeStepLane {
|
||||
grid-template-columns: minmax(0, 1fr) 3.25rem minmax(0, 1fr) 3.25rem minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.fourStepLane {
|
||||
grid-template-columns: minmax(0, 1fr) 3.1rem minmax(0, 1fr) 3.1rem minmax(0, 1fr) 3.1rem minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.node {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 0.65rem;
|
||||
min-width: 0;
|
||||
min-height: 6.25rem;
|
||||
padding: 0.7rem;
|
||||
border: 1px solid var(--diagram-line);
|
||||
border-radius: 0.55rem;
|
||||
color: var(--diagram-ink);
|
||||
background: var(--diagram-surface);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nodeLink:hover {
|
||||
border-color: var(--ifm-color-primary);
|
||||
color: var(--diagram-ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nodeHeader {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nodeIcon,
|
||||
.largeIcon {
|
||||
display: grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
border: 1px solid var(--diagram-line);
|
||||
color: var(--ifm-color-primary);
|
||||
background: var(--diagram-panel);
|
||||
}
|
||||
|
||||
.nodeIcon {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 0.45rem;
|
||||
}
|
||||
|
||||
.nodeIcon img {
|
||||
width: 1.15rem;
|
||||
height: 1.15rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nodeCopy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nodeCopy strong {
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.nodeCopy small {
|
||||
color: var(--diagram-muted);
|
||||
font-size: 0.59rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.nodeBody {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nodeBody code {
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
padding: 0.25rem 0.35rem;
|
||||
border: 1px solid var(--diagram-line);
|
||||
border-radius: 0.3rem;
|
||||
color: var(--diagram-muted);
|
||||
background: var(--diagram-panel);
|
||||
font-size: 0.53rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pillList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.pillList span,
|
||||
.pillList a {
|
||||
padding: 0.15rem 0.3rem;
|
||||
border: 1px solid var(--diagram-line);
|
||||
border-radius: 999px;
|
||||
color: var(--diagram-muted);
|
||||
background: var(--diagram-panel);
|
||||
font-size: 0.5rem;
|
||||
line-height: 1.25;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pillList a:hover {
|
||||
border-color: var(--ifm-color-primary);
|
||||
color: var(--ifm-color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.localDetails,
|
||||
.definitionParts {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.localDetails span,
|
||||
.definitionParts span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
color: var(--diagram-muted);
|
||||
font-size: 0.53rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.definitionParts svg {
|
||||
flex: none;
|
||||
color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
.diagramNote {
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--diagram-line);
|
||||
color: var(--diagram-muted);
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.factList {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.factList span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
padding: 0.45rem;
|
||||
border: 1px solid var(--diagram-line);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--diagram-muted);
|
||||
background: var(--diagram-surface);
|
||||
font-size: 0.56rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.factList svg {
|
||||
flex: none;
|
||||
color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
.boardSource {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-self: center;
|
||||
gap: 0.55rem;
|
||||
min-width: 14rem;
|
||||
padding: 0.65rem 0.8rem;
|
||||
border: 1px solid var(--diagram-line);
|
||||
border-radius: 0.55rem;
|
||||
background: var(--diagram-surface);
|
||||
}
|
||||
|
||||
.largeIcon {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.boardSource > span:last-child {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.boardSource strong {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.boardSource small {
|
||||
color: var(--diagram-muted);
|
||||
font-size: 0.59rem;
|
||||
}
|
||||
|
||||
.layoutGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.layoutCard {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
min-width: 0;
|
||||
padding: 0.65rem;
|
||||
border: 1px solid var(--diagram-line);
|
||||
border-radius: 0.55rem;
|
||||
margin: 0;
|
||||
background: var(--diagram-surface);
|
||||
}
|
||||
|
||||
.layoutHeading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.layoutHeading > span:first-child {
|
||||
display: grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
width: 1.65rem;
|
||||
height: 1.65rem;
|
||||
border: 1px solid var(--diagram-line);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--ifm-color-primary);
|
||||
background: var(--diagram-panel);
|
||||
}
|
||||
|
||||
.layoutHeading > span:last-child {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.layoutHeading strong {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.layoutHeading code {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: var(--diagram-muted);
|
||||
background: transparent;
|
||||
font-size: 0.54rem;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.layoutCard > small {
|
||||
color: var(--diagram-muted);
|
||||
font-size: 0.55rem;
|
||||
}
|
||||
|
||||
.miniBoard {
|
||||
display: grid;
|
||||
grid-auto-rows: 1.2rem;
|
||||
gap: 0.22rem;
|
||||
min-height: 4.25rem;
|
||||
padding: 0.35rem;
|
||||
border: 1px dashed var(--diagram-line);
|
||||
border-radius: 0.35rem;
|
||||
background: var(--diagram-panel);
|
||||
}
|
||||
|
||||
.miniTile,
|
||||
.miniRail {
|
||||
display: block;
|
||||
border: 1px solid var(--diagram-line);
|
||||
border-radius: 0.22rem;
|
||||
background: var(--diagram-surface);
|
||||
}
|
||||
|
||||
.miniTile:nth-child(2) {
|
||||
border-color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
.mobileBoard {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.mobileBoard .miniRail {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobileBoard .miniTile:nth-child(3),
|
||||
.mobileBoard .miniTile:nth-child(5) {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.baseBoard {
|
||||
grid-template-columns: 0.7fr repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.baseBoard .miniRail {
|
||||
grid-row: 1 / span 3;
|
||||
}
|
||||
|
||||
.baseBoard .miniTile:nth-child(3),
|
||||
.baseBoard .miniTile:nth-child(4) {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.customBoard {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr)) 0.7fr;
|
||||
}
|
||||
|
||||
.customBoard .miniRail {
|
||||
grid-column: 6;
|
||||
grid-row: 1 / span 3;
|
||||
}
|
||||
|
||||
.customBoard .miniTile:nth-child(3) {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
.customBoard .miniTile:nth-child(5) {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.threeStepLane,
|
||||
.fourStepLane {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.node {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.factList,
|
||||
.layoutGrid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.miniBoard {
|
||||
min-height: 5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import Link from "@docusaurus/Link";
|
||||
import {
|
||||
IconApi,
|
||||
IconBox,
|
||||
IconBraces,
|
||||
IconBrowser,
|
||||
IconBuildingStore,
|
||||
IconCode,
|
||||
IconDeviceDesktop,
|
||||
IconDeviceMobile,
|
||||
IconKey,
|
||||
IconLayoutBoard,
|
||||
IconMessages,
|
||||
IconPlugConnected,
|
||||
IconRobot,
|
||||
IconServer,
|
||||
IconShieldCheck,
|
||||
IconTool,
|
||||
IconUser,
|
||||
IconWorld,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { sonarrIntegration } from "@site/docs/integrations/sonarr";
|
||||
import { FlowConnector } from "@site/src/components/ui/flow-connector";
|
||||
|
||||
import styles from "./technical-diagrams.module.css";
|
||||
|
||||
const sonarrIconUrl = getIconUrl(sonarrIntegration.iconUrl);
|
||||
|
||||
export function AppFlowDiagram() {
|
||||
return (
|
||||
<figure className={styles.diagram} aria-label="How an app record is used on a board">
|
||||
<div className={styles.threeStepLane}>
|
||||
<DiagramNode icon={<IconBox size={19} />} title="App record" detail="Saved once in Homarr">
|
||||
<div className={styles.pillList}>
|
||||
<span>Name</span>
|
||||
<span>Icon</span>
|
||||
<span>URL</span>
|
||||
<span>Ping URL · optional</span>
|
||||
</div>
|
||||
</DiagramNode>
|
||||
<FlowConnector label="place" />
|
||||
<DiagramNode icon={<IconLayoutBoard size={19} />} title="Board" detail="App tile or Bookmarks widget" />
|
||||
<FlowConnector label="click" animationDelay={350} />
|
||||
<DiagramNode icon={<IconBrowser size={19} />} title="Browser" detail="Opens the app URL directly" />
|
||||
</div>
|
||||
|
||||
<DiagramNote>
|
||||
If a Ping URL is set, the Homarr server checks that address for status. Clicking the tile still opens the app
|
||||
URL.
|
||||
</DiagramNote>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
export function IntegrationRoutesDiagram() {
|
||||
return (
|
||||
<figure className={styles.diagram} aria-label="Browser and server routes from Homarr to Sonarr">
|
||||
<div className={styles.routeBlock}>
|
||||
<RouteHeading icon={<IconBrowser size={16} />} title="Open the service" detail="Browser route" />
|
||||
<div className={styles.threeStepLane}>
|
||||
<DiagramNode icon={<IconLayoutBoard size={19} />} title="App tile" detail="Click from a board" />
|
||||
<FlowConnector label="opens" />
|
||||
<DiagramNode icon={<IconWorld size={19} />} title="App URL" detail="Browser-facing address">
|
||||
<code>https://sonarr.example.com</code>
|
||||
</DiagramNode>
|
||||
<FlowConnector animationDelay={350} />
|
||||
<DiagramNode
|
||||
icon={<img src={sonarrIconUrl} alt="" />}
|
||||
title="Sonarr UI"
|
||||
detail="The browser connects directly"
|
||||
href="/docs/integrations/sonarr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.routeBlock}>
|
||||
<RouteHeading icon={<IconServer size={16} />} title="Use service data" detail="Server route" />
|
||||
<div className={styles.threeStepLane}>
|
||||
<DiagramNode icon={<IconApi size={19} />} title="Widget" detail="Requests data or an action" />
|
||||
<FlowConnector label="Homarr" />
|
||||
<DiagramNode
|
||||
icon={<IconPlugConnected size={19} />}
|
||||
title="Integration"
|
||||
detail="URL and encrypted credentials"
|
||||
href="/docs/management/integrations"
|
||||
>
|
||||
<code>http://sonarr:8989</code>
|
||||
</DiagramNode>
|
||||
<FlowConnector label="API" animationDelay={350} />
|
||||
<DiagramNode
|
||||
icon={<img src={sonarrIconUrl} alt="" />}
|
||||
title="Sonarr API"
|
||||
detail="The Homarr server connects"
|
||||
href="/docs/integrations/sonarr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DiagramNote>
|
||||
Linking the app to the integration gives integration-backed widgets the browser-facing destination.
|
||||
</DiagramNote>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
export function BoardLayoutsDiagram() {
|
||||
return (
|
||||
<figure className={styles.diagram} aria-label="How one board selects independent responsive layouts">
|
||||
<div className={styles.boardSource}>
|
||||
<span className={styles.largeIcon}>
|
||||
<IconLayoutBoard size={23} aria-hidden="true" />
|
||||
</span>
|
||||
<span>
|
||||
<strong>One board</strong>
|
||||
<small>The same apps, widgets, and Containers</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<FlowConnector label="highest matching breakpoint" orientation="vertical" />
|
||||
|
||||
<div className={styles.layoutGrid}>
|
||||
<LayoutCard
|
||||
icon={<IconDeviceMobile size={18} />}
|
||||
title="Mobile"
|
||||
range="starts at 0px"
|
||||
detail="Own positions · no rails"
|
||||
variant="mobile"
|
||||
/>
|
||||
<LayoutCard
|
||||
icon={<IconDeviceDesktop size={18} />}
|
||||
title="Base"
|
||||
range="starts at 768px"
|
||||
detail="Own positions · rails available"
|
||||
variant="base"
|
||||
/>
|
||||
<LayoutCard
|
||||
icon={<IconLayoutBoard size={18} />}
|
||||
title="Custom"
|
||||
range="unique breakpoint"
|
||||
detail="Own positions · rails available"
|
||||
variant="custom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DiagramNote>Moving or resizing an item in one layout does not change its coordinates in the others.</DiagramNote>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
export function AssistantFlowDiagram() {
|
||||
return (
|
||||
<figure className={styles.diagram} aria-label="How Homarr Assistant uses a model provider and Homarr tools">
|
||||
<div className={styles.routeBlock}>
|
||||
<RouteHeading icon={<IconMessages size={16} />} title="Conversation" detail="Every request" />
|
||||
<div className={styles.threeStepLane}>
|
||||
<DiagramNode icon={<IconUser size={19} />} title="You" detail="Prompt, attachment, or @ reference" />
|
||||
<FlowConnector />
|
||||
<DiagramNode
|
||||
icon={<IconRobot size={19} />}
|
||||
title="Assistant server"
|
||||
detail="Loads live context and tool definitions"
|
||||
/>
|
||||
<FlowConnector animationDelay={350} />
|
||||
<DiagramNode
|
||||
icon={<IconWorld size={19} />}
|
||||
title="Provider + model"
|
||||
detail="Instance-wide provider; model may vary by conversation"
|
||||
>
|
||||
<div className={styles.pillList}>
|
||||
<Link to="/docs/workshop/homarr-provider">Homarr</Link>
|
||||
<span>Hosted</span>
|
||||
<span>Local</span>
|
||||
<span>Custom</span>
|
||||
</div>
|
||||
</DiagramNode>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.routeBlock}>
|
||||
<RouteHeading icon={<IconTool size={16} />} title="When the model calls a tool" detail="Only when needed" />
|
||||
<div className={styles.threeStepLane}>
|
||||
<DiagramNode icon={<IconTool size={19} />} title="Tool request" detail="Read data or make a change" />
|
||||
<FlowConnector />
|
||||
<DiagramNode
|
||||
icon={<IconShieldCheck size={19} />}
|
||||
title="Permission boundary"
|
||||
detail="Current user; approval for changes by default"
|
||||
/>
|
||||
<FlowConnector animationDelay={350} />
|
||||
<DiagramNode
|
||||
icon={<IconLayoutBoard size={19} />}
|
||||
title="Homarr resources"
|
||||
detail="Boards · apps · integrations · widgets · services"
|
||||
href="/docs/management/mcp"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DiagramNote>
|
||||
Tool results return to the model; the final answer streams back to the same conversation. The Homarr provider
|
||||
routes model requests through Workshop. Other providers connect directly from the Homarr server.
|
||||
</DiagramNote>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomarrProviderDiagram() {
|
||||
return (
|
||||
<figure className={styles.diagram} aria-label="How the Homarr Assistant provider routes model requests">
|
||||
<div className={styles.fourStepLane}>
|
||||
<DiagramNode icon={<IconMessages size={19} />} title="Assistant" detail="Per-user conversation" />
|
||||
<FlowConnector label="request" />
|
||||
<DiagramNode icon={<IconServer size={19} />} title="Homarr server" detail="Context, permissions, and tools" />
|
||||
<FlowConnector label="token" animationDelay={350} />
|
||||
<DiagramNode
|
||||
icon={<IconBuildingStore size={19} />}
|
||||
title="Workshop provider"
|
||||
detail="Quota and homarr/model routing"
|
||||
href="/docs/workshop"
|
||||
/>
|
||||
<FlowConnector label="model" animationDelay={700} />
|
||||
<DiagramNode icon={<IconWorld size={19} />} title="Upstream model" detail="OpenRouter-compatible endpoint" />
|
||||
</div>
|
||||
|
||||
<div className={styles.factList}>
|
||||
<span>
|
||||
<IconKey size={15} aria-hidden="true" /> No administrator API key
|
||||
</span>
|
||||
<span>
|
||||
<IconApi size={15} aria-hidden="true" /> 50 request units per user and UTC day
|
||||
</span>
|
||||
<span>
|
||||
<IconShieldCheck size={15} aria-hidden="true" /> Workshop does not store prompts or responses
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<DiagramNote>
|
||||
Tool calls still execute inside your Homarr instance with the signed-in user's permissions.
|
||||
</DiagramNote>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkshopFlowDiagram() {
|
||||
return (
|
||||
<figure className={styles.diagram} aria-label="How content moves through the Community Workshop">
|
||||
<div className={styles.fourStepLane}>
|
||||
<DiagramNode icon={<IconCode size={19} />} title="Author" detail="Build shareable content">
|
||||
<div className={styles.pillList}>
|
||||
<span>Custom Widget JSON</span>
|
||||
<span>Custom CSS</span>
|
||||
</div>
|
||||
</DiagramNode>
|
||||
<FlowConnector label="publish" />
|
||||
<DiagramNode
|
||||
icon={<IconBuildingStore size={19} />}
|
||||
title="Workshop"
|
||||
detail="Source, screenshots, and revision"
|
||||
href="pathname:///workshop"
|
||||
/>
|
||||
<FlowConnector label="inspect" animationDelay={350} />
|
||||
<DiagramNode
|
||||
icon={<IconShieldCheck size={19} />}
|
||||
title="Review + import"
|
||||
detail="Validate source and requested access"
|
||||
/>
|
||||
<FlowConnector label="local" animationDelay={700} />
|
||||
<DiagramNode icon={<IconLayoutBoard size={19} />} title="Your Homarr" detail="Configure and place on a board">
|
||||
<div className={styles.localDetails}>
|
||||
<span>Widget → local URLs + secrets</span>
|
||||
<span>CSS → board custom CSS</span>
|
||||
</div>
|
||||
</DiagramNode>
|
||||
</div>
|
||||
|
||||
<DiagramNote>
|
||||
Workshop stores the shareable definition. Instance-specific URLs and credentials stay local.
|
||||
</DiagramNote>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
export function CustomWidgetFlowDiagram() {
|
||||
return (
|
||||
<figure className={styles.diagram} aria-label="How a Custom Widget definition becomes a board tile">
|
||||
<div className={styles.threeStepLane}>
|
||||
<DiagramNode
|
||||
icon={<IconBraces size={19} />}
|
||||
title="Widget definition"
|
||||
detail="homarr-custom-widget-v2"
|
||||
href="/docs/management/custom-widgets/authoring"
|
||||
>
|
||||
<div className={styles.definitionParts}>
|
||||
<span>
|
||||
<IconApi size={14} /> API sources
|
||||
</span>
|
||||
<span>
|
||||
<IconTool size={14} /> Queries + actions
|
||||
</span>
|
||||
<span>
|
||||
<IconCode size={14} /> Safe JSX
|
||||
</span>
|
||||
</div>
|
||||
</DiagramNode>
|
||||
<FlowConnector label="validate" />
|
||||
<DiagramNode
|
||||
icon={<IconServer size={19} />}
|
||||
title="Homarr runtime"
|
||||
detail="Requests on the server; JSX in a restricted renderer"
|
||||
href="/docs/management/custom-widgets/requests-and-security"
|
||||
/>
|
||||
<FlowConnector label="render" animationDelay={350} />
|
||||
<DiagramNode icon={<IconLayoutBoard size={19} />} title="Board tile" detail="Live data and declared actions" />
|
||||
</div>
|
||||
|
||||
<DiagramNote>
|
||||
Source credentials are stored separately and excluded from exports, prompts, and Workshop submissions.
|
||||
</DiagramNote>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
function RouteHeading({ icon, title, detail }: { icon: ReactNode; title: string; detail: string }) {
|
||||
return (
|
||||
<div className={styles.routeHeading}>
|
||||
<span>{icon}</span>
|
||||
<strong>{title}</strong>
|
||||
<small>{detail}</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiagramNode({
|
||||
icon,
|
||||
title,
|
||||
detail,
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
detail: string;
|
||||
href?: string;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const content = (
|
||||
<>
|
||||
<div className={styles.nodeHeader}>
|
||||
<span className={styles.nodeIcon}>{icon}</span>
|
||||
<span className={styles.nodeCopy}>
|
||||
<strong>{title}</strong>
|
||||
<small>{detail}</small>
|
||||
</span>
|
||||
</div>
|
||||
{children && <div className={styles.nodeBody}>{children}</div>}
|
||||
</>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link className={`${styles.node} ${styles.nodeLink}`} to={href}>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={styles.node}>{content}</div>;
|
||||
}
|
||||
|
||||
function LayoutCard({
|
||||
icon,
|
||||
title,
|
||||
range,
|
||||
detail,
|
||||
variant,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
range: string;
|
||||
detail: string;
|
||||
variant: "mobile" | "base" | "custom";
|
||||
}) {
|
||||
return (
|
||||
<section className={styles.layoutCard}>
|
||||
<div className={styles.layoutHeading}>
|
||||
<span>{icon}</span>
|
||||
<span>
|
||||
<strong>{title}</strong>
|
||||
<code>{range}</code>
|
||||
</span>
|
||||
</div>
|
||||
<MiniBoard variant={variant} />
|
||||
<small>{detail}</small>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MiniBoard({ variant }: { variant: "mobile" | "base" | "custom" }) {
|
||||
let className = styles.miniBoard;
|
||||
className = `${className} ${styles[`${variant}Board`]}`;
|
||||
|
||||
return (
|
||||
<div className={className} aria-hidden="true">
|
||||
<span className={styles.miniRail} />
|
||||
<span className={styles.miniTile} />
|
||||
<span className={styles.miniTile} />
|
||||
<span className={styles.miniTile} />
|
||||
<span className={styles.miniTile} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiagramNote({ children }: { children: ReactNode }) {
|
||||
return <figcaption className={styles.diagramNote}>{children}</figcaption>;
|
||||
}
|
||||
|
||||
function getIconUrl(iconUrl: typeof sonarrIntegration.iconUrl) {
|
||||
if (typeof iconUrl === "string") return iconUrl;
|
||||
return iconUrl.light;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
.connector {
|
||||
--flow-accent: var(--ifm-color-primary);
|
||||
--flow-coral: #f17865;
|
||||
--flow-head: var(--diagram-ink, var(--ifm-font-color-base));
|
||||
--flow-line: var(--diagram-line, var(--ifm-color-emphasis-300));
|
||||
--flow-rail: color-mix(in srgb, var(--flow-line) 68%, var(--flow-head));
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
gap: 0.22rem;
|
||||
min-width: 0;
|
||||
color: var(--flow-head);
|
||||
}
|
||||
|
||||
.label {
|
||||
z-index: 1;
|
||||
max-width: 100%;
|
||||
padding: 0.17rem 0.36rem;
|
||||
border: 1px solid color-mix(in srgb, var(--flow-accent) 32%, var(--flow-line));
|
||||
border-radius: 999px;
|
||||
color: var(--diagram-muted, var(--ifm-color-emphasis-700));
|
||||
background: var(--diagram-panel, var(--ifm-background-surface-color));
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.5rem;
|
||||
font-weight: 650;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.track {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
width: calc(100% + 0.5rem);
|
||||
height: 1.1rem;
|
||||
margin-inline: -0.25rem;
|
||||
}
|
||||
|
||||
.rail {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0.18rem;
|
||||
left: 0.18rem;
|
||||
height: 2px;
|
||||
background: var(--flow-rail);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.beam {
|
||||
position: absolute;
|
||||
top: calc(50% - 1.5px);
|
||||
left: 0.23rem;
|
||||
width: 48%;
|
||||
height: 3px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, transparent, var(--flow-coral) 34%, var(--flow-accent) 68%, transparent);
|
||||
filter: drop-shadow(0 0 0.28rem color-mix(in srgb, var(--flow-accent) 48%, transparent));
|
||||
animation: flow-beam-horizontal 2.4s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||
animation-delay: var(--flow-delay);
|
||||
}
|
||||
|
||||
.vertical {
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.vertical .track {
|
||||
width: 1.1rem;
|
||||
height: 2.6rem;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
.vertical .rail {
|
||||
top: 0.16rem;
|
||||
right: auto;
|
||||
bottom: 0.16rem;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
height: auto;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.vertical .beam {
|
||||
top: 0.23rem;
|
||||
left: calc(50% - 1.5px);
|
||||
width: 3px;
|
||||
height: 48%;
|
||||
background: linear-gradient(180deg, transparent, var(--flow-coral) 46%, var(--flow-accent));
|
||||
animation-name: flow-beam-vertical;
|
||||
}
|
||||
|
||||
.visuallyHidden {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: -1px;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@keyframes flow-beam-horizontal {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-35%);
|
||||
}
|
||||
|
||||
18%,
|
||||
72% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(155%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flow-beam-vertical {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-35%);
|
||||
}
|
||||
|
||||
18%,
|
||||
72% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(155%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.connector:not(.vertical) {
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.connector:not(.vertical) .track {
|
||||
width: 1.1rem;
|
||||
height: 2.6rem;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
.connector:not(.vertical) .rail {
|
||||
top: 0.16rem;
|
||||
right: auto;
|
||||
bottom: 0.16rem;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
height: auto;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.connector:not(.vertical) .beam {
|
||||
top: 0.23rem;
|
||||
left: calc(50% - 1.5px);
|
||||
width: 3px;
|
||||
height: 48%;
|
||||
background: linear-gradient(180deg, transparent, var(--flow-coral) 46%, var(--flow-accent));
|
||||
animation-name: flow-beam-vertical;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.beam {
|
||||
opacity: 1;
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import styles from "./flow-connector.module.css";
|
||||
|
||||
type FlowConnectorProps = {
|
||||
label?: string;
|
||||
animationDelay?: number;
|
||||
orientation?: "responsive" | "vertical";
|
||||
};
|
||||
|
||||
export function FlowConnector({ label, animationDelay = 0, orientation = "responsive" }: FlowConnectorProps) {
|
||||
let className = styles.connector;
|
||||
if (orientation === "vertical") className = `${className} ${styles.vertical}`;
|
||||
const style = { "--flow-delay": `${animationDelay}ms` } as CSSProperties;
|
||||
|
||||
let accessibleLabel = <span className={styles.visuallyHidden}>then</span>;
|
||||
if (label) accessibleLabel = <small className={styles.label}>{label}</small>;
|
||||
|
||||
return (
|
||||
<span className={className} style={style}>
|
||||
{accessibleLabel}
|
||||
<span className={styles.track} aria-hidden="true">
|
||||
<span className={styles.rail} />
|
||||
<span className={styles.beam} />
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
export const AddingWidget = () => {
|
||||
return (
|
||||
<p>
|
||||
You can find how to add the widget on the{" "}
|
||||
<a href="../../getting-started/after-the-installation#embed-integration-data-using-widgets">Widgets</a>{" "}
|
||||
documentation page.
|
||||
Add the widget from the board's <strong>Add board content</strong> menu in edit mode.
|
||||
</p>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user