diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..a8c61c7 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,66 @@ +name: Deploy documentation + +on: + push: + branches: + - main + paths: + - "apps/docs/**" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + - "package.json" + - ".github/workflows/deploy-docs.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 11.5.1 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build documentation + run: pnpm build:docs + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: apps/docs/dist + + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/apps/docs/astro.config.mjs b/apps/docs/astro.config.mjs new file mode 100644 index 0000000..ffb6ce5 --- /dev/null +++ b/apps/docs/astro.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + output: 'static', + site: 'https://ale0aranda.github.io', + base: '/github-community-wall' +}); diff --git a/apps/docs/package.json b/apps/docs/package.json new file mode 100644 index 0000000..32b22a9 --- /dev/null +++ b/apps/docs/package.json @@ -0,0 +1,18 @@ +{ + "name": "@community-wall/docs", + "private": true, + "type": "module", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "check": "astro check" + }, + "dependencies": { + "astro": "^7.3.8" + }, + "devDependencies": { + "@astrojs/check": "^0.9.10", + "typescript": "^5.9.3" + } +} diff --git a/apps/docs/public/images/contributors.png b/apps/docs/public/images/contributors.png new file mode 100644 index 0000000..6ad230a Binary files /dev/null and b/apps/docs/public/images/contributors.png differ diff --git a/apps/docs/public/images/followers.png b/apps/docs/public/images/followers.png new file mode 100644 index 0000000..ec8d932 Binary files /dev/null and b/apps/docs/public/images/followers.png differ diff --git a/apps/docs/public/images/opengraph.png b/apps/docs/public/images/opengraph.png new file mode 100644 index 0000000..5c3b106 Binary files /dev/null and b/apps/docs/public/images/opengraph.png differ diff --git a/apps/docs/src/components/CodeBlock.astro b/apps/docs/src/components/CodeBlock.astro new file mode 100644 index 0000000..47611d6 --- /dev/null +++ b/apps/docs/src/components/CodeBlock.astro @@ -0,0 +1,63 @@ +--- +import Icon from "./Icon.astro"; + +interface Props { + code: string; + label?: string; +} + +const { code, label = "Terminal command" } = Astro.props; +--- + +
+
{code}
+ + +
+ + diff --git a/apps/docs/src/components/Icon.astro b/apps/docs/src/components/Icon.astro new file mode 100644 index 0000000..cd66127 --- /dev/null +++ b/apps/docs/src/components/Icon.astro @@ -0,0 +1,34 @@ +--- +interface Props { + name: "github" | "copy" | "arrow" | "book" | "users" | "sun" | "moon"; + size?: number; +} + +const { name, size = 20 } = Astro.props; + +const paths: Record = { + github: + "M9 19c-4.3 1.3-4.3-2.2-6-2.7m12 5v-3.9c0-1.1-.4-1.8-.8-2.2 2.7-.3 5.5-1.3 5.5-6A4.7 4.7 0 0 0 18.4 6c.1-.3.6-1.6-.1-3.3 0 0-1-.3-3.4 1.3a11.8 11.8 0 0 0-6.2 0C6.3 2.4 5.3 2.7 5.3 2.7 4.6 4.4 5.1 5.7 5.2 6A4.7 4.7 0 0 0 4 9.2c0 4.7 2.8 5.7 5.5 6-.4.4-.8 1.1-.8 2.2V22", + copy: "M9 9h11v11H9z M15 9V4H4v11h5", + arrow: "M7 17 17 7M7 7h10v10", + book: "M12 5v16M12 5C9 3 5 3 2 4v15c3-1 7-1 10 2 3-3 7-3 10-2V4c-3-1-7-1-10 1Z", + users: + "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8M13 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z", + sun: "M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z", + moon: "M20.9 13A9 9 0 0 1 11 3.1 9 9 0 1 0 20.9 13Z", +}; +--- + + diff --git a/apps/docs/src/layouts/DocsLayout.astro b/apps/docs/src/layouts/DocsLayout.astro new file mode 100644 index 0000000..fb919e7 --- /dev/null +++ b/apps/docs/src/layouts/DocsLayout.astro @@ -0,0 +1,50 @@ +--- +import "../styles/global.css"; + +interface Props { + title?: string; + description?: string; +} + +const { + title = "GitHub Community Wall — Documentation", + description = "Generate avatar walls for your GitHub community. Learn how to install, configure, and use GitHub Community Wall.", +} = Astro.props; +--- + + + + + + + + + + + + + + + {title} + + + + + + + +
+ +
+ +

