ci: update docker build workflow and refine frontend theme

Refactor the CI/CD pipeline to use Docker Buildx for more efficient builds and implement automated image tagging and pushing to GHCR.

On the frontend, update the theme system to use a neutral zinc-based dark mode instead of the previous warm dark theme. This includes:
- Updating CSS variables in `globals.css` for a more consistent neutral palette.
- Replacing `ring` color usage with `muted-foreground` in various UI components to align with the new design language.
- Adjusting component backgrounds (e.g., `Header`, `Input`, `WidgetCard`) to use `bg-card` for better visual layering.
- Simplifying component styles and removing unnecessary gradients.
This commit is contained in:
Tomas Dvorak
2026-05-05 09:36:35 +02:00
parent 9e7acc868d
commit 3d21aef323
8 changed files with 113 additions and 50 deletions
+67 -4
View File
@@ -3,22 +3,85 @@ name: Docker Build
on: on:
push: push:
branches: [main] branches: [main]
tags: ["v*"]
pull_request: pull_request:
branches: [main] branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ${{ github.repository }}
jobs: jobs:
build-backend: build-backend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Build backend image - name: Set up Docker Buildx
run: docker build -f backend/Dockerfile . uses: docker/setup-buildx-action@v3
- name: Log in to registry (push only)
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/backend
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha
- name: Build and push backend image
uses: docker/build-push-action@v6
with:
context: .
file: backend/Dockerfile
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-frontend: build-frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Build frontend image - name: Set up Docker Buildx
run: docker build -f frontend/Dockerfile ./frontend uses: docker/setup-buildx-action@v3
- name: Log in to registry (push only)
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/frontend
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha
- name: Build and push frontend image
uses: docker/build-push-action@v6
with:
context: ./frontend
file: frontend/Dockerfile
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+28 -28
View File
@@ -31,28 +31,28 @@
--font-geist-mono: "Geist Mono", "ui-monospace", "SFMono-Regular", "Roboto Mono", monospace; --font-geist-mono: "Geist Mono", "ui-monospace", "SFMono-Regular", "Roboto Mono", monospace;
} }
/* ── Dark (Rich warm dark — not pure black) ── */ /* ── Dark (Neutral zinc-dark — no blue, layered depth) ── */
[data-theme="dark"] { [data-theme="dark"] {
--color-background: #0d0d0d; --color-background: #09090b;
--color-foreground: #ececec; --color-foreground: #e4e4e7;
--color-card: #141414; --color-card: #111113;
--color-card-foreground: #ececec; --color-card-foreground: #e4e4e7;
--color-popover: #1a1a1a; --color-popover: #141416;
--color-popover-foreground: #ececec; --color-popover-foreground: #e4e4e7;
--color-primary: #ececec; --color-primary: #e4e4e7;
--color-primary-foreground: #0d0d0d; --color-primary-foreground: #09090b;
--color-secondary: #1a1a1a; --color-secondary: #1a1a1c;
--color-secondary-foreground: #ececec; --color-secondary-foreground: #e4e4e7;
--color-muted: #1a1a1a; --color-muted: #18181b;
--color-muted-foreground: #888888; --color-muted-foreground: #71717a;
--color-accent: #1a1a1a; --color-accent: #1f1f22;
--color-accent-foreground: #ececec; --color-accent-foreground: #e4e4e7;
--color-destructive: #f43f5e; --color-destructive: #f43f5e;
--color-destructive-foreground: #ececec; --color-destructive-foreground: #e4e4e7;
--color-border: #262626; --color-border: #27272a;
--color-ring: #3b82f6; --color-ring: #71717a;
--color-signal: #f43f5e; --color-signal: #f43f5e;
--color-input: #262626; --color-input: #27272a;
--color-overlay: #050505; --color-overlay: #050505;
} }
@@ -131,7 +131,7 @@ body {
opacity: 0.95; opacity: 0.95;
transform: scale(1.03); transform: scale(1.03);
box-shadow: box-shadow:
0px 0px 0px 2px var(--color-ring), 0px 0px 0px 2px var(--color-muted-foreground),
0px 12px 32px rgba(0, 0, 0, 0.25); 0px 12px 32px rgba(0, 0, 0, 0.25);
z-index: 50; z-index: 50;
} }
@@ -145,7 +145,7 @@ body {
position: absolute; position: absolute;
inset: -4px; inset: -4px;
border-radius: inherit; border-radius: inherit;
border: 2px dashed var(--color-ring); border: 2px dashed var(--color-muted-foreground);
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
@@ -154,8 +154,8 @@ body {
.drop-target-line { .drop-target-line {
height: 3px; height: 3px;
border-radius: 2px; border-radius: 2px;
background: var(--color-ring); background: var(--color-muted-foreground);
box-shadow: 0 0 8px var(--color-ring); box-shadow: 0 0 8px var(--color-muted-foreground);
margin: 4px 0; margin: 4px 0;
animation: pulse-line 1.2s ease-in-out infinite; animation: pulse-line 1.2s ease-in-out infinite;
} }
@@ -181,14 +181,14 @@ body {
/* ── Colorful badge variants ── */ /* ── Colorful badge variants ── */
.badge-local { .badge-local {
background: #0f291e; background: #0f1a15;
color: #34d399; color: #34d399;
} }
.badge-external { .badge-external {
background: #162038; background: #1a1a1c;
color: #60a5fa; color: #a1a1aa;
} }
.badge-custom { .badge-custom {
background: #231a38; background: #1a1a1c;
color: #a78bfa; color: #a1a1aa;
} }
@@ -68,7 +68,7 @@ function AddAppTile({ onClick }: { onClick: () => void }) {
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className="service-card group flex aspect-square flex-col items-center justify-center gap-2.5 rounded-[24px] border border-dashed border-border bg-card p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-accent hover:border-ring/40 hover:shadow-border-hover" className="service-card group flex aspect-square flex-col items-center justify-center gap-2.5 rounded-[24px] border border-dashed border-border bg-card p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-accent hover:border-muted-foreground/40 hover:shadow-border-hover"
> >
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-secondary transition-colors group-hover:bg-accent"> <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-secondary transition-colors group-hover:bg-accent">
<Plus className="h-5 w-5 text-muted-foreground transition-colors group-hover:text-foreground" /> <Plus className="h-5 w-5 text-muted-foreground transition-colors group-hover:text-foreground" />
@@ -128,7 +128,7 @@ function DashboardDragOverlay({ activeId, dashboard }: { activeId: string; dashb
if (widget) { if (widget) {
return ( return (
<div className="drag-overlay flex w-56 items-center gap-3 rounded-xl bg-card border border-ring/50 px-4 py-3 shadow-2xl"> <div className="drag-overlay flex w-56 items-center gap-3 rounded-xl bg-card border border-muted-foreground/40 px-4 py-3 shadow-2xl">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-accent"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-accent">
<GripVertical className="h-4 w-4 text-accent-foreground" /> <GripVertical className="h-4 w-4 text-accent-foreground" />
</div> </div>
@@ -216,7 +216,7 @@ export default function DashboardPage() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex h-screen flex-col bg-background"> <div className="flex h-screen flex-col bg-background">
<div className="h-14 border-b border-border/50" /> <div className="h-14 border-b border-border" />
<div className="flex flex-1 items-center justify-center"> <div className="flex flex-1 items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-accent"> <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-accent">
@@ -261,7 +261,7 @@ export default function DashboardPage() {
<main className="mx-auto w-full max-w-6xl flex-1 px-4 py-6"> <main className="mx-auto w-full max-w-6xl flex-1 px-4 py-6">
{isEmpty ? ( {isEmpty ? (
<div className="flex flex-col items-center justify-center gap-6 py-32"> <div className="flex flex-col items-center justify-center gap-6 py-32">
<div className="flex h-20 w-20 items-center justify-center rounded-[24px] bg-gradient-to-br from-secondary to-accent border border-border shadow-border-card"> <div className="flex h-20 w-20 items-center justify-center rounded-[24px] bg-secondary border border-border shadow-border-card">
<LayoutGrid className="h-8 w-8 text-muted-foreground" /> <LayoutGrid className="h-8 w-8 text-muted-foreground" />
</div> </div>
<div className="text-center"> <div className="text-center">
@@ -290,7 +290,7 @@ export default function DashboardPage() {
<Card className="mb-6"> <Card className="mb-6">
<CardHeader className="flex flex-row items-center justify-between pb-3"> <CardHeader className="flex flex-row items-center justify-between pb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-4 w-0.5 rounded-full bg-ring" /> <div className="h-4 w-0.5 rounded-full bg-muted-foreground" />
<CardTitle className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Widgets</CardTitle> <CardTitle className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Widgets</CardTitle>
</div> </div>
<Button variant="ghost" size="sm" onClick={openAddWidget} className="gap-1.5 text-xs rounded-lg hover:bg-accent"> <Button variant="ghost" size="sm" onClick={openAddWidget} className="gap-1.5 text-xs rounded-lg hover:bg-accent">
@@ -310,7 +310,7 @@ export default function DashboardPage() {
) : ( ) : (
<button <button
onClick={openAddWidget} onClick={openAddWidget}
className="flex w-full items-center justify-center gap-2 rounded-xl border border-dashed border-border bg-secondary/50 p-6 text-sm text-muted-foreground transition-all hover:border-ring/40 hover:bg-accent hover:text-foreground" className="flex w-full items-center justify-center gap-2 rounded-xl border border-dashed border-border bg-muted p-6 text-sm text-muted-foreground transition-all hover:border-muted-foreground/40 hover:bg-accent hover:text-foreground"
> >
<Plus className="h-4 w-4" /> Add your first widget <Plus className="h-4 w-4" /> Add your first widget
</button> </button>
@@ -322,7 +322,7 @@ export default function DashboardPage() {
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between pb-3"> <CardHeader className="flex flex-row items-center justify-between pb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-4 w-0.5 rounded-full bg-ring" /> <div className="h-4 w-0.5 rounded-full bg-muted-foreground" />
<CardTitle className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Apps</CardTitle> <CardTitle className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Apps</CardTitle>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@@ -370,7 +370,7 @@ export default function DashboardPage() {
<div> <div>
{groups.length > 0 && ( {groups.length > 0 && (
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<div className="h-4 w-0.5 rounded-full bg-ring" /> <div className="h-4 w-0.5 rounded-full bg-muted-foreground" />
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Ungrouped</span> <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Ungrouped</span>
<span className="text-xs text-muted-foreground font-mono">{ungrouped.length}</span> <span className="text-xs text-muted-foreground font-mono">{ungrouped.length}</span>
</div> </div>
@@ -401,7 +401,7 @@ export default function DashboardPage() {
{groups.length === 0 && ungrouped.length === 0 && ( {groups.length === 0 && ungrouped.length === 0 && (
<button <button
onClick={openAddService} onClick={openAddService}
className="flex w-full items-center justify-center gap-2 rounded-xl border border-dashed border-border bg-secondary/50 p-8 text-sm text-muted-foreground transition-all hover:border-ring/40 hover:bg-accent hover:text-foreground" className="flex w-full items-center justify-center gap-2 rounded-xl border border-dashed border-border bg-muted p-8 text-sm text-muted-foreground transition-all hover:border-muted-foreground/40 hover:bg-accent hover:text-foreground"
> >
<Plus className="h-4 w-4" /> Add your first app <Plus className="h-4 w-4" /> Add your first app
</button> </button>
+2 -2
View File
@@ -48,8 +48,8 @@ export function GroupSection({ group, onEditService, onDeleteService, onEditGrou
className="flex flex-1 items-center gap-2.5 group/title min-w-0" className="flex flex-1 items-center gap-2.5 group/title min-w-0"
onClick={handleToggle} onClick={handleToggle}
> >
<div className="flex h-7 w-7 items-center justify-center rounded-lg transition-colors bg-accent"> <div className="flex h-7 w-7 items-center justify-center rounded-lg transition-colors bg-secondary">
<FolderOpen className="h-3.5 w-3.5 text-accent-foreground" /> <FolderOpen className="h-3.5 w-3.5 text-secondary-foreground" />
</div> </div>
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<span className="text-sm font-semibold truncate">{group.name}</span> <span className="text-sm font-semibold truncate">{group.name}</span>
@@ -157,8 +157,8 @@ export function ServiceCard({
)} )}
onClick={handleClick} onClick={handleClick}
> >
{/* Gradient accent line at top */} {/* Accent line at top */}
<div className="absolute top-0 left-0 right-0 h-[2px] opacity-0 group-hover:opacity-100 transition-opacity bg-ring" /> <div className="absolute top-0 left-0 right-0 h-[2px] opacity-0 group-hover:opacity-100 transition-opacity bg-muted-foreground" />
<div className="flex h-full flex-col items-center justify-center gap-2.5 p-4"> <div className="flex h-full flex-col items-center justify-center gap-2.5 p-4">
+1 -1
View File
@@ -25,7 +25,7 @@ export function Header({
const dateStr = now.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" }); const dateStr = now.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" });
return ( return (
<header className="sticky top-0 z-40 w-full border-b border-border bg-background"> <header className="sticky top-0 z-40 w-full border-b border-border bg-card">
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4"> <div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
+1 -1
View File
@@ -7,7 +7,7 @@ const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLI
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-9 w-full rounded-md border border-border bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", "flex h-9 w-full rounded-md border border-border bg-card px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-muted file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className, className,
)} )}
ref={ref} ref={ref}
+3 -3
View File
@@ -37,9 +37,9 @@ export function WidgetCard({
const typeIcon = widgetTypeIcons[widget.type] || <Activity className="h-3.5 w-3.5" />; const typeIcon = widgetTypeIcons[widget.type] || <Activity className="h-3.5 w-3.5" />;
return ( return (
<Card className="group relative border-0 overflow-hidden rounded-2xl shadow-[0px_0px_0px_1px_var(--color-border)] hover:shadow-border-hover transition-all duration-200"> <Card className="group relative overflow-hidden rounded-2xl border border-border bg-card hover:shadow-border-hover transition-all duration-200">
<div className={cn( <div className={cn(
"absolute top-0 left-0 right-0 h-1 opacity-60 bg-ring" "absolute top-0 left-0 right-0 h-0.5 opacity-40 bg-muted-foreground"
)} /> )} />
<CardHeader className="flex flex-row items-center justify-between pt-4 pb-2 px-4"> <CardHeader className="flex flex-row items-center justify-between pt-4 pb-2 px-4">
<div className="flex items-center gap-2.5 min-w-0"> <div className="flex items-center gap-2.5 min-w-0">
@@ -156,7 +156,7 @@ function ImageContent({ config }: { config: Record<string, unknown> }) {
<img <img
src={imageUrl} src={imageUrl}
alt="Widget image" alt="Widget image"
className="max-h-48 w-full rounded-xl object-cover border border-border/20 shadow-sm" className="max-h-48 w-full rounded-xl object-cover border border-border shadow-sm"
onError={(e) => { onError={(e) => {
(e.target as HTMLImageElement).style.display = "none"; (e.target as HTMLImageElement).style.display = "none";
}} }}