@getvitops/create 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +129 -5
  2. package/README.md +7 -0
  3. package/package.json +17 -2
  4. package/templates/emdash/.env +1 -0
  5. package/templates/emdash/.github/workflows/ci.yml +12 -0
  6. package/templates/emdash/.github/workflows/deploy-dev.yml +17 -19
  7. package/templates/emdash/.github/workflows/deploy-prod.yml +17 -21
  8. package/templates/emdash/.github/workflows/promote.yml +8 -36
  9. package/templates/emdash/.mcp.json +4 -0
  10. package/templates/emdash/.nvmrc +1 -0
  11. package/templates/emdash/AGENTS.md +47 -0
  12. package/templates/emdash/README.md +4 -0
  13. package/templates/emdash/design-system.json +2 -2
  14. package/templates/emdash/package.json +1 -0
  15. package/templates/emdash/scripts/init-github.mjs +94 -10
  16. package/templates/emdash/src/assets/placeholder/wireframe-400x400.png +0 -0
  17. package/templates/emdash/src/assets/placeholder/wireframe-400x400.svg +1 -0
  18. package/templates/emdash/src/assets/placeholder/wireframe-400x600.png +0 -0
  19. package/templates/emdash/src/assets/placeholder/wireframe-400x600.svg +1 -0
  20. package/templates/emdash/src/assets/placeholder/wireframe-600x400.png +0 -0
  21. package/templates/emdash/src/assets/placeholder/wireframe-600x400.svg +1 -0
  22. package/templates/emdash/vite.config.ts +6 -0
  23. package/templates/static/.env +1 -0
  24. package/templates/static/.github/workflows/ci.yml +12 -0
  25. package/templates/static/.github/workflows/deploy-dev.yml +33 -0
  26. package/templates/static/.github/workflows/deploy-prod.yml +29 -0
  27. package/templates/static/.github/workflows/promote.yml +16 -0
  28. package/templates/static/.nvmrc +1 -0
  29. package/templates/static/AGENTS.md +57 -0
  30. package/templates/static/README.md +85 -0
  31. package/templates/static/_gitignore +24 -0
  32. package/templates/static/astro.config.mjs +19 -0
  33. package/templates/static/design-system.json +148 -0
  34. package/templates/static/package.json +39 -0
  35. package/templates/static/scripts/dev-wrangler-config.mjs +28 -0
  36. package/templates/static/scripts/init-github.mjs +186 -0
  37. package/templates/static/scripts/init-hosting.mjs +62 -0
  38. package/templates/static/src/assets/placeholder/wireframe-400x400.png +0 -0
  39. package/templates/static/src/assets/placeholder/wireframe-400x400.svg +1 -0
  40. package/templates/static/src/assets/placeholder/wireframe-400x600.png +0 -0
  41. package/templates/static/src/assets/placeholder/wireframe-400x600.svg +1 -0
  42. package/templates/static/src/assets/placeholder/wireframe-600x400.png +0 -0
  43. package/templates/static/src/assets/placeholder/wireframe-600x400.svg +1 -0
  44. package/templates/static/src/layouts/Layout.astro +26 -0
  45. package/templates/static/src/pages/404.astro +11 -0
  46. package/templates/static/src/pages/index.astro +13 -0
  47. package/templates/static/tsconfig.json +5 -0
  48. package/templates/static/wrangler.jsonc +15 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,129 @@