+ + diff --git a/apps/docs/src/pages/index.astro b/apps/docs/src/pages/index.astro new file mode 100644 index 0000000..8a13e42 --- /dev/null +++ b/apps/docs/src/pages/index.astro @@ -0,0 +1,345 @@ +--- +import CodeBlock from "../components/CodeBlock.astro"; +import Icon from "../components/Icon.astro"; +import DocsLayout from "../layouts/DocsLayout.astro"; + +const repository = "https://github.com/ale0aranda/github-community-wall"; +const baseUrl = import.meta.env.BASE_URL; + +const firstWall = [ + "npx github-community-wall contributors owner/repository \\", + " --output community-wall.png \\", + " --limit 100", +].join("\n"); + +const sources = [ + "npx github-community-wall followers octocat", + "npx github-community-wall members organization", +].join("\n"); + +const customWall = [ + "npx github-community-wall contributors owner/repository \\", + " --theme github-light \\", + " --shape circle \\", + " --format svg \\", + " --output community-wall.svg", +].join("\n"); + +const banner = [ + "npx github-community-wall followers octocat \\", + " --twitter-banner \\", + " --output followers-banner.png", +].join("\n"); + +const readme = [ + "npx github-community-wall readme contributors owner/repository \\", + " --output assets/contributors.png \\", + " --theme github-light", +].join("\n"); +--- + + +
+

GitHub Community Wall

+ +

+ Generate a visual wall featuring the people who are part of your GitHub + community. +

+ +

+ A command-line tool for creating images of followers, contributors, + sponsors, and organizations. Simple, configurable, and ready to share in + your README, website, or a banner. +

+ + + +

+ Open source, built to celebrate your community. +

+ + + + + ale0aranda/github-community-wall + + + + +
+ +
+

+ + Getting started +

+ +

+ You need Node.js 22.13 or later. You can run the tool with + npx without installing it globally, or install it if you will + use it frequently. +

+ +

+ The tool queries GitHub, downloads avatars, and arranges people in an + image. Choose the community, adjust the design, and decide where to save + the result. +

+ + + +
+

Connect to GitHub

+ +

+ Configure a GitHub token in your environment. The + GITHUB_TOKEN variable lets you query the API without + including credentials directly in commands. +

+ + + +

+ Replace the example value with your token. The required permissions + depend on the source; sponsors require access to the authenticated + maintainer's sponsorship data. +

+
+ +
+

Your first wall

+ +

+ To show the people who contribute to a repository, use + contributors followed by owner/repository. + Choose an output file and limit the number of avatars. +

+ + + +

+ Replace owner/repository with your repository. You will get + community-wall.png in the directory where you ran the + command: an image ready to publish. +

+ +

+ Contributor walls exclude bots by default. You can adjust the filters + to decide who appears. +

+
+ +
+

Choose your community

+ +

+ Every community has different ways to participate. In addition to + contributors, you can generate walls for followers, sponsors, + stargazers, watchers, and organization members. +

+ +

+ Use a user for followers and sponsors, a + repository for contributors, stargazers, and + watchers, or an organization for members. +

+ + +
+ +
+

Make it yours

+ +

+ Customize the theme, background, avatar shape, and spacing. Add titles, + subtitles, or a watermark to tailor the result to your project. +

+ +

+ PNG is the default format. You can also export to JPEG, WebP, SVG, HTML, + or JSON. HTML generates a standalone file; JSON includes avatar and + design metadata. +

+ + + +

+ If you use the same options repeatedly, save them in + .community-wall.json. Commands read this file from the + current directory, and terminal options take precedence. +

+ + +
+ +
+

Share and automate

+ +

