From 20f949c2508460e428dfe4f2f31c146c88c447bc Mon Sep 17 00:00:00 2001 From: Vladimir nett00n Budylnikov Date: Sun, 12 Oct 2025 21:56:53 +0400 Subject: [PATCH] Init commit --- .dockerignore | 53 ++ .gitea/README.md | 109 +++ .gitea/workflows/ci.yml | 218 ++++++ .gitea/workflows/docker-publish.yml | 61 ++ .gitea/workflows/release.yml | 135 ++++ .gitignore | 40 ++ .golangci.yml | 28 + .pre-commit-config.yaml | 63 ++ CONTRIBUTING.md | 303 ++++++++ Dockerfile | 55 ++ LICENSE | 674 ++++++++++++++++++ Makefile | 64 ++ README.md | 162 +++++ assets/Screenshot.png | Bin 0 -> 20457 bytes config.example.yaml | 36 + docker-compose.yml | 26 + go.mod | 7 + go.sum | 6 + internal/config/config.go | 111 +++ internal/config/config_test.go | 98 +++ internal/config/load.go | 29 + internal/config/load_test.go | 34 + .../testdata/config-catppuccin-frappe.yaml | 23 + .../testdata/config-catppuccin-latte.yaml | 23 + .../testdata/config-catppuccin-macchiato.yaml | 23 + .../testdata/config-catppuccin-mocha.yaml | 23 + internal/config/testdata/config-gruvbox.yaml | 23 + .../config/testdata/config-invalid-theme.yaml | 12 + internal/config/testdata/config-nord.yaml | 23 + internal/config/validate_test.go | 128 ++++ internal/generator/generator.go | 186 +++++ internal/generator/generator_test.go | 343 +++++++++ static/favicon.png | Bin 0 -> 855 bytes static/logo.png | Bin 0 -> 2043 bytes templates/base.html | 189 +++++ themes/auto.css | 124 ++++ themes/catppuccin-frappe.css | 138 ++++ themes/catppuccin-latte.css | 138 ++++ themes/catppuccin-macchiato.css | 138 ++++ themes/catppuccin-mocha.css | 138 ++++ themes/gruvbox.css | 247 +++++++ themes/nord.css | 247 +++++++ 42 files changed, 4478 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/README.md create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/docker-publish.yml create mode 100644 .gitea/workflows/release.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .pre-commit-config.yaml create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 assets/Screenshot.png create mode 100644 config.example.yaml create mode 100644 docker-compose.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/config/load.go create mode 100644 internal/config/load_test.go create mode 100644 internal/config/testdata/config-catppuccin-frappe.yaml create mode 100644 internal/config/testdata/config-catppuccin-latte.yaml create mode 100644 internal/config/testdata/config-catppuccin-macchiato.yaml create mode 100644 internal/config/testdata/config-catppuccin-mocha.yaml create mode 100644 internal/config/testdata/config-gruvbox.yaml create mode 100644 internal/config/testdata/config-invalid-theme.yaml create mode 100644 internal/config/testdata/config-nord.yaml create mode 100644 internal/config/validate_test.go create mode 100644 internal/generator/generator.go create mode 100644 internal/generator/generator_test.go create mode 100644 static/favicon.png create mode 100644 static/logo.png create mode 100644 templates/base.html create mode 100644 themes/auto.css create mode 100644 themes/catppuccin-frappe.css create mode 100644 themes/catppuccin-latte.css create mode 100644 themes/catppuccin-macchiato.css create mode 100644 themes/catppuccin-mocha.css create mode 100644 themes/gruvbox.css create mode 100644 themes/nord.css diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8e27901 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,53 @@ +# Build artifacts +/bin/ +/build/ +/dist/ +linkbeam +*.exe +*.out +*.test + +# Git +.git/ +.gitignore +.gitattributes + +# CI/CD +.gitea/ +.github/ +.gitlab/ +.circleci/ + +# Editor & IDE +.vscode/ +.idea/ +*.swp +*.swo +*.bak +.DS_Store + +# Testing +coverage.out +coverage.html +*.cover +*.cov + +# Go module cache +/vendor/ + +# Environment +.env +.env.local +.venv/ + +# User config (example tracked separately) +config.yaml + +# Documentation (keep lightweight, exclude these) + +.editorconfig +.gitattributes + +# Pre-commit +.pre-commit-config.yaml +.pre-commit-cache/ diff --git a/.gitea/README.md b/.gitea/README.md new file mode 100644 index 0000000..5f040ff --- /dev/null +++ b/.gitea/README.md @@ -0,0 +1,109 @@ +# Gitea CI/CD Configuration + +This directory contains Gitea Actions workflows for automated testing, building, and deployment. + +## Workflows + +### `ci.yml` - Main CI/CD Pipeline + +Runs on every push and pull request: + +1. **Lint Stage** + - golangci-lint with all enabled linters + - go fmt verification + - go vet static analysis + +2. **Test Stage** + - Run all tests with `-race` flag + - Generate coverage reports + - Upload coverage artifacts + +3. **Build Stage** + - Matrix build for multiple platforms: + - Linux: amd64, 386, arm64, armv7 + - Windows: amd64, 386, arm64 + - macOS: amd64, arm64 + - Build with version and commit info in ldflags + - Upload build artifacts + +4. **Docker Stage** + - Multi-architecture Docker build + - Cache optimization with GitHub Actions cache + - Build for: linux/amd64, linux/arm64, linux/arm/v7 + +### `docker-publish.yml` - Docker Registry Publishing + +Triggered on version tags (`v*`): +- Builds multi-arch Docker images +- Pushes to container registry +- Tags with version numbers and `latest` +- Uses registry caching for faster builds + +### `release.yml` - Release Creation + +Triggered on semver tags (`v*.*.*`): +- Creates GitHub/Gitea release +- Uploads platform-specific binaries (tar.gz for Unix, zip for Windows) +- Generates SHA256 checksums +- Includes release notes + +## Configuration Required + +### Secrets + +Set these in your Gitea repository settings: + +- `GITEA_TOKEN` - Gitea API token with package write permissions +- `GITHUB_TOKEN` - Automatically provided by Gitea Actions + +### Registry Configuration + +Update the registry URL in `docker-publish.yml`: +```yaml +env: + REGISTRY: gitea.example.com # Change to your Gitea instance + IMAGE_NAME: ${{ github.repository }} +``` + +## Local Testing + +### Test builds locally: + +```bash +# Test Go build +make build + +# Test multi-platform build +GOOS=linux GOARCH=arm64 go build -o linkbeam-linux-arm64 cmd/linkbeam/main.go + +# Test Docker build (requires Docker) +docker build -t linkbeam:test . + +# Test Docker with compose +docker-compose up +``` + +## Triggering Releases + +To create a new release: + +```bash +# Tag with semantic version +git tag v1.0.0 +git push origin v1.0.0 + +# This will trigger: +# 1. Full CI pipeline +# 2. Docker image build and push +# 3. Release creation with binaries +``` + +## Build Artifacts + +Artifacts are uploaded for each workflow run: +- Coverage reports (test stage) +- Platform binaries (build stage) +- Docker images (docker stage) +- Release assets (release stage) + +Artifacts are available for 90 days after the workflow run. diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..abf3ccd --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,218 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - main + - develop + tags: + - 'v*' + pull_request: + branches: + - main + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + version: latest + args: --timeout=5m + + - name: Run go fmt check + run: | + if [ -n "$(gofmt -l .)" ]; then + echo "Code is not formatted. Run 'go fmt ./...'" + gofmt -l . + exit 1 + fi + + - name: Run go vet + run: go vet ./... + + test: + name: Test + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Run tests + run: go test -v -race -coverprofile=coverage.out ./... + + - name: Generate coverage report + run: go tool cover -html=coverage.out -o coverage.html + + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: | + coverage.out + coverage.html + + build: + name: Build + runs-on: ubuntu-latest + needs: test + strategy: + matrix: + include: + # Linux builds + - goos: linux + goarch: amd64 + output: linkbeam-linux-amd64 + - goos: linux + goarch: 386 + output: linkbeam-linux-386 + - goos: linux + goarch: arm64 + output: linkbeam-linux-arm64 + - goos: linux + goarch: arm + goarm: 7 + output: linkbeam-linux-armv7 + + # Windows builds + - goos: windows + goarch: amd64 + output: linkbeam-windows-amd64.exe + - goos: windows + goarch: 386 + output: linkbeam-windows-386.exe + - goos: windows + goarch: arm64 + output: linkbeam-windows-arm64.exe + + # macOS builds + - goos: darwin + goarch: amd64 + output: linkbeam-darwin-amd64 + - goos: darwin + goarch: arm64 + output: linkbeam-darwin-arm64 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Build binary + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + GOARM: ${{ matrix.goarm }} + run: | + go build -o dist/${{ matrix.output }} \ + -ldflags="-s -w -X main.Version=${{ github.ref_name }} -X main.Commit=${{ github.sha }}" \ + ./cmd/linkbeam + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.output }} + path: dist/${{ matrix.output }} + + docker: + name: Build Docker Image + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + gitea.example.com/${{ github.repository }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Save Docker image + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + run: | + docker save -o linkbeam-docker-image.tar ${{ steps.meta.outputs.tags }} + + - name: Upload Docker image artifact + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + uses: actions/upload-artifact@v4 + with: + name: docker-image + path: linkbeam-docker-image.tar + + release: + name: Create Release + runs-on: ubuntu-latest + needs: [build, docker] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: release-artifacts + + - name: Create checksums + run: | + cd release-artifacts + find . -type f -name "linkbeam-*" -exec sha256sum {} \; > checksums.txt + + - name: Create release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref_name }} + release_name: Release ${{ github.ref_name }} + draft: false + prerelease: false + + - name: Upload release assets + uses: softprops/action-gh-release@v1 + with: + files: | + release-artifacts/**/* + release-artifacts/checksums.txt diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml new file mode 100644 index 0000000..d9af1b4 --- /dev/null +++ b/.gitea/workflows/docker-publish.yml @@ -0,0 +1,61 @@ +name: Docker Publish + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +env: + REGISTRY: gitea.example.com + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max + build-args: | + VERSION=${{ github.ref_name }} + COMMIT=${{ github.sha }} diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..305b480 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,135 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + +jobs: + create-release: + name: Create Release + runs-on: ubuntu-latest + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + steps: + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref_name }} + release_name: Release ${{ github.ref_name }} + body: | + ## What's Changed + + See [CHANGELOG.md](./CHANGELOG.md) for details. + + ## Installation + + Download the appropriate binary for your platform below. + + ### Docker + ```bash + docker pull gitea.example.com/${{ github.repository }}:${{ github.ref_name }} + ``` + + ### Verification + Verify your download with the provided SHA256 checksums. + draft: false + prerelease: false + + build-and-upload: + name: Build and Upload Assets + needs: create-release + runs-on: ubuntu-latest + strategy: + matrix: + include: + - goos: linux + goarch: amd64 + - goos: linux + goarch: 386 + - goos: linux + goarch: arm64 + - goos: linux + goarch: arm + goarm: 7 + - goos: windows + goarch: amd64 + - goos: windows + goarch: 386 + - goos: darwin + goarch: amd64 + - goos: darwin + goarch: arm64 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Build binary + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + GOARM: ${{ matrix.goarm }} + run: | + BINARY_NAME="linkbeam-${{ matrix.goos }}-${{ matrix.goarch }}" + if [ "${{ matrix.goarm }}" != "" ]; then + BINARY_NAME="${BINARY_NAME}v${{ matrix.goarm }}" + fi + if [ "${{ matrix.goos }}" = "windows" ]; then + BINARY_NAME="${BINARY_NAME}.exe" + fi + + go build -o "${BINARY_NAME}" \ + -ldflags="-s -w -X main.Version=${{ github.ref_name }} -X main.Commit=${{ github.sha }}" \ + ./cmd/linkbeam + + # Create tarball (except for Windows) + if [ "${{ matrix.goos }}" != "windows" ]; then + tar -czf "${BINARY_NAME}.tar.gz" "${BINARY_NAME}" + sha256sum "${BINARY_NAME}.tar.gz" > "${BINARY_NAME}.tar.gz.sha256" + rm "${BINARY_NAME}" + else + zip "${BINARY_NAME}.zip" "${BINARY_NAME}" + sha256sum "${BINARY_NAME}.zip" > "${BINARY_NAME}.zip.sha256" + rm "${BINARY_NAME}" + fi + + - name: Upload Release Asset (tar.gz) + if: matrix.goos != 'windows' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: ./linkbeam-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm && format('v{0}', matrix.goarm) || '' }}.tar.gz + asset_name: linkbeam-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm && format('v{0}', matrix.goarm) || '' }}.tar.gz + asset_content_type: application/gzip + + - name: Upload Release Asset (zip) + if: matrix.goos == 'windows' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: ./linkbeam-${{ matrix.goos }}-${{ matrix.goarch }}.exe.zip + asset_name: linkbeam-${{ matrix.goos }}-${{ matrix.goarch }}.zip + asset_content_type: application/zip + + - name: Upload Checksum + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: ./linkbeam-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm && format('v{0}', matrix.goarm) || '' }}${{ matrix.goos == 'windows' && '.exe.zip.sha256' || '.tar.gz.sha256' }} + asset_name: linkbeam-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm && format('v{0}', matrix.goarm) || '' }}${{ matrix.goos == 'windows' && '.zip.sha256' || '.tar.gz.sha256' }} + asset_content_type: text/plain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..459710e --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Build artifacts +/dist/ +linkbeam +*.exe +*.out +*.test + +# Test coverage +coverage.out +coverage.html +*.cover +*.cov + +# Editor & IDE +.vscode/ +.idea/ +*.swp +*.swo +*.bak +.DS_Store + +# Testing +coverage.out +coverage.html +*.cover +*.cov + +# Go module cache +/vendor/ + +# Environment +.env +.env.local +.venv/ + +# User config (example file is tracked) +config.yaml + +# Pre-commit +.pre-commit-cache/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..326ee76 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,28 @@ +# golangci-lint configuration +# See https://golangci-lint.run/usage/configuration/ + +version: 2 + +linters-settings: + govet: + enable-all: true + +linters: + # Use default linters: errcheck, govet, ineffassign, staticcheck, unused + enable: + - errcheck # Check for unchecked errors (enabled by default) + - govet # Examines Go source code (enabled by default) + - ineffassign # Detect ineffectual assignments (enabled by default) + - staticcheck # Go static analysis (enabled by default) + - unused # Check for unused code (enabled by default) + - gocritic # Additional diagnostics for bugs, performance, and style + +run: + timeout: 5m + tests: true + skip-dirs: + - vendor + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..458f0dc --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,63 @@ +# Pre-commit hooks for Go project +# See https://pre-commit.com for more information + +repos: + # Go-specific hooks + - repo: https://github.com/tekwizely/pre-commit-golang + rev: v1.0.0-rc.1 + hooks: + # Format code + - id: go-fmt + args: [-w] + + # Organize imports + - id: go-imports + args: [-w] + + # Examine Go source code and report suspicious constructs + - id: go-vet + + # Run tests + - id: go-test-mod + - id: go-test-pkg + - id: go-test-repo-mod + - id: go-test-repo-pkg + + # Tidy go.mod and go.sum + - id: go-mod-tidy + + # Run golangci-lint (comprehensive linter) + - id: golangci-lint + args: [--fast] + + # General hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + # Prevent giant files from being committed + - id: check-added-large-files + args: ['--maxkb=1000'] + + # Check for files that would conflict in case-insensitive filesystems + - id: check-case-conflict + + # Check for merge conflicts + - id: check-merge-conflict + + # Check YAML syntax + - id: check-yaml + + # Ensure files end with newline + - id: end-of-file-fixer + exclude: '\.html$' + + # Trim trailing whitespace + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + + # Detect private keys + - id: detect-private-key + + # Don't commit directly to main + - id: no-commit-to-branch + args: ['--branch', 'main', '--branch', 'master'] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1e7d29a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,303 @@ +# Contributing to LinkBeam + +Thank you for your interest in contributing to LinkBeam! This document provides guidelines and instructions for development. + +## Development Setup + +### Prerequisites + +- Go 1.21 or later +- Make (optional, for using Makefile commands) +- golangci-lint (for code quality checks) +- pre-commit (optional, for automated checks) + +### Getting Started + +1. Fork and clone the repository: + +```bash +git clone https://github.com/5mdt/linkbeam.git +cd linkbeam +``` + +2. Install dependencies: + +```bash +go mod download +``` + +3. Build the project: + +```bash +make build +# or: go build -o dist/linkbeam cmd/linkbeam/main.go +``` + +## Pre-commit Hooks + +This project uses pre-commit hooks to ensure code quality. Setting them up is highly recommended. + +### Installation + +```bash +# Install pre-commit (if not already installed) +pip install pre-commit +# or: brew install pre-commit (on macOS) + +# Install golangci-lint (if not already installed) +# See: https://golangci-lint.run/usage/install/ +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +# Install the git hook scripts +pre-commit install +# or: make install-hooks +``` + +### Running Hooks + +```bash +# Run against all files +pre-commit run --all-files +# or: make pre-commit + +# Hooks will automatically run on git commit +git commit -m "your message" +``` + +The pre-commit hooks check: +- Go code formatting (`go fmt`) +- Import organization (`goimports`) +- Code quality (`go vet`, `golangci-lint`) +- Tests (`go test`) +- Module tidiness (`go mod tidy`) +- YAML syntax +- Trailing whitespace +- End of file fixes +- And more + +**Note**: The pre-commit framework will automatically download required Go tools on first run if they're not already installed. + +## Development Commands + +### Building + +```bash +# Build binary (outputs to dist/linkbeam) +make build +# or: go build -o dist/linkbeam cmd/linkbeam/main.go +``` + +### Running + +```bash +# Run with example config +make run-example +# or: go run cmd/linkbeam/main.go --config internal/config/testdata/config.yaml + +# Run with custom config (long flags) +./linkbeam --config config.yaml --template templates/base.html --output dist/index.html + +# Run with custom config (short flags) +./linkbeam -c config.yaml -t templates/base.html -o dist/index.html +``` + +### Testing + +```bash +# Run all tests +make test +# or: go test -v ./... + +# Run tests for a specific package +go test ./internal/config +go test ./internal/generator + +# Run a single test function +go test -v -run TestLoad ./internal/config + +# Run tests with coverage +go test -cover ./... +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out +``` + +### Code Quality + +```bash +# Format code +go fmt ./... +# or: make fmt + +# Run go vet +go vet ./... + +# Run golangci-lint +golangci-lint run +# or: make vet + +# Run all quality checks +make pre-commit +``` + +### Cleaning + +```bash +# Clean build artifacts +make clean +``` + +## Project Structure + +``` +linkbeam/ +├── cmd/linkbeam/ # Main application entry point +│ ├── main.go +│ └── main_test.go +├── internal/ +│ ├── config/ # Configuration loading and validation +│ │ ├── config.go +│ │ ├── config_test.go +│ │ └── testdata/ +│ └── generator/ # Site generation and rendering +│ ├── generator.go +│ └── generator_test.go +├── templates/ # HTML templates +│ └── base.html +├── themes/ # CSS theme files +│ ├── light.css +│ └── dark.css +├── dist/ # Build output: binary and generated site (gitignored) +├── .gitea/ # CI/CD workflows +│ └── workflows/ +├── Makefile # Build automation +├── go.mod # Go module definition +└── go.sum # Go dependencies +``` + +## Architecture + +### Two-Layer Structure + +1. **Config Layer** (`internal/config/`): + - Loads and validates YAML configuration files + - Defines data structures: `Config`, `Link`, `Social` + - Validates theme (must be "light" or "dark") and required fields + - Config validation happens automatically during `config.Load()` + +2. **Generator Layer** (`internal/generator/`): + - **HTML Generation**: Orchestrates template rendering using Go's `html/template` + - `GenerateSite(cfg, templatePath, outPath)` - Creates HTML from templates + - **Asset Management**: Copies CSS themes and static files to output directory + - `CopyAssets(distDir, themeDirs...)` - Copies theme files to dist/themes/ + - **Text Rendering**: Plain-text representation for debugging + - `RenderUserPage(cfg)` - Returns text-based preview of the page + +### Testing Strategy + +- Tests use table-driven patterns and dependency injection +- `cmd/linkbeam/main_test.go`: Mocks the config loader and captures stdout +- Config tests validate YAML parsing and business rules +- Generator tests verify file creation and template execution +- All tests use the testdata in `internal/config/testdata/config.yaml` + +## Making Changes + +### Adding New Features + +1. Create a new branch for your feature: +```bash +git checkout -b feature/your-feature-name +``` + +2. Write tests first (TDD approach recommended) +3. Implement the feature +4. Ensure all tests pass: `go test ./...` +5. Run pre-commit hooks: `pre-commit run --all-files` +6. Commit your changes with a descriptive message + +### Code Style + +- Follow standard Go conventions and idioms +- Use `gofmt` for formatting (handled by pre-commit) +- Write clear, descriptive variable and function names +- Add comments for exported functions and types +- Keep functions focused and concise + +### Testing Guidelines + +- Write tests for all new functionality +- Aim for high test coverage +- Use table-driven tests for multiple test cases +- Use testdata files for fixtures +- Test both success and error cases + +### Commit Messages + +- Use clear, descriptive commit messages +- Start with a verb in present tense (e.g., "Add", "Fix", "Update") +- Reference issue numbers when applicable +- Keep the first line under 72 characters + +Examples: +``` +Add support for custom CSS themes +Fix avatar path resolution in config loader +Update template to include meta tags +``` + +## CI/CD Pipeline + +The project uses Gitea Actions for continuous integration and deployment. + +### Workflows + +1. **CI Pipeline** (`.gitea/workflows/ci.yml`): + - Runs on push to main/develop and pull requests + - **Lint**: golangci-lint, go fmt, go vet + - **Test**: Run tests with race detection and coverage + - **Build**: Multi-platform builds (Linux, Windows, macOS) for multiple architectures + - **Docker**: Build multi-arch Docker images + +2. **Docker Publish** (`.gitea/workflows/docker-publish.yml`): + - Triggered on version tags (v*) + - Builds and pushes Docker images to container registry + - Multi-architecture: linux/amd64, linux/arm64, linux/arm/v7 + +3. **Release** (`.gitea/workflows/release.yml`): + - Triggered on semver tags (v*.*.*) + - Creates releases with binaries for all platforms + - Generates SHA256 checksums for verification + +### Local Testing with Docker + +```bash +# Test Docker build +docker build -t linkbeam:test . + +# Run Docker container +docker run -v $(pwd)/config.yaml:/app/config/config.yaml \ + -v $(pwd)/dist:/app/dist \ + linkbeam:test + +# Test with docker-compose +docker-compose up +``` + +## Submitting Changes + +1. Ensure your code passes all tests and checks +2. Push your branch to your fork +3. Open a pull request with a clear description of your changes +4. Wait for CI checks to pass +5. Respond to any review feedback + +## Questions or Issues? + +If you have questions or run into issues: +- Check existing issues on the repository +- Open a new issue with a clear description +- Join our community discussions (if available) + +## License + +By contributing to LinkBeam, you agree that your contributions will be licensed under the GNU General Public License v3.0. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..838cf1d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# Build stage +FROM golang:1.21-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git make + +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build \ + -a -installsuffix cgo \ + -ldflags="-s -w -X main.Version=${VERSION:-dev} -X main.Commit=${COMMIT:-unknown}" \ + -o linkbeam \ + ./cmd/linkbeam + +# Final stage +FROM alpine:latest + +# Install ca-certificates for HTTPS +RUN apk --no-cache add ca-certificates + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /app/linkbeam /app/linkbeam + +# Copy templates and themes +COPY templates /app/templates +COPY themes /app/themes + +# Create config directory +RUN mkdir -p /app/config + +# Expose port (if needed for future HTTP server) +EXPOSE 8080 + +# Set default config path +ENV CONFIG_PATH=/app/config/config.yaml + +# Run as non-root user +RUN addgroup -g 1000 linkbeam && \ + adduser -D -u 1000 -G linkbeam linkbeam && \ + chown -R linkbeam:linkbeam /app + +USER linkbeam + +ENTRYPOINT ["/app/linkbeam"] +CMD ["--config", "/app/config/config.yaml", "--output", "/app/dist/index.html"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ee09154 --- /dev/null +++ b/Makefile @@ -0,0 +1,64 @@ +.PHONY: build test run clean help + +# Build the linkbeam binary +build: + @mkdir -p dist + go build -o dist/linkbeam cmd/linkbeam/main.go + @echo "Binary built at: dist/linkbeam" + +# Run all tests +test: + go test -v ./... + +# Run with default config +run: + go run cmd/linkbeam/main.go + +# Run with custom config +run-example: + go run cmd/linkbeam/main.go --config internal/config/testdata/config.yaml + +# Clean build artifacts +clean: + rm -rf dist/ + rm -f linkbeam + +# Run tests with coverage +test-coverage: + go test -v -cover ./... + +# Format code +fmt: + go fmt ./... + +# Run go vet +vet: + go vet ./... + +# Run linter +lint: + golangci-lint run + +# Install pre-commit hooks +install-hooks: + pre-commit install + +# Run pre-commit checks on all files +pre-commit: + pre-commit run --all-files + +# Show help +help: + @echo "LinkBeam Makefile Commands:" + @echo " make build - Build the linkbeam binary" + @echo " make test - Run all tests" + @echo " make run - Run with default config.yaml" + @echo " make run-example - Run with example config" + @echo " make clean - Remove build artifacts" + @echo " make test-coverage - Run tests with coverage" + @echo " make fmt - Format code" + @echo " make vet - Run go vet" + @echo " make lint - Run golangci-lint" + @echo " make install-hooks - Install pre-commit hooks" + @echo " make pre-commit - Run pre-commit on all files" + @echo " make help - Show this help message" diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ae5970 --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +# LinkBeam + +![logo](./static/logo.png) + +A lightweight static site generator for creating beautiful personal link-in-bio pages. +Built with Go, LinkBeam transforms a simple YAML configuration into a themed HTML page. + +## Features +![Screenshot](./assets/Screenshot.png) +- Simple YAML-based configuration - Define your links and profile in a single YAML file +- Multiple built-in themes - Auto, Nord, Gruvbox, and Catppuccin variants (Latte, Frappé, Macchiato, Mocha) +- Theme switcher - Users can toggle between system/light/dark modes +- Font Awesome icons - Full support for Font Awesome icons with customizable CDN +- Social media links - Dedicated section for social profiles with icon-only display +- Responsive design - Works beautifully on mobile, tablet, and desktop +- Fast static site generation - Built with Go for blazing-fast builds +- Clean, minimal aesthetics - Focus on your content, not clutter +- [Catppuccin](https://catppuccin.com/), [gruvbox](https://github.com/morhetz/gruvbox) and [nord](https://www.nordtheme.com/) color palletes available by default + +## Installation + +### From Source + +```bash +# Clone the repository +git clone https://github.com/5mdt/linkbeam.git +cd linkbeam + +# Build the binary +make build +``` + +Binary will be output to `dist/linkbeam`. + +### From Docker + +```bash +# Build the image locally (Docker images not published yet) +docker build -t linkbeam . + +# Run with your config +docker run -v $(pwd)/config.yaml:/app/config/config.yaml \ + -v $(pwd)/dist:/app/dist \ + linkbeam + +# Or using docker-compose +docker-compose up +``` + +## Quick Start + +1. Create a `config.yaml` file (see `config.example.yaml` for reference): + +```yaml +name: "Your Name" +bio: "A short bio about yourself" +avatar: "static/logo.png" # Can be a local path or URL +theme: "auto" # Options: auto, nord, gruvbox, catppuccin-mocha, catppuccin-latte, etc. + +# Optional: Customize Font Awesome CDN (defaults to Font Awesome 6.5.1 if not specified) +# font_awesome_cdn: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" + +links: + - title: "My Website" + url: "https://yourwebsite.com" + icon: "fas fa-globe" + - title: "Blog" + url: "https://yourblog.com" + icon: "fas fa-blog" + - title: "GitHub" + url: "https://github.com/yourusername" + icon: "fab fa-github" + +socials: + - platform: "Twitter" + url: "https://twitter.com/yourusername" + icon: "fab fa-twitter" + - platform: "LinkedIn" + url: "https://linkedin.com/in/yourusername" + icon: "fab fa-linkedin" + - platform: "Instagram" + url: "https://instagram.com/yourusername" + icon: "fab fa-instagram" + +footer: + - text: "© 2025 Your Name" + - text: "Made with LinkBeam" +``` + +2. Generate your site: + +```bash +./linkbeam +``` + +3. Open `dist/index.html` in your browser + +## Usage + +```bash +./linkbeam [options] + +Options: + -c, --config string path to config YAML file (default "config.yaml") + -t, --template string path to HTML template (default "templates/base.html") + -o, --output string path to output HTML file (default "dist/index.html") + -v, --version print version information +``` + +### Examples + +```bash +# Generate with default config.yaml +./linkbeam + +# Check version +./linkbeam -v + +# Generate with custom config +./linkbeam -c myconfig.yaml -o public/index.html +``` + +## Configuration + +### Required Fields + +- `name`: Your display name (cannot be empty) + +### Optional Fields + +- `bio`: A short description about yourself +- `avatar`: Path to your avatar/profile image (local path or URL starting with `http://` or `https://`) +- `theme`: Theme name (default: `auto`) + - Available themes: `auto`, `nord`, `gruvbox`, `catppuccin-mocha`, `catppuccin-latte`, `catppuccin-frappe`, `catppuccin-macchiato` + - The `auto` theme follows system preferences and allows users to toggle between system/light/dark modes +- `font_awesome_cdn`: Custom Font Awesome CDN URL (defaults to Font Awesome 6.5.1 if not specified) +- `links`: Array of link objects + - `title`: Link text (required) + - `url`: Link destination (required) + - `icon`: Font Awesome icon class (optional, e.g., `fas fa-globe`, `fab fa-github`) +- `socials`: Array of social media links (displayed as icon-only buttons) + - `platform`: Platform name (used for accessibility) + - `url`: Social media profile URL + - `icon`: Font Awesome icon class (e.g., `fab fa-twitter`, `fab fa-linkedin`) +- `footer`: Array of footer text blocks + - `text`: Footer text content + +### Theme Details + +All themes support light/dark mode switching: +- **auto**: Minimalist monochrome theme with system preference detection +- **nord**: Arctic-inspired color palette (Nord theme) +- **gruvbox**: Warm retro groove colors (Gruvbox theme) +- **catppuccin-***: Soothing pastel themes (Catppuccin Latte, Frappé, Macchiato, Mocha) + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and contribution guidelines. + +## License + +This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details. diff --git a/assets/Screenshot.png b/assets/Screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..e6e9cf8a3c9a91df18d2bb8760d91f92d7179f83 GIT binary patch literal 20457 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 literal 0 HcmV?d00001 diff --git a/static/logo.png b/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7fb7e99d0b6579c693f3d021dce36cad939fdb9a GIT binary patch literal 2043 zcmc&#eNfV897oP6nQtx2L?D_ouJUCQMV*EcrFJe@)>Wj=(wyD2;UqK_f0R~UFDlk$ zHAU93)h>-hLPhcgOVJ~xohO`%QbWTO(a8c8k-7i)-R`=zKej)fd+vGp-uLtQd_Ujk zx$+e$*2M|#1c5+Y_Q&l_fI#e`{~9odwVi&}&kxroJ6b~Q9tdv$@fHGc6z<V zc8p)=JBd7hGE{fm0V+8WfRhYt7wrkYRCb_q{pg*Y4L_D$>TY^NjvMh$$Vx-Ab`EuI z#^E+gzTxvXdJkW`$cly^ye5=0)@cS*SlXMS+MJd=c@6FI?TWc(nzm)}bZ_oKgVB&$ZwpiAZcpkZs!iQa??at~fr$mCD6h%y$~6s88;bQ~ZUkfdHvG{u z8iwA4TTY6R8qXsA@~OIOn{1KJ02kdb#!~4x}^Ol~B$Il}^=UR!g*tZDqQ*;v_TnQS!}1K8Wf*rmM{QJc?Xs zt*=TV#-ir^_@+6=E-mx+q}_F_EJnIDd1RY8@2w#Apan91&}JK-F|6@Fb_XW<_`A&9sWG zZOp5KCoddw_KztkeRSDZnDO|St>{(Tda;I3AhzT>6~zn=$Bj6*8^mf-dC@tlLN<`A zbz`EQ_B`w3i?j@x zj(&n6b{t3|dKHl+^1jbKuppAB+%eO*VqYVG>79~i57m+zA84}?!{eNdsUO+)PiR02 zVCYj|4t|#RGH6+F*iBD%^lDb$4V_=qBzF8zcy)&ZN~SAn`TRD>R_sYOT07-Y-Q$2V zAk#N8cWD<_DmH)&6=ZMMF5o&>f{OnSABX?&T77jQr*ng4V%qU$8YgPSqyEva-~h zOIwj*>aSVf2 zy(j0(im}!^tC^Wr)rj)t4JDFdZsl0vKBF}Tl|cY%Z12Y^K-0(Eig591Uz~WX1cGR# z==g#3SrZOktZFOBEy$8|rk&wRdQZa^p1B=u_fNn7h!<4=mZ(P#tWKYTbp=_M-o*<5 zy_vauHb?iXl$k%RY-3D%*43^@ut8SxDuz%%HvZt3{onis|8tBQ+8xLlrj&U+U;E7= N`}dLd^7fo4{u9 + + + + + + {{ .Name }} - LinkBeam + + + {{- if .FontAwesomeCDN }} + + {{- else }} + + {{- end }} + + + + +
+ {{- if .Avatar }} + {{ .Name }} + {{- end }} +

