Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7131d2b
chore: add .playwright-mcp to .gitignore
odenirdev Jul 16, 2026
5d5a717
docs: design spec for preset-layout / grid system (v1)
odenirdev Jul 16, 2026
0c02605
feat(web): add layout breakpoints and Responsive prop shape
odenirdev Jul 18, 2026
fad8220
feat(web): emit container width, paddingX, and grid gap tokens
odenirdev Jul 18, 2026
ba9b85c
feat(web): add <Container> layout primitive
odenirdev Jul 18, 2026
93c97b6
feat(web): add <Grid> 12-column layout primitive
odenirdev Jul 18, 2026
36151b6
feat(web): add <Column> layout primitive
odenirdev Jul 18, 2026
af55877
feat(web): add <Row> flexbox layout primitive
odenirdev Jul 18, 2026
cf4ba27
feat(web): export layout primitives from package entrypoint
odenirdev Jul 18, 2026
5a610d2
feat(web): add layout primitive CSS + breakpoint coordination test
odenirdev Jul 18, 2026
a8d33a4
refactor(web): Hero consumes Container+Grid+Column for responsive 2-c…
odenirdev Jul 18, 2026
eed8336
refactor(web): Cta wraps content in inner data-cta="frame" for Contai…
odenirdev Jul 18, 2026
45ab440
refactor(web): Footer wraps small in a Container; typography moves to…
odenirdev Jul 18, 2026
58bfd1b
refactor(web): Navbar + NavLinks consume Container/Row; layout primit…
odenirdev Jul 18, 2026
35ae2ac
feat(web): add MobileNav hamburger drawer for responsive nav UX
odenirdev Jul 18, 2026
706156b
fix(web): MobileNav closes only on true backdrop click; assert focus …
odenirdev Jul 18, 2026
1f37787
refactor(web): full-width main, prose atoms via selector, minimal chr…
odenirdev Jul 18, 2026
e331c15
docs: layout primitives family + changeset for the layout/grid v1
odenirdev Jul 18, 2026
dd9e010
docs: document MobileNav backdrop-only close contract in CLAUDE.md
odenirdev Jul 18, 2026
2f7b924
Refactor code structure for improved readability and maintainability
odenirdev Jul 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/consent-hydration-warning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@ogs-tech/press-web': patch
---

fix: suppress the React hydration warning caused by the consent bootstrap script

The pre-paint consent script stamps `<html data-press-consent="decided">`
before React hydrates, but the server-rendered element never carries that
attribute (reading the cookie in the RSC tree would force the route dynamic).
React 19 reported a hydration mismatch for every returning visitor who had
already decided. The host template's `<html>` now sets
`suppressHydrationWarning` — the canonical fix for pre-paint attribute
stamping (the next-themes pattern); it silences only that element's attribute
diff, so real content mismatches in children still surface.
63 changes: 63 additions & 0 deletions .changeset/layout-fine-tuning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
'@ogs-tech/press-web': patch
'@ogs-tech/create-press': patch
---

fix: layout/grid fine-tuning — Column span, uniform prose column, full-width footer, scrollable drawer, grid-showcase seed

`@ogs-tech/press-web`:

- **`<Column>` span no longer collapses to one track.** The `grid-column: span N`
shorthand stores the span in the START longhand, so the subsequent
`grid-column-start: auto` (the undeclared-`start` case) erased it — every
Column rendered as span 1. The span now rides on `grid-column-end`.
- **One editorial prose column for every atom.** `--press-container-prose`
moves from `72ch` to `42rem` (≈72ch at the 16px body size): a `ch` value
resolves against each consuming element's font, which gave a 28px heading a
~2× wider "prose" column than a paragraph. The column is now left-aligned to
the lg-container rail (same margin math as `<Container maxWidth="lg">`)
instead of viewport-centered, so atoms and organisms share a single left
axis at every viewport.
- **Hero gap is tier-scaled** (`{ base: 'md', lg: 'lg' }`). A 12-track grid
always carries 11 interior column-gaps, so a flat `lg` (48px) forced a 528px
minimum width and overflowed phone viewports even with every column stacked.
- **Footer goes full-width** (`maxWidth="full"`), matching the Navbar so both
chrome surfaces share edge-to-edge geometry — chrome is full-bleed, content
Containers are the constrained ones.
- **The mobile nav drawer panel scrolls** (`max-height` + `overflow-y: auto` +
`overscroll-behavior: contain`): body scroll is locked while the drawer is
open, so an overflowing menu tail was previously unreachable.
- **One button family.** Every button-shaped link the engine renders (button
atom, navbar CTA on desktop and in the drawer, hero CTA, cta-banner button)
now shares a single visual contract: an always-present transparent border
keeps both voices the same height, the secondary outline rides on
`currentColor` (it used the neutral `--press-color-border`, which read as
unfinished next to primary-colored text), and all surfaces gain hover /
active states derived from the variant's own colors via `color-mix`
(primary darkens toward ink, never black) plus a `:focus-visible` ring.
The cookie-consent banner buttons get the same secondary border, hover,
and focus treatment. No new tokens; flat strokes-over-shadows preserved.
- **Quote citations are anchored** with a typographic em-dash (`cite::before`)
— the cite sits outside the blockquote's left border and floated without it.

