@gemboss/website-ui 0.12.0 → 0.14.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.
- package/AGENTS.md +18 -5
- package/CHANGELOG.md +65 -0
- package/README.md +6 -3
- package/bin/gemboss-website.mjs +13 -1
- package/bin/lib/build.mjs +101 -0
- package/bin/lib/check.mjs +47 -3
- package/bin/lib/init.mjs +93 -3
- package/css/article.css +1 -1
- package/css/components.css +87 -27
- package/css/header.css +13 -0
- package/css/ratings.css +0 -1
- package/css/sections.css +2 -3
- package/css/tokens.css +5 -0
- package/demo/header.html +1 -1
- package/docs/components.md +48 -2
- package/docs/theming.md +38 -11
- package/js/website.js +2 -2
- package/js/website.mjs +1 -1
- package/package.json +1 -1
- package/system.json +3 -1
- package/templates/page.html +1 -1
- package/templates/sections/form.html +2 -0
- package/templates/sections/hero.html +2 -1
- package/templates/sections/split.html +1 -1
- package/templates/site/app/[[...path]]/route.js +23 -0
- package/templates/site/assets/kit-gaps.css +3 -0
- package/templates/site/assets/site.css +16 -0
- package/templates/site/gitignore +4 -0
- package/templates/site/lib/pages.mjs +41 -0
- package/templates/site/next.config.mjs +15 -0
- package/templates/site/stage-public.mjs +49 -0
- package/templates/site/vercel.json +5 -0
- package/website.css +109 -33
- package/website.min.css +1 -1
package/AGENTS.md
CHANGED
|
@@ -17,6 +17,7 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
17
17
|
| `templates/header.html` | the header of every site, with its two options (the drill-down mobile menu, the language switcher): fill it once, in the site's shared partial |
|
|
18
18
|
| `templates/footer.html` | the footer of every site (the footer of gemboss.ai): fill it once, in the site's shared partial |
|
|
19
19
|
| `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose, article, post list, a short form, testimonials… |
|
|
20
|
+
| `templates/site/` | 0.13.0: what a new site owns from the start (the Next.js wrapper that serves its static pages, its page list, its public allow-list, `site.css`, `kit-gaps.css`): `npx gemboss-website init --site` writes it |
|
|
20
21
|
| `demo/index.html` | every component on one page: open it to see what exists before you build anything |
|
|
21
22
|
| `demo/brand-profiles.html` | a GemPages-style brand: its accent, the accent button, soft corners, the floating header |
|
|
22
23
|
| `demo/article.html`, `demo/blog.html` | a whole blog post (breadcrumbs, meta, contents, prose, callouts, FAQ) and the blog index (post list, pagination) |
|
|
@@ -39,13 +40,14 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
39
40
|
square and a chart line. **The accent is never body text, never words in a heading, never a section band, and
|
|
40
41
|
never a fill you set yourself.**
|
|
41
42
|
3. **Actions are rectangles, chips are pills.** Buttons: 40px tall, sentence case, 14px/600, the action radius of the
|
|
42
|
-
site's profile (8px; 12px in `soft`). Chips, toggles, badges: fully rounded
|
|
43
|
+
site's profile (8px; 12px in `soft`). Chips, toggles, badges: fully rounded; a status chip inside a drawn screen
|
|
44
|
+
(`.gw-status`, 0.14.0) takes the action radius, as the product draws it. Cards: the card radius (12px; 20px in
|
|
43
45
|
`soft`), large panels the panel radius (16px; 24px). No other radii: the site picks its profile once
|
|
44
46
|
(`data-gw-radius`), components follow.
|
|
45
47
|
4. **A card sits.** A card (`.gw-card`, a plan) is a white box with a 1px `--gw-line` border and the resting shadow
|
|
46
48
|
`--gw-shadow`, the same as the GemBoss dashboard. It never lifts on hover. To separate anything else, use space or a
|
|
47
49
|
hairline before a shadow. The only shadows are `--gw-shadow` (rest), `--gw-shadow-sm` and `--gw-shadow-raised`
|
|
48
|
-
(popovers: the mega menu, menus).
|
|
50
|
+
(popovers: the mega menu, menus; and a window, `.gw-window`, which floats over its section as on gemboss.ai).
|
|
49
51
|
5. **Only the scales.** Spacing: 0 2 4 6 8 12 16 20 24 32 40 48 64 80 96 120 (px). Type: 11 12 13 14 16 18 20 22 24 30,
|
|
50
52
|
plus the two display sizes for h1/h2. No half pixels, no "just 15px", no colour outside the tokens.
|
|
51
53
|
6. **Sentence case everywhere.** Headings, buttons, labels, menu items. No uppercase with letter spacing.
|
|
@@ -60,6 +62,13 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
60
62
|
|
|
61
63
|
## 2. Build a page
|
|
62
64
|
|
|
65
|
+
**A new site** (0.13.0, the way gemboss.ai and gemians.com are built): `npx gemboss-website init --site` in an empty
|
|
66
|
+
folder writes the header and footer partials, `index.html` and `404.html`, `assets/site.css`, `assets/kit-gaps.css` and
|
|
67
|
+
the Next.js wrapper that serves the static pages, and adds the scripts to `package.json`. Then each page is a file with
|
|
68
|
+
`<!-- gw:header -->` and `<!-- gw:footer -->` markers, and `npx gemboss-website build` writes the partials into every
|
|
69
|
+
page (with `aria-current="page"` on its own links) and copies the kit into `assets/kit/`. Never hand-copy a header into
|
|
70
|
+
a page. A page of such a site starts from `index.html`, not from `templates/page.html`.
|
|
71
|
+
|
|
63
72
|
1. Copy `templates/page.html` into the site. It links `website.css` and `website.js`, has the skip link, the
|
|
64
73
|
header, the `<main>` and the footer. Fill the header and footer once (they are the same on every page).
|
|
65
74
|
**The footer comes from `templates/footer.html`.** A brand fills its rows (the columns, the family rows, the
|
|
@@ -83,7 +92,7 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
83
92
|
- **Colour roles** (CSS variables, light and dark): `--gw-bg`, `--gw-bg-soft` (the neutral raised fill),
|
|
84
93
|
`--gw-surface`, `--gw-ink`, `--gw-ink-2`, `--gw-muted`, `--gw-line`, `--gw-line-strong`, `--gw-action`,
|
|
85
94
|
`--gw-on-action`, `--gw-accent`, `--gw-accent-strong`, `--gw-accent-soft`, `--gw-success`, `--gw-warning` (and
|
|
86
|
-
their tints `--gw-success-soft`, `--gw-warning-soft
|
|
95
|
+
their tints `--gw-success-soft`, `--gw-warning-soft`; success's text form `--gw-success-strong`, 0.14.0), `--gw-danger` (an error, a destructive button; `--gw-danger-soft`,
|
|
87
96
|
`--gw-on-danger`), `--gw-line-control` (a form control's border, 3:1), `--gw-night` (the dark band). Wash hues `--gw-wash-pink|violet|sky|mint` exist for the hero wash and illustrations only.
|
|
88
97
|
- **Dark mode** follows the visitor's system and the theme toggle (`data-theme` on `<html>`). Use the variables and
|
|
89
98
|
it works; a hard-coded colour breaks it.
|
|
@@ -225,7 +234,9 @@ A brand other than GemBoss chooses **only** these, and nothing else:
|
|
|
225
234
|
Do not set `--gw-accent` itself: the kit points it at those per theme, and a direct override loses as soon as the
|
|
226
235
|
theme toggle is pressed. Accent text uses `--gw-accent-strong`, so it must reach 4.5:1 on `--gw-bg` and
|
|
227
236
|
`--gw-bg-soft`;
|
|
228
|
-
- **the wash hues**, **the logo** in the header and footer
|
|
237
|
+
- **the wash hues**, **the logo** in the header and footer (a mark beside the name, or one drawing of both:
|
|
238
|
+
`.gw-brand__logo`, with a second file for dark grounds when the brand has one), and **the favicon**;
|
|
239
|
+
- **the announcement bar's and the Stage's colours** (`--gw-announce-*`, `--gw-stage-*`), only when it shows them;
|
|
229
240
|
- **three named profiles**, once for the site, never free values:
|
|
230
241
|
- action colour: `data-gw-action="accent"` on `<html>` fills the primary button with the accent (default `ink`).
|
|
231
242
|
Its words (`--gw-on-accent`) must reach 4.5:1 on `--gw-accent` and `--gw-accent-strong`, light and dark, or
|
|
@@ -242,7 +253,9 @@ examples: `demo/theme-example.html` (accent only) and `demo/brand-profiles.html`
|
|
|
242
253
|
- Link the kit, then the site's own CSS after it: `website.css` → `site.css`.
|
|
243
254
|
- The site's own CSS is for content the kit cannot know (an illustration, a product mock-up) and uses only the
|
|
244
255
|
tokens. Its classes start with the site's prefix (for example `gp-` on GemPages), never `gw-`.
|
|
245
|
-
- Shared header and footer markup lives in one place in the site (a partial or component), not copied per page
|
|
256
|
+
- Shared header and footer markup lives in one place in the site (a partial or component), not copied per page:
|
|
257
|
+
`partials/header.html` and `partials/footer.html`, written into the pages by `npx gemboss-website build` (CI runs
|
|
258
|
+
`build --check`, which fails when a page is out of date).
|
|
246
259
|
|
|
247
260
|
## 9. When the kit is missing something
|
|
248
261
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,71 @@ Every release lists what changed for a site that uses the kit. A release that ad
|
|
|
4
4
|
says so with `Closes gap #N` (the GitHub issue number), so `npx gemboss-website upgrade` can tell that site which
|
|
5
5
|
stopgap (`kitgap-*`) it can now delete.
|
|
6
6
|
|
|
7
|
+
## 0.14.0 (2026-10-27)
|
|
8
|
+
|
|
9
|
+
The blocks gemboss.ai shares between its pages, 1:1 in the kit: the FAQ, the check list, the window around a product
|
|
10
|
+
screen, and a new status chip (Chris 26/10/2026: step 2 of the pages' bodies onto the kit). gemboss.ai's pages now draw
|
|
11
|
+
these with the kit's classes and were measured against the pages before, pixel by pixel. Where gemboss.ai had a small
|
|
12
|
+
accessibility slip, the kit's version won (Chris 27/10/2026): those three spots are named below. A minor version: a site
|
|
13
|
+
on 0.13.0 sees these parts change look, none of the markup.
|
|
14
|
+
|
|
15
|
+
- **FAQ** (`.gw-faq`): an item 2px 24px inside; the question in the brand face alone (`--gw-font-action`) at the line
|
|
16
|
+
height around it; the + is the face's own glyph in the accent, 24px, turned 45 degrees into × when open (was drawn
|
|
17
|
+
lines); an answer's paragraph at most 33em (was 68ch) and rising 4px into place as the item opens (none under reduced
|
|
18
|
+
motion). On phones (up to 560px) the rows tighten to 4px above and below, never under 44px (gemboss.ai went to 37px:
|
|
19
|
+
the kit's 44px won).
|
|
20
|
+
- **Check list** (`.gw-list--check`): 14px at 1.5 lines, the words 28px in, the mark a 16px tile on the accent's tint with
|
|
21
|
+
the tick in the accent (was a bare tick). The tick follows the theme and the brand (gemboss.ai's was a fixed iris, and
|
|
22
|
+
ink on /partners, so it stayed light in dark mode: the kit's won). A plain link in an item keeps the link style
|
|
23
|
+
(/audit showed one as plain text: the kit's won).
|
|
24
|
+
- **Window** (`.gw-window`): it floats (`--gw-shadow-raised`, was the resting shadow), 13px at 20px lines. The bar is
|
|
25
|
+
40px with 12px sides, three 8px dots 6px apart drawn as real circles, the words 16px after them, `<b>` in ink, one line
|
|
26
|
+
cut when too long; up to 560px the words wrap and the bar grows, its parts at the top.
|
|
27
|
+
- **Status chip** (`.gw-status`, new): gemboss.ai's "pills", a state inside a drawn screen as the GemBoss app draws it: a
|
|
28
|
+
rounded rectangle at the action radius, 28px, 12px/500; `--success` (live), `--accent` (a test), `--early` (neutral,
|
|
29
|
+
the dot in the accent), an optional `__dot`. A badge (`.gw-badge`) stays the pill that labels a thing on the page.
|
|
30
|
+
- **Token** `--gw-success-strong` (new, `system.json` `color.light/dark`): success's text form, `#0a6a4c` light (5.6:1 on
|
|
31
|
+
its tint) and `#34c38f` dark, the words of a live status chip.
|
|
32
|
+
- Docs: `AGENTS.md` lists the window among the raised shadows and the status chip among the shapes; `docs/components.md`
|
|
33
|
+
describes each part's measures.
|
|
34
|
+
|
|
35
|
+
## 0.13.0 (2026-10-26)
|
|
36
|
+
|
|
37
|
+
What building a second site on the kit taught it. gemians.com was rebuilt on 0.12.0 from npm, kit sections only (Chris,
|
|
38
|
+
26/10/2026: inherit from the GemBoss website kit, fix the kit where it gets in the way, then update Gemians and
|
|
39
|
+
GemBoss), and every place the kit fell short became one of these. A minor version: nothing a 0.12.0 site uses changes
|
|
40
|
+
its look, except a list right under a section's head (below), which now gets the head's spacing it was always meant to.
|
|
41
|
+
|
|
42
|
+
- **A new site in one command**: `npx gemboss-website init --site` writes what gemboss.ai and gemians.com both had to
|
|
43
|
+
write by hand: the header and footer partials (from the kit's templates), `index.html` and `404.html` with the
|
|
44
|
+
build's markers, `assets/site.css` (the brand's `:root` block, commented) and `assets/kit-gaps.css`, and the Next.js
|
|
45
|
+
wrapper that serves the static pages byte for byte (`app/[[...path]]/route.js`, `lib/pages.mjs`, `next.config.mjs`
|
|
46
|
+
with clean URLs, `stage-public.mjs` with its allow-list, `vercel.json`, `.gitignore`), from the new
|
|
47
|
+
`templates/site/`. It never overwrites a file and adds the scripts a site needs to `package.json`.
|
|
48
|
+
- **`npx gemboss-website build`** (new): copies the kit into `assets/kit/` as published, writes `partials/header.html`
|
|
49
|
+
and `partials/footer.html` into every page between `<!-- gw:header -->` and `<!-- gw:footer -->` (their leading
|
|
50
|
+
comments, the templates' notes, stay out), puts `aria-current="page"` on the header's links to that page, and the
|
|
51
|
+
kit's version on its two links (`?v=0.13.0`). `--check` writes nothing and fails when a page is out of date (CI).
|
|
52
|
+
One build for every site, so a fix reaches all of them with the kit, instead of a script per site that drifts.
|
|
53
|
+
- **A logo drawn as one file** (`.gw-brand__logo`, new): the mark and the wordmark together, 26px tall, in place of
|
|
54
|
+
`__mark` and `__name`, its `alt` the brand's name. A logo made for dark grounds (Gemians' white wordmark) comes with a
|
|
55
|
+
second file, `.gw-brand__logo--dark`, shown in dark only (the system's setting or the toggle). `check`:
|
|
56
|
+
`brand-markup`.
|
|
57
|
+
- **A photo beside words** (`.gw-figure`, new): a `<figure>` with its picture at the card radius and a 14px muted
|
|
58
|
+
caption 12px under it, in a split or under a hero's actions (48px below, as a window). Until now only the prose had
|
|
59
|
+
one; a split could hold a product window but not a photo of the team.
|
|
60
|
+
- **A list under a section's head gets the head's spacing**: `.gw-steps`, `.gw-stats`, `.gw-post-list` and
|
|
61
|
+
`.gw-testimonials` no longer reset their own margin, which beat 0.12.0's "what follows a head starts 40px below it
|
|
62
|
+
(48px from 768px)", so the steps sat right under the heading. The base still zeroes a list's margins, at no
|
|
63
|
+
specificity.
|
|
64
|
+
- **A brand's announcement bar and Stage**: `--gw-announce-*` and `--gw-stage-*` may be set in the brand's `:root` block
|
|
65
|
+
(`system.json` always said "a brand sets its own bar here", and `check` called it `token-redefined`). `check` computes
|
|
66
|
+
the bar's words and link on its ground and the Stage's words on its gradient (`brand-contrast`, 4.5:1).
|
|
67
|
+
- Docs: `docs/theming.md` asks 4.5:1 only of the accent's text form (`--gw-accent-strong-*`); the plain accent is never
|
|
68
|
+
text in the kit (icons, marks, the focus ring), so 3:1 is its bar (WCAG 1.4.11), and a gold brand keeps its gold. A
|
|
69
|
+
brand mark is 34×34 (the docs and `templates/page.html` still said 26). A contact page's form is its first screen
|
|
70
|
+
with the page's one `<h1>` (`templates/sections/form.html`). The demo's announcement pill says "New", in sentence case.
|
|
71
|
+
|
|
7
72
|
## 0.12.0 (2026-10-26)
|
|
8
73
|
|
|
9
74
|
The grammar of every page, 1:1 with gemboss.ai: the announcement bar, the skip link, the button, the type (headings,
|
package/README.md
CHANGED
|
@@ -19,7 +19,8 @@ it. Made so that a team working with Claude Code, even without a designer, ships
|
|
|
19
19
|
| `demo/` | mọi thành phần trên một trang; `theme-example.html` cho thấy một thương hiệu khác đổi màu nhấn; `brand-profiles.html` là một thương hiệu kiểu GemPages bật cả 3 hồ sơ; `article.html` là một bài blog trọn vẹn, `blog.html` là trang danh sách bài; `forms.html` là mọi trạng thái của ô nhập, newsletter ở footer, tường review và huy hiệu điểm (dữ liệu mẫu); `footer.html` là footer đầy đủ như gemboss.ai, kèm nút chuyển sang bản kiểu GemPages; `header.html` là header của gemboss.ai trên điện thoại (menu đi sâu từng mục, thanh nút dính đáy, nút đổi ngôn ngữ với ba ngôn ngữ), cũng có bản kiểu GemPages; `spotlight.html` là đốm sáng trên thẻ gói giá, thẻ link, thẻ nổi bật của mega menu và nút trong dải tối, cũng có bản kiểu GemPages |
|
|
20
20
|
| `docs/` | danh mục class (`components.md`), đổi thương hiệu (`theming.md`), triết lý (`philosophy.md`), hợp đồng markup |
|
|
21
21
|
| `system.json` | mọi giá trị của hệ thống (màu, khoảng cách, chữ, bo góc, bóng, khung): nguồn duy nhất |
|
|
22
|
-
| CLI `gemboss-website` | `init`, `check`, `gap`, `upgrade` |
|
|
22
|
+
| CLI `gemboss-website` | `init` (và `init --site`), `build`, `check`, `gap`, `upgrade` |
|
|
23
|
+
| `templates/site/` | 0.13.0: bộ khung một site mới (Next.js phục vụ trang HTML tĩnh, danh sách trang, danh sách tệp công khai, `site.css`, `kit-gaps.css`), `init --site` chép ra |
|
|
23
24
|
|
|
24
25
|
Cần Node 18 trở lên cho CLI. CSS và JS chạy trên mọi trình duyệt hiện đại, không cần bước build.
|
|
25
26
|
|
|
@@ -40,9 +41,9 @@ nó đọc `AGENTS.md` của kit trước, dựng từ mẫu, và chạy `check`
|
|
|
40
41
|
**Site HTML tĩnh hoặc không có bước build:** dùng CDN của npm, ghim đúng phiên bản.
|
|
41
42
|
|
|
42
43
|
```html
|
|
43
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.
|
|
44
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.14.0/website.min.css">
|
|
44
45
|
<link rel="stylesheet" href="/site.css"> <!-- CSS riêng của site, luôn SAU kit -->
|
|
45
|
-
<script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.
|
|
46
|
+
<script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.14.0/js/website.js" defer></script>
|
|
46
47
|
```
|
|
47
48
|
|
|
48
49
|
Chèn markup của kit sau khi trang đã tải (block của builder, bản xem trước CMS): gọi `gwWebsite.init(phầnMới)` ngay
|
|
@@ -113,6 +114,8 @@ viết hoa vẫn cấm. Công thức và các phép kiểm tương phản: `docs
|
|
|
113
114
|
| lệnh | làm gì |
|
|
114
115
|
|---|---|
|
|
115
116
|
| `npx gemboss-website init` | nối luật của kit vào Claude Code của dự án (CLAUDE.md + skill) |
|
|
117
|
+
| `npx gemboss-website init --site` | 0.13.0: dựng một site mới như gemboss.ai và gemians.com: partial header và footer, `index.html` và `404.html` có dấu `<!-- gw:header -->` / `<!-- gw:footer -->`, `site.css`, `kit-gaps.css`, phần Next.js phục vụ trang tĩnh; không ghi đè tệp có sẵn, thêm script còn thiếu vào `package.json` |
|
|
118
|
+
| `npx gemboss-website build` | 0.13.0: chép kit vào `assets/kit/`, ghi partial header và footer vào mọi trang (đánh dấu `aria-current="page"` cho link của chính trang đó); `--check` cho CI, báo đỏ khi trang chưa cập nhật |
|
|
116
119
|
| `npx gemboss-website check` | kiểm mọi `.html` và `.css` của site: màu, khoảng cách, chữ, bo góc ngoài thang, bóng, nút tự tô màu, nhãn viết hoa giãn chữ, đè class `gw-`, đổi token của kit, hồ sơ thương hiệu sai tên hoặc sai chỗ, chữ trên nút màu nhấn dưới 4,5:1, dấu gạch dài, ảnh thiếu `alt`, link mơ hồ, sao thiếu giá trị bằng chữ, điểm hay review không có link nguồn, cột footer gập thiếu tiêu đề trong summary, wordmark không ẩn với trình đọc màn hình, mục của menu đi sâu thiếu nút Back hay tiêu đề, nút ngôn ngữ không ẩn sẵn hay bản dịch thiếu `hreflang`… và gợi ý chỗ kit có thể đang thiếu |
|
|
117
120
|
| `npx gemboss-website gap` | báo một chỗ kit còn thiếu: mở GitHub Issue ở repo của kit (nếu `gh` đã đăng nhập và có quyền), không thì ghi `kit-gaps/<tên>.md` để gửi |
|
|
118
121
|
| `npx gemboss-website upgrade` | so bản đang dùng với bản mới nhất, in thay đổi, gap đã có trong kit, và lệnh nâng cấp |
|
package/bin/gemboss-website.mjs
CHANGED
|
@@ -8,10 +8,21 @@ export const USAGE = `gemboss-website (${PKG.name} ${PKG.version})
|
|
|
8
8
|
Usage: npx gemboss-website <command> [options] (run it in the site's root folder)
|
|
9
9
|
|
|
10
10
|
Commands
|
|
11
|
-
init [--force]
|
|
11
|
+
init [--force] [--site]
|
|
12
12
|
Add or refresh the kit's block in CLAUDE.md (between <!-- ${PKG.name}:start/end --> markers;
|
|
13
13
|
text outside them is never touched) and install the Claude Code skill
|
|
14
14
|
.claude/skills/gemboss-website-ui/SKILL.md (replaced only if init wrote it, or with --force).
|
|
15
|
+
--site also start a whole site, as gemboss.ai and gemians.com are built: the header and
|
|
16
|
+
footer partials, index.html and 404.html with their markers, assets/site.css and
|
|
17
|
+
kit-gaps.css, and the Next.js wrapper that serves the static pages (app/, lib/pages.mjs,
|
|
18
|
+
next.config.mjs, stage-public.mjs, vercel.json, .gitignore). Never overwrites a file;
|
|
19
|
+
adds the missing scripts to package.json.
|
|
20
|
+
|
|
21
|
+
build [--check]
|
|
22
|
+
Copy the kit into assets/kit/ and write partials/header.html and partials/footer.html into every
|
|
23
|
+
page between its <!-- gw:header --> / <!-- gw:footer --> markers, with aria-current="page" on
|
|
24
|
+
the header's links to that page. Pages: lib/pages.mjs pages() and 404.html (else every .html in
|
|
25
|
+
the root). --check writes nothing and exits 1 when a file is out of date (for CI).
|
|
15
26
|
|
|
16
27
|
check [paths...] [--json] [--fix-list] [--ignore <path>]... [--include-kit]
|
|
17
28
|
Check every .html/.htm/.css (and <style>, style="") under the paths (default: here) against
|
|
@@ -59,6 +70,7 @@ async function main(argv) {
|
|
|
59
70
|
console.log(PKG.version);
|
|
60
71
|
return 0;
|
|
61
72
|
case "init": return (await import("./lib/init.mjs")).cmdInit(rest);
|
|
73
|
+
case "build": return (await import("./lib/build.mjs")).cmdBuild(rest);
|
|
62
74
|
case "check": return (await import("./lib/check.mjs")).cmdCheck(rest);
|
|
63
75
|
case "gap": return (await import("./lib/gap.mjs")).cmdGap(rest);
|
|
64
76
|
case "upgrade": return (await import("./lib/upgrade.mjs")).cmdUpgrade(rest);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// `gemboss-website build [--check]`: the parts every page of a static site shares, written into each page in place, so
|
|
2
|
+
// the HTML on disk is the HTML served (0.13.0, from gemboss-www and gemians-www):
|
|
3
|
+
// 1. the kit itself: website.min.css and js/website.js → assets/kit/website.css and assets/kit/website.js, as published;
|
|
4
|
+
// 2. partials/header.html and partials/footer.html → between <!-- gw:header --> … <!-- /gw:header --> (and gw:footer)
|
|
5
|
+
// in every page, with aria-current="page" on the header's links to that page;
|
|
6
|
+
// 3. the kit's version on its two links (?v=0.13.0), so a new kit is never served from an old cache.
|
|
7
|
+
// The pages are the site's lib/pages.mjs (`pages()`: Map of URL → file, the same list its Next route serves) plus
|
|
8
|
+
// 404.html; without that file, every .html in the site's root folder.
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { dirname, join, resolve } from "node:path";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { PKG, PKG_DIR, UsageError, c, parseArgs } from "./util.mjs";
|
|
13
|
+
|
|
14
|
+
export const KIT_DIR = "assets/kit";
|
|
15
|
+
const CURRENT = /\b(?:gw-nav__link|gw-nav__trigger|gw-mobile-menu__top|gw-mobile-menu__link|gw-mega__link)\b/;
|
|
16
|
+
|
|
17
|
+
/** file → clean URL, as Vercel's cleanUrls and the site's Next route: index.html → /, a/index.html → /a, a.html → /a. */
|
|
18
|
+
export function routeOf(file) {
|
|
19
|
+
const stem = file.replace(/\.html$/, "");
|
|
20
|
+
if (stem === "index") return "/";
|
|
21
|
+
return "/" + stem.replace(/\/index$/, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The header for one page: aria-current="page" on each of its links (nav, mobile menu, mega rows) to that URL. */
|
|
25
|
+
export function markCurrent(html, route) {
|
|
26
|
+
return html.replace(/<a\b([^>]*)>/g, (tag, attrs) => {
|
|
27
|
+
const cls = (/\bclass\s*=\s*"([^"]*)"/.exec(attrs) || [])[1] || "";
|
|
28
|
+
const href = (/\bhref\s*=\s*"([^"]*)"/.exec(attrs) || [])[1];
|
|
29
|
+
if (!CURRENT.test(cls) || href !== route || /\baria-current\s*=/.test(attrs)) return tag;
|
|
30
|
+
return `<a${attrs} aria-current="page">`;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Put `body` between the page's <!-- gw:name --> markers; null when the page has none. */
|
|
35
|
+
export function fillRegion(html, name, body) {
|
|
36
|
+
const re = new RegExp(`(<!-- gw:${name} -->)[\\s\\S]*?(<!-- /gw:${name} -->)`);
|
|
37
|
+
return re.test(html) ? html.replace(re, (_, a, b) => `${a}\n${body}\n${b}`) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function sitePages(root) {
|
|
41
|
+
const lib = join(root, "lib", "pages.mjs");
|
|
42
|
+
let files;
|
|
43
|
+
if (existsSync(lib)) {
|
|
44
|
+
const mod = await import(pathToFileURL(lib).href);
|
|
45
|
+
if (typeof mod.pages !== "function") throw new UsageError("lib/pages.mjs must export pages() (a Map of URL → file)");
|
|
46
|
+
files = [...mod.pages().entries()].map(([route, file]) => ({ route, file }));
|
|
47
|
+
} else {
|
|
48
|
+
files = readdirSync(root).filter((f) => f.endsWith(".html") && f !== "404.html").map((file) => ({ route: routeOf(file), file }));
|
|
49
|
+
}
|
|
50
|
+
if (existsSync(join(root, "404.html"))) files.push({ route: "/404", file: "404.html" });
|
|
51
|
+
return files;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function cmdBuild(argv) {
|
|
55
|
+
const opts = parseArgs(argv, { check: "bool" });
|
|
56
|
+
if (opts._.length) throw new UsageError(`build takes no arguments (got ${opts._.join(" ")}); run it in the site's root folder`);
|
|
57
|
+
const root = process.cwd();
|
|
58
|
+
const stale = [], wrote = [];
|
|
59
|
+
const put = (file, text) => {
|
|
60
|
+
const abs = resolve(root, file);
|
|
61
|
+
if (existsSync(abs) && readFileSync(abs, "utf8") === text) return;
|
|
62
|
+
if (opts.check) { stale.push(file); return; }
|
|
63
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
64
|
+
writeFileSync(abs, text);
|
|
65
|
+
wrote.push(file);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
put(`${KIT_DIR}/website.css`, `/* ${PKG.name} ${PKG.version}, copied by gemboss-website build: do not edit */\n` + readFileSync(join(PKG_DIR, "website.min.css"), "utf8"));
|
|
69
|
+
put(`${KIT_DIR}/website.js`, readFileSync(join(PKG_DIR, "js", "website.js"), "utf8"));
|
|
70
|
+
|
|
71
|
+
// a partial's leading comments are notes for its author (the kit templates' instructions), not for every page
|
|
72
|
+
const partial = (name) => { const p = join(root, "partials", `${name}.html`); return existsSync(p) ? readFileSync(p, "utf8").replace(/^(?:\s*<!--[\s\S]*?-->)+/, "").trim() : null; };
|
|
73
|
+
const header = partial("header"), footer = partial("footer");
|
|
74
|
+
const pages = await sitePages(root);
|
|
75
|
+
const problems = [];
|
|
76
|
+
for (const { route, file } of pages) {
|
|
77
|
+
const abs = join(root, file);
|
|
78
|
+
if (!existsSync(abs)) { problems.push(`${file}: listed by lib/pages.mjs, not found`); continue; }
|
|
79
|
+
let html = readFileSync(abs, "utf8");
|
|
80
|
+
for (const [name, body] of [["header", header && markCurrent(header, route)], ["footer", footer]]) {
|
|
81
|
+
if (!html.includes(`<!-- gw:${name} -->`)) continue;
|
|
82
|
+
if (body === null) { problems.push(`${file}: has <!-- gw:${name} --> but partials/${name}.html is missing`); continue; }
|
|
83
|
+
const next = fillRegion(html, name, body);
|
|
84
|
+
if (next === null) problems.push(`${file}: <!-- gw:${name} --> without its closing <!-- /gw:${name} -->`);
|
|
85
|
+
else html = next;
|
|
86
|
+
}
|
|
87
|
+
html = html.replace(/((?:href|src)\s*=\s*"\/assets\/kit\/website\.(?:css|js))(?:\?v=[^"]*)?"/g, `$1?v=${PKG.version}"`);
|
|
88
|
+
put(file, html);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const p of problems) console.error(` ${c.red(p)}`);
|
|
92
|
+
if (opts.check) {
|
|
93
|
+
for (const f of stale) console.error(` out of date: ${f}`);
|
|
94
|
+
const bad = stale.length + problems.length;
|
|
95
|
+
console.log(bad ? c.red(`build: RED, ${stale.length} file(s) out of date, ${problems.length} problem(s). Run: npx gemboss-website build`) : c.green(`build: green, ${pages.length} pages, kit ${PKG.version}`));
|
|
96
|
+
return bad ? 1 : 0;
|
|
97
|
+
}
|
|
98
|
+
for (const f of wrote) console.log(` wrote ${f}`);
|
|
99
|
+
console.log(`build: ${pages.length} pages, kit ${PKG.version}${wrote.length ? "" : ", nothing to write"}`);
|
|
100
|
+
return problems.length ? 1 : 0;
|
|
101
|
+
}
|
package/bin/lib/check.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { basename, resolve } from "node:path";
|
|
5
5
|
import { PKG, SYSTEM, UsageError, blank, c, isKitPath, lineIndex, parseArgs, rel, walkFiles } from "./util.mjs";
|
|
6
|
-
import { MIN_CONTRAST, actionPairs, ratio } from "./contrast.mjs";
|
|
6
|
+
import { MIN_CONTRAST, actionPairs, contrast, ratio } from "./contrast.mjs";
|
|
7
7
|
|
|
8
8
|
const RADII = new Set(SYSTEM.radius.scale);
|
|
9
9
|
// A brand's named profiles (system.json `profiles`): one attribute on <html> each, and the floating header's class.
|
|
@@ -31,7 +31,7 @@ export const RULES = {
|
|
|
31
31
|
},
|
|
32
32
|
"token-redefined": {
|
|
33
33
|
level: "error", title: "Kit token redefined",
|
|
34
|
-
why: "A brand chooses its accent family and wash hues (variables), its logo, and three named profiles; nothing else. Changing another --gw-* token changes every component on the site.",
|
|
34
|
+
why: "A brand chooses its accent family and wash hues (variables), the colours of its announcement bar and of its Stage (--gw-announce-*, --gw-stage-*), its logo, and three named profiles; nothing else. Changing another --gw-* token changes every component on the site.",
|
|
35
35
|
fix: `Remove it. The action colour and the corners are profiles, not tokens: ${profileList()} on <html>; the floating header is the class ${FLOAT_CLASS} (docs/theming.md). If the brand needs something else, report a gap: npx gemboss-website gap --kind token.`,
|
|
36
36
|
},
|
|
37
37
|
"radius-off-scale": {
|
|
@@ -129,6 +129,16 @@ export const RULES = {
|
|
|
129
129
|
why: `With data-gw-action="accent" the primary button is filled with the brand's accent. Its words (--gw-on-accent) must reach ${MIN_CONTRAST}:1 on --gw-accent and on --gw-accent-strong (hover and pressed), in light and in dark, or the main action of every page is hard to read.`,
|
|
130
130
|
fix: "Darken --gw-accent-light and --gw-accent-strong-light (or change --gw-on-accent-light), lighten the -dark ones (dark words on them), until every pair passes; write them as #rrggbb. Or keep the ink button: remove data-gw-action.",
|
|
131
131
|
},
|
|
132
|
+
"brand-contrast": {
|
|
133
|
+
level: "error", title: "Words on the brand's announcement bar or Stage below 4.5:1",
|
|
134
|
+
why: `A brand that sets its own announcement bar (--gw-announce-*) or Stage (--gw-stage-*) picks colours the kit cannot check for it: the bar's words and its link on its ground, and the Stage's words on both ends of its gradient, must reach ${MIN_CONTRAST}:1, or the sentence over every page and the mega menu's pictures are hard to read.`,
|
|
135
|
+
fix: "Darken the ground (--gw-announce-bg, --gw-stage-top, --gw-stage-bottom) or lighten the words (--gw-announce-ink, --gw-announce-link, --gw-stage-ink) until every pair passes; write them as #rrggbb.",
|
|
136
|
+
},
|
|
137
|
+
"brand-markup": {
|
|
138
|
+
level: "error", title: "Brand logo without its markup",
|
|
139
|
+
why: "A logo drawn as one file (.gw-brand__logo, the mark and the wordmark together) stands in for the brand's name: its alt is that name, or the home link has no name for a screen reader. Its width and height keep the bar from jumping while it loads. A file for dark grounds (.gw-brand__logo--dark) is the same logo in another colour and needs the first one right before it.",
|
|
140
|
+
fix: "Write <a class=\"gw-brand\" href=\"/\"><img class=\"gw-brand__logo\" src=\"/logo.png\" alt=\"Brand\" width=\"150\" height=\"26\"><img class=\"gw-brand__logo gw-brand__logo--dark\" src=\"/logo-on-dark.png\" alt=\"Brand\" width=\"150\" height=\"26\"></a> (docs/theming.md, the logo).",
|
|
141
|
+
},
|
|
132
142
|
"toc-target": {
|
|
133
143
|
level: "error", title: "Contents link to a heading that is not on the page",
|
|
134
144
|
why: "A .gw-toc link whose #id names no element on the page jumps nowhere, and website.js cannot mark its section. It happens when a heading is renamed or removed and the list is not.",
|
|
@@ -339,7 +349,9 @@ const isRootish = (sel) => splitTop(sel).every((s) => s && ROOTISH.test(s));
|
|
|
339
349
|
// its pictures' width and its columns (0.11.0: style="--gw-mega-w: 1120px; --gw-mega-pv: 380px; --gw-mega-cols: 2"); the colour of
|
|
340
350
|
// a label's square (0.12.0: --gw-label-mark, on the label or around it)
|
|
341
351
|
const INPUTS = new Set(["--gw-stars", "--gw-mega-w", "--gw-mega-pv", "--gw-mega-cols", "--gw-label-mark"]);
|
|
342
|
-
|
|
352
|
+
// docs/theming.md: the eight accent variables (-light/-dark), the wash, and (0.13.0) the announcement bar's and the
|
|
353
|
+
// Stage's colours, which system.json always said a brand sets
|
|
354
|
+
const THEMABLE = /^--gw-(?:accent(?:-[\w-]+)?|on-accent(?:-(?:light|dark))?|wash(?:-[\w-]+)?|announce-[\w-]+|stage-[\w-]+)$/;
|
|
343
355
|
|
|
344
356
|
const COLOR_PROPS = /^(?:color|background(?:-color|-image)?|border(?:-(?:top|right|bottom|left|block|inline)(?:-(?:start|end))?)?(?:-color)?|outline(?:-color)?|fill|stroke|text-decoration(?:-color)?|caret-color|accent-color|column-rule(?:-color)?|text-emphasis(?:-color)?|-webkit-text-fill-color|-webkit-text-stroke(?:-color)?|scrollbar-color|stop-color|flood-color|lighting-color)$/;
|
|
345
357
|
const NAMED = new Set(("white black red green blue yellow orange purple pink gray grey silver gold navy teal maroon olive lime aqua " +
|
|
@@ -480,6 +492,28 @@ function brandAccent(cssRules, page) {
|
|
|
480
492
|
return vals;
|
|
481
493
|
}
|
|
482
494
|
|
|
495
|
+
/** The brand's announcement bar and Stage, when its CSS sets them: their words on their grounds (brand-contrast). */
|
|
496
|
+
function checkBrandContrast(out, add) {
|
|
497
|
+
const vals = {};
|
|
498
|
+
for (const { file, rules } of out.cssRules.filter((x) => !x.inline)) {
|
|
499
|
+
for (const r of rules) {
|
|
500
|
+
if (r.atBlock || r.at.length || !r.selector || !isRootish(r.selector)) continue;
|
|
501
|
+
for (const d of r.decls) if (/^--gw-(?:announce|stage)-/.test(d.prop)) vals[d.prop] = { value: d.value.replace(/!important\s*$/i, "").trim(), at: `${file}:${d.line}` };
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const pairs = [["announce", "ink", "bg"], ["announce", "link", "bg"], ["stage", "ink", "top"], ["stage", "ink", "bottom"]];
|
|
505
|
+
for (const [part, text, ground] of pairs) {
|
|
506
|
+
const t = vals[`--gw-${part}-${text}`], g = vals[`--gw-${part}-${ground}`];
|
|
507
|
+
if (!t && !g) continue; // the kit's own pair, checked by the kit
|
|
508
|
+
const kit = (k) => SYSTEM.color[part][k];
|
|
509
|
+
const tv = t ? t.value : kit(text), gv = g ? g.value : kit(ground);
|
|
510
|
+
const r = contrast(tv, gv);
|
|
511
|
+
if (r !== null && r >= MIN_CONTRAST) continue;
|
|
512
|
+
const at = (t || g).at, [file, line] = [at.slice(0, at.lastIndexOf(":")), Number(at.slice(at.lastIndexOf(":") + 1))];
|
|
513
|
+
add("brand-contrast", { file, line, excerpt: `--gw-${part}-${text} ${tv} on --gw-${part}-${ground} ${gv} ${r === null ? "cannot be read as a colour (write #rrggbb)" : `is ${ratio(r)}, needs ${MIN_CONTRAST}:1`}` });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
483
517
|
/** Pages with data-gw-action="accent": the words of the primary button on its fills, light and dark (action-contrast). */
|
|
484
518
|
function checkActionContrast(out, add) {
|
|
485
519
|
for (const pg of out.pageProfiles.filter((p) => p.action === "accent")) {
|
|
@@ -659,6 +693,15 @@ function analyzeHtml(file, text, out) {
|
|
|
659
693
|
].filter(Boolean);
|
|
660
694
|
if (missing.length) add("stars-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 100)} missing: ${missing.join(", ")}`));
|
|
661
695
|
}
|
|
696
|
+
if (tag === "img" && cls.includes("gw-brand__logo")) {
|
|
697
|
+
const before = scanText.slice(0, m.index).replace(/\s+$/, "");
|
|
698
|
+
const missing = [
|
|
699
|
+
!(a.alt || "").trim() && "alt with the brand's name (the logo stands in for it)",
|
|
700
|
+
!(a.width && a.height) && "width and height",
|
|
701
|
+
cls.includes("gw-brand__logo--dark") && !/<img\b[^>]*\bclass\s*=\s*["'][^"']*\bgw-brand__logo\b(?![\w-])[^"']*["'][^>]*>$/i.test(before) && "the light file right before it (<img class=\"gw-brand__logo\">)",
|
|
702
|
+
].filter(Boolean);
|
|
703
|
+
if (missing.length) add("brand-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
|
|
704
|
+
}
|
|
662
705
|
if (cls.includes("gw-footer__wordmark") && (a["aria-hidden"] || "").trim() !== "true") {
|
|
663
706
|
add("footer-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: aria-hidden="true" (the wordmark is decoration)`));
|
|
664
707
|
}
|
|
@@ -825,6 +868,7 @@ export function runCheck(paths, { ignore = [], includeKit = false } = {}) {
|
|
|
825
868
|
}
|
|
826
869
|
|
|
827
870
|
checkActionContrast(out, add);
|
|
871
|
+
checkBrandContrast(out, add);
|
|
828
872
|
|
|
829
873
|
// Classes styled in site CSS: first definition of each.
|
|
830
874
|
const styled = new Map();
|
package/bin/lib/init.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `gemboss-website init`: connect the kit's rules to the project's Claude Code (CLAUDE.md block + skill).
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { PKG_DIR, PKG_NAME, UsageError, parseArgs, rel } from "./util.mjs";
|
|
5
5
|
|
|
@@ -59,8 +59,87 @@ export function upsertBlock(text, block) {
|
|
|
59
59
|
return { text: text + sep + b + eol, action: "added" };
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// ------------------------------------------------------------------------------------------- init --site
|
|
63
|
+
|
|
64
|
+
/** The files a new site owns from the start, from templates/site/ (gitignore → .gitignore: npm drops dotfiles). */
|
|
65
|
+
function siteTemplateFiles(dir = join(PKG_DIR, "templates", "site"), base = "") {
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const name of readdirSync(dir)) {
|
|
68
|
+
const abs = join(dir, name), r = base ? `${base}/${name}` : name;
|
|
69
|
+
if (statSync(abs).isDirectory()) out.push(...siteTemplateFiles(abs, r));
|
|
70
|
+
else out.push({ from: abs, to: r === "gitignore" ? ".gitignore" : r });
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A page of the site from templates/page.html: the header and footer become the build's markers, the kit's paths the
|
|
77
|
+
* site's (assets/kit/), and kit-gaps.css follows site.css. `main` replaces the template's <main> when given.
|
|
78
|
+
*/
|
|
79
|
+
export function sitePage(main) {
|
|
80
|
+
let html = readFileSync(join(PKG_DIR, "templates", "page.html"), "utf8");
|
|
81
|
+
const cut = (re, what) => { if (!re.test(html)) throw new Error(`templates/page.html: no ${what} to replace`); html = html.replace(re, ""); };
|
|
82
|
+
html = html.replace(/<!--\s*\n\s*Page skeleton:[\s\S]*?-->\n/, `<!-- A page of this site. Its header and footer are partials/header.html and partials/footer.html, written between the
|
|
83
|
+
markers by \`npx gemboss-website build\`. Compose <main> from node_modules/${PKG_NAME}/templates/sections/. -->\n`);
|
|
84
|
+
cut(/\n\s*<div class="gw-announce"[\s\S]*?<\/header>\n/, "header");
|
|
85
|
+
html = html.replace(/(<a class="gw-skip"[^\n]*\n)/, "$1 <!-- gw:header -->\n <!-- /gw:header -->\n\n");
|
|
86
|
+
cut(/\n\s*<!-- the short footer[^\n]*-->\s*<footer class="gw-footer">[\s\S]*?<\/footer>\n/, "footer");
|
|
87
|
+
html = html.replace(/(\s*<\/main>\n)/, "$1\n <!-- gw:footer -->\n <!-- /gw:footer -->\n");
|
|
88
|
+
html = html.split("[/kit]").join("/assets/kit").replace("[/assets]/site.css", "/assets/site.css")
|
|
89
|
+
.replace(/(<link rel="stylesheet" href="\/assets\/site\.css">)/, '$1\n <link rel="stylesheet" href="/assets/kit-gaps.css">')
|
|
90
|
+
.replace("[/favicon.svg]", "/favicon.svg");
|
|
91
|
+
if (main) html = html.replace(/<main id="main">[\s\S]*?<\/main>/, `<main id="main">\n${main}\n </main>`);
|
|
92
|
+
return html;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const NOT_FOUND_MAIN = ` <section class="gw-hero gw-hero--compact">
|
|
96
|
+
<div class="gw-wrap">
|
|
97
|
+
<p class="gw-label">404</p>
|
|
98
|
+
<h1><span class="gw-dim">This page is not here.</span> The rest of the site is.</h1>
|
|
99
|
+
<div class="gw-actions">
|
|
100
|
+
<a class="gw-btn" href="/">Go to the home page</a>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</section>`;
|
|
104
|
+
|
|
105
|
+
export const SITE_SCRIPTS = {
|
|
106
|
+
"build:site": "gemboss-website build",
|
|
107
|
+
build: "gemboss-website build && node stage-public.mjs && next build",
|
|
108
|
+
start: "next start",
|
|
109
|
+
check: "gemboss-website build --check && node stage-public.mjs --check && gemboss-website check . --ignore node_modules --ignore public",
|
|
110
|
+
};
|
|
111
|
+
export const SITE_DEPS = "next@16.4.2 react@19.2.8 react-dom@19.2.8";
|
|
112
|
+
|
|
113
|
+
function initSite(cwd, done) {
|
|
114
|
+
const write = (to, text) => {
|
|
115
|
+
const abs = resolve(cwd, to);
|
|
116
|
+
if (existsSync(abs)) { done.push([to, "kept (exists)"]); return; }
|
|
117
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
118
|
+
writeFileSync(abs, text);
|
|
119
|
+
done.push([to, "created"]);
|
|
120
|
+
};
|
|
121
|
+
for (const { from, to } of siteTemplateFiles()) write(to, readFileSync(from, "utf8"));
|
|
122
|
+
write("partials/header.html", readFileSync(join(PKG_DIR, "templates", "header.html"), "utf8"));
|
|
123
|
+
write("partials/footer.html", readFileSync(join(PKG_DIR, "templates", "footer.html"), "utf8"));
|
|
124
|
+
write("index.html", sitePage());
|
|
125
|
+
write("404.html", sitePage(NOT_FOUND_MAIN).replace("<title>[Page title] · [Brand]</title>", "<title>Page not found · [Brand]</title>"));
|
|
126
|
+
|
|
127
|
+
const pkgPath = resolve(cwd, "package.json");
|
|
128
|
+
const pkg = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")) : { name: "[site]-www", private: true, type: "module" };
|
|
129
|
+
pkg.scripts = pkg.scripts || {};
|
|
130
|
+
const added = Object.keys(SITE_SCRIPTS).filter((k) => !(k in pkg.scripts));
|
|
131
|
+
for (const k of added) pkg.scripts[k] = SITE_SCRIPTS[k];
|
|
132
|
+
const typed = pkg.type === "module";
|
|
133
|
+
if (!typed && !pkg.type) pkg.type = "module";
|
|
134
|
+
if (added.length || !existsSync(pkgPath) || !typed) {
|
|
135
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
136
|
+
done.push(["package.json", added.length ? `scripts added: ${added.join(", ")}` : "written"]);
|
|
137
|
+
} else done.push(["package.json", "scripts already there"]);
|
|
138
|
+
if (pkg.type !== "module") done.push(["package.json", `"type" is "${pkg.type}": the .mjs files work, next.config.mjs too`]);
|
|
139
|
+
}
|
|
140
|
+
|
|
62
141
|
export function cmdInit(argv) {
|
|
63
|
-
const opts = parseArgs(argv, { force: "bool" });
|
|
142
|
+
const opts = parseArgs(argv, { force: "bool", site: "bool" });
|
|
64
143
|
if (opts._.length) throw new UsageError(`init takes no arguments (got ${opts._.join(" ")}); run it in the project's root folder`);
|
|
65
144
|
const cwd = process.cwd();
|
|
66
145
|
const done = [];
|
|
@@ -89,11 +168,22 @@ export function cmdInit(argv) {
|
|
|
89
168
|
}
|
|
90
169
|
}
|
|
91
170
|
|
|
171
|
+
if (opts.site) initSite(cwd, done);
|
|
172
|
+
|
|
92
173
|
const w = Math.max(...done.map(([p]) => p.length));
|
|
93
174
|
console.log(`gemboss-website init (${PKG_NAME})`);
|
|
94
175
|
for (const [p, what] of done) console.log(` ${p.padEnd(w)} ${what}`);
|
|
95
176
|
console.log("");
|
|
96
177
|
console.log(`Claude Code now reads ${KIT}/AGENTS.md before website work in this project.`);
|
|
97
|
-
|
|
178
|
+
if (opts.site) {
|
|
179
|
+
console.log("Next:");
|
|
180
|
+
console.log(` 1. npm install ${SITE_DEPS}`);
|
|
181
|
+
console.log(" 2. Fill partials/header.html and partials/footer.html once (delete what the site does not use),");
|
|
182
|
+
console.log(" the brand's :root block in assets/site.css, and add favicon.svg");
|
|
183
|
+
console.log(" 3. npm run build:site, then compose index.html from the kit's templates/sections/");
|
|
184
|
+
console.log(" 4. npm run check, and look at every page at 320px and 1366px");
|
|
185
|
+
} else {
|
|
186
|
+
console.log("Next: start a page from the kit's templates/page.html, then run: npx gemboss-website check");
|
|
187
|
+
}
|
|
98
188
|
return 0;
|
|
99
189
|
}
|
package/css/article.css
CHANGED
|
@@ -203,7 +203,7 @@
|
|
|
203
203
|
.gw-card__media first, a .gw-label for the category, the title (its link covers the card, so the card is one target
|
|
204
204
|
and the link's name is the title alone), one or two lines of excerpt and a .gw-article-meta row at the foot. One
|
|
205
205
|
column on phones, two from 768px, three from 1080px. --compact: one column of rows, no card and no picture. */
|
|
206
|
-
.gw-post-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px;
|
|
206
|
+
.gw-post-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; padding: 0; list-style: none; }
|
|
207
207
|
.gw-post-list > li { display: flex; min-width: 0; }
|
|
208
208
|
.gw-post { position: relative; display: flex; flex: 1 1 auto; flex-direction: column; min-width: 0; }
|
|
209
209
|
.gw-post:hover { border-color: var(--gw-line-strong); }
|