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

@@ -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;
}
}