feat: configurable icon_size (small/medium/large)

This commit is contained in:
maxsoch 2026-07-24 14:46:51 +02:00
parent 40ae190f32
commit d36f16cea1
7 changed files with 124 additions and 58 deletions

View file

@ -1,15 +1,14 @@
site: site:
name: "Soch Family" name: "Soch Family"
title: " " title: " "
#title: "Les applications de la famille"
logoUrl: "https://immich.socheleau.fr/api/assets/e010423f-ca95-45a4-a9ee-9bb1a5a48688/thumbnail?slug=us_6&size=preview&c=IhgKJYiHg4ZfhXh6h5aJp5eAdQlJ&edited=true" logoUrl: "https://immich.socheleau.fr/api/assets/e010423f-ca95-45a4-a9ee-9bb1a5a48688/thumbnail?slug=us_6&size=preview&c=IhgKJYiHg4ZfhXh6h5aJp5eAdQlJ&edited=true"
#description: "Accès rapide à nos services"
showFooter: false showFooter: false
theme: theme:
mode: "dark" mode: "dark"
accent: "#8b5cf6" accent: "#8b5cf6"
background: "gradient" background: "gradient"
icon_size: "large"
authentication: authentication:
logout_url: "https://auth.socheleau.fr/logout" logout_url: "https://auth.socheleau.fr/logout"
@ -19,66 +18,114 @@ authentication:
header_group: "Remote-Groups" header_group: "Remote-Groups"
sections: sections:
- id: "global" - id: "soch-services"
title: "Global" title: "Soch's services"
#description: "Services accessibles à toute la famille"
cardLayout: "horizontal" cardLayout: "horizontal"
visible_by_group: "arr"
links: links:
- id: "nextcloud" - id: "authelia"
name: "Nextcloud" name: "Authelia"
description: "Fichiers et documents" description: "Authentification unique"
url: "https://cloud.example.com" url: "https://auth.socheleau.fr"
icon: "Cloud" icon: "Shield"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg" iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg"
color: "#2daae1" color: "#1a7f37"
openInNewTab: false openInNewTab: false
visible_by_user: "jean"
- id: "jellyfin" - id: "plex"
name: "Jellyfin" name: "Plex"
description: "Films et séries" description: "Films et séries"
url: "https://jellyfin.example.com" url: "https://plex.socheleau.fr"
icon: "Film" icon: "Film"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jellyfin.svg" iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plex.svg"
color: "#aa5cc3" color: "#e5a00d"
openInNewTab: false openInNewTab: false
- id: "navidrome" - id: "bitwarden"
name: "Navidrome" name: "Bitwarden"
description: "Musique" description: "Mots de passe"
url: "https://music.example.com" url: "https://bitwarden.socheleau.fr"
icon: "Music" icon: "Shield"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/navidrome.svg" iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/bitwarden.svg"
color: "#22c55e" color: "#175ddc"
openInNewTab: false openInNewTab: false
- id: "minecraft" - id: "immich"
name: "Minecraft" name: "Immich"
description: "Serveur de jeu" description: "Photos et vidéos"
url: "https://minecraft.example.com" url: "https://immich.socheleau.fr"
icon: "Gamepad2" icon: "Image"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/minecraft.svg" iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg"
color: "#f97316" color: "#4250af"
openInNewTab: false openInNewTab: false
- id: "custom" - id: "freshrss"
title: "Liens personnalisés" name: "FreshRSS"
description: "Flux RSS"
url: "https://rss.socheleau.fr"
icon: "Rss"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/freshrss.svg"
color: "#0979b0"
openInNewTab: false
- id: "tandoor"
name: "Tandoor"
description: "Recettes de cuisine"
url: "https://tandoor.socheleau.fr"
icon: "ChefHat"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/tandoor-recipes.svg"
color: "#dc7b2a"
openInNewTab: false
- id: "readeck"
name: "Readeck"
description: "Lecture différée"
url: "https://readeck.socheleau.fr"
icon: "BookOpen"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/readeck.svg"
color: "#14b8a6"
openInNewTab: false
- id: "homes"
title: "Homes"
cardLayout: "horizontal" cardLayout: "horizontal"
links: links:
- id: "homeassistant" - id: "arasu"
name: "Home Assistant" name: "Arasu"
description: "Domotique" description: "Domotique"
url: "https://ha.example.com" url: "https://arasu.socheleau.fr"
icon: "House" icon: "House"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/home-assistant.svg" iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/home-assistant.svg"
color: "#22c55e" color: "#22c55e"
openInNewTab: false openInNewTab: false
- id: "router" - id: "arr"
name: "Routeur" title: "*arr"
description: "Administration réseau" cardLayout: "horizontal"
url: "http://192.168.1.1" visible_by_group: "arr"
icon: "Router" links:
color: "#f59e0b" - id: "radarr"
name: "Radarr"
description: "Gestion de films"
url: "https://radarr.arr.socheleau.fr"
icon: "Clapperboard"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/radarr.svg"
color: "#e73a2e"
openInNewTab: false
- id: "sonarr"
name: "Sonarr"
description: "Gestion de séries"
url: "https://sonarr.arr.socheleau.fr"
icon: "Tv"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr.svg"
color: "#35c5f4"
openInNewTab: false
- id: "prowlarr"
name: "Prowlarr"
description: "Indexeur de torrents"
url: "https://prowlarr.arr.socheleau.fr"
icon: "Search"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/prowlarr.svg"
color: "#e65c21"
openInNewTab: false openInNewTab: false