1
1
  # @getvitops/create
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Scaffolded `static` and `emdash` projects now ship neutral placeholder wireframe images under
8
+ `src/assets/placeholder/` (square, portrait, landscape; SVG + PNG) for layout work before real
9
+ imagery exists. Delete the directory once real assets land.
10
+
11
+ ## 0.5.0
12
+
13
+ ### Minor Changes
14
+
15
+ - **The framework now says out loud which patterns are foundational, and reports it when you
16
+ reach past them.** From repeated downstream reports: sites were inventing a `.wrap` class and
17
+ using it everywhere instead of `.centered`, and `.subgrid` — the class for any set of cards —
18
+ was going essentially unused. Neither failure is visible in a build: every class involved is
19
+ real, nothing errors, and the page renders.
20
+
21
+ **`.subgrid` was missing from the generated class reference entirely.** So was `.cluster`, and
22
+ so was `.region`. `vitops docs classes` is the doc an agent is told to fetch for "which class
23
+ do I apply", and the framework's answer for a set of cards was not in it — which is most of why
24
+ it never got used. All three are now documented, along with `grid-auto`, and the reference opens
25
+ with a **Foundations** section that states the six substitutions as
26
+ _temptation → what to write instead_ rather than as a vocabulary list. The same table is now in
27
+ the shipped agent skill and in the `@getvitops/create` template's `AGENTS.md`, because a pointer
28
+ to a doc only helps someone who already suspects there is something to look up. (`css`/`bricks`
29
+ and `tailwind` alike — this is documentation and the `TIERS` manifest, not emitted CSS.)
30
+
31
+ **`vitops lint` gained three reuse rules and a markup pass.** The existing `.centered` rule only
32
+ fired when the hand-written CSS already referenced `--width-measure`, which is precisely the
33
+ author who was never going to hand-roll a container. It now also catches a page-scale
34
+ `max-width` (≥ 48rem, or a `ch` reading measure) with auto margins, and a
35
+ container-shaped class name — `wrap`, `wrapper`, `container`, `inner`, `shell`, … — carrying any
36
+ width cap at all. Reading the cap out of `min()` / `clamp()` too. New rules report a
37
+ hand-written `repeat()` grid as `.subgrid` (or `.grid-auto`), and a new **markup** pass reports a
38
+ repeated card set laid out without either — a loop that renders a card, or three or more cards
39
+ written out. That last one is the only check that can see this drift at all, since a card list
40
+ built from utility classes contains no bad class and no hand-written CSS.
41
+
42
+ All of these are `suggestion` severity, so they do not fail your build unless you pass
43
+ `--strict`. They found five real instances in this repo's own docs site.
44
+
45
+ **`vitops lint [files...]`** now takes explicit paths instead of scanning `--src`, so it can be
46
+ wired into a pre-commit hook — `vp`'s `staged` key appends the staged files to whatever it runs,
47
+ and a command that refused positionals could not be put in the one place the feedback lands at
48
+ the moment the code is written. Unreadable and non-source paths are skipped rather than fatal. The
49
+ `@getvitops/create` emdash template wires it up with `--strict` and adds a `lint:design` script.
50
+
51
+ **Two answers for "the whole card is a link", because one cannot exist.** Reported downstream:
52
+ agents dislike the `<li>` wrapper when a card is a link and write `<li><a class="card">` instead.
53
+ That shape is **wrong in a way that renders fine**, which is why it survives review — the `<li>`
54
+ is the grid item, so the anchor is an ordinary block inside it and the tranches within the anchor
55
+ never reach the parent's shared row lines. The alignment `.subgrid` exists for silently does not
56
+ happen, and the anchor does not fill the cell either. Putting the anchor in the grid's place
57
+ (`<ul><a></ul>`) is invalid HTML, so there was genuinely no correct shape to reach for.
58
+
59
+ There are now two, and they trade against each other because **no CSS-only technique can make a
60
+ whole card clickable and leave its text selectable** — a transparent overlay necessarily receives
61
+ the pointer-drag:
62
+
63
+ | Want | Use |
64
+ | ---------------------------------------- | ------------------------------------------- |
65
+ | zero JS, whole card clickable | `.stretched-link` — **text not selectable** |
66
+ | selectable text **and** a clickable card | `<Cards>` / `<wc-cards>` — needs JS |
67
+
68
+ `.stretched-link` goes on a link inside the card; its `::after` covers the card.
69
+ **`<wc-cards>` is a new tier-2 element** that adds no overlay and instead tells a click apart from
70
+ the end of a drag, so text selection survives. Its fallback is the card's own link, fully usable
71
+ with no JS, and the pointer cursor is applied by the element — so the affordance never appears
72
+ without the behaviour. `<Cards>` emits it for you. They are alternatives, never layered: with an
73
+ overlay present the JS has nothing to do, and `vitops lint` reports the combination. (All formats
74
+ — pattern partials, deliberately not `utilities.css`, which the tailwind format skips on the
75
+ invariant that everything left in it is a name Tailwind ships itself.)
76
+
77
+ ### Breaking
78
+ - **`<Subgrid />` renders its slot verbatim — author the `<li>` items yourself.**
79
+
80
+ ```diff
81
+ <Subgrid>
82
+ - <article class="card">…</article>
83
+ + <li class="card subgrid-card">…</li>
84
+ </Subgrid>
85
+ ```
86
+
87
+ It used to parse the slotted HTML and rebuild each child as an `<li>`, carrying over only
88
+ `class` and `style`. That existed solely because a `<ul>` may contain nothing else, and it cost
89
+ more than it bought: the child's tag was discarded, `id`/`data-*`/`aria-*` were silently
90
+ dropped, and `href` could not survive at all. Nothing is copied now, so nothing is lost. `as`
91
+ picks the container (`ul` by default, `ol`, `div`), and all other props are forwarded — they
92
+ were previously discarded, including `role`.
93
+
94
+ - **`<Subgrid />` and `<Tree />` now emit `role="list"`,** as does the Bricks `sitenav` element.
95
+ `list-style: none` stops Safari + VoiceOver announcing a `<ul>` as a list, so every marker-less
96
+ framework list was silently losing the semantics its `<ul>` was chosen for. If you hand-write
97
+ `.subgrid`, `.list`, `.facet-list`, `.nav-items`, `.collapse-menu` or `.tree` markup, add
98
+ `role="list"` yourself — the framework cannot add it to markup it does not render, and each
99
+ partial now says so at the `list-style` reset.
100
+ - **`<Cards />` no longer adds `class="card"` to slotted children,** because it no longer parses
101
+ them. Write the class on the item. It now emits `<wc-cards>` around a `<Subgrid>`, which is what
102
+ makes the whole card clickable — and like `<Tree />`, **it emits its own element, so do not wrap
103
+ it in `<wc-cards>` yourself.** It also previously passed a `role="list"` that `<Subgrid>` silently
104
+ discarded, so that never took effect.
105
+ - **`.subgrid-card` now sets `position: relative`**, so `stretched-link` inside it needs no extra
106
+ class. If you were absolutely positioning a descendant of a `.subgrid-card` against an ancestor
107
+ _outside_ that card, it will now resolve against the card. Move the positioning context
108
+ explicitly. (`css`/`bricks`/`tailwind` — the pattern partial is inlined into all three.) Not
109
+ reachable by `vitops lint --fix`: the fix depends on which ancestor you meant, which the
110
+ linter cannot know.
111
+
112
+ ### Fixed
113
+ - **`packages/generator/src/docs.ts` contained a literal NUL byte** (`CODE_SLOT`, written as the
114
+ raw character rather than an escape). libmagic classified the file as `data` rather than text, so
115
+ grep and ripgrep treated the largest doc emitter in the repo as **binary and silently skipped
116
+ it** — every search for a string in it returned nothing, with no error. It is now written as the `\u0000` escape.
117
+ Behaviour is identical; the file is searchable.
118
+ - **`.grid-auto` is documented.** It was a real framework class for auto-fit card grids with no
119
+ entry in the class reference, so the honest alternative to `subgrid` was as invisible as
120
+ `subgrid` itself.
121
+ - **`.raised` replaces the advice to use `.relative` above a `stretched-link` overlay**, which
122
+ could not work: a positioned element at `z-index: auto` does not rise above an explicit
123
+ `z-index` regardless of DOM order, so a second link or button in the card stayed underneath and
124
+ unclickable. `.raised` sets both, against the same `--z-tier-raised` token the overlay now uses
125
+ instead of a hard-coded `1`.
126
+
3
127
  ## 0.4.0