{{ .Name }}

+

{{ .Bio }}

+
+
+ +
+ {{- if .Socials }} +
+ {{- range .Socials }} + + {{- if .Icon }}{{- end }} + + {{- end }} +
+ {{- end }} + {{- if .Footer }} +
+ {{- range .Footer }} +

{{ .Text }}

+ {{- end }} +
+ {{- end }} + + + diff --git a/themes/auto.css b/themes/auto.css new file mode 100644 index 0000000..e4e548f --- /dev/null +++ b/themes/auto.css @@ -0,0 +1,124 @@ +/* auto.css - Super-simple monochrome theme for LinkBeam */ + +/* Default: Light mode */ +:root { + --bg-color: #ffffff; + --text-color: #000000; + --text-secondary: #666666; + --link-bg: #f5f5f5; + --link-hover: #e0e0e0; + --border-color: #cccccc; +} + +/* System dark mode preference */ +@media (prefers-color-scheme: dark) { + :root { + --bg-color: #000000; + --text-color: #ffffff; + --text-secondary: #999999; + --link-bg: #1a1a1a; + --link-hover: #2a2a2a; + --border-color: #333333; + } +} + +/* Manual overrides */ +html[data-theme="light"] { + --bg-color: #ffffff; + --text-color: #000000; + --text-secondary: #666666; + --link-bg: #f5f5f5; + --link-hover: #e0e0e0; + --border-color: #cccccc; +} + +html[data-theme="dark"] { + --bg-color: #000000; + --text-color: #ffffff; + --text-secondary: #999999; + --link-bg: #1a1a1a; + --link-hover: #2a2a2a; + --border-color: #333333; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: sans-serif; + background: var(--bg-color); + color: var(--text-color); + line-height: 1.5; +} + +header { + text-align: center; + padding: 2rem 1rem; + max-width: 600px; + margin: 0 auto; +} + +header img { + border: 1px solid var(--border-color); +} + +header h1 { + margin: 1rem 0 0.5rem; + font-size: 1.5rem; + color: var(--text-color); +} + +header p { + margin: 0.5rem 0; + color: var(--text-secondary); +} + +main { + max-width: 600px; + margin: 0 auto; + padding: 1rem; +} + +main ul { + list-style: none; + padding: 0; + margin: 0; +} + +main li { + margin-bottom: 0.5rem; +} + +main a { + display: block; + padding: 1rem; + background-color: var(--link-bg); + border: 1px solid var(--border-color); + text-decoration: none; + color: var(--text-color); + text-align: center; +} + +main a:hover { + background-color: var(--link-hover); +} + +main a i { + margin-right: 0.5rem; +} + +footer { + text-align: center; + padding: 2rem 1rem; + max-width: 600px; + margin: 0 auto; +} + +footer p { + margin: 0.5rem 0; + color: var(--text-secondary); + font-size: 0.9rem; +} diff --git a/themes/catppuccin-frappe.css b/themes/catppuccin-frappe.css new file mode 100644 index 0000000..bd81097 --- /dev/null +++ b/themes/catppuccin-frappe.css @@ -0,0 +1,138 @@ +/* catppuccin-frappe.css - Catppuccin Frappé theme for LinkBeam (with light/dark toggle) */ +/* Based on: https://github.com/catppuccin/catppuccin */ + +/* Default: Latte (light) */ +:root { + --bg-color: #eff1f5; + --text-color: #4c4f69; + --text-secondary: #6c6f85; + --link-bg: #e6e9ef; + --link-hover: #dce0e8; + --border-color: #ccd0da; + --accent-color: #7287fd; +} + +/* System dark mode: Switch to Frappé */ +@media (prefers-color-scheme: dark) { + :root { + --bg-color: #303446; + --text-color: #c6d0f5; + --text-secondary: #a5adce; + --link-bg: #414559; + --link-hover: #51576d; + --border-color: #626880; + --accent-color: #babbf1; + } +} + +/* Manual overrides */ +html[data-theme="light"] { + --bg-color: #eff1f5; + --text-color: #4c4f69; + --text-secondary: #6c6f85; + --link-bg: #e6e9ef; + --link-hover: #dce0e8; + --border-color: #ccd0da; + --accent-color: #7287fd; +} + +html[data-theme="dark"] { + --bg-color: #303446; + --text-color: #c6d0f5; + --text-secondary: #a5adce; + --link-bg: #414559; + --link-hover: #51576d; + --border-color: #626880; + --accent-color: #babbf1; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--bg-color); + color: var(--text-color); + line-height: 1.6; + min-height: 100vh; +} + +header { + text-align: center; + padding: 3rem 1rem 2rem; + max-width: 600px; + margin: 0 auto; +} + +header img { + border-radius: 50%; + border: 3px solid var(--border-color); + margin-bottom: 1.5rem; +} + +header h1 { + margin: 0.5rem 0; + font-size: 2rem; + font-weight: 700; + color: var(--accent-color); +} + +header p { + margin: 0.75rem 0; + color: var(--text-secondary); +} + +main { + max-width: 600px; + margin: 0 auto; + padding: 1rem 1rem 3rem; +} + +main ul { + list-style: none; + padding: 0; + margin: 0; +} + +main li { + margin-bottom: 1rem; +} + +main a { + display: block; + padding: 1rem 1.5rem; + background-color: var(--link-bg); + border: 2px solid var(--border-color); + border-radius: 8px; + text-decoration: none; + color: var(--text-color); + font-weight: 600; + text-align: center; + transition: all 0.2s ease; +} + +main a:hover { + background-color: var(--link-hover); + border-color: var(--accent-color); + transform: translateY(-2px); +} + +main a i { + margin-right: 0.5rem; +} + +footer { + text-align: center; + padding: 2rem 1rem 3rem; + max-width: 600px; + margin: 0 auto; +} + +footer p { + margin: 0.5rem 0; + color: var(--text-secondary); + font-size: 0.9rem; +} diff --git a/themes/catppuccin-latte.css b/themes/catppuccin-latte.css new file mode 100644 index 0000000..9d12dc5 --- /dev/null +++ b/themes/catppuccin-latte.css @@ -0,0 +1,138 @@ +/* catppuccin-latte.css - Catppuccin Latte theme for LinkBeam (with light/dark toggle) */ +/* Based on: https://github.com/catppuccin/catppuccin */ + +/* Default: Latte (light) */ +:root { + --bg-color: #eff1f5; + --text-color: #4c4f69; + --text-secondary: #6c6f85; + --link-bg: #e6e9ef; + --link-hover: #dce0e8; + --border-color: #ccd0da; + --accent-color: #7287fd; +} + +/* System dark mode: Switch to Mocha */ +@media (prefers-color-scheme: dark) { + :root { + --bg-color: #1e1e2e; + --text-color: #cdd6f4; + --text-secondary: #bac2de; + --link-bg: #313244; + --link-hover: #45475a; + --border-color: #585b70; + --accent-color: #b4befe; + } +} + +/* Manual overrides */ +html[data-theme="light"] { + --bg-color: #eff1f5; + --text-color: #4c4f69; + --text-secondary: #6c6f85; + --link-bg: #e6e9ef; + --link-hover: #dce0e8; + --border-color: #ccd0da; + --accent-color: #7287fd; +} + +html[data-theme="dark"] { + --bg-color: #1e1e2e; + --text-color: #cdd6f4; + --text-secondary: #bac2de; + --link-bg: #313244; + --link-hover: #45475a; + --border-color: #585b70; + --accent-color: #b4befe; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--bg-color); + color: var(--text-color); + line-height: 1.6; + min-height: 100vh; +} + +header { + text-align: center; + padding: 3rem 1rem 2rem; + max-width: 600px; + margin: 0 auto; +} + +header img { + border-radius: 50%; + border: 3px solid var(--border-color); + margin-bottom: 1.5rem; +} + +header h1 { + margin: 0.5rem 0; + font-size: 2rem; + font-weight: 700; + color: var(--accent-color); +} + +header p { + margin: 0.75rem 0; + color: var(--text-secondary); +} + +main { + max-width: 600px; + margin: 0 auto; + padding: 1rem 1rem 3rem; +} + +main ul { + list-style: none; + padding: 0; + margin: 0; +} + +main li { + margin-bottom: 1rem; +} + +main a { + display: block; + padding: 1rem 1.5rem; + background-color: var(--link-bg); + border: 2px solid var(--border-color); + border-radius: 8px; + text-decoration: none; + color: var(--text-color); + font-weight: 600; + text-align: center; + transition: all 0.2s ease; +} + +main a:hover { + background-color: var(--link-hover); + border-color: var(--accent-color); + transform: translateY(-2px); +} + +main a i { + margin-right: 0.5rem; +} + +footer { + text-align: center; + padding: 2rem 1rem 3rem; + max-width: 600px; + margin: 0 auto; +} + +footer p { + margin: 0.5rem 0; + color: var(--text-secondary); + font-size: 0.9rem; +} diff --git a/themes/catppuccin-macchiato.css b/themes/catppuccin-macchiato.css new file mode 100644 index 0000000..5bbf2fb --- /dev/null +++ b/themes/catppuccin-macchiato.css @@ -0,0 +1,138 @@ +/* catppuccin-macchiato.css - Catppuccin Macchiato theme for LinkBeam (with light/dark toggle) */ +/* Based on: https://github.com/catppuccin/catppuccin */ + +/* Default: Latte (light) */ +:root { + --bg-color: #eff1f5; + --text-color: #4c4f69; + --text-secondary: #6c6f85; + --link-bg: #e6e9ef; + --link-hover: #dce0e8; + --border-color: #ccd0da; + --accent-color: #7287fd; +} + +/* System dark mode: Switch to Macchiato */ +@media (prefers-color-scheme: dark) { + :root { + --bg-color: #24273a; + --text-color: #cad3f5; + --text-secondary: #a5adcb; + --link-bg: #363a4f; + --link-hover: #494d64; + --border-color: #5b6078; + --accent-color: #b7bdf8; + } +} + +/* Manual overrides */ +html[data-theme="light"] { + --bg-color: #eff1f5; + --text-color: #4c4f69; + --text-secondary: #6c6f85; + --link-bg: #e6e9ef; + --link-hover: #dce0e8; + --border-color: #ccd0da; + --accent-color: #7287fd; +} + +html[data-theme="dark"] { + --bg-color: #24273a; + --text-color: #cad3f5; + --text-secondary: #a5adcb; + --link-bg: #363a4f; + --link-hover: #494d64; + --border-color: #5b6078; + --accent-color: #b7bdf8; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--bg-color); + color: var(--text-color); + line-height: 1.6; + min-height: 100vh; +} + +header { + text-align: center; + padding: 3rem 1rem 2rem; + max-width: 600px; + margin: 0 auto; +} + +header img { + border-radius: 50%; + border: 3px solid var(--border-color); + margin-bottom: 1.5rem; +} + +header h1 { + margin: 0.5rem 0; + font-size: 2rem; + font-weight: 700; + color: var(--accent-color); +} + +header p { + margin: 0.75rem 0; + color: var(--text-secondary); +} + +main { + max-width: 600px; + margin: 0 auto; + padding: 1rem 1rem 3rem; +} + +main ul { + list-style: none; + padding: 0; + margin: 0; +} + +main li { + margin-bottom: 1rem; +} + +main a { + display: block; + padding: 1rem 1.5rem; + background-color: var(--link-bg); + border: 2px solid var(--border-color); + border-radius: 8px; + text-decoration: none; + color: var(--text-color); + font-weight: 600; + text-align: center; + transition: all 0.2s ease; +} + +main a:hover { + background-color: var(--link-hover); + border-color: var(--accent-color); + transform: translateY(-2px); +} + +main a i { + margin-right: 0.5rem; +} + +footer { + text-align: center; + padding: 2rem 1rem 3rem; + max-width: 600px; + margin: 0 auto; +} + +footer p { + margin: 0.5rem 0; + color: var(--text-secondary); + font-size: 0.9rem; +} diff --git a/themes/catppuccin-mocha.css b/themes/catppuccin-mocha.css new file mode 100644 index 0000000..62e71c8 --- /dev/null +++ b/themes/catppuccin-mocha.css @@ -0,0 +1,138 @@ +/* catppuccin-mocha.css - Catppuccin Mocha theme for LinkBeam (with light/dark toggle) */ +/* Based on: https://github.com/catppuccin/catppuccin */ + +/* Default: Latte (light) */ +:root { + --bg-color: #eff1f5; + --text-color: #4c4f69; + --text-secondary: #6c6f85; + --link-bg: #e6e9ef; + --link-hover: #dce0e8; + --border-color: #ccd0da; + --accent-color: #7287fd; +} + +/* System dark mode: Switch to Mocha */ +@media (prefers-color-scheme: dark) { + :root { + --bg-color: #1e1e2e; + --text-color: #cdd6f4; + --text-secondary: #bac2de; + --link-bg: #313244; + --link-hover: #45475a; + --border-color: #585b70; + --accent-color: #b4befe; + } +} + +/* Manual overrides */ +html[data-theme="light"] { + --bg-color: #eff1f5; + --text-color: #4c4f69; + --text-secondary: #6c6f85; + --link-bg: #e6e9ef; + --link-hover: #dce0e8; + --border-color: #ccd0da; + --accent-color: #7287fd; +} + +html[data-theme="dark"] { + --bg-color: #1e1e2e; + --text-color: #cdd6f4; + --text-secondary: #bac2de; + --link-bg: #313244; + --link-hover: #45475a; + --border-color: #585b70; + --accent-color: #b4befe; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--bg-color); + color: var(--text-color); + line-height: 1.6; + min-height: 100vh; +} + +header { + text-align: center; + padding: 3rem 1rem 2rem; + max-width: 600px; + margin: 0 auto; +} + +header img { + border-radius: 50%; + border: 3px solid var(--border-color); + margin-bottom: 1.5rem; +} + +header h1 { + margin: 0.5rem 0; + font-size: 2rem; + font-weight: 700; + color: var(--accent-color); +} + +header p { + margin: 0.75rem 0; + color: var(--text-secondary); +} + +main { + max-width: 600px; + margin: 0 auto; + padding: 1rem 1rem 3rem; +} + +main ul { + list-style: none; + padding: 0; + margin: 0; +} + +main li { + margin-bottom: 1rem; +} + +main a { + display: block; + padding: 1rem 1.5rem; + background-color: var(--link-bg); + border: 2px solid var(--border-color); + border-radius: 8px; + text-decoration: none; + color: var(--text-color); + font-weight: 600; + text-align: center; + transition: all 0.2s ease; +} + +main a:hover { + background-color: var(--link-hover); + border-color: var(--accent-color); + transform: translateY(-2px); +} + +main a i { + margin-right: 0.5rem; +} + +footer { + text-align: center; + padding: 2rem 1rem 3rem; + max-width: 600px; + margin: 0 auto; +} + +footer p { + margin: 0.5rem 0; + color: var(--text-secondary); + font-size: 0.9rem; +} diff --git a/themes/gruvbox.css b/themes/gruvbox.css new file mode 100644 index 0000000..00be801 --- /dev/null +++ b/themes/gruvbox.css @@ -0,0 +1,247 @@ +/* gruvbox.css - Gruvbox theme for LinkBeam (Automatic light/dark) */ +/* Based on the Gruvbox color scheme: https://github.com/morhetz/gruvbox */ + +/* Default: Light mode */ +:root { + /* Gruvbox Light background colors */ + --bg-color: #fbf1c7; + --text-color: #3c3836; + --text-secondary: #504945; + --link-bg: #f9f5d7; + --link-hover: #f2e5bc; + --border-color: #d5c4a1; + + /* Gruvbox accent colors (green/aqua tones) */ + --accent-color: #689d6a; + --accent-light: #98971a; + + /* Shadows with warm tones for Gruvbox Light */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); + + /* Gradients using Gruvbox light shades */ + --bg-gradient-start: #fbf1c7; + --bg-gradient-end: #f9f5d7; + + /* Glow effects using Gruvbox green/aqua colors */ + --glow-color: rgba(104, 157, 106, 0.3); + --glow-hover: rgba(104, 157, 106, 0.2); + --shimmer: rgba(104, 157, 106, 0.1); +} + +/* System dark mode preference */ +@media (prefers-color-scheme: dark) { + :root { + /* Gruvbox Dark background colors */ + --bg-color: #282828; + --text-color: #ebdbb2; + --text-secondary: #d5c4a1; + --link-bg: #3c3836; + --link-hover: #504945; + --border-color: #665c54; + + /* Gruvbox accent colors (aqua/green tones) */ + --accent-color: #8ec07c; + --accent-light: #b8bb26; + + /* Shadows with warm tones for Gruvbox Dark */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.6); + + /* Gradients using Gruvbox dark shades */ + --bg-gradient-start: #282828; + --bg-gradient-end: #3c3836; + + /* Glow effects using Gruvbox green/aqua colors */ + --glow-color: rgba(142, 192, 124, 0.4); + --glow-hover: rgba(142, 192, 124, 0.3); + --shimmer: rgba(142, 192, 124, 0.15); + } +} + +/* Manual overrides (highest specificity) */ +html[data-theme="light"] { + /* Gruvbox Light background colors */ + --bg-color: #fbf1c7; + --text-color: #3c3836; + --text-secondary: #504945; + --link-bg: #f9f5d7; + --link-hover: #f2e5bc; + --border-color: #d5c4a1; + + /* Gruvbox accent colors (green/aqua tones) */ + --accent-color: #689d6a; + --accent-light: #98971a; + + /* Shadows with warm tones for Gruvbox Light */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); + + /* Gradients using Gruvbox light shades */ + --bg-gradient-start: #fbf1c7; + --bg-gradient-end: #f9f5d7; + + /* Glow effects using Gruvbox green/aqua colors */ + --glow-color: rgba(104, 157, 106, 0.3); + --glow-hover: rgba(104, 157, 106, 0.2); + --shimmer: rgba(104, 157, 106, 0.1); +} + +html[data-theme="dark"] { + /* Gruvbox Dark background colors */ + --bg-color: #282828; + --text-color: #ebdbb2; + --text-secondary: #d5c4a1; + --link-bg: #3c3836; + --link-hover: #504945; + --border-color: #665c54; + + /* Gruvbox accent colors (aqua/green tones) */ + --accent-color: #8ec07c; + --accent-light: #b8bb26; + + /* Shadows with warm tones for Gruvbox Dark */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.6); + + /* Gradients using Gruvbox dark shades */ + --bg-gradient-start: #282828; + --bg-gradient-end: #3c3836; + + /* Glow effects using Gruvbox green/aqua colors */ + --glow-color: rgba(142, 192, 124, 0.4); + --glow-hover: rgba(142, 192, 124, 0.3); + --shimmer: rgba(142, 192, 124, 0.15); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%); + background-attachment: fixed; + color: var(--text-color); + line-height: 1.6; + min-height: 100vh; +} + +header { + text-align: center; + padding: 3rem 1rem 2rem; + max-width: 600px; + margin: 0 auto; +} + +header img { + border-radius: 50%; + border: 4px solid var(--link-bg); + box-shadow: var(--shadow-lg), 0 0 0 1px var(--border-color); + margin-bottom: 1.5rem; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +header img:hover { + transform: scale(1.05); + box-shadow: var(--shadow-lg), 0 0 20px var(--glow-color); +} + +header h1 { + margin: 0.5rem 0; + font-size: 2rem; + font-weight: 700; + background: linear-gradient(135deg, var(--accent-color), var(--accent-light)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +header p { + margin: 0.75rem 0; + color: var(--text-secondary); + font-size: 1rem; + line-height: 1.5; +} + +main { + max-width: 600px; + margin: 0 auto; + padding: 1rem 1rem 3rem; +} + +main ul { + list-style: none; + padding: 0; + margin: 0; +} + +main li { + margin-bottom: 1rem; +} + +main a { + display: block; + padding: 1.25rem 1.5rem; + background-color: var(--link-bg); + border: 2px solid var(--border-color); + border-radius: 12px; + text-decoration: none; + color: var(--text-color); + font-weight: 600; + text-align: center; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: var(--shadow-sm); + position: relative; + overflow: hidden; +} + +main a::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, var(--shimmer), transparent); + transition: left 0.5s ease; +} + +main a:hover::before { + left: 100%; +} + +main a:hover { + background-color: var(--link-hover); + border-color: var(--accent-color); + transform: translateY(-4px) scale(1.02); + box-shadow: var(--shadow-md), 0 0 20px var(--glow-hover); +} + +main a:active { + transform: translateY(-2px) scale(1.01); + box-shadow: var(--shadow-sm); +} + +main a i { + margin-right: 0.5rem; +} + +footer { + text-align: center; + padding: 2rem 1rem 3rem; + max-width: 600px; + margin: 0 auto; +} + +footer p { + margin: 0; + color: var(--text-secondary); + font-size: 0.9rem; +} diff --git a/themes/nord.css b/themes/nord.css new file mode 100644 index 0000000..42249fc --- /dev/null +++ b/themes/nord.css @@ -0,0 +1,247 @@ +/* nord.css - Nord theme for LinkBeam (Automatic light/dark) */ +/* Based on the Nord color palette: https://www.nordtheme.com/ */ + +/* Default: Light mode */ +:root { + /* Nord Snow Storm (light whites/grays) */ + --bg-color: #eceff4; + --text-color: #2e3440; + --text-secondary: #3b4252; + --link-bg: #e5e9f0; + --link-hover: #d8dee9; + --border-color: #d8dee9; + + /* Nord Frost (blue accent colors) */ + --accent-color: #5e81ac; + --accent-light: #81a1c1; + + /* Shadows with lighter tones for Nord Light */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); + + /* Gradients using Nord Snow Storm shades */ + --bg-gradient-start: #eceff4; + --bg-gradient-end: #e5e9f0; + + /* Glow effects using Nord Frost colors */ + --glow-color: rgba(94, 129, 172, 0.3); + --glow-hover: rgba(94, 129, 172, 0.2); + --shimmer: rgba(94, 129, 172, 0.1); +} + +/* System dark mode preference */ +@media (prefers-color-scheme: dark) { + :root { + /* Nord Polar Night (dark blues/grays) */ + --bg-color: #2e3440; + --text-color: #eceff4; + --text-secondary: #d8dee9; + --link-bg: #3b4252; + --link-hover: #434c5e; + --border-color: #4c566a; + + /* Nord Frost (blue accent colors) */ + --accent-color: #88c0d0; + --accent-light: #8fbcbb; + + /* Shadows with darker tones for Nord */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.6); + + /* Gradients using Nord Polar Night shades */ + --bg-gradient-start: #2e3440; + --bg-gradient-end: #3b4252; + + /* Glow effects using Nord Frost colors */ + --glow-color: rgba(136, 192, 208, 0.4); + --glow-hover: rgba(136, 192, 208, 0.3); + --shimmer: rgba(136, 192, 208, 0.15); + } +} + +/* Manual overrides (highest specificity) */ +html[data-theme="light"] { + /* Nord Snow Storm (light whites/grays) */ + --bg-color: #eceff4; + --text-color: #2e3440; + --text-secondary: #3b4252; + --link-bg: #e5e9f0; + --link-hover: #d8dee9; + --border-color: #d8dee9; + + /* Nord Frost (blue accent colors) */ + --accent-color: #5e81ac; + --accent-light: #81a1c1; + + /* Shadows with lighter tones for Nord Light */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); + + /* Gradients using Nord Snow Storm shades */ + --bg-gradient-start: #eceff4; + --bg-gradient-end: #e5e9f0; + + /* Glow effects using Nord Frost colors */ + --glow-color: rgba(94, 129, 172, 0.3); + --glow-hover: rgba(94, 129, 172, 0.2); + --shimmer: rgba(94, 129, 172, 0.1); +} + +html[data-theme="dark"] { + /* Nord Polar Night (dark blues/grays) */ + --bg-color: #2e3440; + --text-color: #eceff4; + --text-secondary: #d8dee9; + --link-bg: #3b4252; + --link-hover: #434c5e; + --border-color: #4c566a; + + /* Nord Frost (blue accent colors) */ + --accent-color: #88c0d0; + --accent-light: #8fbcbb; + + /* Shadows with darker tones for Nord */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.6); + + /* Gradients using Nord Polar Night shades */ + --bg-gradient-start: #2e3440; + --bg-gradient-end: #3b4252; + + /* Glow effects using Nord Frost colors */ + --glow-color: rgba(136, 192, 208, 0.4); + --glow-hover: rgba(136, 192, 208, 0.3); + --shimmer: rgba(136, 192, 208, 0.15); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%); + background-attachment: fixed; + color: var(--text-color); + line-height: 1.6; + min-height: 100vh; +} + +header { + text-align: center; + padding: 3rem 1rem 2rem; + max-width: 600px; + margin: 0 auto; +} + +header img { + border-radius: 50%; + border: 4px solid var(--link-bg); + box-shadow: var(--shadow-lg), 0 0 0 1px var(--border-color); + margin-bottom: 1.5rem; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +header img:hover { + transform: scale(1.05); + box-shadow: var(--shadow-lg), 0 0 20px var(--glow-color); +} + +header h1 { + margin: 0.5rem 0; + font-size: 2rem; + font-weight: 700; + background: linear-gradient(135deg, var(--accent-color), var(--accent-light)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +header p { + margin: 0.75rem 0; + color: var(--text-secondary); + font-size: 1rem; + line-height: 1.5; +} + +main { + max-width: 600px; + margin: 0 auto; + padding: 1rem 1rem 3rem; +} + +main ul { + list-style: none; + padding: 0; + margin: 0; +} + +main li { + margin-bottom: 1rem; +} + +main a { + display: block; + padding: 1.25rem 1.5rem; + background-color: var(--link-bg); + border: 2px solid var(--border-color); + border-radius: 12px; + text-decoration: none; + color: var(--text-color); + font-weight: 600; + text-align: center; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: var(--shadow-sm); + position: relative; + overflow: hidden; +} + +main a::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, var(--shimmer), transparent); + transition: left 0.5s ease; +} + +main a:hover::before { + left: 100%; +} + +main a:hover { + background-color: var(--link-hover); + border-color: var(--accent-color); + transform: translateY(-4px) scale(1.02); + box-shadow: var(--shadow-md), 0 0 20px var(--glow-hover); +} + +main a:active { + transform: translateY(-2px) scale(1.01); + box-shadow: var(--shadow-sm); +} + +main a i { + margin-right: 0.5rem; +} + +footer { + text-align: center; + padding: 2rem 1rem 3rem; + max-width: 600px; + margin: 0 auto; +} + +footer p { + margin: 0; + color: var(--text-secondary); + font-size: 0.9rem; +}