View file

@ -1,6 +1,6 @@
services: services:
sochboard: sochboard:
build: . image: registry2.socheleau.fr/maxence/sochboard:latest
container_name: sochboard container_name: sochboard
ports: ports:
- "3000:3000" - "3000:3000"

View file

@ -22,7 +22,7 @@ export function Dashboard({ config, isAuthenticated = false, username = null, cu
/> />
<main className="flex flex-col gap-12"> <main className="flex flex-col gap-12">
{config.sections.map((section, index) => ( {config.sections.map((section, index) => (
<DashboardSection key={section.id} section={section} sectionIndex={index} currentUser={currentUser} userGroups={userGroups} /> <DashboardSection key={section.id} section={section} sectionIndex={index} currentUser={currentUser} userGroups={userGroups} iconSize={config.theme.icon_size} />
))} ))}
</main> </main>
{config.site.showFooter !== false && ( {config.site.showFooter !== false && (
@ -32,4 +32,4 @@ export function Dashboard({ config, isAuthenticated = false, username = null, cu
)} )}
</div> </div>
) )
} }

View file

@ -1,15 +1,16 @@
import { ServiceCard } from "./ServiceCard" import { ServiceCard } from "./ServiceCard"
import { isVisible } from "@/lib/visibility" import { isVisible } from "@/lib/visibility"
import type { SectionConfig } from "@/types/config" import type { SectionConfig, IconSize } from "@/types/config"
interface DashboardSectionProps { interface DashboardSectionProps {
section: SectionConfig section: SectionConfig
sectionIndex?: number sectionIndex?: number
currentUser?: string | null currentUser?: string | null
userGroups?: string[] userGroups?: string[]
iconSize?: IconSize
} }
export function DashboardSection({ section, sectionIndex = 0, currentUser = null, userGroups = [] }: DashboardSectionProps) { export function DashboardSection({ section, sectionIndex = 0, currentUser = null, userGroups = [], iconSize = "medium" }: DashboardSectionProps) {
if (!isVisible(section, currentUser, userGroups)) return null if (!isVisible(section, currentUser, userGroups)) return null
const visibleLinks = section.links.filter((link) => isVisible(link, currentUser, userGroups)) const visibleLinks = section.links.filter((link) => isVisible(link, currentUser, userGroups))
@ -32,13 +33,14 @@ export function DashboardSection({ section, sectionIndex = 0, currentUser = null
<p className="mt-1 text-sm text-[var(--text-secondary)]">{section.description}</p> <p className="mt-1 text-sm text-[var(--text-secondary)]">{section.description}</p>
)} )}
</div> </div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 sm:gap-4"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 sm:gap-4">
{visibleLinks.map((link, index) => ( {visibleLinks.map((link, index) => (
<ServiceCard <ServiceCard
key={link.id} key={link.id}
link={link} link={link}
index={index} index={index}
layout={section.cardLayout} layout={section.cardLayout}
iconSize={iconSize}
/> />
))} ))}
</div> </div>

View file