`@ogs-tech/create-press`:

- The seeded "Hello from press" home becomes a layout-system showcase — it
opens with the responsive 2-column `preset-organism.hero` (image + CTA) and
closes with a centered `preset-organism.cta` banner around the ~72ch prose
atom sequence. Demo content is deduplicated by design: one image, three
distinct CTA labels.
- The seed fills the bootstrap's bare navbar with demo navigation (Home, an
external GitHub item, a "Get started" CTA) so the header and the MobileNav
hamburger render out of the box.
- The example `Callout` custom block now wraps itself in `<Container>`,
demonstrating the non-atom custom-block contract (organisms own their own
width; only atoms are re-centered by the engine's prose selector).
- The seeded spacer between the GitHub button and the cta banner drops from
`lg` to `md` — the banner already carries its own section margin, and the
stacked pair read as a ~120px dead zone.
- Every seeded CTA now has its own destination: the cta banner points at the
press page on the OGS site (`https://useogs.com/press`, the launch-day
route), the navbar "Get started" opens the published
`@ogs-tech/create-press` npm page (the real scaffold quickstart), and the
hero "Read the docs" / "Star on GitHub" keep their repo semantics.
45 changes: 45 additions & 0 deletions .changeset/layout-grid-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
'@ogs-tech/press-web': major
---

feat!: engine-owned layout primitives (Container / Grid / Row / Column) + responsive nav

`@ogs-tech/press-web` now ships four React layout primitives — `Container`,
`Grid`, `Row`, `Column` — under `packages/web/src/layout/` (public export from
the package entrypoint). Primitives are server-first, zero-runtime: every
responsive behavior is expressed through a three-tier `Responsive<T>` prop
shape (`base=0`, `md=768px`, `lg=1024px`) that lowers to inline CSS custom
properties consumed by `theme.css` via a `var(a, var(b, var(c, default)))`
cascade. All four preset organisms — `Hero`, `Cta`, `Navbar`, `Footer` — are
refactored to consume the primitives: `Hero` gains a responsive 2-column layout
(text 7 / image 5 on md+, stacked at base), `Cta` moves its boxy visual onto an
inner `data-cta="frame"` wrapper, `Navbar` composes brand + links + CTA through
nested Rows, and `Footer` wraps its `<small>` in a Container.

A companion `MobileNav` client component adds a hamburger drawer for narrow
viewports (below `md`): toggle, aria-expanded, aria-modal="true" dialog,
Escape to close, body scroll lock, focus management. The desktop nav Row is
hidden below `md` and the hamburger takes over — no viewport-observer JS, just
a small stateful client component matched to a CSS media-query swap.

The `preset-layout` CMS palette category stays declared and labelled but ships
**zero components** — reserved for future organism-nested config components
(pattern: `preset-molecule.nav-item`).

BREAKING (visual): `main` no longer caps content at 72ch. Every preset atom
(`preset-atom.*`) and every custom atom (`custom-atom.*`) rendered inside `main`
preserves the ~72ch editorial reading width via a new CSS selector — the change
is transparent for adopters using engine atoms. Migration for adopter CUSTOM
NON-ATOM blocks that relied on the old cap: wrap in
`<Container maxWidth="prose">` (or `"lg"` for the wider content width) to
restore the desired width.

BREAKING (interactive client component): the navbar now mounts a client-side
`MobileNav`. Adopters overriding `preset-organism.navbar` via
`components={{ 'preset-organism.navbar': MyNavbar }}` are unaffected (their
component is rendered instead). Adopters KEEPING the engine navbar receive the
hamburger + drawer automatically.

Contract stability: `PressSchema`, `PageBody`, `HeaderBlocks`, `FooterBlocks`
are byte-identical. No CMS re-seed, no admin re-login, no data migration. No
`press-cms` bump — the CMS side is untouched.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ build/

# Git worktrees are local-only workspace; never track them
.claude/worktrees/
.playwright-mcp/
107 changes: 99 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,89 @@ Release (changesets): add a changeset under `.changeset/` for any engine change,

## Architecture — the moving parts

### Layout primitives (`packages/web/src/layout/`)

Engine-owned responsive layout is code, not content. Four React primitives —
`Container`, `Grid`, `Row`, `Column` — live in `packages/web/src/layout/`,
exported from `@ogs-tech/press-web`. Every primitive emits semantic HTML +
`data-press-layout="<primitive>"` + per-instance CSS custom properties; visual
rules read the vars via a `var(a, var(b, var(c, default)))` cascade in
`theme.css` so three-tier responsive behavior (`base 0` / `md 768px` /
`lg 1024px`) is expressed in CSS with zero runtime JS. The `Responsive<T>` prop
shape (`T | { base: T; md?: T; lg?: T }`) is uniform across every responsive
primitive prop. Container is the one non-responsive primitive — it picks a
single tier from a fixed width scale (`prose | sm | md | lg | xl | full`).
Two CSS subtleties are load-bearing: (1) Column's span rides on
`grid-column-END` — the `grid-column: span N` shorthand stores the span in the
START longhand, so a later `grid-column-start: auto` (the undeclared-`start`
case) would erase it and collapse every column to one track. (2) A 12-track
grid always carries 11 interior column-gaps, so a Grid's minimum width is
`11 × gap` even when every column spans 12 — a flat `gap="lg"` (48px) means a
528px floor that overflows phones; organisms declare a tier-scaled gap
(`{ base: 'md', lg: 'lg' }`, the Hero pattern) instead.

**Why two surfaces named `layout`.** (1) DEV-facing — the React primitives above,
consumed by engine organisms, future page-set-plugin templates, and adopter
custom blocks. (2) CMS-facing — the `preset-layout` Atomic Design category
stays declared in `PRESET_LAYERS` and labelled in the admin picker but ships
ZERO components today. The palette is reserved for future *nested-only* config
components (pattern: `preset-molecule.nav-item`) that a future organism admits
via a `component:` field. Layout is NEVER placed by the editor as a top-level
block — the Strapi 5 constraint "a component cannot contain a `dynamiczone`"
rules out polymorphic-child nesting inside a component.

**Data-attr namespace is distinct from blocks.** Primitives use
`data-press-layout="<primitive>"`, deliberately not `data-block="preset-*"`.
Primitives never have a `__component`, never appear in `PageBody`, never flow
through `BlockRenderer`.

**Breakpoints are TS constants, not CSS vars.** `@media (min-width: var(--x))`
is unsupported in production browsers, so `BREAKPOINTS` in
`src/layout/breakpoints.ts` and the literal pixel values in `theme.css` media
queries are the two sources — `src/layout/breakpoints.test.ts` reads
`theme.css` and asserts both sides match, catching any drift.

**Tokens live in `FIXED_TOKENS`, not adopter config.** Container widths,
`paddingX`, and the three grid gap sizes are engine-fixed (same policy as
`--press-space-*` / `--press-text-*`). Values are duplicated literals — not
`var()`-referenced against `--press-space-*` — because FIXED_TOKENS is the
source of truth and cross-referencing scales makes future edits fragile. Every
new var goes through `buildThemeStyle`'s single `:root` injection point.

**Shell is full-width; atoms preserve prose width via a selector.** `main` has
no `max-width`; a single rule (`main [data-block^="preset-atom."],
main [data-block^="custom-atom."] { max-width: var(--press-container-prose);
… }`) restores ~72ch editorial reading width for every preset atom AND every
custom atom — without touching a single atom `.tsx`. The `prose` token is
**rem-anchored** (`42rem` ≈ 72ch at the 16px body size) on purpose: a `ch`
value resolves against each consuming element's font, which would give a 28px
heading a ~2× wider "prose" column than a paragraph — the editorial column
must be identical for every atom. The column is **left-aligned to the lg
container rail**, not viewport-centered: its margin-start mirrors the
`<Container maxWidth="lg">` centering math + gutter, so atoms share one left
axis with hero/cta/callout at every viewport (a centered narrow column next to
lg organisms produced a zig-zag of left edges). Organisms and non-atom customs are excluded
on purpose: they own their own `<Container>` (the scaffold's example `Callout`
demonstrates the pattern). Header and footer chrome shells keep only the
border stroke + vertical padding; horizontal composition is the refactored
organisms' job — and BOTH chrome organisms (Navbar, Footer) use
`maxWidth="full"`: chrome is edge-to-edge, content Containers are the
constrained ones.

**Mobile nav is the one client-side responsive component.** `chrome/mobile-nav.tsx`
is a `'use client'` hamburger + drawer mounted inside `Navbar`, matched by CSS
media queries to the desktop nav Row (`[data-navbar-desktop]` visible ≥768px;
`[data-mobile-nav="toggle"]` visible <768px). Escape closes; a backdrop click
closes only when the click target IS the backdrop (`target === currentTarget` —
clicks inside the panel never close); body scroll locks while open — which is
why the drawer panel itself scrolls (`max-height` + `overflow-y: auto` +
`overscroll-behavior: contain`), or a long menu's tail would be unreachable;
aria-expanded/aria-modal wired; focus moves to the first link on open and
restores to the toggle on close. Deliberate exception to the
"server-first, zero-runtime layout" default — a viewport-observer approach
would drag the entire layout system into client-space; a fixed CSS breakpoint
+ small toggle state is the minimal viable contract.

### Materialization (`.press/web`)

The Next host is **not** scaffolded. `packages/web/templates/host/` is copied to
Expand Down Expand Up @@ -96,8 +179,13 @@ PRODUCT/plugin id (`plugin::press-cms.*`, `/api/press/schema`), never as a categ
one layer, unified from the old `section.*`/`chrome.*` palettes.
- `preset-config.*` — seo, theme-colors, theme-radius, cookie-consent, cookie-category:
non-block settings referenced by `component:` fields on Site Settings, never a DZ member.
- `preset-layout` + `preset-template` are RESERVED (labelled, no components yet):
layout ← the Grid System task; template ← page-set plugins.
- `preset-layout` is RESERVED (labelled, no components yet). The Grid System
task shipped the DEV-facing layout primitives under
`packages/web/src/layout/` (see "Layout primitives" above); the CMS-facing
category stays labelled and empty, seat for a future *nested-only* config
component (pattern: `preset-molecule.nav-item`) that a future organism admits
via a `component:` field.
- `preset-template` is RESERVED (labelled, no components yet) — page-set plugins.
- **Preset PLACEMENT is declared statically, per content-type — NOT by the category.**
The category carries the layer; where a preset block may go is listed in each
`schema.json`: `preset-atom.*` in all three engine DZs (page `body`, site-setting
Expand All @@ -123,7 +211,9 @@ PRODUCT/plugin id (`plugin::press-cms.*`, `/api/press/schema`), never as a categ
serializer follows these nested refs; the generator emits nested-only components without
`__component` and adds `HeaderBlocks`/`FooterBlocks` unions. `bootstrap()` seeds
`header: [preset-organism.navbar]`, `footer: [preset-organism.footer]` exactly once
(plugin-store flag) — an editor-emptied zone is respected.
(plugin-store flag) — an editor-emptied zone is respected. The bootstrap navbar is
BARE (no items/cta); the CLI's `seed.mjs` fills it with demo navigation (Home,
external GitHub, "Get started" CTA) in the same idempotent pass that fills identity.
- **Picker presentation (admin bundle):** every engine component JSON sets `info.icon`
(Strapi's fixed icon enum); the plugin's `./strapi-admin` bundle (`cms/admin/src/index.ts`)
exists solely to `registerTrads` the category labels — the picker resolves accordion
Expand All @@ -133,11 +223,12 @@ PRODUCT/plugin id (`plugin::press-cms.*`, `/api/press/schema`), never as a categ
completeness; `preset-layout`/`preset-template` are labelled ahead of their components.
Labels are presentation-only; uids never change for display. Adopter `src/admin/app.tsx`
translations override the engine's.
- **Page templates:** `bootstrap()` also seeds a "Privacy Policy" page (slug
`privacy-policy`) exactly once (`privacyPageSeeded` flag,
`lib/seed-page-privacy-policy.ts`) — a DRAFT composed of `preset-atom.*` atoms with
placeholder guidance, never auto-published; an adopter page already on the slug
or an editor-deleted page is respected forever.
- **Page templates:** the once-shipped "Privacy Policy" bootstrap seed was RETIRED;
what remains is `lib/seed-page.ts` — a generic, idempotent `seedPage(strapi, opts)`
primitive (flag-first, slug-collision-respecting, DRAFT-only) that is deliberately
exported-but-unused, awaiting future page-seeding consumers (Plugin/Legal,
archetype templates). `bootstrap()` seeds NO page today; the only page an adopter
starts with is the CLI seed's published `home`.
- On the web side, `BlockRenderer` merges the engine registries with the adopter map by
`__component`: `{ ...atomBlocks, ...organismBlocks, ...components }` — engine
`preset-atom.*` atoms (`src/atom-blocks.ts`), engine `preset-organism.*` organisms
Expand Down
2 changes: 1 addition & 1 deletion apps/playground/packages/cms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@
"npm": ">=6.0.0"
},
"strapi": {
"uuid": "b00875a1-05d6-42b5-b5c3-ef79987149da"
"uuid": "3df03067-2b3d-45b9-a1e5-f942710a84a4"
}
}
Loading
Loading