+ Include the wall in your README, share it on your website, or use it in + a post. The --twitter-banner option generates a + 1500 × 500 follower banner for X. +

+ + + +

+ The readme command updates the wall block between its + markers. If they do not exist, it adds the block to the document. +

+ + + +

+ To keep the image up to date, you can run the tool on a schedule with + GitHub Actions. The repository includes an example workflow that + generates and saves the walls. +

+ + + View automation example + + +
+
+ +
+

+ + Your community +

+ +

+ Two ways to showcase the people who follow your work and help build it. +

+ +
+
+ + Avatar wall generated from GitHub followers + + +
+

Followers

+

People who follow your work.

+
+
+ +
+ + Avatar wall generated from repository contributors + + +
+

Contributors

+

People who build with you.

+
+
+
+
+ +
+

Need help?

+ +

+ Run --help to explore the options. Use + doctor to check your configuration and token without + displaying it. +

+ + + + +
+ +
+

Open source · MIT

+ +
+ + + +
+
+
+ + diff --git a/apps/docs/src/styles/global.css b/apps/docs/src/styles/global.css new file mode 100644 index 0000000..09354c8 --- /dev/null +++ b/apps/docs/src/styles/global.css @@ -0,0 +1,483 @@ +:root { + color-scheme: light; + + --background: #f8f9fb; + --surface: #ffffff; + --code-background: #f2f4f7; + --text: #172033; + --muted: #69758a; + --border: #e2e7ef; + --accent: #2675f5; + --accent-soft: #eaf2ff; + --shadow: 0 2px 8px rgb(23 32 51 / 3%); + + font-family: + Inter, Geist, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-synthesis: none; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +:root[data-theme="dark"] { + color-scheme: dark; + + --background: #101113; + --surface: #16181c; + --code-background: #181b21; + --text: #edf0f5; + --muted: #9aa4b5; + --border: #292e38; + --accent: #78a8ff; + --accent-soft: #202e46; + --shadow: none; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: 2rem; +} + +body { + margin: 0; + background: var(--background); + color: var(--text); + font-size: 15px; + line-height: 1.7; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +button { + font: inherit; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +a { + touch-action: manipulation; +} + +a:focus-visible, +button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 5px; +} + +::selection { + background: var(--accent-soft); + color: var(--text); +} + +.document { + width: min(100% - 3rem, 680px); + margin-inline: auto; + padding-block: 76px 32px; +} + +h1, +h2, +h3, +p, +figure { + margin: 0; +} + +h1, +h2, +h3 { + color: var(--text); + font-weight: 600; + line-height: 1.35; +} + +h1 { + margin-bottom: 14px; + font-size: clamp(25px, 4vw, 30px); + letter-spacing: -0.045em; +} + +h2 { + font-size: 20px; + letter-spacing: -0.035em; +} + +h3 { + margin-bottom: 9px; + font-size: 17px; + letter-spacing: -0.025em; +} + +p { + color: var(--muted); +} + +p+p { + margin-top: 12px; +} + +strong { + color: var(--text); + font-weight: 500; +} + +.lead { + margin-bottom: 9px; + font-size: 16px; + line-height: 1.65; +} + +.intro>.code-block { + margin-top: 24px; +} + +.repository-caption { + margin-top: 22px; + margin-bottom: 13px; +} + +.repository-link { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 17px 19px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--surface); + box-shadow: var(--shadow); + transition: background-color 150ms ease, border-color 150ms ease; +} + +.repository-link:hover { + border-color: var(--muted); + background: var(--code-background); +} + +.repository-name { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; + font-size: 14px; + font-weight: 500; +} + +.repository-name span { + overflow-wrap: anywhere; +} + +.repository-link>svg { + flex-shrink: 0; + color: var(--muted); +} + +.guide { + margin-top: 45px; +} + +.section-title { + display: flex; + align-items: center; + gap: 11px; + margin-bottom: 17px; +} + +.section-title svg { + flex-shrink: 0; + color: var(--accent); +} + +.topic { + margin-top: 31px; +} + +code { + font-family: + "SFMono-Regular", Consolas, "Liberation Mono", monospace; +} + +p code { + padding: 2px 4px; + border-radius: 4px; + background: var(--code-background); + color: var(--text); + font-size: 0.86em; + overflow-wrap: anywhere; +} + +.code-block { + position: relative; + margin-block: 16px; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface); + box-shadow: var(--shadow); +} + +.code-block pre { + margin: 0; + padding: 17px 54px 17px 18px; + overflow-x: auto; + color: var(--text); + font-size: 12px; + line-height: 1.8; + tab-size: 2; + scrollbar-width: thin; +} + +.copy-button { + position: absolute; + top: 12px; + right: 10px; + display: grid; + width: 32px; + height: 32px; + place-items: center; + border: 0; + border-radius: 7px; + background: var(--surface); + color: var(--muted); + cursor: pointer; + transition: color 150ms ease, background-color 150ms ease; +} + +.copy-button:hover { + background: var(--code-background); + color: var(--text); +} + +.copy-button[data-copied="true"] { + background: var(--accent-soft); + color: var(--accent); +} + +.note { + font-size: 13px; + line-height: 1.65; +} + +.text-link { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 5px; + margin-left: -5px; + border-radius: 5px; + color: var(--accent); + font-size: 13px; + transition: background-color 150ms ease; +} + +.text-link:hover { + background: var(--accent-soft); +} + +.topic>.text-link { + margin-top: 10px; +} + +.examples { + margin-top: 40px; +} + +.examples>p { + margin-bottom: 17px; +} + +.example-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.example { + overflow: hidden; + padding: 15px; + border: 1px solid var(--border); + border-radius: 13px; + background: var(--surface); + box-shadow: var(--shadow); +} + +.example>a { + display: grid; + height: 118px; + place-items: center; + overflow: hidden; + border-radius: 7px; + background: var(--code-background); +} + +.example img { + display: block; + width: 100%; + height: 100%; + object-fit: contain; +} + +.example figcaption { + padding-top: 14px; +} + +.example h3 { + margin-bottom: 4px; + font-size: 14px; +} + +.example p { + font-size: 12px; + line-height: 1.6; +} + +.help { + margin-top: 32px; +} + +.help h2 { + margin-bottom: 9px; + font-size: 17px; +} + +.help-links { + display: flex; + flex-wrap: wrap; + gap: 8px 22px; + margin-top: 12px; +} + +.footer { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + margin-top: 37px; +} + +.footer p { + font-size: 12px; +} + +.theme-switch { + display: flex; + gap: 3px; + padding: 4px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--surface); +} + +.theme-switch button { + display: grid; + width: 32px; + height: 28px; + place-items: center; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--muted); + cursor: pointer; +} + +.theme-switch button[aria-pressed="true"] { + background: var(--accent-soft); + color: var(--accent); +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + +.skip-link { + position: fixed; + top: 12px; + left: 12px; + z-index: 10; + padding: 8px 12px; + transform: translateY(-150%); + border-radius: 6px; + background: var(--surface); +} + +.skip-link:focus { + transform: translateY(0); +} + +@media (max-width: 600px) { + body { + font-size: 14px; + } + + .document { + width: calc(100% - 36px); + padding-top: 38px; + } + + .lead { + font-size: 15px; + } + + .repository-link { + padding: 15px; + } + + .repository-name { + gap: 9px; + font-size: 12px; + } + + .guide { + margin-top: 34px; + } + + .example-grid { + gap: 10px; + } + + .example { + padding: 10px; + } + + .example>a { + height: 90px; + } + + .example p { + font-size: 11px; + } +} + +@media (max-width: 360px) { + .example-grid { + grid-template-columns: 1fr; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + transition: none !important; + } +} diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json new file mode 100644 index 0000000..8bf91d3 --- /dev/null +++ b/apps/docs/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/biome.json b/biome.json index 7619314..2236dcd 100644 --- a/biome.json +++ b/biome.json @@ -1,6 +1,9 @@ { "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "extends": ["@ale0aranda/rules/biome/base", "@ale0aranda/rules/biome/node"], + "files": { + "includes": ["!apps/docs/**/*.astro", "!apps/docs/**/*.css"] + }, "linter": { "rules": { "complexity": {