@ -1,18 +1,26 @@
import { IconRenderer } from "./IconRenderer" import { IconRenderer } from "./IconRenderer"
import type { LinkConfig } from "@/types/config" import type { LinkConfig, IconSize } from "@/types/config"
interface ServiceCardProps { interface ServiceCardProps {
link: LinkConfig link: LinkConfig
index?: number index?: number
layout?: "horizontal" | "vertical" layout?: "horizontal" | "vertical"
iconSize?: IconSize
} }
export function ServiceCard({ link, index = 0, layout = "horizontal" }: ServiceCardProps) { const sizeMap: Record<IconSize, { hCont: number; hImg: string; hIcon: number; vCont: number; vImg: string; vIcon: number }> = {
small: { hCont: 10, hImg: "size-5", hIcon: 18, vCont: 12, vImg: "size-6", vIcon: 22 },
medium: { hCont: 12, hImg: "size-7", hIcon: 24, vCont: 14, vImg: "size-8", vIcon: 30 },
large: { hCont: 12, hImg: "size-8", hIcon: 28, vCont: 14, vImg: "size-9", vIcon: 34 },
}
export function ServiceCard({ link, index = 0, layout = "horizontal", iconSize = "medium" }: ServiceCardProps) {
const linkProps = link.openInNewTab const linkProps = link.openInNewTab
? { target: "_blank", rel: "noopener noreferrer" } ? { target: "_blank", rel: "noopener noreferrer" }
: {} : {}
const isHorizontal = layout === "horizontal" const isHorizontal = layout === "horizontal"
const s = sizeMap[iconSize]
return ( return (
<a <a
@ -24,16 +32,18 @@ export function ServiceCard({ link, index = 0, layout = "horizontal" }: ServiceC
{isHorizontal ? ( {isHorizontal ? (
<> <>
<div <div
className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg" className="flex shrink-0 items-center justify-center overflow-hidden rounded-lg"
style={{ style={{
width: s.hCont * 4,
height: s.hCont * 4,
backgroundColor: `${link.color || "#8b5cf6"}20`, backgroundColor: `${link.color || "#8b5cf6"}20`,
color: link.color || "#8b5cf6", color: link.color || "#8b5cf6",
}} }}
> >
{link.iconUrl ? ( {link.iconUrl ? (
<img src={link.iconUrl} alt="" className="size-5 object-contain" /> <img src={link.iconUrl} alt="" className={`${s.hImg} object-contain`} />
) : ( ) : (
<IconRenderer name={link.icon} size={18} /> <IconRenderer name={link.icon} size={s.hIcon} />
)} )}
</div> </div>
<div className="flex min-w-0 flex-1 flex-col"> <div className="flex min-w-0 flex-1 flex-col">
@ -73,16 +83,18 @@ export function ServiceCard({ link, index = 0, layout = "horizontal" }: ServiceC
<> <>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div <div
className="flex size-12 items-center justify-center overflow-hidden rounded-xl" className="flex items-center justify-center overflow-hidden rounded-xl"
style={{ style={{
width: s.vCont * 4,
height: s.vCont * 4,
backgroundColor: `${link.color || "#8b5cf6"}20`, backgroundColor: `${link.color || "#8b5cf6"}20`,
color: link.color || "#8b5cf6", color: link.color || "#8b5cf6",
}} }}
> >
{link.iconUrl ? ( {link.iconUrl ? (
<img src={link.iconUrl} alt="" className="size-6 object-contain" /> <img src={link.iconUrl} alt="" className={`${s.vImg} object-contain`} />
) : ( ) : (
<IconRenderer name={link.icon} size={22} /> <IconRenderer name={link.icon} size={s.vIcon} />
)} )}
</div> </div>
{link.openInNewTab && ( {link.openInNewTab && (

View file

@ -32,6 +32,7 @@ export const themeSchema = z.object({
.regex(hexColorRegex, "Invalid accent color") .regex(hexColorRegex, "Invalid accent color")
.default("#8b5cf6"), .default("#8b5cf6"),
background: z.enum(["gradient", "solid"]).default("gradient"), background: z.enum(["gradient", "solid"]).default("gradient"),
icon_size: z.enum(["small", "medium", "large"]).optional().default("medium"),
}) })
export const siteSchema = z.object({ export const siteSchema = z.object({
@ -57,6 +58,7 @@ export const dashboardConfigSchema = z.object({
mode: "dark", mode: "dark",
accent: "#8b5cf6", accent: "#8b5cf6",
background: "gradient", background: "gradient",
icon_size: "medium",
}), }),
authentication: authenticationSchema.optional(), authentication: authenticationSchema.optional(),
sections: z.array(sectionSchema).default([]), sections: z.array(sectionSchema).default([]),

View file

@ -7,10 +7,13 @@ export interface SiteConfig {
showFooter?: boolean showFooter?: boolean
} }
export type IconSize = "small" | "medium" | "large"
export interface ThemeConfig { export interface ThemeConfig {
mode: "dark" | "light" mode: "dark" | "light"
accent: string accent: string
background: "gradient" | "solid" background: "gradient" | "solid"
icon_size?: IconSize
} }
export interface LinkConfig { export interface LinkConfig {