mirror of
https://github.com/basnijholt/compose-farm.git
synced 2026-02-17 04:02:12 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7caf006e07 | ||
|
|
45040b75f1 | ||
|
|
fa1c5c1044 | ||
|
|
67e832f687 | ||
|
|
da986fab6a | ||
|
|
5dd6e2ca05 | ||
|
|
16435065de |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,3 +44,4 @@ compose-farm.yaml
|
||||
coverage.xml
|
||||
.env
|
||||
homepage/
|
||||
site/
|
||||
|
||||
@@ -155,7 +155,7 @@ If you need containers on different hosts to communicate seamlessly, you need Do
|
||||
|
||||
```bash
|
||||
# One-liner (installs uv if needed)
|
||||
curl -fsSL https://raw.githubusercontent.com/basnijholt/compose-farm/main/bootstrap.sh | sh
|
||||
curl -fsSL https://compose-farm.nijho.lt/install | sh
|
||||
|
||||
# Or if you already have uv/pip
|
||||
uv tool install compose-farm
|
||||
@@ -237,7 +237,7 @@ The keys will persist across restarts.
|
||||
|
||||
## Configuration
|
||||
|
||||
Create `~/.config/compose-farm/compose-farm.yaml` (or `./compose-farm.yaml` in your working directory):
|
||||
Create `compose-farm.yaml` in the directory where you'll run commands (e.g., `/opt/stacks`). This keeps config near your stacks. Alternatively, use `~/.config/compose-farm/compose-farm.yaml` for a global config, or symlink from one to the other with `cf config symlink`.
|
||||
|
||||
### Single-host example
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ cf --help, -h # Show help
|
||||
Make reality match your configuration. The primary reconciliation command.
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/apply.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/apply.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
```bash
|
||||
@@ -187,7 +187,7 @@ cf restart --all
|
||||
Update stacks (pull + build + down + up).
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/update.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/update.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
```bash
|
||||
@@ -275,7 +275,7 @@ cf ps --host nuc
|
||||
Show stack logs.
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/logs.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/logs.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
```bash
|
||||
|
||||
@@ -63,7 +63,7 @@ def test_demo_themes(recording_page: Page, server_url: str) -> None:
|
||||
pause(page, 400)
|
||||
|
||||
# Type to filter to a light theme (theme button pre-populates "theme:")
|
||||
slow_type(page, "#cmd-input", " cup", delay=100)
|
||||
slow_type(page, "#cmd-input", "cup", delay=100)
|
||||
pause(page, 500)
|
||||
page.keyboard.press("Enter")
|
||||
pause(page, 1000)
|
||||
@@ -75,7 +75,7 @@ def test_demo_themes(recording_page: Page, server_url: str) -> None:
|
||||
page.wait_for_selector("#cmd-palette[open]", timeout=2000)
|
||||
pause(page, 300)
|
||||
|
||||
slow_type(page, "#cmd-input", " dark", delay=100)
|
||||
slow_type(page, "#cmd-input", "dark", delay=100)
|
||||
pause(page, 400)
|
||||
page.keyboard.press("Enter")
|
||||
pause(page, 800)
|
||||
|
||||
@@ -18,13 +18,13 @@ Before you begin, ensure you have:
|
||||
## Installation
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/install.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/install.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
### One-liner (recommended)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/basnijholt/compose-farm/main/bootstrap.sh | sh
|
||||
curl -fsSL https://compose-farm.nijho.lt/install | sh
|
||||
```
|
||||
|
||||
This installs [uv](https://docs.astral.sh/uv/) if needed, then installs compose-farm.
|
||||
@@ -123,7 +123,21 @@ nas:/volume1/compose /opt/compose nfs defaults 0 0
|
||||
|
||||
### Create Config File
|
||||
|
||||
Create `~/.config/compose-farm/compose-farm.yaml`:
|
||||
Create `compose-farm.yaml` in the directory where you'll run commands. For example, if your stacks are in `/opt/stacks`, place the config there too:
|
||||
|
||||
```bash
|
||||
cd /opt/stacks
|
||||
cf config init
|
||||
```
|
||||
|
||||
Alternatively, use `~/.config/compose-farm/compose-farm.yaml` for a global config. You can also symlink a working directory config to the global location:
|
||||
|
||||
```bash
|
||||
# Create config in your stacks directory, symlink to ~/.config
|
||||
cf config symlink /opt/stacks/compose-farm.yaml
|
||||
```
|
||||
|
||||
This way, `cf` commands work from anywhere while the config lives with your stacks.
|
||||
|
||||
#### Single host example
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@ It also works great on a single host with one folder per stack; just map stacks
|
||||
|
||||
**CLI:**
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/quickstart.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/quickstart.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
**[Web UI](web-ui.md):**
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/web-workflow.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/web-workflow.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
## Why Compose Farm?
|
||||
@@ -96,7 +96,7 @@ pip install compose-farm
|
||||
|
||||
### Configuration
|
||||
|
||||
Create `~/.config/compose-farm/compose-farm.yaml`:
|
||||
Create `compose-farm.yaml` in the directory where you'll run commands (e.g., `/opt/stacks`), or in `~/.config/compose-farm/`:
|
||||
|
||||
```yaml
|
||||
compose_dir: /opt/compose
|
||||
@@ -114,6 +114,8 @@ stacks:
|
||||
radarr: hp
|
||||
```
|
||||
|
||||
See [Configuration](configuration.md) for all options and the full search order.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
@@ -136,7 +138,7 @@ cf logs -f plex
|
||||
- **Auto-migration**: Change a host assignment, run `cf up`, stack moves automatically
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/migration.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/migration.webm" type="video/webm">
|
||||
</video>
|
||||
- **Parallel execution**: Multiple stacks start/stop concurrently
|
||||
- **State tracking**: Knows which stacks are running where
|
||||
|
||||
2
bootstrap.sh → docs/install
Executable file → Normal file
2
bootstrap.sh → docs/install
Executable file → Normal file
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
# Compose Farm bootstrap script
|
||||
# Usage: curl -fsSL https://raw.githubusercontent.com/basnijholt/compose-farm/main/bootstrap.sh | sh
|
||||
# Usage: curl -fsSL https://compose-farm.nijho.lt/install | sh
|
||||
#
|
||||
# This script installs uv (if needed) and then installs compose-farm as a uv tool.
|
||||
|
||||
21
docs/javascripts/video-fix.js
Normal file
21
docs/javascripts/video-fix.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// Fix Safari video autoplay issues
|
||||
(function() {
|
||||
function initVideos() {
|
||||
document.querySelectorAll('video[autoplay]').forEach(function(video) {
|
||||
video.load();
|
||||
video.play().catch(function() {});
|
||||
});
|
||||
}
|
||||
|
||||
// For initial page load (needed for Chrome)
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initVideos);
|
||||
} else {
|
||||
initVideos();
|
||||
}
|
||||
|
||||
// For MkDocs instant navigation (needed for Safari)
|
||||
if (typeof document$ !== 'undefined') {
|
||||
document$.subscribe(initVideos);
|
||||
}
|
||||
})();
|
||||
@@ -19,7 +19,7 @@ Then open [http://localhost:8000](http://localhost:8000).
|
||||
Console terminal, config editor, stack navigation, actions (up, logs, update), dashboard overview, and theme switching - all in one flow.
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/web-workflow.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/web-workflow.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
### Stack Actions
|
||||
@@ -27,7 +27,7 @@ Console terminal, config editor, stack navigation, actions (up, logs, update), d
|
||||
Navigate to any stack and use the command palette to trigger actions like restart, pull, update, or view logs. Output streams in real-time via WebSocket.
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/web-stack.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/web-stack.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
### Theme Switching
|
||||
@@ -35,7 +35,7 @@ Navigate to any stack and use the command palette to trigger actions like restar
|
||||
35 themes available via the command palette. Type `theme:` to filter, then use arrow keys to preview themes live before selecting.
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/web-themes.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/web-themes.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
### Command Palette
|
||||
@@ -43,7 +43,7 @@ Navigate to any stack and use the command palette to trigger actions like restar
|
||||
Press `Ctrl+K` (or `Cmd+K` on macOS) to open the command palette. Use fuzzy search to quickly navigate, trigger actions, or change themes.
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/web-navigation.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/web-navigation.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
## Pages
|
||||
@@ -70,7 +70,7 @@ Press `Ctrl+K` (or `Cmd+K` on macOS) to open the command palette. Use fuzzy sear
|
||||
- Monaco editor with syntax highlighting
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/web-console.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/web-console.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
### Container Shell
|
||||
@@ -78,7 +78,7 @@ Press `Ctrl+K` (or `Cmd+K` on macOS) to open the command palette. Use fuzzy sear
|
||||
Click the Shell button on any running container to exec into it directly from the browser.
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/web-shell.webm#t=0.001" type="video/webm">
|
||||
<source src="/assets/web-shell.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -10,11 +11,22 @@ from typing import TYPE_CHECKING
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import ValidationError
|
||||
from rich.logging import RichHandler
|
||||
|
||||
from compose_farm.web.deps import STATIC_DIR, get_config
|
||||
from compose_farm.web.routes import actions, api, pages
|
||||
from compose_farm.web.streaming import TASK_TTL_SECONDS, cleanup_stale_tasks
|
||||
|
||||
# Configure logging with Rich handler for compose_farm.web modules
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[RichHandler(rich_tracebacks=True, show_path=False)],
|
||||
)
|
||||
# Set our web modules to INFO level (uvicorn handles its own logging)
|
||||
logging.getLogger("compose_farm.web").setLevel(logging.INFO)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
|
||||
@@ -64,3 +64,19 @@ async def refresh_state() -> dict[str, Any]:
|
||||
config = get_config()
|
||||
task_id = _start_task(lambda tid: run_cli_streaming(config, ["refresh"], tid))
|
||||
return {"task_id": task_id, "command": "refresh"}
|
||||
|
||||
|
||||
@router.post("/pull-all")
|
||||
async def pull_all() -> dict[str, Any]:
|
||||
"""Pull latest images for all stacks."""
|
||||
config = get_config()
|
||||
task_id = _start_task(lambda tid: run_cli_streaming(config, ["pull", "--all"], tid))
|
||||
return {"task_id": task_id, "command": "pull --all"}
|
||||
|
||||
|
||||
@router.post("/update-all")
|
||||
async def update_all() -> dict[str, Any]:
|
||||
"""Update all stacks (pull + build + down + up)."""
|
||||
config = get_config()
|
||||
task_id = _start_task(lambda tid: run_cli_streaming(config, ["update", "--all"], tid))
|
||||
return {"task_id": task_id, "command": "update --all"}
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import shlex
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
@@ -23,6 +24,8 @@ from compose_farm.paths import find_config_path
|
||||
from compose_farm.state import load_state
|
||||
from compose_farm.web.deps import get_config, get_templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(tags=["api"])
|
||||
|
||||
|
||||
@@ -144,6 +147,12 @@ async def _get_container_states(
|
||||
config, stack, host_name, "ps -a --format json", stream=False
|
||||
)
|
||||
if not result.success:
|
||||
logger.warning(
|
||||
"Failed to get container states for %s on %s: %s",
|
||||
stack,
|
||||
host_name,
|
||||
result.stderr or result.stdout,
|
||||
)
|
||||
return containers
|
||||
|
||||
# Build state map: name -> (state, exit_code)
|
||||
@@ -350,6 +359,7 @@ async def read_console_file(
|
||||
except PermissionError:
|
||||
raise HTTPException(status_code=403, detail=f"Permission denied: {path}") from None
|
||||
except Exception as e:
|
||||
logger.exception("Failed to read file %s from host %s", path, host)
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@@ -373,4 +383,5 @@ async def write_console_file(
|
||||
except PermissionError:
|
||||
raise HTTPException(status_code=403, detail=f"Permission denied: {path}") from None
|
||||
except Exception as e:
|
||||
logger.exception("Failed to write file %s to host %s", path, host)
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/* Tooltips - ensure they appear above sidebar and other elements */
|
||||
.tooltip::before,
|
||||
.tooltip::after {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Sidebar inputs - remove focus outline (DaisyUI 5 uses outline + outline-offset) */
|
||||
#sidebar .input:focus,
|
||||
#sidebar .input:focus-within,
|
||||
|
||||
@@ -601,7 +601,9 @@ function playFabIntro() {
|
||||
}
|
||||
|
||||
function filter() {
|
||||
const q = input.value.toLowerCase();
|
||||
// Normalize: collapse spaces and ensure space after colon for matching
|
||||
// This allows "theme:dark", "theme: dark", "theme: dark" to all match "theme: dark"
|
||||
const q = input.value.toLowerCase().replace(/\s+/g, ' ').replace(/:(\S)/g, ': $1');
|
||||
filtered = commands.filter(c => c.name.toLowerCase().includes(q));
|
||||
selected = Math.max(0, Math.min(selected, filtered.length - 1));
|
||||
}
|
||||
@@ -634,7 +636,7 @@ function playFabIntro() {
|
||||
input.value = initialFilter;
|
||||
filter();
|
||||
// If opening theme picker, select current theme
|
||||
if (initialFilter === 'theme:') {
|
||||
if (initialFilter.startsWith('theme:')) {
|
||||
const currentIdx = filtered.findIndex(c => c.themeId === originalTheme);
|
||||
if (currentIdx >= 0) selected = currentIdx;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<span class="font-semibold rainbow-hover">Compose Farm</span>
|
||||
</header>
|
||||
|
||||
<main id="main-content" class="flex-1 p-6 overflow-y-auto">
|
||||
<main id="main-content" class="flex-1 p-6">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "partials/components.html" import page_header, collapse, stat_card, table, action_btn %}
|
||||
{% from "partials/icons.html" import check, refresh_cw, save, settings, server, database %}
|
||||
{% from "partials/icons.html" import check, refresh_cw, save, settings, server, database, cloud_download, rotate_cw %}
|
||||
{% block title %}Dashboard - Compose Farm{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -17,7 +17,9 @@
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
{{ action_btn("Apply", "/api/apply", "primary", "Make reality match config", check()) }}
|
||||
{{ action_btn("Refresh", "/api/refresh", "outline", "Update state from reality", refresh_cw()) }}
|
||||
<button id="save-config-btn" class="btn btn-outline">{{ save() }} Save Config</button>
|
||||
{{ action_btn("Pull All", "/api/pull-all", "outline", "Pull latest images for all stacks", cloud_download()) }}
|
||||
{{ action_btn("Update All", "/api/update-all", "outline", "Update all stacks (pull + build + down + up)", rotate_cw()) }}
|
||||
<div class="tooltip" data-tip="Save compose-farm.yaml config file"><button id="save-config-btn" class="btn btn-outline">{{ save() }} Save Config</button></div>
|
||||
</div>
|
||||
|
||||
{% include "partials/terminal.html" %}
|
||||
|
||||
@@ -25,12 +25,13 @@
|
||||
|
||||
{# Action button with htmx #}
|
||||
{% macro action_btn(label, url, style="outline", title=None, icon=None) %}
|
||||
{% if title %}<div class="tooltip" data-tip="{{ title }}">{% endif %}
|
||||
<button hx-post="{{ url }}"
|
||||
hx-swap="none"
|
||||
class="btn btn-{{ style }}"
|
||||
{% if title %}title="{{ title }}"{% endif %}>
|
||||
class="btn btn-{{ style }}">
|
||||
{% if icon %}{{ icon }}{% endif %}{{ label }}
|
||||
</button>
|
||||
{% if title %}</div>{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{# Stat card for dashboard #}
|
||||
|
||||
@@ -18,10 +18,12 @@
|
||||
<span class="badge badge-warning">{{ container.State }}</span>
|
||||
{% endif %}
|
||||
<code class="text-sm flex-1">{{ container.Name }}</code>
|
||||
<button class="btn btn-sm btn-outline"
|
||||
onclick="initExecTerminal('{{ stack }}', '{{ container.Name }}', '{{ host }}')">
|
||||
{{ terminal() }} Shell
|
||||
</button>
|
||||
<div class="tooltip tooltip-left" data-tip="Open shell in container">
|
||||
<button class="btn btn-sm btn-outline"
|
||||
onclick="initExecTerminal('{{ stack }}', '{{ container.Name }}', '{{ host }}')">
|
||||
{{ terminal() }} Shell
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<!-- Other -->
|
||||
{{ action_btn("Pull", "/api/stack/" ~ name ~ "/pull", "outline", "Pull latest images (no restart)", cloud_download()) }}
|
||||
{{ action_btn("Logs", "/api/stack/" ~ name ~ "/logs", "outline", "Show recent logs", file_text()) }}
|
||||
<button id="save-btn" class="btn btn-outline">{{ save() }} Save All</button>
|
||||
<div class="tooltip" data-tip="Save compose and .env files"><button id="save-btn" class="btn btn-outline">{{ save() }} Save All</button></div>
|
||||
</div>
|
||||
|
||||
{% call collapse("Compose File", badge=compose_path, icon=file_code()) %}
|
||||
|
||||
@@ -6,6 +6,7 @@ import asyncio
|
||||
import contextlib
|
||||
import fcntl
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pty
|
||||
import shlex
|
||||
@@ -21,6 +22,8 @@ from compose_farm.executor import is_local, ssh_connect_kwargs
|
||||
from compose_farm.web.deps import get_config
|
||||
from compose_farm.web.streaming import CRLF, DIM, GREEN, RED, RESET, tasks
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Shell command to prefer bash over sh
|
||||
SHELL_FALLBACK = "command -v bash >/dev/null && exec bash || exec sh"
|
||||
|
||||
@@ -214,6 +217,7 @@ async def exec_websocket(
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.exception("WebSocket exec error for %s on %s", container, host)
|
||||
with contextlib.suppress(Exception):
|
||||
await websocket.send_text(f"{RED}Error: {e}{RESET}{CRLF}")
|
||||
finally:
|
||||
@@ -258,6 +262,7 @@ async def shell_websocket(
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.exception("WebSocket shell error for host %s", host)
|
||||
with contextlib.suppress(Exception):
|
||||
await websocket.send_text(f"{RED}Error: {e}{RESET}{CRLF}")
|
||||
finally:
|
||||
|
||||
@@ -11,6 +11,7 @@ copyright = "Copyright © 2025 Bas Nijholt"
|
||||
repo_url = "https://github.com/basnijholt/compose-farm"
|
||||
repo_name = "GitHub"
|
||||
edit_uri = "edit/main/docs"
|
||||
extra_javascript = ["javascripts/video-fix.js"]
|
||||
|
||||
nav = [
|
||||
{ "Home" = "index.md" },
|
||||
@@ -48,16 +49,25 @@ features = [
|
||||
]
|
||||
|
||||
[[project.theme.palette]]
|
||||
media = "(prefers-color-scheme)"
|
||||
toggle.icon = "lucide/sun-moon"
|
||||
toggle.name = "Switch to light mode"
|
||||
|
||||
[[project.theme.palette]]
|
||||
media = "(prefers-color-scheme: light)"
|
||||
scheme = "default"
|
||||
primary = "teal"
|
||||
primary = "indigo"
|
||||
accent = "indigo"
|
||||
toggle.icon = "lucide/sun"
|
||||
toggle.name = "Switch to dark mode"
|
||||
|
||||
[[project.theme.palette]]
|
||||
media = "(prefers-color-scheme: dark)"
|
||||
scheme = "slate"
|
||||
primary = "teal"
|
||||
toggle.icon = "lucide/moon"
|
||||
toggle.name = "Switch to light mode"
|
||||
primary = "indigo"
|
||||
accent = "orange"
|
||||
toggle.icon = "lucide/moon-star"
|
||||
toggle.name = "Switch to system preference"
|
||||
|
||||
[project.theme.font]
|
||||
text = "Inter"
|
||||
@@ -67,6 +77,9 @@ code = "JetBrains Mono"
|
||||
logo = "lucide/server"
|
||||
repo = "lucide/github"
|
||||
|
||||
[project.extra]
|
||||
generator = false
|
||||
|
||||
[[project.extra.social]]
|
||||
icon = "fontawesome/brands/github"
|
||||
link = "https://github.com/basnijholt/compose-farm"
|
||||
|
||||
Reference in New Issue
Block a user