4
128
 
5
129
  ### Minor Changes
@@ -111,15 +235,15 @@
111
235
  which `@getvitops/astro` re-exports wholesale. So from `astro.config.mjs`:
112
236
 
113
237
  ```js
114
- import { generateIconInclude } from '@getvitops/astro';
115
- import icon from 'astro-icon';
238
+ import { generateIconInclude } from "@getvitops/astro";
239
+ import icon from "astro-icon";
116
240
 
117
241
  integrations: [
118
242
  icon({
119
243
  include: generateIconInclude({
120
- ui: 'fa7-solid',
121
- brand: 'simple-icons',
122
- semantic: ['menu', 'close', 'search', 'github'],
244
+ ui: "fa7-solid",
245
+ brand: "simple-icons",
246
+ semantic: ["menu", "close", "search", "github"],
123
247
  }),
124
248
  }),
125
249
  ];
package/README.md CHANGED
@@ -73,6 +73,13 @@ provisions the D1/R2 resources on first deploy).
73
73
  Template dependencies must use concrete npm ranges (no `workspace:*` /
74
74
  `catalog:` — the scaffolded project lives outside this monorepo).
75
75
 
76
+ `assets/` (this package root, unpublished) is the source of truth for
77
+ placeholder imagery shared across templates — `vp create` copies only the
78
+ one directory a template names, so there's no step that could copy a
79
+ sibling `assets/` for you. Mirror any new file into each template's
80
+ `src/assets/placeholder/` by hand; `templates.test.ts` asserts every mirror
81
+ stays byte-identical to the source.
82
+
76
83
  ## Changelog
77
84
 
78
85
  This package's history: [`CHANGELOG.md`](./CHANGELOG.md) (shipped in the npm tarball, so it
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "@getvitops/create",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Project templates for the Vitops design system — scaffold wired-up sites with `vp create @getvitops`.",
5
+ "homepage": "https://docs.vitops.ca",
6
+ "bugs": "https://github.com/getvitops/design-system/issues",
5
7
  "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/getvitops/design-system.git",
11
+ "directory": "packages/create"
12
+ },
6
13
  "files": [
7
14
  "templates",
8
15
  "CHANGELOG.md"
@@ -12,10 +19,18 @@
12
19
  "access": "public"
13
20
  },
14
21
  "devDependencies": {
15
- "vitest": "^4.1.9"
22
+ "vitest": "4.1.9"
23
+ },
24
+ "engines": {
25
+ "node": ">=24"
16
26
  },
17
27
  "createConfig": {
18
28
  "templates": [
29
+ {
30
+ "name": "static",
31
+ "description": "Static Astro website on Cloudflare Workers static assets with the Vitops design system pre-wired",
32
+ "template": "./templates/static"
33
+ },
19
34
  {
20
35
  "name": "emdash",
21
36
  "description": "EmDash CMS website on Cloudflare Workers (D1 + R2) with the Vitops design system pre-wired",
@@ -0,0 +1 @@
1
+ CLOUDFLARE_API_TOKEN=your_cloudflare_api_token
@@ -0,0 +1,12 @@
1
+ # Runs on every PR: typecheck + design-lint + build. deploy-dev.yml /
2
+ # deploy-prod.yml call the same reusable `site-ci.yml` themselves before they
3
+ # deploy, so this is what gives a PR the identical signal before it's even
4
+ # merged to `dev` — a broken build never reaches the dev worker.
5
+ name: CI
6
+
7
+ on:
8
+ pull_request:
9
+
10
+ jobs:
11
+ ci:
12
+ uses: getvitops/design-system/.github/workflows/site-ci.yml@v1
@@ -3,6 +3,11 @@
3
3
  # `dev` is the integration branch — every push here refreshes the review site.
4
4
  # Promote to production with the "Promote dev → main" workflow (promote.yml).
5
5
  #
6
+ # Deploys via the vitops toolchain's reusable EmDash deploy workflow — see
7
+ # https://github.com/getvitops/design-system/blob/main/.github/workflows/site-deploy-emdash.yml
8
+ # for what it does. Pinned to @v1 so a fix there reaches this repo without a
9
+ # manual re-paste; bump the tag deliberately if you want to pick up a change.
10
+ #
6
11
  # Required repo secrets:
7
12
  # CLOUDFLARE_API_TOKEN — token with Workers + D1 + R2 edit on this account
8
13
  # CLOUDFLARE_ACCOUNT_ID — the Cloudflare account id
@@ -14,28 +19,21 @@ on:
14
19
  push:
15
20
  branches: [dev]
16
21
 
17
- permissions:
18
- contents: read
19
-
20
22
  concurrency:
21
23
  group: deploy-dev
22
24
  cancel-in-progress: true
23
25
 
24
26
  jobs:
25
27
  deploy:
26
- runs-on: ubuntu-latest
27
- environment: dev
28
- steps:
29
- - uses: actions/checkout@v4
30
- - uses: pnpm/action-setup@v4
31
- - uses: actions/setup-node@v4
32
- with:
33
- node-version: '24'
34
- cache: pnpm
35
- - run: pnpm install --frozen-lockfile
36
- - name: Build + deploy dev worker
37
- run: pnpm run deploy:dev
38
- env:
39
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
40
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
41
- DEV_D1_ID: ${{ secrets.DEV_D1_ID }}
28
+ uses: getvitops/design-system/.github/workflows/site-deploy-emdash.yml@v1
29
+ with:
30
+ environment: dev
31
+ is-dev: true
32
+ # Set this to your dev worker's URL (or dev.<domain>) once known, so a
33
+ # bad migration fails the deploy instead of surfacing to the first
34
+ # visitor — see the reusable workflow's docblock.
35
+ health-check-url: ''
36
+ secrets:
37
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
38
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
39
+ DEV_D1_ID: ${{ secrets.DEV_D1_ID }}
@@ -1,36 +1,32 @@
1
1
  # Deploys `main` to production.
2
2
  # `main` should be branch-protected — it then only advances via the Promote
3
- # workflow (promote.yml), so a push here always represents promoted code.
3
+ # workflow (promote.yml), so a push here always represents promoted code. The
4
+ # `production` GitHub Environment (set up by `scripts/init-github.mjs`) has
5
+ # required reviewers restricted to admins, so this PAUSES for approval before
6
+ # deploying.
4
7
  #
5
- # Required repo secrets:
6
- # CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID
8
+ # Deploys via the vitops toolchain's reusable EmDash deploy workflow — see
9
+ # https://github.com/getvitops/design-system/blob/main/.github/workflows/site-deploy-emdash.yml
10
+ #
11
+ # Required repo secrets: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID
7
12
  name: Deploy prod
8
13
 
9
14
  on:
10
15
  push:
11
16
  branches: [main]
12
17
 
13
- permissions:
14
- contents: read
15
-
16
18
  concurrency:
17
19
  group: deploy-prod
18
20
  cancel-in-progress: false
19
21
 
20
22
  jobs:
21
23
  deploy:
22
- runs-on: ubuntu-latest
23
- environment: production
24
- steps:
25
- - uses: actions/checkout@v4
26
- - uses: pnpm/action-setup@v4
27
- - uses: actions/setup-node@v4
28
- with:
29
- node-version: '24'
30
- cache: pnpm
31
- - run: pnpm install --frozen-lockfile
32
- - name: Build + deploy prod worker
33
- run: pnpm run deploy
34
- env:
35
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
36
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
24
+ uses: getvitops/design-system/.github/workflows/site-deploy-emdash.yml@v1
25
+ with:
26
+ environment: production
27
+ is-dev: false
28
+ # Set this to your production URL once known.
29
+ health-check-url: ''
30
+ secrets:
31
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
32
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
@@ -2,48 +2,20 @@
2
2
  # Opens a PR from dev into the protected `main` branch and enables auto-merge;
3
3
  # the merge triggers Deploy prod.
4
4
  #
5
- # Branch protection on `main` means the merge only completes when its rules are
6
- # satisfied. Choose ONE:
7
- # (a) let a human approve the PR (required reviews) — safest, full audit trail;
8
- # (b) set repo secret PROMOTE_TOKEN to a PAT / GitHub App token allowed to
9
- # bypass protection, for hands-off promotion.
10
- # The default GITHUB_TOKEN cannot approve its own PR, so with required reviews
11
- # the PR waits for a human — which is usually what you want for a prod promotion.
5
+ # Deploys via the vitops toolchain's reusable promote workflow — see
6
+ # https://github.com/getvitops/design-system/blob/main/.github/workflows/site-promote.yml
7
+ # for the branch-protection / PROMOTE_TOKEN options.
12
8
  #
13
- # Schema/content promotion is deliberately NOT here — the content model lives in
14
- # the EmDash database, not git. Apply schema changes to prod via the admin or
15
- # the site's MCP server, additive changes before the code deploy that uses
9
+ # Schema/content promotion is deliberately NOT here — the content model lives
10
+ # in the EmDash database, not git. Apply schema changes to prod via the admin
11
+ # or the site's MCP server, additive changes before the code deploy that uses
16
12
  # them, removals after the code that stops using them.
17
13
  name: Promote dev → main
18
14
 
19
15
  on:
20
16
  workflow_dispatch:
21
17
 
22
- permissions:
23
- contents: write
24
- pull-requests: write
25
-
26
18
  jobs:
27
19
  promote:
28
- runs-on: ubuntu-latest
29
- steps:
30
- - uses: actions/checkout@v4
31
- with:
32
- fetch-depth: 0
33
- - name: Open promotion PR and enable auto-merge
34
- env:
35
- GH_TOKEN: ${{ secrets.PROMOTE_TOKEN || github.token }}
36
- run: |
37
- set -euo pipefail
38
- if gh pr view dev --base main --json number >/dev/null 2>&1; then
39
- echo "Promotion PR already open; ensuring auto-merge is enabled."
40
- else
41
- # printf, not a multi-line string: an unindented continuation line
42
- # would end this YAML block and break the whole workflow file.
43
- gh pr create --base main --head dev \
44
- --title "Promote dev → main" \
45
- --body "$(printf '%s\n\n%s' \
46
- "Automated code promotion from \`dev\`. Merging deploys to production." \
47
- "Remember: schema/content changes live in the EmDash database, not git — promote them separately if this release changes the content model.")"
48
- fi
49
- gh pr merge dev --auto --merge
20
+ uses: getvitops/design-system/.github/workflows/site-promote.yml@v1
21
+ secrets: inherit
@@ -7,6 +7,10 @@
7
7
  "emdash-docs": {
8
8
  "type": "http",
9
9
  "url": "https://docs.emdashcms.com/mcp"
10
+ },
11
+ "semrush": {
12
+ "type": "http",
13
+ "url": "https://mcp.semrush.com/v1/mcp"
10
14
  }
11
15
  }
12
16
  }
@@ -0,0 +1 @@
1
+ 24
@@ -7,6 +7,10 @@ styled by the Vitops design system.
7
7
  (colors, type, spacing, patterns, animations). Never edit
8
8
  `src/styles/tailwind.css` — it is generated from the JSON by the
9
9
  `vitops()` integration on every dev/build and is gitignored.
10
+ - `src/assets/placeholder/` holds neutral wireframe stand-ins (square,
11
+ portrait, landscape; SVG + PNG) for layout work before real imagery
12
+ exists. Delete the directory once real assets land — it's separate from
13
+ `src/assets/logo.svg`, the actual brand asset.
10
14
  - Content lives in the EmDash database, not in the repo. Query it with
11
15
  `getEmDashCollection` / `getEmDashEntry` from `emdash`; render Portable Text
12
16
  with `<PortableText />` from `emdash/ui` (the Vitops blocks from
@@ -20,6 +24,49 @@ styled by the Vitops design system.
20
24
  - Prefer framework CSS classes (utilities, patterns, type roles like
21
25
  `font-heading`) over bespoke CSS. Run `npx vitops agents` to append the
22
26
  full generated class-vocabulary reference to this file.
27
+ - **Layout foundations — these are the answer, not a starting point.** Six
28
+ substitutions carry the structure of nearly every page:
29
+ | About to write… | Write instead |
30
+ | --- | --- |
31
+ | a `wrap`/`wrapper`/`container`/`inner` class | **`centered`** |
32
+ | `max-width` + `margin-inline: auto` | **`centered`** |
33
+ | `padding-block` on a section | **`region`** |
34
+ | margins between headings/paragraphs/lists | **`rhythm`** |
35
+ | `display: grid` + `grid-template-columns: repeat(n, …)` | **`subgrid`** |
36
+ | `repeat(auto-fit, minmax(…))` | **`grid-auto`** |
37
+ | `display: flex` + `gap` for a row of controls | **`cluster`** |
38
+
39
+ `centered` is a grid of _named tracks_, not a max-width box — a child widens
40
+ itself with `breakout`/`spotlight`/`fullbleed` without the parent knowing, which
41
+ is the thing a bespoke `.wrap` can never do. **More than one card means
42
+ `subgrid`**: a plain grid aligns the outer boxes but not the tranches inside
43
+ them, so headings and CTAs land at different heights across the set. Write
44
+ `<ul class="subgrid subgrid-cols-3" role="list">` with
45
+ `<li class="card subgrid-card">` items, or `<Subgrid />` (which renders the slot
46
+ verbatim — author the `<li>`s yourself; it adds `role="list"` for you, and a
47
+ hand-written one needs it because a marker-less `<ul>` stops being announced as a
48
+ list in Safari).
49
+
50
+ **When the whole card is a link the card is still the item**, and pick by whether
51
+ the card's text must stay selectable: `stretched-link` on a link inside it is
52
+ zero-JS but kills text selection, while `<Cards>` (which wraps the list in
53
+ `<wc-cards>`) keeps selection and falls back to that same link with no JS. Never
54
+ both — the overlay wins and the JS never runs. Never `<li><a class="card">`: the
55
+ `li` is the grid item, so the anchor's tranches never align and it does not fill
56
+ the cell; it renders fine, which is why it keeps getting written.
57
+
58
+ `npm run lint:design` reports all of these, and the pre-commit hook fails on them
59
+ — the fix is the class, not a justification for the CSS.
60
+
61
+ - **Prerender by default.** `output: 'server'` is required (the admin, media/API
62
+ routes, preview and scheduled publishing need a server), so prerendering is
63
+ opt-in per route: put `export const prerender = true` on every page that
64
+ doesn't need per-request data, and give dynamic routes a `getStaticPaths()`
65
+ that queries the collection at build time. A route with no `prerender` export
66
+ is re-rendered for every visitor on every request. The cost is that a
67
+ prerendered page reflects the database as of the last build, so an admin
68
+ publish needs a redeploy — leave a route server-rendered only where that is
69
+ unacceptable (preview, personalised content, must-be-live-in-seconds).
23
70
  - The Worker cron trigger in `wrangler.jsonc` drives scheduled publishing —
24
71
  don't remove it.
25
72
  - Hosting (adapter + EmDash database/storage) comes from `vitopsHosting()`
@@ -16,6 +16,10 @@ Scaffolded from `vp create @getvitops:emdash`.
16
16
  - **`@getvitops/emdash`** — design-system Portable Text blocks in the editor
17
17
  slash menu: image compare, copy snippet, banner, disclosure, carousel
18
18
 
19
+ `src/assets/placeholder/` ships neutral wireframe images for layout work
20
+ before real imagery exists — delete once real assets land (separate from
21
+ `src/assets/logo.svg`, the brand asset).
22
+
19
23
  ## Local development
20
24
 
21
25
  ```bash
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://unpkg.com/@getvitops/generator/schema.json",
2
+ "$schema": "./node_modules/@getvitops/generator/schema.json",
3
3
  "colors": {
4
4
  "palette": {
5
5
  "brand": { "seed": "#2e9b73" },
@@ -54,7 +54,7 @@
54
54
  "p": "1rem",
55
55
  "fs": "1rem"
56
56
  },
57
- "radii": { "pill": "999px", "circle": "50%" },
57
+ "radii": { "circle": "50%" },
58
58
  "groups": {
59
59
  "control": {
60
60
  "ds": "var(--ds-default)",
@@ -13,6 +13,7 @@
13
13
  "init:hosting": "node scripts/init-hosting.mjs",
14
14
  "typecheck": "astro check",
15
15
  "build:css": "vitops generate -i design-system.json -f tailwind -o src/styles",
16
+ "lint:design": "vitops lint -i design-system.json -f tailwind --strict",
16
17
  "prepare": "vp config"
17
18
  },
18
19
  "dependencies": {
@@ -1,15 +1,29 @@
1
1
  // One-time GitHub bootstrap: creates a GitHub repo named after this project
2
- // (package.json name), pushes `main` + `dev`, and makes `dev` the default
3
- // branch — the shape the deploy workflows expect (deploy-dev on push to dev,
4
- // deploy-prod on push to main, promote.yml between them).
2
+ // (package.json name), pushes `main` + `dev`, makes `dev` the default branch,
3
+ // and provisions the branch-protection + environment policy the deploy
4
+ // workflows assume (deploy-dev on push to dev, deploy-prod on push to main,
5
+ // promote.yml between them):
6
+ //
7
+ // - `main` requires a PR + the `ci` status check — it only advances via
8
+ // promote.yml, never a direct push.
9
+ // - the `production` GitHub Environment requires review from an admin
10
+ // collaborator before deploy-prod.yml is allowed to run — this is the
11
+ // "promoting to prod needs admin sign-off" gate.
12
+ // - `dev` is left UNPROTECTED on purpose — that's what lets an agent (or
13
+ // anyone with push access) commit and merge there directly for
14
+ // integration; only the promotion to `main` is gated.
5
15
  //
6
16
  // pnpm run init:github # private repo (default)
7
17
  // pnpm run init:github -- --public
8
18
  // pnpm run init:github -- --dry-run
9
19
  //
10
- // Idempotent: safe to re-run — existing git repo, commits, dev branch, and
11
- // origin remote are all left alone. Requires the GitHub CLI (`gh`), logged in
12
- // (`gh auth login`).
20
+ // Idempotent: safe to re-run — existing git repo, commits, dev branch, origin
21
+ // remote, branch protection and environments are all left alone or updated
22
+ // in place. Requires the GitHub CLI (`gh`), logged in (`gh auth login`).
23
+ //
24
+ // NOTE: branch protection and environment required-reviewers are GitHub
25
+ // Pro/Team features on a PRIVATE repo. Each `gh api` call below prints the
26
+ // manual equivalent and continues rather than aborting when it 403s.
13
27
 
14
28
  import { execSync, spawnSync } from 'node:child_process';
15
29
  import { readFileSync } from 'node:fs';
@@ -48,6 +62,26 @@ function run(cmd) {
48
62
  }
49
63
  }
50
64
 
65
+ /**
66
+ * Like `run`, but for `gh api` calls that provision an org/plan-gated
67
+ * feature (branch protection, environment reviewers). A 403 there means the
68
+ * repo's plan doesn't support it — not that the bootstrap failed — so this
69
+ * prints the manual equivalent and keeps going instead of exiting.
70
+ */
71
+ function runPolicy(cmd, manualHint) {
72
+ if (dryRun) {
73
+ console.log(`[dry-run] ${cmd}`);
74
+ return;
75
+ }
76
+ console.log(`$ ${cmd}`);
77
+ const result = spawnSync(cmd, { shell: true, encoding: 'utf8' });
78
+ if (result.status === 0) return;
79
+ console.warn(
80
+ `\nWARNING: \`${cmd}\` failed (likely needs GitHub Pro/Team on a private repo).\n` +
81
+ `Set it up manually instead:\n ${manualHint}\n`,
82
+ );
83
+ }
84
+
51
85
  // --- Preflight: gh installed and authenticated -------------------------------
52
86
  if (check(['gh', '--version']).status !== 0) {
53
87
  console.error('ERROR: the GitHub CLI (`gh`) is required. Install it from https://cli.github.com');
@@ -64,7 +98,7 @@ if (quiet('git rev-parse --is-inside-work-tree') === null) {
64
98
  }
65
99
  if (quiet('git rev-parse HEAD') === null) {
66
100
  run('git add -A');
67
- run('git commit -m "Initial commit (scaffolded from @getvitops/create emdash template)"');
101
+ run('git commit -m "Initial commit (scaffolded from @getvitops/create)"');
68
102
  }
69
103
 
70
104
  // The workflows key off `main`; normalize whatever the initial branch is.
@@ -86,9 +120,58 @@ run('git push -u origin main dev');
86
120
  run('gh repo edit --default-branch dev');
87
121
  run('git checkout dev');
88
122
 
123
+ // --- Policy: protect main, gate production behind admin review --------------
124
+ const nwo = quiet('gh repo view --json nameWithOwner -q .nameWithOwner');
125
+ if (nwo) {
126
+ runPolicy(
127
+ `gh api -X PUT repos/${nwo}/branches/main/protection --input - <<'JSON'
128
+ {
129
+ "required_status_checks": { "strict": true, "contexts": ["ci"] },
130
+ "enforce_admins": false,
131
+ "required_pull_request_reviews": { "required_approving_review_count": 1 },
132
+ "restrictions": null,
133
+ "allow_force_pushes": false,
134
+ "allow_deletions": false
135
+ }
136
+ JSON`,
137
+ `Settings → Branches → protect 'main' (require a PR, 1 approval, the 'ci' ` +
138
+ `status check, no force-push/deletion).`,
139
+ );
140
+
141
+ const admins = quiet(
142
+ `gh api repos/${nwo}/collaborators?permission=admin -q '[.[].login]' 2>/dev/null`,
143
+ );
144
+ const reviewers = admins
145
+ ? JSON.parse(admins).map((login) => ({ type: 'User', id_or_login: login }))
146
+ : [];
147
+
148
+ runPolicy(
149
+ `gh api -X PUT repos/${nwo}/environments/production --input - <<'JSON'
150
+ {
151
+ "reviewers": ${JSON.stringify(reviewers)},
152
+ "deployment_branch_policy": { "protected_branches": true, "custom_branch_policies": false }
153
+ }
154
+ JSON`,
155
+ `Settings → Environments → 'production' → require reviewers = your admins, ` +
156
+ `restrict deployment branches to 'main'. This is the prod-needs-admin-approval gate.`,
157
+ );
158
+
159
+ // `dev` gets an environment too (for the deploy-dev workflow's `environment:
160
+ // dev` reference), but deliberately no reviewers and no branch restriction —
161
+ // that's what keeps it open for direct pushes/merges.
162
+ runPolicy(
163
+ `gh api -X PUT repos/${nwo}/environments/dev --input - <<'JSON'
164
+ { "reviewers": [] }
165
+ JSON`,
166
+ `Settings → Environments → create 'dev' with no required reviewers.`,
167
+ );
168
+ } else {
169
+ console.warn('WARNING: could not resolve the repo name-with-owner; skipping policy setup.');
170
+ }
171
+
89
172
  // --- What's left (manual, needs your Cloudflare account) ---------------------
90
173
  console.log(`
91
- Repo ready: main (production) + dev (integration, default branch).
174
+ Repo ready: main (protected, production) + dev (integration, default branch).
92
175
  NOTE: the first workflow runs will fail until the secrets below exist.
93
176
 
94
177
  Next steps:
@@ -97,6 +180,7 @@ Next steps:
97
180
  2. Secrets: gh secret set CLOUDFLARE_API_TOKEN
98
181
  gh secret set CLOUDFLARE_ACCOUNT_ID
99
182
  gh secret set DEV_D1_ID
100
- 3. Protect main: Settings → Branches → protect 'main' (require the
101
- promotion PR + review), so prod only ships via promote.yml.
183
+ 3. Verify policy: Settings → Branches (main protected?) and
184
+ Settings → Environments → production (reviewers set?) —
185
+ printed above if this ran on a plan that supports them.
102
186
  `);
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 400 400"><rect width="100%" height="100%" fill="#DDDDDD"/><path fill="#999999" d="M87.3 204.72h11.66v-12.54q0-1.77.24-3.92zm17.36 0h4.82v3.79q0 .53-.35.91-.34.39-.98.39h-3.49v9.35h-5.7v-9.35H82.79q-.66 0-1.16-.42-.51-.41-.64-1.02l-.69-3.31 18.16-24.25h6.2zm35.59-4.72q0 5.01-1.08 8.71-1.07 3.69-2.97 6.1-1.89 2.42-4.47 3.59-2.59 1.18-5.58 1.18-3 0-5.55-1.18-2.56-1.17-4.44-3.59-1.88-2.41-2.94-6.1-1.06-3.7-1.06-8.71 0-5.03 1.06-8.72 1.06-3.68 2.94-6.09t4.44-3.59q2.55-1.18 5.55-1.18 2.99 0 5.58 1.18 2.58 1.18 4.47 3.59 1.9 2.41 2.97 6.09 1.08 3.69 1.08 8.72m-6.76 0q0-4.16-.61-6.89t-1.63-4.35q-1.02-1.61-2.35-2.26-1.32-.65-2.75-.65-1.41 0-2.72.65t-2.32 2.26q-1 1.62-1.6 4.35t-.6 6.89.6 6.89 1.6 4.35q1.01 1.61 2.32 2.26t2.72.65q1.43 0 2.75-.65 1.33-.65 2.35-2.26 1.02-1.62 1.63-4.35t.61-6.89m37.5 0q0 5.01-1.08 8.71-1.07 3.69-2.97 6.1-1.89 2.42-4.47 3.59-2.59 1.18-5.58 1.18-3 0-5.55-1.18-2.56-1.17-4.44-3.59-1.88-2.41-2.94-6.1-1.06-3.7-1.06-8.71 0-5.03 1.06-8.72 1.06-3.68 2.94-6.09t4.44-3.59q2.55-1.18 5.55-1.18 2.99 0 5.58 1.18 2.58 1.18 4.47 3.59 1.9 2.41 2.97 6.09 1.08 3.69 1.08 8.72m-6.76 0q0-4.16-.61-6.89t-1.63-4.35q-1.02-1.61-2.35-2.26-1.32-.65-2.75-.65-1.41 0-2.72.65t-2.32 2.26q-1 1.62-1.6 4.35t-.6 6.89.6 6.89 1.6 4.35q1.01 1.61 2.32 2.26t2.72.65q1.43 0 2.75-.65 1.33-.65 2.35-2.26 1.02-1.62 1.63-4.35t.61-6.89m48.84 10.39-3.47 3.42-8.99-8.99-9.06 9.04-3.47-3.42 9.06-9.09-8.64-8.64 3.45-3.44 8.64 8.64 8.58-8.59 3.5 3.45-8.61 8.61zm22.95-5.67h11.66v-12.54q0-1.77.24-3.92zm17.35 0h4.83v3.79q0 .53-.35.91-.34.39-.98.39h-3.5v9.35h-5.69v-9.35h-16.17q-.66 0-1.16-.42-.51-.41-.64-1.02l-.69-3.31 18.15-24.25h6.2zm35.59-4.72q0 5.01-1.07 8.71-1.07 3.69-2.97 6.1-1.89 2.42-4.48 3.59-2.58 1.18-5.57 1.18-3 0-5.56-1.18-2.55-1.17-4.43-3.59-1.89-2.41-2.95-6.1-1.06-3.7-1.06-8.71 0-5.03 1.06-8.72 1.06-3.68 2.95-6.09 1.88-2.41 4.43-3.59 2.56-1.18 5.56-1.18 2.99 0 5.57 1.18 2.59 1.18 4.48 3.59 1.9 2.41 2.97 6.09 1.07 3.69 1.07 8.72m-6.75 0q0-4.16-.61-6.89t-1.63-4.35q-1.02-1.61-2.35-2.26-1.32-.65-2.75-.65-1.41 0-2.72.65t-2.32 2.26q-1.01 1.62-1.6 4.35-.6 2.73-.6 6.89t.6 6.89q.59 2.73 1.6 4.35 1.01 1.61 2.32 2.26t2.72.65q1.43 0 2.75-.65 1.33-.65 2.35-2.26 1.02-1.62 1.63-4.35t.61-6.89m37.49 0q0 5.01-1.07 8.71-1.07 3.69-2.97 6.1-1.89 2.42-4.48 3.59-2.58 1.18-5.57 1.18-3 0-5.56-1.18-2.55-1.17-4.43-3.59-1.89-2.41-2.95-6.1-1.06-3.7-1.06-8.71 0-5.03 1.06-8.72 1.06-3.68 2.95-6.09 1.88-2.41 4.43-3.59 2.56-1.18 5.56-1.18 2.99 0 5.57 1.18 2.59 1.18 4.48 3.59 1.9 2.41 2.97 6.09 1.07 3.69 1.07 8.72m-6.75 0q0-4.16-.61-6.89t-1.63-4.35q-1.02-1.61-2.35-2.26-1.32-.65-2.75-.65-1.41 0-2.72.65t-2.32 2.26q-1.01 1.62-1.6 4.35-.6 2.73-.6 6.89t.6 6.89q.59 2.73 1.6 4.35 1.01 1.61 2.32 2.26t2.72.65q1.43 0 2.75-.65 1.33-.65 2.35-2.26 1.02-1.62 1.63-4.35t.61-6.89"/></svg>