docs: improve workshop and homepage
This commit is contained in:
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} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user