diff --git a/.github/workflows/landing-visual.yml b/.github/workflows/landing-visual.yml new file mode 100644 index 00000000..bc5275f6 --- /dev/null +++ b/.github/workflows/landing-visual.yml @@ -0,0 +1,110 @@ +name: Landing Visual Regression + +on: + pull_request: + branches: + - main + paths: + - 'shared/assets/**' + - 'templates/**/Landing*' + - 'templates/**/landing*' + - 'templates/**/page.module.css' + - 'templates/**/index.astro' + - 'templates/**/Newtab.*' + - 'templates/**/Popup.*' + - 'scripts/landing-visual/**' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + visual: + name: Visual regression on ${{ matrix.template }} + strategy: + fail-fast: false + matrix: + template: + - react-vite-starter + - nextjs-starter + - remix-starter + - astro-starter + - webextension-react-vite-starter + port: + - 5173 + include: + - template: nextjs-starter + port: 3000 + - template: astro-starter + port: 4321 + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + + - name: Install Playwright + working-directory: scripts/landing-visual + run: | + npm install + npx playwright install chromium --with-deps + + - name: Scaffold test project + id: scaffold + continue-on-error: true + run: | + CI=true npx create-awesome-node-app visual-test-project \ + -t "file://${PWD}?subdir=templates/${{ matrix.template }}" + echo "Project scaffolded" + + - name: Install and start dev server + if: steps.scaffold.outcome == 'success' + working-directory: visual-test-project + run: | + npm install + case "${{ matrix.template }}" in + webextension-react-vite-starter) + npm run build + echo "Build succeeded — webextension has no dev server" + exit 0 + ;; + nextjs-starter) + npm run dev -- --port "${{ matrix.port }}" & + ;; + *) + npm run dev -- --host 0.0.0.0 --port "${{ matrix.port }}" & + ;; + esac + echo "Waiting for dev server on port ${{ matrix.port }}..." + for i in $(seq 1 30); do + curl -sf "http://localhost:${{ matrix.port }}" > /dev/null 2>&1 && echo "Server ready on port ${{ matrix.port }}" && break + sleep 1 + done + curl -sf "http://localhost:${{ matrix.port }}" > /dev/null 2>&1 || { echo "Server failed to start on port ${{ matrix.port }}"; exit 1; } + + - name: Run visual tests + if: steps.scaffold.outcome == 'success' && matrix.template != 'webextension-react-vite-starter' + working-directory: scripts/landing-visual + run: | + npx playwright test --project=chromium + env: + TEMPLATE: ${{ matrix.template }} + BASE_URL: http://localhost:${{ matrix.port }} + + - name: Upload screenshots (always) + if: always() && steps.scaffold.outcome == 'success' && matrix.template != 'webextension-react-vite-starter' + uses: actions/upload-artifact@v4 + with: + name: screenshots-${{ matrix.template }} + path: | + scripts/landing-visual/test-results/ + scripts/landing-visual/__screenshots__/ diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index ef1f5916..d7bdec98 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -10,23 +10,69 @@ on: - reopened workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read jobs: + validate-registry: + name: Validate template registry + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + - name: Validate templates.json + run: node scripts/validate-templates.js + + landing-audit: + name: Landing page audit + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + - name: Audit landing pages + run: node scripts/landing-audit.js + smoke-test: - name: Smoke test scaffolded projects + name: Smoke test ${{ matrix.template }} + needs: [validate-registry, landing-audit] strategy: fail-fast: false matrix: include: + - template: react-vite-starter + addons: "" + build-env: "" - template: nextjs-starter addons: "" - - template: nestjs-boilerplate + build-env: "" + - template: astro-starter addons: "" - - template: react-vite-boilerplate - addons: '--addons "file://${PWD}?subdir=extensions/react-material-ui" "file://${PWD}?subdir=extensions/react-zustand"' + build-env: "" + - template: remix-starter + addons: "" + build-env: "" + - template: webextension-react-vite-starter + addons: "" + build-env: "" + - template: turborepo-starter + addons: "" + build-env: "" runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -47,6 +93,8 @@ jobs: - name: Run basic checks working-directory: smoke-test-project + env: + SKIP_ENV_VALIDATION: 'true' run: | npm install npm run format --if-present @@ -55,3 +103,51 @@ jobs: npm run type-check --if-present || echo "Type-check warnings found (non-blocking)" SKIP_ENV_VALIDATION=true npm run build --if-present || echo "Build warnings found (non-blocking)" echo "Smoke test passed for ${{ matrix.template }}" + + extension-isolation: + name: Isolate ${{ matrix.combination.template }} + ${{ matrix.combination.extension }} + needs: [validate-registry, landing-audit] + strategy: + fail-fast: false + matrix: + combination: + - template: react-vite-starter + extension: react-tailwindcss + - template: react-vite-starter + extension: react-shadcn + - template: react-vite-starter + extension: react-material-ui + - template: nextjs-starter + extension: nextjs-tailwindcss + - template: nextjs-starter + extension: nextjs-shadcn + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + + - name: Scaffold with extension + run: | + CI=true npx create-awesome-node-app isolate-test-project \ + -t "file://${PWD}?subdir=templates/${{ matrix.combination.template }}" \ + --addons "file://${PWD}?subdir=extensions/${{ matrix.combination.extension }}" + echo "Project scaffolded with ${{ matrix.combination.extension }}" + + - name: Run basic checks + working-directory: isolate-test-project + env: + SKIP_ENV_VALIDATION: 'true' + run: | + npm install + npm run format --if-present + npm run lint:fix --if-present + npm run lint --if-present || echo "Lint warnings found (non-blocking)" + npm run type-check --if-present || echo "Type-check warnings found (non-blocking)" + npm run build --if-present || echo "Build warnings found (non-blocking)" + echo "Isolated test passed for ${{ matrix.combination.template }} + ${{ matrix.combination.extension }}" diff --git a/.github/workflows/test-combinations.yml b/.github/workflows/test-combinations.yml index ce1fd615..072a3e98 100644 --- a/.github/workflows/test-combinations.yml +++ b/.github/workflows/test-combinations.yml @@ -4,10 +4,25 @@ on: push: branches: - main + pull_request: + branches: + - main + paths: + - 'templates.json' + - 'extensions/**' + - '.github/workflows/test-combinations.yml' + types: + - opened + - synchronize + - reopened schedule: - - cron: "0 0 * * 0" # Weekly full matrix on Sunday at midnight UTC + - cron: "0 0 * * 0" workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read @@ -43,7 +58,6 @@ jobs: })); const extensions = data.extensions; - // Build local URL for each extension using its url path const extLocalUrl = (ext) => { const match = ext.url.match(/\/extensions\/([^/]+)(?:\/|$)/); const dir = match ? match[1] : null; @@ -78,7 +92,6 @@ jobs: if (ext) selectedExtensions.push(ext); } - // Derive local template URL from the url field in templates.json const tplMatch = template.url.match(/\/templates\/([^/]+)(?:\/|$)/); const tplDir = tplMatch ? tplMatch[1] : null; const templateUrl = tplDir ? \`file://${repoDir}?subdir=templates/${tplDir}\` : template.slug; @@ -98,6 +111,7 @@ jobs: matrix: combination: ${{ fromJson(needs.generate-combinations.outputs.matrix) }} runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -138,7 +152,6 @@ jobs: tree -I 'node_modules|dist|.git' - # Full matrix - tests ALL combinations (runs weekly, not on PRs) generate-full-matrix: name: Generate full test matrix if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' @@ -166,7 +179,6 @@ jobs: const extensions = data.extensions; - // Build local URL for each extension using its url path const extLocalUrl = (ext) => { const match = ext.url.match(/\/extensions\/([^/]+)(?:\/|$)/); const dir = match ? match[1] : null; @@ -185,7 +197,6 @@ jobs: return false; }; - // Generate ALL non-conflicting combinations (template x all compatible extensions) const combinations = []; for (const template of data.templates) { @@ -194,7 +205,6 @@ jobs: return extTypes.includes(template.type); }); - // Test with all mutually-compatible extensions for this template const selectedExtensions = []; for (const ext of compatibleExtensions) { if (!hasIncompatibility(selectedExtensions, ext)) { @@ -205,7 +215,6 @@ jobs: if (selectedExtensions.length > 0) { const extUrls = selectedExtensions.map(ext => extLocalUrl(ext)).filter(Boolean); - // Derive local template URL from the url field in templates.json const tplMatch = template.url.match(/\/templates\/([^/]+)(?:\/|$)/); const tplDir = tplMatch ? tplMatch[1] : null; const templateUrl = tplDir ? \`file://${repoDir}?subdir=templates/${tplDir}\` : template.slug; @@ -232,6 +241,7 @@ jobs: matrix: combination: ${{ fromJson(needs.generate-full-matrix.outputs.matrix) }} runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/docs/DEFAULT_LANDING_DESIGN.md b/docs/DEFAULT_LANDING_DESIGN.md index de6cf756..8636b245 100644 --- a/docs/DEFAULT_LANDING_DESIGN.md +++ b/docs/DEFAULT_LANDING_DESIGN.md @@ -22,10 +22,10 @@ | Token | Hex | Usage | |---|---|---| -| `brand-amber` | `#f59e0b` | Primary accent, highlights, CTAs | -| `brand-amber-dark` | `#d97706` | Hover states, secondary accents | -| `brand-teal` | `#0d9488` | Success, links, secondary accent | -| `brand-teal-soft` | `#14b8a6` | Soft teal surfaces | +| `--cna-amber` | `#f59e0b` | Primary accent, highlights, CTAs | +| `--cna-amber-dark` | `#d97706` | Hover states, secondary accents | +| `--cna-teal` | `#0d9488` | Success, links, secondary accent | +| `--cna-teal-soft` | `#14b8a6` | Soft teal surfaces | ### Neutral colors @@ -104,13 +104,13 @@ font-family: 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Me ### Shadows ```css ---shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); ---shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); ---shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); ---shadow-glow: 0 0 40px -10px rgba(245, 158, 11, 0.25); +--cna-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); +--cna-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); +--cna-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); +--cna-shadow-glow: 0 0 40px -10px rgba(245, 158, 11, 0.25); ``` -Use `shadow-glow` sparingly on hero containers. +Use `--cna-shadow-glow` sparingly on hero containers. --- @@ -119,7 +119,7 @@ Use `shadow-glow` sparingly on hero containers. ### Entrance animation ```css -@keyframes fade-in-up { +@keyframes cna-fade-in-up { from { opacity: 0; transform: translateY(12px); @@ -134,8 +134,8 @@ Use `shadow-glow` sparingly on hero containers. Apply with staggered delays: ```css -animation: fade-in-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) backwards; -animation-delay: calc(var(--index) * 80ms); +animation: cna-fade-in-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; +animation-delay: calc(var(--index, 0) * 80ms); ``` ### Hover transitions @@ -147,9 +147,9 @@ transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s eas Card hover: ```css -.feature-card:hover { +.cna-landing__card:hover { transform: translateY(-3px); - box-shadow: var(--shadow-lg), var(--shadow-glow); + box-shadow: var(--cna-shadow-lg), var(--cna-shadow-glow); } ``` @@ -208,7 +208,7 @@ Every template landing should follow this layout: | [Primary CTA] [Secondary] | | | +----------------------------------+ -| Feature cards (3-6 items) | +| Feature cards (6 items) | +----------------------------------+ | Docs + next steps | +----------------------------------+ @@ -225,21 +225,22 @@ Each feature card contains: 3. One-sentence description. 4. Optional doc link rendered as a plain link with a label and arrow. -Example cards per template: +Each template has exactly **6 feature cards** with consistent icons and tone. Canonical data is in [`shared/landings/features.ts`](../shared/landings/features.ts). -- **React Vite**: React 19, Vite 8, React Router 7, TypeScript, ESLint + Prettier, feature-based structure. -- **Next.js**: Next.js 16 App Router, TypeScript, feature-based structure, login example, ESLint + Prettier. -- **Remix**: React Router v7, TypeScript, ESLint + Prettier, fast builds. -- **Astro**: Astro 7, static output, partial hydration, TypeScript. +- **React Vite**: Vite, React, React Router v7, TypeScript + ESLint + Prettier, feature-based structure, addon ready. +- **Next.js**: Next.js App Router, React, TypeScript + ESLint + Prettier, feature-based structure, login example, addon ready. +- **Remix**: React Router v7, React, TypeScript, Vite, feature-based structure, addon ready. +- **Astro**: Astro, React-ready, TypeScript, feature-based structure, static output, addon ready. +- **WebExtension**: Browser extension shell, React + Vite, TypeScript, feature-based structure, CNA identity, addon ready. ### Documentation links -Link to the generated docs folder using relative paths: +Link to the generated docs folder using root-absolute paths: -- `docs/README.md` -- `docs/PROJECT_STRUCTURE.md` -- `docs/COMPONENTS_AND_STYLING.md` -- `docs/STATE_MANAGEMENT.md` +- `/docs/README.md` +- `/docs/PROJECT_STRUCTURE.md` +- `/docs/COMPONENTS_AND_STYLING.md` +- `/docs/STATE_MANAGEMENT.md` Use a simple link with a label and an arrow. A shared `DocsLink` helper is optional when the template already has a component library installed. @@ -266,7 +267,7 @@ Use a simple link with a label and an arrow. A shared `DocsLink` helper is optio ### Astro -- Use scoped `