diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..232b98d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,167 @@ +name: Build, test, and deploy + +on: + push: + branches: ['**'] + delete: + workflow_dispatch: + +permissions: + contents: write + deployments: write + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + if: github.event_name != 'delete' + runs-on: ubuntu-latest + outputs: + branch: ${{ steps.meta.outputs.branch }} + base_path: ${{ steps.meta.outputs.base_path }} + is_production: ${{ steps.meta.outputs.is_production }} + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile || bun install + + - name: Determine deploy metadata + id: meta + run: | + BRANCH="${GITHUB_REF_NAME}" + SANITIZED="$(echo "$BRANCH" | tr '/' '-' | tr -cd 'a-zA-Z0-9._-')" + if [ "$BRANCH" = "main" ]; then + echo "base_path=/" >> "$GITHUB_OUTPUT" + echo "branch=main" >> "$GITHUB_OUTPUT" + echo "is_production=true" >> "$GITHUB_OUTPUT" + else + echo "base_path=/preview/$SANITIZED/" >> "$GITHUB_OUTPUT" + echo "branch=$SANITIZED" >> "$GITHUB_OUTPUT" + echo "is_production=false" >> "$GITHUB_OUTPUT" + fi + + - name: Run unit and component tests + run: bun test tests/catalog tests/standards tests/views tests/enhancements tests/build + + - name: Build site + env: + SITE_BASE_URL: ${{ steps.meta.outputs.base_path }} + run: bun run build + + - name: Run a11y tests against built site + run: bun test tests/a11y + + - name: Upload dist artifact + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 3 + + publish: + if: github.event_name == 'push' + needs: build-and-test + runs-on: ubuntu-latest + environment: + name: ${{ needs.build-and-test.outputs.is_production == 'true' && 'production' || 'preview' }} + url: ${{ needs.build-and-test.outputs.is_production == 'true' && 'https://labs.flexion.us/' || format('https://labs.flexion.us/preview/{0}/', needs.build-and-test.outputs.branch) }} + steps: + - name: Check out or bootstrap gh-pages + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + git init gh-pages-work + cd gh-pages-work + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then + git fetch --depth=1 origin gh-pages + git checkout -B gh-pages FETCH_HEAD + else + echo 'gh-pages branch missing — creating orphan.' + git checkout --orphan gh-pages + git reset --hard + fi + + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist + + - name: Sync dist into gh-pages + run: | + set -euo pipefail + BASE_PATH="${{ needs.build-and-test.outputs.base_path }}" + cd gh-pages-work + if [ "$BASE_PATH" = "/" ]; then + find . -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'preview' -exec rm -rf {} + + cp -r ../dist/. ./ + else + rel="${BASE_PATH#/}" + rel="${rel%/}" + rm -rf "$rel" + mkdir -p "$(dirname "$rel")" + cp -r ../dist "$rel" + fi + if [ ! -f CNAME ]; then + cp ../CNAME . 2>/dev/null || true + fi + + - name: Commit and push gh-pages + working-directory: gh-pages-work + run: | + set -euo pipefail + git add -A + if git diff --cached --quiet; then + echo 'No changes to publish.' + exit 0 + fi + git commit -m "Deploy ${GITHUB_SHA::7} to ${{ needs.build-and-test.outputs.base_path }}" + git push origin gh-pages + + cleanup-preview: + if: github.event_name == 'delete' && github.event.ref_type == 'branch' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Check out gh-pages (skip if absent) + id: checkout + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + git init . + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then + git fetch --depth=1 origin gh-pages + git checkout -B gh-pages FETCH_HEAD + echo 'exists=true' >> "$GITHUB_OUTPUT" + else + echo 'gh-pages branch does not exist — nothing to clean up.' + echo 'exists=false' >> "$GITHUB_OUTPUT" + fi + + - name: Remove preview directory + if: steps.checkout.outputs.exists == 'true' + run: | + set -euo pipefail + SANITIZED="$(echo "${{ github.event.ref }}" | tr '/' '-' | tr -cd 'a-zA-Z0-9._-')" + DIR="preview/$SANITIZED" + if [ -d "$DIR" ]; then + rm -rf "$DIR" + git add -A + git commit -m "Remove preview for deleted branch $SANITIZED" + git push origin gh-pages + fi diff --git a/.github/workflows/refresh-catalog.yml b/.github/workflows/refresh-catalog.yml new file mode 100644 index 0000000..d8fb0e5 --- /dev/null +++ b/.github/workflows/refresh-catalog.yml @@ -0,0 +1,51 @@ +name: Refresh catalog + +on: + schedule: + - cron: '0 9 * * *' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + refresh: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - run: bun install --frozen-lockfile || bun install + + - name: Run refresh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bun run refresh:catalog + + - name: Open PR if snapshot changed + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + if git diff --quiet data/repos.json; then + echo 'No changes; nothing to do.' + exit 0 + fi + DATE="$(date -u +%F)" + BRANCH="catalog/refresh-$DATE" + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git checkout -B "$BRANCH" + git add data/repos.json + git commit -m "chore(catalog): refresh snapshot $DATE" + git push --force-with-lease origin "$BRANCH" + if ! gh pr view "$BRANCH" >/dev/null 2>&1; then + gh pr create --base main --head "$BRANCH" \ + --title "Refresh catalog snapshot — $DATE" \ + --body "Automated snapshot refresh from the GitHub API." + fi + gh pr merge "$BRANCH" --auto --squash diff --git a/.gitignore b/.gitignore index 06f80fc..cb3af15 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist/ .direnv/ .DS_Store *.log +bun.lock diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..3b0c5e5 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +labs.flexion.us diff --git a/README.md b/README.md index b4c82d6..9a54ba3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ -# flexion.github.io -flexion organization github pages +# Flexion Labs + +Source for [labs.flexion.us](https://labs.flexion.us/) — a public-facing site that showcases Flexion's open source portfolio, indexes our public repositories, and publishes our open source commitment. + +## Layout + +- `src/` — all source code: + - `src/build/` — the Bun + Hono SSG build driver. + - `src/catalog/` — catalog domain logic: types, loading, merging, stewardship evaluation, refresh script. + - `src/design/` — the design system: all stateless UI components, CSS, assets. + - `src/design/components/` — per-component directories (forms-lab pattern): `tag/`, `button/`, `link/`, `select/`, `card/`, `header/`, `footer/`, `repo-card/`, etc. + - Each component has `index.tsx` (JSX), `styles.css` (CSS), `examples.tsx` (design system showcase). + - CSS layers: `reset, tokens, base, compositions, layout, components, utilities`. + - `src/pages/` — route entrypoints that pass data into design components. +- `data/` — catalog data: generated snapshot (`repos.json`) and hand-authored overrides (`overrides.yml`). +- `content/` — the words we publish, as markdown. +- `docs/` — durable behavioral documentation for contributors and agents. +- `notes/` — ephemeral planning and specs. + +## Getting started + +```bash +bun install +bun run build # writes static site to dist/ +bun test # runs the full test suite +``` + +See `docs/README.md` for the project orientation. diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..8370a01 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["./tests/setup.ts"] diff --git a/content/about.md b/content/about.md new file mode 100644 index 0000000..468b4c5 --- /dev/null +++ b/content/about.md @@ -0,0 +1,15 @@ +--- +title: About +--- + +# About Flexion Labs + +Flexion Labs is where we publish our open source work. It's curated by Flexion — a firm that helps government agencies and nonprofits modernize software that matters. + +## Engage + +- **Adopt** a project. Read the README, try it, open an issue if something is unclear. +- **Contribute** a fix or an improvement. Every repo lists its contribution process. +- **Partner** with us on new public infrastructure. Reach out through the main Flexion site. + +[flexion.us](https://flexion.us/) is the home for Flexion the company. This site is the home for Flexion's open source work. diff --git a/content/commitment.md b/content/commitment.md new file mode 100644 index 0000000..d4ed2c4 --- /dev/null +++ b/content/commitment.md @@ -0,0 +1,52 @@ +--- +title: Open source commitment +--- + +# Flexion Labs open source commitment + +*Status: Working draft. Pending delivery leadership and ownership group review.* + +## Why we value openness + +Open source solves practical problems. For the government agencies we serve, it eliminates vendor lock-in, reduces sustainability risk, and lowers total cost of ownership. For Flexion, it creates options — the freedom to build on proven foundations, to redirect resources from infrastructure to the problems that matter, and to demonstrate technical capability in ways closed projects never can. + +Transparency improves quality. When code is open, more eyes find more bugs. Agencies and oversight bodies can audit how public systems work. The discipline of building in the open — knowing anyone can read the code — raises the bar for the work itself. + +Openness compounds value across jurisdictions. Investment in one agency's solution becomes infrastructure that others can adopt. This commons-building isn't overhead — it's how we create defensible competitive advantage while empowering agencies to control their technology. Open source attracts talent who value transparency and civic impact, and contributors can become hires. + +Openness also makes sense for the public. Publicly funded work should create reusable public infrastructure. Citizens benefit from transparency into the systems that serve them. And when agencies embrace open source, the result is higher public satisfaction with government technology. We don't ask anyone to take our word for it — the code is there to inspect, use, and build on. + +## What we commit to + +We are open by default. Code, architecture, and interfaces are open unless there's a specific reason otherwise — security concerns, client requirements, or competitive considerations. We will never take an open source project to a closed model. Once we release something as open source, it remains available under that license. + +We use licensing appropriate to each project's context. Government-funded work uses public domain (CC0) to maximize reuse and avoid contractual friction. Commercial client work follows client preferences, typically proposing public domain. For projects where Flexion makes significant independent investment and maintains long-term stewardship, we use permissive open source licenses (Apache 2.0) with contributor agreements that preserve flexibility for the project's future. Small enhancements to existing open source projects follow those projects' conventions. + +We commit to clear, professional repository standards: proper documentation, contributor guidance, legal terms, and getting-started materials. Code alone isn't enough — a project that's hard to understand or adopt isn't truly open. + +Our competitive advantage comes from delivery expertise and proven capabilities, not from hiding code. We compete by being better at building, deploying, and supporting solutions — not by locking them down. + +## How we operate + +We develop in the open for projects intended to be open source. Public by default means the work is visible from the start, not just after delivery. We are mindful of security considerations and maintain clear vulnerability disclosure processes. + +We govern our projects incrementally, starting with Flexion authority and evolving toward broader community governance as projects mature and attract use. For client work, the governance model depends on context — for traditional client engagements, operational decisions belong to the empowered client; for Flexion Solutions, Flexion governs directly. + +Contribution processes are documented and all work is done through pull requests. We welcome contributions with clear guidance. We build our stewardship practices by dogfooding them on internal projects first, so we learn what works before applying it externally. + +We recognize that governance stagnation — projects going dormant because no one takes ownership — is a primary risk of open source. We address this through honest communication about support levels, explicit project tiers, and a commitment to never let projects silently decay. If a project is no longer actively maintained, we say so clearly. + +## What we steward + +We recognize that resources are finite and not all projects warrant the same level of investment. We support a tiered approach to stewardship, with honest communication about what we will and won't provide for each project. + +For actively maintained projects, we commit to security patch management and defined response commitments. For projects available as-is, we say so clearly. We don't over-promise maintenance. Abandoned repositories damage credibility and create a security risk — if we can't maintain something, we explicitly archive it rather than letting it decay silently. + +Open source work is funded primarily through consulting engagements with clients who value openness. We also make strategic investments in projects where the business case warrants it — where open source creates market opportunities, builds community, or strengthens civic infrastructure. Different projects use different funding models: traditional hourly billing, retainers, maintenance within team structures, grants, and partnerships. + +## Maintenance tiers + +- **Active** — Flexion commits to security patch management and defined response commitments. Bug reports are triaged. Pull requests are reviewed on a predictable cadence. +- **As-is** — Available without active maintenance. The code works (or worked at one point); future updates are not promised. +- **Archived** — No longer maintained. The GitHub archive flag is set. The repo is read-only. Listed for transparency. +- **Unreviewed** — A human has not yet classified this repo. Defaults to this state; visible on the site so gaps are honest. diff --git a/content/home.md b/content/home.md new file mode 100644 index 0000000..b61bbaa --- /dev/null +++ b/content/home.md @@ -0,0 +1,4 @@ +--- +hero: Public infrastructure, in the open. +intro: Flexion Labs gathers our open source work in one place — products we steward, tools we share, and the commitment behind them. +--- diff --git a/content/work/document-extractor.md b/content/work/document-extractor.md new file mode 100644 index 0000000..fd9fb45 --- /dev/null +++ b/content/work/document-extractor.md @@ -0,0 +1,20 @@ +--- +title: Document Extractor +summary: Turn PDFs and images of forms into structured data — without vendor lock-in. +--- + +## What it solves + +Agencies receive millions of scanned forms every year and pay commercial vendors to turn them into structured data. Document Extractor provides an open alternative that agencies can deploy, audit, and extend. + +## Who it's for + +Teams modernizing paper-based intake workflows. Works on scanned PDFs, phone-camera photos, and faxes. + +## Status + +Active. Deployed with agency partners. + +## Get started + +The repository documents supported document types, extraction models, and how to extend the pipeline. diff --git a/content/work/forms.md b/content/work/forms.md new file mode 100644 index 0000000..213e7bc --- /dev/null +++ b/content/work/forms.md @@ -0,0 +1,20 @@ +--- +title: Forms +summary: Accessible, USWDS-aligned form experiences that work for every resident and every agency. +--- + +## What it solves + +Public forms are the front door to government services — and often the worst-performing part of the experience. Forms focuses on WCAG-conformant, plain-language, multi-step forms that reduce drop-off and produce clean data on submit. + +## Who it's for + +Agency teams building new forms or rehabilitating legacy ones. Works with the agency's existing back-end and identity stack. + +## Status + +Active. In use across multiple deployments. + +## Get started + +The repository includes component documentation, examples, and deployment guidance. diff --git a/content/work/messaging.md b/content/work/messaging.md new file mode 100644 index 0000000..e026d1e --- /dev/null +++ b/content/work/messaging.md @@ -0,0 +1,20 @@ +--- +title: Messaging +summary: A public-sector-grade platform for sending SMS and email notifications about benefits, deadlines, and outages. +--- + +## What it solves + +Public agencies need to reach residents quickly when something changes — a benefit status, an appointment, a service outage. Existing commercial messaging platforms are expensive, opaque about deliverability, and often lock agencies into per-message pricing that grows with the population they serve. + +## Who it's for + +State and local agencies that want self-hosted, auditable messaging infrastructure with clear deliverability reporting. + +## Status + +Active. Used in production with multiple agency partners. + +## Get started + +Read the README on GitHub for the deployment guide and API reference. diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..78517db --- /dev/null +++ b/data/README.md @@ -0,0 +1,16 @@ +# Catalog + +The catalog is the inventory of Flexion's open source work. It drives every view on `labs.flexion.us`. + +## Files + +- `repos.json` — a machine-generated snapshot from the GitHub API. Rewritten daily by the `refresh-catalog` workflow. +- `overrides.yml` — hand-authored metadata keyed by repo name. Each entry may set `tier`, `category`, `featured`, and `hidden`. + +## Fields + +See `src/catalog/types.ts` for the canonical type. The fields that humans set are described in `docs/catalog.md`. + +## Refresh cadence + +The `refresh-catalog` workflow runs daily at 09:00 UTC. If the resulting snapshot differs from the committed one, it opens a PR. If CI is green, the PR auto-merges. diff --git a/data/overrides.yml b/data/overrides.yml new file mode 100644 index 0000000..72d7250 --- /dev/null +++ b/data/overrides.yml @@ -0,0 +1,2 @@ +# Hand-authored overrides keyed by repo name. +# See docs/catalog.md for supported fields. diff --git a/data/repos.json b/data/repos.json new file mode 100644 index 0000000..5319aad --- /dev/null +++ b/data/repos.json @@ -0,0 +1,1097 @@ +[ + { + "name": "10x-nad-st", + "description": "National Address Database Submission Tool", + "url": "https://github.com/flexion/10x-nad-st", + "homepage": "", + "language": null, + "license": "CC0-1.0", + "pushedAt": "2025-03-07T17:47:03Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "10x-uswds-dataviz", + "description": null, + "url": "https://github.com/flexion/10x-uswds-dataviz", + "homepage": null, + "language": null, + "license": null, + "pushedAt": "2020-03-11T21:53:07Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": true, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "10x-uswds-forms", + "description": "10x USWDS Advanced Form Controls (was USWDS Forms Experience)", + "url": "https://github.com/flexion/10x-uswds-forms", + "homepage": "", + "language": null, + "license": null, + "pushedAt": "2020-03-17T15:47:25Z", + "archived": false, + "fork": false, + "stars": 2, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "asset-manager", + "description": "A generic asset manager, written in Django", + "url": "https://github.com/flexion/asset-manager", + "homepage": "", + "language": "Python", + "license": null, + "pushedAt": "2014-08-11T22:21:14Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": true, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "aws-codebuild-runner-project-tf-module", + "description": "A Terraform module to build a codebuild hosted runner project", + "url": "https://github.com/flexion/aws-codebuild-runner-project-tf-module", + "homepage": null, + "language": "HCL", + "license": null, + "pushedAt": "2026-03-06T22:13:04Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "bash_shell_script_starter", + "description": "Starter / template for writing Bash scripts", + "url": "https://github.com/flexion/bash_shell_script_starter", + "homepage": "", + "language": "Shell", + "license": null, + "pushedAt": "2023-03-22T14:51:52Z", + "archived": false, + "fork": false, + "stars": 5, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "canvas-lms", + "description": "The open LMS by Instructure, Inc.", + "url": "https://github.com/flexion/canvas-lms", + "homepage": "https://github.com/instructure/canvas-lms/wiki", + "language": "Ruby", + "license": "AGPL-3.0", + "pushedAt": "2024-10-25T18:57:45Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "cdk-playground", + "description": null, + "url": "https://github.com/flexion/cdk-playground", + "homepage": null, + "language": "Dockerfile", + "license": null, + "pushedAt": "2021-04-19T02:54:11Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "cerebral", + "description": "Declarative state and side effects management for popular JavaScript frameworks", + "url": "https://github.com/flexion/cerebral", + "homepage": "http://cerebraljs.com", + "language": null, + "license": "MIT", + "pushedAt": "2021-09-18T06:49:29Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "cg-cli-tools", + "description": "A Github action for using CF CLI tools while deploying and managing apps on cloud.gov", + "url": "https://github.com/flexion/cg-cli-tools", + "homepage": null, + "language": "Shell", + "license": "NOASSERTION", + "pushedAt": "2020-07-02T19:35:42Z", + "archived": true, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "charlie", + "description": "18F's Slack bot, Charlie. Based on Hubot.", + "url": "https://github.com/flexion/charlie", + "homepage": "", + "language": null, + "license": "NOASSERTION", + "pushedAt": "2021-12-03T14:44:40Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "check-contributor-allowlist-action", + "description": null, + "url": "https://github.com/flexion/check-contributor-allowlist-action", + "homepage": null, + "language": "JavaScript", + "license": null, + "pushedAt": "2023-03-17T21:52:41Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "claude-domestique", + "description": "Claude Code plugin for strategic session workflow management - like a cycling domestique for your development process", + "url": "https://github.com/flexion/claude-domestique", + "homepage": null, + "language": "JavaScript", + "license": null, + "pushedAt": "2026-03-12T13:35:57Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "coffeescript-koans", + "description": "Koans to learn CoffeeScript", + "url": "https://github.com/flexion/coffeescript-koans", + "homepage": null, + "language": "JavaScript", + "license": "NOASSERTION", + "pushedAt": "2013-04-12T16:27:41Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "cqf-ruler", + "description": "CQL Measure Processing Component", + "url": "https://github.com/flexion/cqf-ruler", + "homepage": null, + "language": null, + "license": "Apache-2.0", + "pushedAt": "2020-09-05T06:09:46Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "custom-post-type-ui", + "description": "Admin UI for creating custom post types and custom taxonomies in WordPress", + "url": "https://github.com/flexion/custom-post-type-ui", + "homepage": null, + "language": null, + "license": null, + "pushedAt": "2014-10-14T14:27:37Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": true, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "defectdojo_api", + "description": "Python API library for DefectDojo", + "url": "https://github.com/flexion/defectdojo_api", + "homepage": null, + "language": "Python", + "license": "MIT", + "pushedAt": "2021-09-27T18:35:51Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "Destin-Design-Repo", + "description": "For the Destin Municipal Bid", + "url": "https://github.com/flexion/Destin-Design-Repo", + "homepage": null, + "language": null, + "license": null, + "pushedAt": "2026-04-03T17:37:03Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "Destin-Municipal-Prototype", + "description": "For the Destin Municipal Bid", + "url": "https://github.com/flexion/Destin-Municipal-Prototype", + "homepage": null, + "language": null, + "license": null, + "pushedAt": "2026-04-03T18:53:31Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "devops-deployment-metrics", + "description": "Generate DevOps deployment metrics from GitHub repositories using a GitHub Action workflow to deploy a product", + "url": "https://github.com/flexion/devops-deployment-metrics", + "homepage": "", + "language": "Python", + "license": "MIT", + "pushedAt": "2026-04-25T01:42:20Z", + "archived": false, + "fork": false, + "stars": 5, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "django", + "description": "The Web framework for perfectionists with deadlines.", + "url": "https://github.com/flexion/django", + "homepage": "https://www.djangoproject.com/", + "language": "Python", + "license": "BSD-3-Clause", + "pushedAt": "2015-03-03T00:57:34Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "django-axes", + "description": "Keep track of failed login attempts in Django-powered sites.", + "url": "https://github.com/flexion/django-axes", + "homepage": "", + "language": "Python", + "license": "MIT", + "pushedAt": "2014-07-10T20:21:10Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "django-ckeditor", + "description": "Django admin CKEditor integration.", + "url": "https://github.com/flexion/django-ckeditor", + "homepage": "", + "language": "JavaScript", + "license": "NOASSERTION", + "pushedAt": "2014-08-08T16:53:55Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "django-DefectDojo", + "description": "DefectDojo is an open-source application vulnerability correlation and security orchestration tool.", + "url": "https://github.com/flexion/django-DefectDojo", + "homepage": "https://www.defectdojo.org/", + "language": "HTML", + "license": "BSD-3-Clause", + "pushedAt": "2023-01-25T04:01:59Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "docker-karma-protractor", + "description": "dockerised karma-runner", + "url": "https://github.com/flexion/docker-karma-protractor", + "homepage": null, + "language": "Shell", + "license": "GPL-3.0", + "pushedAt": "2015-04-29T07:32:00Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": true, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "document-extractor", + "description": "Document Extractor to automate document data extraction with ML and OCR.", + "url": "https://github.com/flexion/document-extractor", + "homepage": "", + "language": "TypeScript", + "license": "CC0-1.0", + "pushedAt": "2025-06-05T15:13:35Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "ef-cms", + "description": "An Electronic Filing / Case Management System.", + "url": "https://github.com/flexion/ef-cms", + "homepage": null, + "language": null, + "license": null, + "pushedAt": "2025-08-20T14:09:04Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "ef-cms-ustc", + "description": "An Electronic Filing / Case Management System.", + "url": "https://github.com/flexion/ef-cms-ustc", + "homepage": "https://dawson.ustaxcourt.gov/", + "language": null, + "license": "NOASSERTION", + "pushedAt": "2025-08-26T22:24:33Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "epa-prototype", + "description": "A public repository for Flexion's EAP prototype response", + "url": "https://github.com/flexion/epa-prototype", + "homepage": null, + "language": "JavaScript", + "license": "CC0-1.0", + "pushedAt": "2015-12-31T01:42:09Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": true, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "flexcoins", + "description": null, + "url": "https://github.com/flexion/flexcoins", + "homepage": null, + "language": "GDScript", + "license": "MIT", + "pushedAt": "2026-04-18T20:51:24Z", + "archived": false, + "fork": false, + "stars": 1, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "flexion-ads-18f-response", + "description": "GSA 18F Agile Delivery Services Prototype Response (Pool 3)", + "url": "https://github.com/flexion/flexion-ads-18f-response", + "homepage": "http://pool3.18f.flexion.us/", + "language": "JavaScript", + "license": "NOASSERTION", + "pushedAt": "2017-02-27T16:31:13Z", + "archived": false, + "fork": false, + "stars": 2, + "hasReadme": true, + "hasLicense": true, + "hasContributing": true + }, + { + "name": "flexion-ads-18f-save-my-picnic", + "description": "GSA 18F Agile Delivery Services Prototype Response (Pool 2)", + "url": "https://github.com/flexion/flexion-ads-18f-save-my-picnic", + "homepage": "http://pool2.18f.flexion.us/", + "language": "CSS", + "license": "CC0-1.0", + "pushedAt": "2015-07-07T14:58:35Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": true, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "flexion-sig-security", + "description": "A collection of notes, research, proof of concept code, and issue tracking for various Flexion security initiatives.", + "url": "https://github.com/flexion/flexion-sig-security", + "homepage": null, + "language": null, + "license": null, + "pushedAt": "2023-03-15T15:23:38Z", + "archived": false, + "fork": false, + "stars": 2, + "hasReadme": true, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "flexion-sig-security-tf-modules", + "description": "A collection of Terraform modules for work and play amongst the Flexion security crowd!", + "url": "https://github.com/flexion/flexion-sig-security-tf-modules", + "homepage": null, + "language": null, + "license": null, + "pushedAt": "2020-11-13T02:17:43Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": true, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "flexion.github.io", + "description": "Flexion Labs", + "url": "https://github.com/flexion/flexion.github.io", + "homepage": "https://flexion.github.io/", + "language": "Nix", + "license": null, + "pushedAt": "2026-04-27T20:46:17Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": true, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "fs-intake-module", + "description": "Module for intake of special use applications for Forest Service Application Permits", + "url": "https://github.com/flexion/fs-intake-module", + "homepage": null, + "language": "TypeScript", + "license": "NOASSERTION", + "pushedAt": "2018-03-03T04:38:49Z", + "archived": false, + "fork": true, + "stars": 2, + "hasReadme": true, + "hasLicense": true, + "hasContributing": true + }, + { + "name": "hast-util-sanitize", + "description": "utility to sanitize hast nodes", + "url": "https://github.com/flexion/hast-util-sanitize", + "homepage": "https://unifiedjs.com", + "language": "JavaScript", + "license": "MIT", + "pushedAt": "2021-02-25T22:18:22Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "hubot-google-hangouts", + "description": "/hangout me command for Hubot", + "url": "https://github.com/flexion/hubot-google-hangouts", + "homepage": "", + "language": "CoffeeScript", + "license": "MIT", + "pushedAt": "2015-06-19T20:42:51Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": true, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "hud-disaster-data", + "description": "Working pilot of a portal that collects and displays Community Development Block Grant-Disaster Recovery (CDBG-DR) data.", + "url": "https://github.com/flexion/hud-disaster-data", + "homepage": "https://hud-disaster-data-staging.app.cloud.gov/#/", + "language": "HTML", + "license": "NOASSERTION", + "pushedAt": "2018-09-28T21:59:37Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": true, + "hasLicense": true, + "hasContributing": true + }, + { + "name": "javascript-koans", + "description": "Koans to learn Javascript", + "url": "https://github.com/flexion/javascript-koans", + "homepage": "", + "language": "JavaScript", + "license": "MIT", + "pushedAt": "2013-04-10T15:54:42Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "jubilant-computing-machine", + "description": "a pre-conifigured, containerized image of the Vale prose linter", + "url": "https://github.com/flexion/jubilant-computing-machine", + "homepage": "", + "language": "Shell", + "license": null, + "pushedAt": "2025-01-27T14:52:45Z", + "archived": false, + "fork": false, + "stars": 1, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "lookml", + "description": "Hold models used to run Looker", + "url": "https://github.com/flexion/lookml", + "homepage": null, + "language": "LookML", + "license": null, + "pushedAt": "2017-08-05T21:54:20Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "mini-project-1", + "description": null, + "url": "https://github.com/flexion/mini-project-1", + "homepage": null, + "language": null, + "license": null, + "pushedAt": "2014-06-09T21:53:03Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": true, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "mob-timer", + "description": null, + "url": "https://github.com/flexion/mob-timer", + "homepage": null, + "language": "Svelte", + "license": null, + "pushedAt": "2021-10-14T19:29:43Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "msab-arts-locator", + "description": null, + "url": "https://github.com/flexion/msab-arts-locator", + "homepage": null, + "language": "JavaScript", + "license": null, + "pushedAt": "2023-03-06T02:32:36Z", + "archived": false, + "fork": false, + "stars": 3, + "hasReadme": true, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "node-client", + "description": "A skeleton node HTTP client", + "url": "https://github.com/flexion/node-client", + "homepage": null, + "language": "JavaScript", + "license": null, + "pushedAt": "2018-05-17T15:55:19Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": true, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "node-js-crate", + "description": "node-js pallet crate", + "url": "https://github.com/flexion/node-js-crate", + "homepage": null, + "language": "Clojure", + "license": null, + "pushedAt": "2012-12-03T03:55:15Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": true, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "notifications-admin", + "description": "The UI of Notify.gov", + "url": "https://github.com/flexion/notifications-admin", + "homepage": "https://notify.gov", + "language": "Python", + "license": "NOASSERTION", + "pushedAt": "2026-03-05T23:25:41Z", + "archived": false, + "fork": true, + "stars": 1, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "notifications-api", + "description": "The API powering Notify.gov", + "url": "https://github.com/flexion/notifications-api", + "homepage": "", + "language": "Python", + "license": "NOASSERTION", + "pushedAt": "2026-03-02T17:50:52Z", + "archived": false, + "fork": true, + "stars": 1, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "odoo-ai-assistant", + "description": "AI-powered Odoo 18 documentation assistant with multi-model comparison and RAG metrics", + "url": "https://github.com/flexion/odoo-ai-assistant", + "homepage": null, + "language": "Python", + "license": null, + "pushedAt": "2026-04-20T23:03:41Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "open-webui", + "description": "User-friendly AI Interface (Supports Ollama, OpenAI API, ...)", + "url": "https://github.com/flexion/open-webui", + "homepage": "https://openwebui.com", + "language": "Python", + "license": "NOASSERTION", + "pushedAt": "2026-04-20T17:29:25Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "opencode", + "description": "The open source coding agent.", + "url": "https://github.com/flexion/opencode", + "homepage": "https://opencode.ai", + "language": "TypeScript", + "license": "MIT", + "pushedAt": "2026-04-27T19:21:37Z", + "archived": false, + "fork": true, + "stars": 2, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "openwork", + "description": "An open-source alternative to Claude Cowork built for teams, powered by opencode", + "url": "https://github.com/flexion/openwork", + "homepage": "https://openworklabs.com", + "language": "TypeScript", + "license": "NOASSERTION", + "pushedAt": "2026-04-21T12:54:39Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "pa11y-ci", + "description": "Pa11y CI is a CI-centric accessibility test runner, built using Pa11y", + "url": "https://github.com/flexion/pa11y-ci", + "homepage": "", + "language": "JavaScript", + "license": "LGPL-3.0", + "pushedAt": "2023-03-28T14:40:48Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "pallet", + "description": "Automates controlling and provisioning cloud server instances. DevOps for the JVM.", + "url": "https://github.com/flexion/pallet", + "homepage": "http://palletops.com", + "language": "Clojure", + "license": null, + "pushedAt": "2013-04-12T03:15:17Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": true, + "hasLicense": false, + "hasContributing": true + }, + { + "name": "pig-latin-cli", + "description": "Target test app", + "url": "https://github.com/flexion/pig-latin-cli", + "homepage": null, + "language": "JavaScript", + "license": null, + "pushedAt": "2026-03-05T12:28:13Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "Planet-Express", + "description": "Rackspace navigator, because Supernova is super silly", + "url": "https://github.com/flexion/Planet-Express", + "homepage": null, + "language": "Python", + "license": "MIT", + "pushedAt": "2015-01-26T19:45:43Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": true, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "pre-commit-java", + "description": "A collection of git hooks for Java to be used with the pre-commit framework.", + "url": "https://github.com/flexion/pre-commit-java", + "homepage": "", + "language": "Shell", + "license": "GPL-3.0", + "pushedAt": "2024-06-02T09:33:26Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "PyDrive2", + "description": "Google Drive API Python wrapper library. Maintained fork of PyDrive.", + "url": "https://github.com/flexion/PyDrive2", + "homepage": "https://docs.iterative.ai/PyDrive2", + "language": "Python", + "license": "NOASSERTION", + "pushedAt": "2022-05-13T14:43:28Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "qpp-design-exercise", + "description": "QPP Design Exercise for CMS", + "url": "https://github.com/flexion/qpp-design-exercise", + "homepage": null, + "language": "CSS", + "license": null, + "pushedAt": "2017-03-01T17:29:01Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": true, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "radar", + "description": null, + "url": "https://github.com/flexion/radar", + "homepage": null, + "language": "CSS", + "license": null, + "pushedAt": "2024-02-02T12:11:26Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "reportstream-sftp-ingestion", + "description": "Allow healthcare partners use SFTP to send messages to CDC ReportStream.", + "url": "https://github.com/flexion/reportstream-sftp-ingestion", + "homepage": "", + "language": null, + "license": "Apache-2.0", + "pushedAt": "2025-01-16T22:01:31Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "serverless-dynamodb-autoscaling", + "description": "Serverless Plugin for Amazon DynamoDB Auto Scaling configuration.", + "url": "https://github.com/flexion/serverless-dynamodb-autoscaling", + "homepage": "https://sbstjn.com/serverless-dynamodb-auto-scaling-with-cloudformation.html", + "language": "TypeScript", + "license": "MIT", + "pushedAt": "2018-08-08T16:26:44Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": true, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "strapi-provider-upload-aws-s3-auth", + "description": null, + "url": "https://github.com/flexion/strapi-provider-upload-aws-s3-auth", + "homepage": null, + "language": "JavaScript", + "license": "NOASSERTION", + "pushedAt": "2020-10-30T12:43:40Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "tech-radar-generator", + "description": null, + "url": "https://github.com/flexion/tech-radar-generator", + "homepage": null, + "language": null, + "license": "MIT", + "pushedAt": "2024-04-23T23:45:34Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "trusted-intermediary", + "description": "Bringing together healthcare partners by reducing the connection burden.", + "url": "https://github.com/flexion/trusted-intermediary", + "homepage": "", + "language": null, + "license": "Apache-2.0", + "pushedAt": "2025-01-16T21:19:47Z", + "archived": false, + "fork": true, + "stars": 1, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "typescript-collab-fork", + "description": "Exercism exercises in TypeScript.", + "url": "https://github.com/flexion/typescript-collab-fork", + "homepage": "https://exercism.org/tracks/typescript", + "language": "JavaScript", + "license": "MIT", + "pushedAt": "2026-01-15T17:54:53Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "uswds", + "description": "The U.S. Web Design System helps the federal government build fast, accessible, mobile-friendly websites.", + "url": "https://github.com/flexion/uswds", + "homepage": "https://designsystem.digital.gov", + "language": "SCSS", + "license": "NOASSERTION", + "pushedAt": "2025-08-22T17:10:03Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "uswds-elements", + "description": "USWDS Elements", + "url": "https://github.com/flexion/uswds-elements", + "homepage": "https://federalist-ab6c0bdb-eccd-4b26-bb5f-b0154661e999.sites.pages.cloud.gov/site/uswds/web-components/", + "language": "CSS", + "license": null, + "pushedAt": "2025-12-18T21:03:05Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "uswds-react", + "description": null, + "url": "https://github.com/flexion/uswds-react", + "homepage": null, + "language": "SCSS", + "license": null, + "pushedAt": "2022-05-16T22:31:10Z", + "archived": false, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "uswds-sandbox", + "description": "11ty site for rapid web prototyping and testing new work with USWDS", + "url": "https://github.com/flexion/uswds-sandbox", + "homepage": "", + "language": null, + "license": null, + "pushedAt": "2025-07-07T13:19:50Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + }, + { + "name": "uswds-site", + "description": "The U.S. Web Design System documentation website, a design system for the federal government.", + "url": "https://github.com/flexion/uswds-site", + "homepage": "https://designsystem.digital.gov", + "language": "HTML", + "license": "NOASSERTION", + "pushedAt": "2025-08-22T17:07:34Z", + "archived": false, + "fork": true, + "stars": 0, + "hasReadme": false, + "hasLicense": true, + "hasContributing": false + }, + { + "name": "uswds-usability-test", + "description": null, + "url": "https://github.com/flexion/uswds-usability-test", + "homepage": null, + "language": "HTML", + "license": null, + "pushedAt": "2020-07-21T03:15:17Z", + "archived": true, + "fork": false, + "stars": 0, + "hasReadme": false, + "hasLicense": false, + "hasContributing": false + } +] diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..bd6b2ca --- /dev/null +++ b/docs/README.md @@ -0,0 +1,19 @@ +# Flexion Labs — project docs + +This directory explains how the site behaves and how to work in it. It is written for humans and for agentic tools equally — both should be able to read a page here and reconstruct the intent of the code without opening the code. + +## Structure + +- **`views/`** — one file per page describing its behavior; maps to `src/web/pages/` and `tests/views/`. +- **`catalog.md`** — what the catalog is, how it's assembled, and how to change it. +- **`content.md`** — how to author prose: hero, commitment, about, per-repo overlays. +- **`stewardship.md`** — tier definitions and how the health checks are evaluated. +- **`deployment.md`** — branch previews, production, the gh-pages layout, and the CNAME. +- **`testing.md`** — the TDD strategy, where tests live, and how to run each suite. +- **`styling.md`** — design tokens, cascade layers, and component conventions. + +## Working in this project + +- Source code lives under `src/` with four top-level concerns: `build/` (SSG tooling), `catalog/` (domain logic + stewardship evaluation), `design/` (the design system — all stateless UI components, CSS, assets), and `pages/` (route entrypoints). Data and content stay at the repo root (`data/`, `content/`). Each design component has its own directory with `index.tsx`, `styles.css`, and `examples.tsx`. +- TDD is the norm. Pure logic (catalog, standards) is covered by unit tests; views have behavior tests against a shared fixture catalog; the build has a smoke test; a11y is scanned with axe-core. +- Commits are small and focused. Every task in the implementation plan ends with one. diff --git a/docs/catalog.md b/docs/catalog.md new file mode 100644 index 0000000..5fec84d --- /dev/null +++ b/docs/catalog.md @@ -0,0 +1,38 @@ +# Catalog + +The catalog is the inventory of Flexion's open source work. Every view on the site reads from it. + +## Sources + +1. **`data/repos.json`** — machine-generated snapshot from the GitHub API. Rewritten daily by the refresh workflow. Fields match `GithubSnapshotEntry` in `src/catalog/types.ts`. +2. **`data/overrides.yml`** — hand-authored metadata keyed by repo name. Fields: `tier`, `category`, `featured`, `hidden`. Entries are PR-reviewed. +3. **`content/work/.md`** — optional rich copy for a repo's detail page. Front-matter supplies `title` and `summary`; the body is rendered into the page. + +## Merging + +`src/catalog/load.ts` reads all three, merges them through `mergeCatalog`, applies defaults (`applyDefaults`), and returns a `Catalog` array. Every view builds from this value. + +## Defaults + +When `overrides.yml` has no entry for a repo, defaults are derived per field. Archived takes precedence over fork for tier: + +- **Category**: `fork: true` → `'fork'`; otherwise `'uncategorized'`. +- **Tier**: `archived: true` → `'archived'`; else `fork: true` → `'as-is'`; else `'unreviewed'`. + +Overrides always win over either rule. + +"Unreviewed" is publicly visible. It says "a human has not yet classified this repo," which is honest and actionable. + +## Refresh pipeline + +The `refresh-catalog` workflow runs daily at 09:00 UTC (and on manual dispatch). It: + +1. Paginates `GET /orgs/flexion/repos?type=public`. +2. For each repo, checks for `README.md`, `LICENSE`, and `CONTRIBUTING.md` via the contents API. +3. Writes the result to `data/repos.json`, sorted alphabetically. +4. If the file is unchanged, exits cleanly. +5. Otherwise opens a PR on a `catalog/refresh-YYYY-MM-DD` branch and enables auto-merge. Green CI merges the PR; a human can always review and block. + +## Changing overrides + +Open a PR that edits `data/overrides.yml`. Typical changes: promoting a repo to `tier: active`, flagging a repo as `featured: true`, or hiding a repo temporarily while its content is being updated. diff --git a/docs/content.md b/docs/content.md new file mode 100644 index 0000000..1134fca --- /dev/null +++ b/docs/content.md @@ -0,0 +1,18 @@ +# Content authoring + +Prose lives in `content/`. Every file is markdown with optional YAML front-matter. + +## Files + +- **`content/home.md`** — front-matter only, carrying `hero` and `intro` for the home page. +- **`content/commitment.md`** — the full open source commitment. Front-matter: `title`. Body rendered into `/commitment/`. +- **`content/about.md`** — Flexion Labs' about page. Front-matter: `title`. Body rendered into `/about/`. +- **`content/work/.md`** — per-repo overlays for detail pages. Front-matter: `title`, `summary`. Body renders into the detail page's main content. + +## Voice + +Follow Flexion's brand voice: plain, professional, active, humble-not-shy. Use "we" to speak as Flexion; avoid first-person singular. Prefer concrete examples over abstractions. Avoid marketing copy, breathless claims, and undefined acronyms. + +## Updating the commitment + +The working-draft Google Doc is the source while the commitment is still under review. Once it's ratified, `content/commitment.md` is the canonical text and the Doc should defer to it. diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..058c486 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,35 @@ +# Deployment + +The site is hosted on GitHub Pages with a custom domain. + +## Branch layout + + gh-pages/ + index.html # main (production) + work/, commitment/, about/, work/health/ + preview/ + / + index.html + … + +- Production: `https://labs.flexion.us/` +- Preview: `https://labs.flexion.us/preview//` + +## Workflow + +`.github/workflows/deploy.yml` runs on every push: + +1. Installs Bun, runs the test suite, builds the site with `SITE_BASE_URL` set to `/` for `main` or `/preview//` for other branches. +2. Runs the axe-core a11y scan against the built output. +3. Checks out `gh-pages`, syncs the build output into the right directory, commits, and pushes. +4. Registers a GitHub Deployment against the `production` or `preview` environment so the URL appears in the PR "Deployments" panel. + +When a branch is deleted, a cleanup job removes `preview//` from `gh-pages`. + +## Custom domain + +`CNAME` at the source repo root contains `labs.flexion.us`. The workflow copies it to `gh-pages` if absent. + +## Base path + +`SITE_BASE_URL` is the single knob that changes how internal links and asset URLs are emitted. The default is `/`. Any non-root value is normalized to `/path/`. diff --git a/docs/stewardship.md b/docs/stewardship.md new file mode 100644 index 0000000..52a94f1 --- /dev/null +++ b/docs/stewardship.md @@ -0,0 +1,24 @@ +# Stewardship + +The site reports, publicly, how each repo measures up to Flexion's stewardship standards. + +## Tiers + +- **Active** — Flexion commits to security patch management and defined response times. Bug reports are triaged; pull requests are reviewed on a predictable cadence. +- **As-is** — Available without promised maintenance. Future updates are not guaranteed. +- **Archived** — No longer maintained. GitHub's archive flag is set. Listed for transparency. +- **Unreviewed** — A human has not yet classified this repo. Defaults to this state. + +## Checks + +Defined in `src/catalog/repo-checks.ts`. Every public repo is evaluated against: + +1. **README** — `README.md` at the root. +2. **License** — a detectable license (GitHub's license field or a LICENSE file). +3. **Contributing** — `CONTRIBUTING.md` at the root. +4. **Activity** — most recent push within 6 months (pass), 6–18 months (warn), older (fail). Archived repos pass by policy. +5. **Tier assigned** — tier is anything other than `unreviewed`. + +## Hiding per-repo failures + +Before launch, leadership may decide to show aggregate counts only. Set `SHOW_PER_REPO_FAILURES` to `false` in `src/catalog/repo-checks.ts` and redeploy; the health view will hide the per-repo table and display a short aggregate instead. diff --git a/docs/styling.md b/docs/styling.md new file mode 100644 index 0000000..b9a6a9f --- /dev/null +++ b/docs/styling.md @@ -0,0 +1,33 @@ +# Styling + +Hand-rolled CSS with cascade layers, design tokens, and container queries. No Sass, PostCSS, or utility frameworks. + +## Layers + +`src/design/index.css` declares the layer order: + + @layer reset, tokens, base, compositions, layout, components, utilities; + +Every rule belongs to one layer. Later layers override earlier ones with predictable precedence; specificity is rarely the tool you need. + +## Tokens + +Every Flexion brand color is defined in `src/design/tokens.css` as a custom property — even colors the v1 design does not use. Semantic tokens (`--color-ink`, `--color-link`, `--color-focus-ring`, `--color-tier-*`, `--color-pass/warn/fail`) compose the palette into meaning. When adding a component, prefer semantic tokens over palette tokens. + +## Components + +Component CSS is co-located with each component in `src/design/components//styles.css` under `@layer components`. Each component class is prefixed with its name (`.repo-card`, `.repo-card__summary`, etc.). BEM-style naming keeps specificity flat and collisions unlikely. + +## Container queries + +Components that reflow (catalog cards, featured strip, detail page columns) use `@container` queries rather than viewport queries so they adapt when embedded in a narrower parent. + +## Accessibility + +- Focus is visible on every interactive element (`:focus-visible` styled in `base.css`). +- Motion respects `prefers-reduced-motion`. +- Color pairings are AAA where feasible; AA minimum. When in doubt, check the Flexion palette PDF for approved combinations. + +## Progressive enhancement + +HTML Web Components in `src/design/components//client.ts` decorate existing HTML. CSS uses the `:not(:defined)` pseudo-class to hide bits that only make sense when JS has registered the component (e.g. the copy button). diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..02ed94c --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,28 @@ +# Testing + +Tests live in `tests/` and mirror the source layout. `bun test` runs everything. + +## Layers + +1. **Unit tests** for pure logic: catalog defaults, merge, overlay loader, standards evaluation, URL helpers, refresh snapshot-building. +2. **View behavior tests** that render each Hono JSX view against `tests/fixtures/catalog.ts` and assert on the DOM. +3. **Build smoke test** that runs the SSG end-to-end against a temp `outDir` and verifies every expected page exists. +4. **Enhancement tests** that exercise each HTML Web Component in a happy-dom environment. +5. **Accessibility scan** that runs axe-core against the rendered `dist/` pages. + +## TDD discipline + +Every new feature starts with a failing test. The implementation plan (`notes/plans/2026-04-27-flexion-labs-website.md`) lays out the red/green/refactor rhythm task by task. New work should follow the same rhythm. + +## Fixtures + +- `tests/fixtures/catalog.ts` — shared catalog used by most view tests. Expanding the fixture is welcome; view tests should not invent their own full catalogs. +- `tests/fixtures/overlays/` — on-disk markdown used by `loadOverlay` tests. + +## Running a single suite + +Use indented code blocks to avoid nested fences: + + bun test tests/catalog # all catalog unit tests + bun test tests/views/home.test.tsx + bun run build && bun test tests/a11y diff --git a/docs/views/about.md b/docs/views/about.md new file mode 100644 index 0000000..ccbd699 --- /dev/null +++ b/docs/views/about.md @@ -0,0 +1,23 @@ +# About (`/about/`) + +## Purpose + +Explains what Flexion Labs is, who maintains it, and how interested parties can engage — adopt, contribute, or partner — without going through a sales funnel. + +## Inputs + +- `body` — markdown body of `content/about.md`. +- `config` — base path, build time. + +## Behavior + +- **When the page loads, then** the markdown in `content/about.md` renders into `
`. +- **When the page loads, then** a link to the main Flexion site (`https://flexion.us/`) appears in the copy. + +## Fallbacks + +- None beyond content page rendering. + +## Tests + +`tests/views/content-page.test.tsx`. diff --git a/docs/views/commitment.md b/docs/views/commitment.md new file mode 100644 index 0000000..e047992 --- /dev/null +++ b/docs/views/commitment.md @@ -0,0 +1,23 @@ +# Commitment (`/commitment/`) + +## Purpose + +Publishes Flexion's open source commitment statement so anyone — agency, partner, contributor — can see exactly what Flexion commits to. + +## Inputs + +- `body` — markdown body of `content/commitment.md` (front-matter stripped). +- `config` — base path, build time. + +## Behavior + +- **When the page loads, then** the markdown in `content/commitment.md` renders into `
`. Headings produce `

`–`

`; paragraphs produce `

`. +- **When the `` is set, then** it uses the front-matter `title` ("Open source commitment" as of v1). + +## Fallbacks + +- The markdown file must exist; the build fails loudly if it is missing rather than rendering an empty page. + +## Tests + +`tests/views/content-page.test.tsx`. diff --git a/docs/views/health.md b/docs/views/health.md new file mode 100644 index 0000000..496fc65 --- /dev/null +++ b/docs/views/health.md @@ -0,0 +1,28 @@ +# Repo health (`/work/health/`) + +## Purpose + +Transparency surface. Publicly reports how each repo measures up to Flexion's stewardship standards. + +## Inputs + +- `catalog` — the merged catalog. +- `now` — current time, for the activity check. +- `config` — base path, build time. +- `showPerRepo` — `SHOW_PER_REPO_FAILURES` from `src/catalog/repo-checks.ts`. + +## Behavior + +- **When the page loads, then** the summary line reads `N of M repos meet the documented standards`, where `M` is the number of non-hidden repos and `N` is the count passing every check. +- **When `showPerRepo` is true, then** a table renders with one row per non-hidden repo and one column per check. Each cell carries a `health-cell--<result>` class reflecting `pass`, `warn`, or `fail`. +- **When `showPerRepo` is false, then** the table is absent and a short paragraph explains that per-repo details are hidden. +- **When a repo has `tier: archived`, then** the activity check passes regardless of `pushedAt`. +- **When a repo has `tier: unreviewed`, then** the tier-assigned check fails. + +## Fallbacks + +- No JavaScript → the table renders in the default server-side sort (snapshot order). With JavaScript, the `<sortable-table>` component turns column headers into sort controls. + +## Tests + +`tests/views/health.test.tsx` and `tests/enhancements/sortable-table.test.ts`. diff --git a/docs/views/home.md b/docs/views/home.md new file mode 100644 index 0000000..c2fa0d7 --- /dev/null +++ b/docs/views/home.md @@ -0,0 +1,27 @@ +# Home (`/`) + +## Purpose + +First impression for every visitor. Explains what Flexion Labs is, highlights featured labs, grounds the pitch with real numbers, and hands visitors off to the right next step. + +## Inputs + +- `catalog` — the merged catalog. +- `hero` — `{ hero, intro }` read from `content/home.md` front-matter. +- `config` — base path, build time. + +## Behavior + +- **When the page loads, then** the hero statement is the `<h1>`, followed by the intro paragraph. +- **When there are repos flagged `featured: true` and not hidden, then** one card is rendered per featured repo in the order they appear in the catalog. +- **When there are no featured repos, then** the featured section renders its heading and an empty grid (acceptable for v1; may be hardened later). +- **When the catalog has N non-hidden repos, then** the stats strip renders `N public projects`, the count of `tier: active` repos as `actively maintained`, and the count of distinct languages. +- **When the page loads, then** three audience paths link to `/work/`, `/commitment/`, and `/about/`. + +## Fallbacks + +- None — every section renders; empty collections produce empty grids. + +## Tests + +`tests/views/home.test.tsx` encodes each behavior above. diff --git a/docs/views/work-detail.md b/docs/views/work-detail.md new file mode 100644 index 0000000..4f672ba --- /dev/null +++ b/docs/views/work-detail.md @@ -0,0 +1,28 @@ +# Work detail (`/work/<slug>/`) + +## Purpose + +One page per public repository. Written for program managers and evaluators — lead with the problem and outcomes, link to the code for developers. + +## Inputs + +- `entry` — the specific `CatalogEntry`. +- `now` — current time, for the standards evaluation. +- `config` — base path, build time. + +## Behavior + +- **When the entry has an overlay with a `body`, then** the body is rendered as HTML inside the main column. +- **When the entry has no overlay but has a `summary` or GitHub `description`, then** that text is rendered as a single paragraph. +- **When the entry has none of the above, then** the main column shows "No description yet." +- **When the page loads, then** the header shows the title (`overlay.title` or repo name), tier badge, and category badge. +- **When the page loads, then** the aside shows the standards checklist (via `StandardsList`) and a definition list with language, license, and last push date. +- **When the entry has a non-null `homepage`, then** a "Homepage" link renders next to the "View on GitHub" link. + +## Fallbacks + +- A repo with no license field, no LICENSE file, and no README is still rendered; the standards list simply marks failures. + +## Tests + +`tests/views/work-detail.test.tsx`. diff --git a/docs/views/work-index.md b/docs/views/work-index.md new file mode 100644 index 0000000..51d2ea4 --- /dev/null +++ b/docs/views/work-index.md @@ -0,0 +1,26 @@ +# Work index (`/work/`) + +## Purpose + +The signature catalog view. Lists every public repository with enough context to decide whether to click through. + +## Inputs + +- `catalog` — the merged catalog. +- `config` — base path, build time. + +## Behavior + +- **When the page loads, then** one card renders per non-hidden catalog entry. +- **When an entry is hidden (`hidden: true`), then** it is absent from the DOM. The rendered HTML never contains a hidden repo's slug. +- **When the list renders, then** default sort is: `featured: true` first; then `tier: active`; then by `pushedAt` descending. Within featured entries the snapshot order is preserved. +- **When a user changes the tier or category select, then** the `<catalog-filter>` component hides list items whose `data-tier` / `data-category` does not match. With JavaScript disabled, the filter form still renders but does nothing; the full list remains visible. + +## Fallbacks + +- No JavaScript → filter chips are inert; full list is visible. +- Empty catalog → the list renders empty; the intro paragraph still explains what the page is. + +## Tests + +`tests/views/work-index.test.tsx` and `tests/enhancements/catalog-filter.test.ts`. diff --git a/notes/plans/2026-04-27-flexion-labs-website.md b/notes/plans/2026-04-27-flexion-labs-website.md index d7e1f77..af065ec 100644 --- a/notes/plans/2026-04-27-flexion-labs-website.md +++ b/notes/plans/2026-04-27-flexion-labs-website.md @@ -6,7 +6,7 @@ **Architecture:** Bun + Hono SSG renders every route to static HTML at build time. A committed JSON snapshot (refreshed daily from the GitHub API) plus a hand-authored YAML overrides file plus per-repo markdown overlays feed a single merged catalog. Hand-rolled CSS with cascade layers, container queries, and design tokens sourced from Flexion's brand palette. HTML Web Components wrap rendered HTML to add filter/sort/copy behaviors as progressive enhancement. GitHub Pages hosts production at the domain root and branch previews under `/preview/<branch>/`, surfaced via GitHub Deployments. -**Tech Stack:** Bun (runtime + package manager + test runner), TypeScript, Hono (with `@hono/ssg`), `hono/jsx` for components, `yaml` for overrides, `marked` for markdown, `happy-dom` for component tests, `@axe-core/playwright` or `axe-core` with `happy-dom` for a11y, GitHub Actions for CI. +**Tech Stack:** Bun (runtime + package manager + test runner), TypeScript, Hono (the SSG helper is a subpath export — `import { toSSG } from 'hono/ssg'` — no extra dependency needed), `hono/jsx` for components, `yaml` for overrides, `marked` for markdown, `happy-dom` for component tests, `axe-core` with `happy-dom` for a11y, GitHub Actions for CI. **Plan location convention:** Per user preferences, this plan is saved to `notes/plans/` rather than `docs/superpowers/plans/`. Ephemeral planning lives in `notes/`; durable behavioral docs live in `docs/`. @@ -151,7 +151,6 @@ Top-level domain layout (Screaming Architecture). Subdirectories reveal intent b }, "dependencies": { "hono": "^4.6.0", - "@hono/ssg": "^0.2.0", "marked": "^14.0.0", "yaml": "^2.6.0" }, diff --git a/notes/specs/2026-04-27-flexion-labs-website-design.md b/notes/specs/2026-04-27-flexion-labs-website-design.md index bdce5f4..1a46bc0 100644 --- a/notes/specs/2026-04-27-flexion-labs-website-design.md +++ b/notes/specs/2026-04-27-flexion-labs-website-design.md @@ -150,11 +150,12 @@ type CatalogEntry = { ### 3.3 Defaults -When `overrides.yml` has no entry for a repo, defaults are applied per field. Rules fire in order; a field set by an earlier rule is not overwritten: +When `overrides.yml` has no entry for a repo, defaults are derived per field with `archived` taking precedence over `fork` for tier: -1. `fork: true` → `category: 'fork'`, `tier: 'as-is'` -2. `archived: true` (on GitHub) → `tier: 'archived'` (category may already be set from rule 1; if not, it stays `'uncategorized'`) -3. No field set yet → `tier: 'unreviewed'`, `category: 'uncategorized'` +- **Category**: `fork: true` → `'fork'`; otherwise `'uncategorized'`. +- **Tier**: `archived: true` → `'archived'`; else `fork: true` → `'as-is'`; else `'unreviewed'`. + +Overrides always win over either rule. The "unreviewed" tier is publicly visible and honest — it says "a human has not yet classified this repo." diff --git a/package.json b/package.json new file mode 100644 index 0000000..54703e8 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "flexion-labs-website", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run src/build/entry.tsx --watch", + "build": "bun run src/build/entry.tsx", + "test": "bun test", + "refresh:catalog": "bun run src/catalog/refresh.ts" + }, + "dependencies": { + "hono": "^4.6.0", + "marked": "^14.0.0", + "yaml": "^2.6.0" + }, + "devDependencies": { + "@types/bun": "latest", + "happy-dom": "^15.0.0", + "axe-core": "^4.10.0", + "typescript": "^5.6.0" + } +} diff --git a/src/build/config.ts b/src/build/config.ts new file mode 100644 index 0000000..04ef140 --- /dev/null +++ b/src/build/config.ts @@ -0,0 +1,22 @@ +export function getBasePath(raw: string | undefined): string { + if (!raw || raw === '' || raw === '/') return '/' + const trimmed = raw.replace(/^\/+/, '').replace(/\/+$/, '') + return `/${trimmed}/` +} + +export function url(path: string, basePath: string): string { + const normalised = path.startsWith('/') ? path.slice(1) : path + return basePath + normalised +} + +export type SiteConfig = { + basePath: string + buildTime: string +} + +export function createConfig(env: NodeJS.ProcessEnv = process.env): SiteConfig { + return { + basePath: getBasePath(env.SITE_BASE_URL), + buildTime: new Date().toISOString(), + } +} diff --git a/src/build/entry.tsx b/src/build/entry.tsx new file mode 100644 index 0000000..f114f24 --- /dev/null +++ b/src/build/entry.tsx @@ -0,0 +1,163 @@ +import { mkdir, writeFile, copyFile, readdir, stat } from 'node:fs/promises' +import { join, dirname, relative } from 'node:path' +import { renderToHtml } from './render' +import { createConfig, getBasePath } from './config' +import { loadCatalog } from '../catalog/load' +import { loadHero } from './hero' +import { allRoutes } from './routes' +import { Home } from '../pages/home' +import { WorkIndex } from '../pages/work/index' +import { WorkDetail } from '../pages/work/detail' +import { Health } from '../pages/work/health' +import { Commitment } from '../pages/commitment' +import { About } from '../pages/about' +import { DesignSystem } from '../pages/design-system' +import { SHOW_PER_REPO_FAILURES } from '../catalog/repo-checks' + +export type BuildOptions = { + rootDir: string + outDir: string + basePath?: string + now?: Date +} + +export async function buildSite(options: BuildOptions): Promise<void> { + const rootDir = options.rootDir + const outDir = options.outDir + const now = options.now ?? new Date() + const config = { + basePath: options.basePath + ? getBasePath(options.basePath) + : createConfig(process.env).basePath, + buildTime: now.toISOString(), + } + + const [catalog, hero] = await Promise.all([ + loadCatalog(rootDir), + loadHero(rootDir), + ]) + const commitmentBody = await loadContentBody( + join(rootDir, 'content', 'commitment.md'), + ) + const aboutBody = await loadContentBody(join(rootDir, 'content', 'about.md')) + + const routes = allRoutes(catalog) + + for (const route of routes) { + const html = await render( + route, + catalog, + hero, + commitmentBody, + aboutBody, + config, + now, + ) + const outPath = join( + outDir, + route.path === '/' + ? 'index.html' + : route.path.replace(/^\//, '').replace(/\/$/, '/index.html'), + ) + await mkdir(dirname(outPath), { recursive: true }) + await writeFile(outPath, html, 'utf8') + } + + await copyCssTree(join(rootDir, 'src', 'design'), join(outDir, 'design')) + await Bun.build({ + entrypoints: [join(rootDir, 'src', 'design', 'register.ts')], + outdir: join(outDir, 'enhancements'), + target: 'browser', + naming: '[name].js', + minify: true, + sourcemap: 'linked', + }) + await copyTree(join(rootDir, 'src', 'design', 'assets'), join(outDir, 'assets')) +} + +async function render( + route: ReturnType<typeof allRoutes>[number], + catalog: Awaited<ReturnType<typeof loadCatalog>>, + hero: Awaited<ReturnType<typeof loadHero>>, + commitmentBody: string, + aboutBody: string, + config: { basePath: string; buildTime: string }, + now: Date, +): Promise<string> { + switch (route.view) { + case 'home': + return renderToHtml(<Home catalog={catalog} hero={hero} config={config} />) + case 'work-index': + return renderToHtml(<WorkIndex catalog={catalog} config={config} />) + case 'health': + return renderToHtml( + <Health + catalog={catalog} + now={now} + config={config} + showPerRepo={SHOW_PER_REPO_FAILURES} + />, + ) + case 'commitment': + return renderToHtml(<Commitment body={commitmentBody} config={config} />) + case 'about': + return renderToHtml(<About body={aboutBody} config={config} />) + case 'design-system': + return renderToHtml(<DesignSystem config={config} />) + case 'work-detail': { + const entry = catalog.find((e) => e.name === route.slug)! + return renderToHtml(<WorkDetail entry={entry} now={now} config={config} />) + } + } +} + +async function loadContentBody(path: string): Promise<string> { + const raw = await Bun.file(path).text() + return raw.replace(/^---\n[\s\S]*?\n---\n?/, '').trim() +} + +async function copyCssTree(src: string, dst: string): Promise<void> { + let entries: string[] + try { + entries = await readdir(src) + } catch { + return + } + await mkdir(dst, { recursive: true }) + for (const entry of entries) { + const from = join(src, entry) + const to = join(dst, entry) + const info = await stat(from) + if (info.isDirectory()) { + await copyCssTree(from, to) + } else if (entry.endsWith('.css')) { + await copyFile(from, to) + } + } +} + +async function copyTree(src: string, dst: string): Promise<void> { + let entries: string[] + try { + entries = await readdir(src) + } catch { + return + } + await mkdir(dst, { recursive: true }) + for (const entry of entries) { + const from = join(src, entry) + const to = join(dst, entry) + const info = await stat(from) + if (info.isDirectory()) { + await copyTree(from, to) + } else { + await copyFile(from, to) + } + } +} + +if (import.meta.main) { + const outDir = process.env.OUT_DIR ?? join(process.cwd(), 'dist') + await buildSite({ rootDir: process.cwd(), outDir }) + console.log(`Built site to ${relative(process.cwd(), outDir)}/`) +} diff --git a/src/build/hero.ts b/src/build/hero.ts new file mode 100644 index 0000000..886f0c5 --- /dev/null +++ b/src/build/hero.ts @@ -0,0 +1,16 @@ +import { parse as parseYaml } from 'yaml' +import { join } from 'node:path' +import type { HeroContent } from '../pages/home' + +const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/ + +export async function loadHero(rootDir: string): Promise<HeroContent> { + const file = Bun.file(join(rootDir, 'content', 'home.md')) + const raw = await file.text() + const match = raw.match(FRONTMATTER_RE) + const parsed = match ? (parseYaml(match[1]) as Record<string, unknown>) : {} + return { + hero: typeof parsed.hero === 'string' ? parsed.hero : 'Flexion Labs', + intro: typeof parsed.intro === 'string' ? parsed.intro : '', + } +} diff --git a/src/build/render.ts b/src/build/render.ts new file mode 100644 index 0000000..8dd1106 --- /dev/null +++ b/src/build/render.ts @@ -0,0 +1,8 @@ +import type { HtmlEscapedString } from 'hono/utils/html' + +export async function renderToHtml( + element: HtmlEscapedString | Promise<HtmlEscapedString>, +): Promise<string> { + const resolved = await Promise.resolve(element) + return '<!doctype html>\n' + resolved.toString() +} diff --git a/src/build/routes.ts b/src/build/routes.ts new file mode 100644 index 0000000..f6ca310 --- /dev/null +++ b/src/build/routes.ts @@ -0,0 +1,23 @@ +import type { Catalog } from '../catalog/types' + +export type Route = { + path: string // always starts with "/" and ends with "/" + view: 'home' | 'work-index' | 'work-detail' | 'health' | 'commitment' | 'about' | 'design-system' + slug?: string +} + +export function allRoutes(catalog: Catalog): Route[] { + const routes: Route[] = [ + { path: '/', view: 'home' }, + { path: '/work/', view: 'work-index' }, + { path: '/work/health/', view: 'health' }, + { path: '/commitment/', view: 'commitment' }, + { path: '/about/', view: 'about' }, + { path: '/design-system/', view: 'design-system' }, + ] + for (const entry of catalog) { + if (entry.hidden) continue + routes.push({ path: `/work/${entry.name}/`, view: 'work-detail', slug: entry.name }) + } + return routes +} diff --git a/src/catalog/defaults.ts b/src/catalog/defaults.ts new file mode 100644 index 0000000..33bab15 --- /dev/null +++ b/src/catalog/defaults.ts @@ -0,0 +1,45 @@ +import type { + Category, + GithubSnapshotEntry, + OverrideEntry, + Tier, +} from './types' + +type Resolved = { + tier: Tier + category: Category + featured: boolean + hidden: boolean +} + +export function applyDefaults( + snapshot: GithubSnapshotEntry, + override: OverrideEntry, +): Resolved { + let tier: Tier | undefined = override.tier + let category: Category | undefined = override.category + + // Apply category defaults first + if (snapshot.fork) { + category ??= 'fork' + } + category ??= 'uncategorized' + + // Apply tier defaults with archived taking precedence + if (!tier) { + if (snapshot.archived) { + tier = 'archived' + } else if (snapshot.fork) { + tier = 'as-is' + } else { + tier = 'unreviewed' + } + } + + return { + tier, + category, + featured: override.featured ?? false, + hidden: override.hidden ?? false, + } +} diff --git a/src/catalog/load.ts b/src/catalog/load.ts new file mode 100644 index 0000000..2aaca15 --- /dev/null +++ b/src/catalog/load.ts @@ -0,0 +1,44 @@ +import { parse as parseYaml } from 'yaml' +import { readdirSync } from 'node:fs' +import { join } from 'node:path' +import { mergeCatalog } from './merge' +import { loadOverlay } from './overlays' +import type { Catalog, GithubSnapshotEntry, OverrideEntry, Overlay } from './types' + +export async function loadCatalog(rootDir: string): Promise<Catalog> { + const snapshot = await readSnapshot(join(rootDir, 'data', 'repos.json')) + const overrides = await readOverrides(join(rootDir, 'data', 'overrides.yml')) + const overlays = await readOverlays(join(rootDir, 'content', 'work')) + return mergeCatalog(snapshot, overrides, overlays) +} + +async function readSnapshot(path: string): Promise<GithubSnapshotEntry[]> { + const file = Bun.file(path) + if (!(await file.exists())) return [] + return (await file.json()) as GithubSnapshotEntry[] +} + +async function readOverrides( + path: string, +): Promise<Record<string, OverrideEntry>> { + const file = Bun.file(path) + if (!(await file.exists())) return {} + const parsed = parseYaml(await file.text()) + return (parsed ?? {}) as Record<string, OverrideEntry> +} + +async function readOverlays(dir: string): Promise<Map<string, Overlay>> { + const overlays = new Map<string, Overlay>() + let files: string[] = [] + try { + files = readdirSync(dir).filter((f) => f.endsWith('.md')) + } catch { + return overlays + } + for (const file of files) { + const slug = file.replace(/\.md$/, '') + const overlay = await loadOverlay(join(dir, file)) + if (overlay) overlays.set(slug, overlay) + } + return overlays +} diff --git a/src/catalog/maintenance-tiers.md b/src/catalog/maintenance-tiers.md new file mode 100644 index 0000000..36dae26 --- /dev/null +++ b/src/catalog/maintenance-tiers.md @@ -0,0 +1,6 @@ +# Maintenance tiers + +- **active** — Flexion commits to security patch management and defined response times. Bug reports are triaged. Pull requests are reviewed on a predictable cadence. +- **as-is** — Available without active maintenance. The code works (or worked at one point); future updates are not promised. Forks and prototypes live here by default. +- **archived** — No longer maintained. GitHub's archive flag is set. The repo is read-only. Listed for transparency. +- **unreviewed** — A human has not yet classified this repo. Defaults to this state; visible on the site so gaps are honest. diff --git a/src/catalog/merge.ts b/src/catalog/merge.ts new file mode 100644 index 0000000..659b956 --- /dev/null +++ b/src/catalog/merge.ts @@ -0,0 +1,23 @@ +import { applyDefaults } from './defaults' +import type { + CatalogEntry, + GithubSnapshotEntry, + OverrideEntry, + Overlay, +} from './types' + +export function mergeCatalog( + snapshot: ReadonlyArray<GithubSnapshotEntry>, + overrides: Record<string, OverrideEntry>, + overlays: ReadonlyMap<string, Overlay>, +): CatalogEntry[] { + return snapshot.map((entry) => { + const override = overrides[entry.name] ?? {} + const resolved = applyDefaults(entry, override) + return { + ...entry, + ...resolved, + overlay: overlays.get(entry.name) ?? null, + } + }) +} diff --git a/src/catalog/overlays.ts b/src/catalog/overlays.ts new file mode 100644 index 0000000..49299f2 --- /dev/null +++ b/src/catalog/overlays.ts @@ -0,0 +1,27 @@ +import { parse as parseYaml } from 'yaml' +import type { Overlay } from './types' + +const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/ + +export async function loadOverlay(path: string): Promise<Overlay | null> { + const file = Bun.file(path) + if (!(await file.exists())) return null + const raw = await file.text() + + const match = raw.match(FRONTMATTER_RE) + if (!match) { + return { body: raw.trim() || undefined } + } + const frontMatter = (parseYaml(match[1]) ?? {}) as Record<string, unknown> + const body = match[2].trim() + + return { + title: stringOrUndefined(frontMatter.title), + summary: stringOrUndefined(frontMatter.summary), + body: body || undefined, + } +} + +function stringOrUndefined(value: unknown): string | undefined { + return typeof value === 'string' ? value : undefined +} diff --git a/src/catalog/refresh.ts b/src/catalog/refresh.ts new file mode 100644 index 0000000..6a686d2 --- /dev/null +++ b/src/catalog/refresh.ts @@ -0,0 +1,104 @@ +import { writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import type { GithubSnapshotEntry } from './types' + +export type FetchImpl = (input: string, init?: RequestInit) => Promise<Response> +export type FileCheck = (org: string, repo: string, path: string) => Promise<boolean> + +export type BuildSnapshotOptions = { + org: string + fetch: FetchImpl + fileCheck: FileCheck + token?: string +} + +type ApiRepo = { + name: string + description: string | null + html_url: string + homepage: string | null + language: string | null + license: { spdx_id?: string } | null + pushed_at: string + archived: boolean + fork: boolean + stargazers_count: number + private: boolean +} + +export async function buildSnapshot( + options: BuildSnapshotOptions, +): Promise<GithubSnapshotEntry[]> { + const repos = await fetchAllRepos(options) + const snapshot: GithubSnapshotEntry[] = [] + for (const repo of repos) { + if (repo.private) continue + const [hasReadme, hasLicenseFile, hasContributing] = await Promise.all([ + options.fileCheck(options.org, repo.name, 'README.md'), + options.fileCheck(options.org, repo.name, 'LICENSE'), + options.fileCheck(options.org, repo.name, 'CONTRIBUTING.md'), + ]) + snapshot.push({ + name: repo.name, + description: repo.description, + url: repo.html_url, + homepage: repo.homepage, + language: repo.language, + license: repo.license?.spdx_id ?? null, + pushedAt: repo.pushed_at, + archived: repo.archived, + fork: repo.fork, + stars: repo.stargazers_count, + hasReadme, + hasLicense: Boolean(repo.license?.spdx_id) || hasLicenseFile, + hasContributing, + }) + } + return snapshot +} + +async function fetchAllRepos(options: BuildSnapshotOptions): Promise<ApiRepo[]> { + const headers: Record<string, string> = { + accept: 'application/vnd.github+json', + 'user-agent': 'flexion-labs-refresh', + } + if (options.token) headers.authorization = `Bearer ${options.token}` + + const repos: ApiRepo[] = [] + let page = 1 + while (true) { + const res = await options.fetch( + `https://api.github.com/orgs/${options.org}/repos?per_page=100&page=${page}&type=public`, + { headers }, + ) + if (!res.ok) throw new Error(`GitHub API ${res.status} ${res.statusText}`) + const batch = (await res.json()) as ApiRepo[] + repos.push(...batch) + if (batch.length < 100) break + page += 1 + } + return repos +} + +export async function writeSnapshot( + rootDir: string, + snapshot: GithubSnapshotEntry[], +): Promise<void> { + const path = join(rootDir, 'data', 'repos.json') + const sorted = [...snapshot].sort((a, b) => a.name.localeCompare(b.name)) + await writeFile(path, JSON.stringify(sorted, null, 2) + '\n', 'utf8') +} + +if (import.meta.main) { + const token = process.env.GITHUB_TOKEN + const org = process.env.FLEXION_ORG ?? 'flexion' + const fileCheck: FileCheck = async (o, r, p) => { + const res = await fetch(`https://api.github.com/repos/${o}/${r}/contents/${p}`, { + headers: token ? { authorization: `Bearer ${token}`, accept: 'application/vnd.github+json' } : { accept: 'application/vnd.github+json' }, + }) + return res.ok + } + const snapshot = await buildSnapshot({ org, fetch, fileCheck, token }) + await writeSnapshot(process.cwd(), snapshot) + console.log(`Wrote ${snapshot.length} entries to data/repos.json`) +} diff --git a/src/catalog/repo-checks.ts b/src/catalog/repo-checks.ts new file mode 100644 index 0000000..084b9fe --- /dev/null +++ b/src/catalog/repo-checks.ts @@ -0,0 +1,42 @@ +import type { CatalogEntry } from './types' + +export type CheckResult = 'pass' | 'warn' | 'fail' + +export type RepoEvaluation = { + readme: CheckResult + license: CheckResult + contributing: CheckResult + activity: CheckResult + tierAssigned: CheckResult + overallPass: boolean +} + +const SIX_MONTHS_MS = 1000 * 60 * 60 * 24 * 30 * 6 +const EIGHTEEN_MONTHS_MS = SIX_MONTHS_MS * 3 + +export const SHOW_PER_REPO_FAILURES = true + +export function evaluateRepo(entry: CatalogEntry, now: Date): RepoEvaluation { + const readme = entry.hasReadme ? 'pass' : 'fail' + const license = entry.hasLicense ? 'pass' : 'fail' + const contributing = entry.hasContributing ? 'pass' : 'fail' + const tierAssigned = entry.tier === 'unreviewed' ? 'fail' : 'pass' + const activity = evaluateActivity(entry, now) + + const overallPass = + readme === 'pass' && + license === 'pass' && + contributing === 'pass' && + tierAssigned === 'pass' && + activity !== 'fail' + + return { readme, license, contributing, activity, tierAssigned, overallPass } +} + +function evaluateActivity(entry: CatalogEntry, now: Date): CheckResult { + if (entry.tier === 'archived') return 'pass' + const age = now.getTime() - new Date(entry.pushedAt).getTime() + if (age < SIX_MONTHS_MS) return 'pass' + if (age < EIGHTEEN_MONTHS_MS) return 'warn' + return 'fail' +} diff --git a/src/catalog/stewardship.md b/src/catalog/stewardship.md new file mode 100644 index 0000000..ef64c69 --- /dev/null +++ b/src/catalog/stewardship.md @@ -0,0 +1,15 @@ +# Standards + +This directory encodes Flexion's stewardship standards. Every public repo is evaluated against the checks defined here; the results drive `/work/health/`. + +## Checks + +- **README** — repo has a README.md at the root. +- **License** — repo has a detectable license (GitHub's license field OR a LICENSE file). +- **Contributing** — repo has a CONTRIBUTING.md at the root. +- **Activity** — most recent push is within 6 months (pass), 6–18 months (warn), or older (fail). Archived repos pass by policy — they're not expected to receive updates. +- **Tier assigned** — a human has classified the repo with an explicit tier in `catalog/overrides.yml`. Unclassified repos fail this check. + +## Hiding per-repo failures + +`SHOW_PER_REPO_FAILURES` in `repo-checks.ts` controls whether `/work/health/` shows specific repo names next to failures. Set it to `false` before launch if leadership prefers aggregate reporting. diff --git a/src/catalog/types.ts b/src/catalog/types.ts new file mode 100644 index 0000000..33bddd0 --- /dev/null +++ b/src/catalog/types.ts @@ -0,0 +1,48 @@ +export type Tier = 'active' | 'as-is' | 'archived' | 'unreviewed' + +export type Category = + | 'product' + | 'tool' + | 'workshop' + | 'prototype' + | 'fork' + | 'uncategorized' + +export type GithubSnapshotEntry = { + name: string + description: string | null + url: string + homepage: string | null + language: string | null + license: string | null + pushedAt: string // ISO 8601 + archived: boolean + fork: boolean + stars: number + hasReadme: boolean + hasLicense: boolean + hasContributing: boolean +} + +export type OverrideEntry = { + tier?: Tier + category?: Category + featured?: boolean + hidden?: boolean +} + +export type Overlay = { + title?: string + summary?: string + body?: string +} + +export type CatalogEntry = GithubSnapshotEntry & { + tier: Tier + category: Category + featured: boolean + hidden: boolean + overlay: Overlay | null +} + +export type Catalog = ReadonlyArray<CatalogEntry> diff --git a/src/design/assets/apple-touch-icon.png b/src/design/assets/apple-touch-icon.png new file mode 100644 index 0000000..7cfa7a9 Binary files /dev/null and b/src/design/assets/apple-touch-icon.png differ diff --git a/src/design/assets/favicon-192x192.png b/src/design/assets/favicon-192x192.png new file mode 100644 index 0000000..c5dc916 Binary files /dev/null and b/src/design/assets/favicon-192x192.png differ diff --git a/src/design/assets/favicon-32x32.png b/src/design/assets/favicon-32x32.png new file mode 100644 index 0000000..35fae3f Binary files /dev/null and b/src/design/assets/favicon-32x32.png differ diff --git a/src/design/assets/flexion_tornado.svg b/src/design/assets/flexion_tornado.svg new file mode 100644 index 0000000..b3ef2b0 --- /dev/null +++ b/src/design/assets/flexion_tornado.svg @@ -0,0 +1,9 @@ +<svg width="752" height="760" viewBox="0 0 752 760" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M79.0142 423.953L316.057 343.576L519.86 446.295L306.521 525.036L79.0142 423.953Z" fill="#A02816"/> +<path d="M79.8317 423.953L601.326 264.017L693.691 0L0 235.408" fill="#ED5338"/> +<path d="M0 235.408L67.0259 213.066L326.411 348.753L79.2868 423.953L0 235.408Z" fill="#DF4829"/> +<path d="M286.359 528.033L284.997 672.166L484.167 598.056L519.587 446.022" fill="#ED5338"/> +<path d="M287.177 528.034L135.142 584.161L181.733 707.314L286.359 671.622L287.177 528.034Z" fill="#DF4829"/> +<path d="M181.733 706.769L286.359 759.9V671.349L181.733 706.769Z" fill="#A02816"/> +<path d="M751.453 24.7943C751.453 28.8812 750.363 32.9682 748.183 36.5102C746.004 40.0522 743.279 43.0493 739.465 45.229C735.65 47.4087 731.836 48.4986 727.749 48.4986C723.662 48.4986 719.575 47.4087 715.76 45.229C711.946 43.0493 709.221 40.0522 707.042 36.5102C705.134 32.9682 704.044 28.8812 704.044 24.7943C704.044 20.7073 705.134 16.6204 707.314 13.0783C709.494 9.53632 712.218 6.53923 716.033 4.35953C719.575 2.17982 723.662 1.08997 727.749 1.08997C731.836 1.08997 735.923 2.17982 739.465 4.35953C743.279 6.53923 746.004 9.26386 748.183 13.0783C750.363 16.6204 751.181 20.7073 751.181 24.7943H751.453ZM747.911 24.7943C747.911 21.2522 747.094 17.9827 745.186 14.9856C743.279 11.9885 740.827 9.53632 737.83 7.62908C734.833 5.72184 731.291 4.90445 727.749 4.90445C724.207 4.90445 721.21 5.72184 717.94 7.35662C714.943 8.9914 712.491 11.4436 710.584 14.4407C708.676 17.4378 707.859 20.9798 707.859 24.5218C707.859 28.0638 708.676 31.3334 710.584 34.6029C712.491 37.6 714.671 40.0522 717.94 41.9594C720.937 43.8667 724.207 44.6841 727.749 44.6841C731.291 44.6841 734.56 43.8667 737.83 41.9594C740.827 40.0522 743.279 37.8725 745.186 34.6029C747.094 31.6058 747.911 28.0638 747.911 24.5218V24.7943ZM733.198 38.145L727.476 27.7914H723.662V38.145H717.94V11.4436H728.839C732.108 11.4436 734.56 12.261 736.195 13.6233C737.83 14.9856 738.647 16.8928 738.647 19.345C738.647 21.7972 738.102 23.1595 737.012 24.2493C735.923 25.6116 734.56 26.429 732.926 26.974L740.01 37.8725H733.47L733.198 38.145ZM732.653 19.6175C732.653 18.2551 732.381 17.4378 731.563 16.8928C730.746 16.3479 729.656 15.803 728.021 15.803H723.662V23.9769H728.294C729.656 23.9769 730.746 23.7044 731.563 22.887C732.381 22.0696 732.653 20.9798 732.653 19.8899V19.6175Z" fill="#2D2E30"/> +</svg> diff --git a/src/design/assets/mstile-270x270.png b/src/design/assets/mstile-270x270.png new file mode 100644 index 0000000..31a0962 Binary files /dev/null and b/src/design/assets/mstile-270x270.png differ diff --git a/src/design/base.css b/src/design/base.css new file mode 100644 index 0000000..0443777 --- /dev/null +++ b/src/design/base.css @@ -0,0 +1,128 @@ +@layer base { + html { + color-scheme: light; + } + body { + font-family: var(--font-sans); + font-size: var(--step-0); + color: var(--color-ink); + background: var(--color-surface); + } + h1, h2, h3, h4 { + line-height: 1.2; + font-weight: 700; + font-family: var(--font-sans); + margin-block-start: 1.5em; + margin-block-end: 0.5em; + } + h1 { font-size: var(--step-4); line-height: 1.15; margin-block-start: 0; } + h2 { font-size: var(--step-3); } + h3 { font-size: var(--step-2); } + h4 { font-size: var(--step-1); } + p { + margin-block-end: 1em; + } + p, ul, ol, dl, table { + max-inline-size: var(--measure-prose); + } + /* Restore list styling inside prose containers (markdown output) */ + :where(article, .content-page) :where(ul) { + padding-inline-start: 1.5em; + margin-block-end: 1em; + list-style: disc; + } + :where(article, .content-page) :where(ol) { + padding-inline-start: 1.5em; + margin-block-end: 1em; + list-style: decimal; + } + :where(article, .content-page) :where(li + li) { + margin-block-start: 0.25em; + } + /* Navigation lists never have bullets — overrides any container restoration */ + nav ul, nav ol { + list-style: none; + padding-inline-start: 0; + margin-block-end: 0; + } + dt { + font-weight: 600; + } + dd { + margin-block-end: 0.5em; + } + blockquote { + border-inline-start: 4px solid var(--color-surface-alt); + padding-inline-start: var(--space-5); + padding-block: var(--space-3); + margin-block-end: 1em; + color: var(--color-ink-subtle); + max-inline-size: var(--measure-prose); + } + code { + font-family: var(--font-mono); + font-size: 0.9em; + background: var(--color-surface-alt); + padding: 0.1em 0.3em; + border-radius: var(--radius-sm); + } + pre { + font-family: var(--font-mono); + font-size: var(--step--1); + line-height: 1.6; + background: var(--color-surface-alt); + padding: var(--space-4); + border-radius: var(--radius-md); + overflow-x: auto; + margin-block-end: 1em; + max-inline-size: var(--measure-wide); + } + pre code { + background: none; + padding: 0; + font-size: inherit; + } + hr { + border: none; + border-block-start: 1px solid var(--color-surface-alt); + margin-block: var(--space-6); + } + small { + font-size: var(--step--1); + } + /* Prevent leading/trailing margin on first/last flow elements in a container */ + :where(h1, h2, h3, h4, p, ul, ol, dl, blockquote, pre):first-child { + margin-block-start: 0; + } + :where(h1, h2, h3, h4, p, ul, ol, dl, blockquote, pre):last-child { + margin-block-end: 0; + } + a { + color: var(--color-link); + text-decoration-thickness: 0.08em; + text-underline-offset: 0.18em; + } + a:hover { + color: var(--color-link-hover); + } + :focus-visible { + outline: 2px solid var(--color-focus-ring); + outline-offset: 2px; + border-radius: var(--radius-sm); + } + .skip-link { + position: absolute; + inline-size: 1px; + block-size: 1px; + overflow: hidden; + clip-path: inset(50%); + } + .skip-link:focus { + position: static; + inline-size: auto; + block-size: auto; + clip-path: none; + padding: var(--space-2) var(--space-4); + background: var(--color-surface); + } +} diff --git a/src/design/common/content-page.css b/src/design/common/content-page.css new file mode 100644 index 0000000..eabbab2 --- /dev/null +++ b/src/design/common/content-page.css @@ -0,0 +1,9 @@ +.content-page { + /* Flow spacing comes from element margins in base.css. + Add extra separation above section headings. */ +} +.content-page > :first-child { + margin-block-start: 0; +} +.content-page h2 { margin-block-start: var(--space-7); } +.content-page h3 { margin-block-start: var(--space-6); } diff --git a/src/design/common/content-page.tsx b/src/design/common/content-page.tsx new file mode 100644 index 0000000..abfc1c6 --- /dev/null +++ b/src/design/common/content-page.tsx @@ -0,0 +1,21 @@ +import { raw } from 'hono/html' +import { marked } from 'marked' +import { Layout } from './layout' +import type { SiteConfig } from '../../build/config' + +export function ContentPage({ + title, + body, + config, +}: { + title: string + body: string + config: SiteConfig +}) { + const html = marked.parse(body, { async: false }) as string + return ( + <Layout title={title} config={config}> + <article class="content-page">{raw(html)}</article> + </Layout> + ) +} diff --git a/src/design/common/layout.tsx b/src/design/common/layout.tsx new file mode 100644 index 0000000..45e2c67 --- /dev/null +++ b/src/design/common/layout.tsx @@ -0,0 +1,40 @@ +import type { Child } from 'hono/jsx' +import { raw } from 'hono/html' +import { Header } from '../components/header' +import { Footer } from '../components/footer' +import { url } from '../../build/config' +import type { SiteConfig } from '../../build/config' + +export function Layout({ + title, + config, + children, +}: { + title: string | null + config: SiteConfig + children: Child +}) { + const documentTitle = title ? `${title} — Flexion Labs` : 'Flexion Labs' + return ( + <html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>{documentTitle} + + + + + + + + + + +

+
{children}
+