@gemboss/website-ui 0.12.0 → 0.13.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 +14 -2
- package/CHANGELOG.md +37 -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 +7 -0
- package/css/header.css +13 -0
- package/css/ratings.css +0 -1
- package/css/sections.css +2 -3
- package/demo/header.html +1 -1
- package/docs/components.md +19 -0
- package/docs/theming.md +38 -11
- package/js/website.js +2 -2
- package/js/website.mjs +1 -1
- package/package.json +1 -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 +24 -6
- package/website.min.css +1 -1
package/docs/theming.md
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
A GemCommerce brand (GemPages, Gemians…) uses the kit as it is and chooses six things, **and nothing else**:
|
|
4
4
|
|
|
5
5
|
1. **the accent variables** (eight, light and dark),
|
|
6
|
-
2. **the wash hues** (only if the brand has its own),
|
|
7
|
-
|
|
6
|
+
2. **the wash hues** (only if the brand has its own), and the colours of its **announcement bar** and **Stage** (0.13.0,
|
|
7
|
+
only if it uses them and wants its own),
|
|
8
|
+
3. **the logo** (and the favicon): a mark beside the name, or one drawing of both (0.13.0),
|
|
8
9
|
4. **the action colour mode**: `data-gw-action="ink|accent"` on `<html>`,
|
|
9
10
|
5. **the radius profile**: `data-gw-radius="tight|soft"` on `<html>`,
|
|
10
11
|
6. **the header style**: the flat bar, or `.gw-header--float`.
|
|
@@ -46,9 +47,30 @@ brand's page lights in the brand's own colour, in light, in dark and in night ba
|
|
|
46
47
|
takes the corners of the element it lights, so both radius profiles hold. A brand does not choose its colour, its
|
|
47
48
|
strength or its size (they are tokens), and never a second colour.
|
|
48
49
|
|
|
49
|
-
The logo and the favicon are files, not variables
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
The logo and the favicon are files, not variables, in the header and footer partials and `<link rel="icon">` in the
|
|
51
|
+
page head. The logo is one of two shapes:
|
|
52
|
+
|
|
53
|
+
- **a mark beside the name** (GemBoss): `<img class="gw-brand__mark">` (34×34, SVG preferred, `alt=""` because the name
|
|
54
|
+
is written next to it) and `<span class="gw-brand__name">`;
|
|
55
|
+
- **one drawing of both** (0.13.0, Gemians: the mark and the wordmark in one file): `<img class="gw-brand__logo">`, 26px
|
|
56
|
+
tall, its width the file's, its `alt` the brand's name. A logo that only works on a dark ground (a white wordmark)
|
|
57
|
+
comes with a second file in ink for the light theme, or the other way round: the file for light grounds first, then
|
|
58
|
+
the file for dark grounds as `class="gw-brand__logo gw-brand__logo--dark"`. The kit shows one per theme (the system's
|
|
59
|
+
setting or the toggle). Keep the brand's own drawing: recolour the wordmark, never redraw it in the kit's type.
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<a class="gw-brand" href="/"><img class="gw-brand__logo" src="/assets/img/logo.png" alt="Gemians" width="150" height="26"><img class="gw-brand__logo gw-brand__logo--dark" src="/assets/img/logo-on-dark.png" alt="Gemians" width="150" height="26"></a>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`check` reports a logo without its name in `alt`, without `width` and `height`, or a dark file without the light one
|
|
66
|
+
right before it (`brand-markup`).
|
|
67
|
+
|
|
68
|
+
**The announcement bar and the Stage** (0.13.0). The bar over the header (`--gw-announce-*`, `system.json`
|
|
69
|
+
`color.announce`) and the Stage the mega menu's pictures sit on (`--gw-stage-*`, `color.stage`) are GemBoss's iris by
|
|
70
|
+
default. A brand that shows them may set its own in the same `:root` block: the bar's `bg`, `ink`, `line`, `tag`,
|
|
71
|
+
`tag-ink`, `link`, `link-line`; the Stage's `glow`, `glow-2`, `top`, `bottom`, `dot`, `ink`. Both stay the same in
|
|
72
|
+
light and dark. `check` computes the bar's words and link on its ground and the Stage's words on both ends of its
|
|
73
|
+
gradient, and fails below 4.5:1 (`brand-contrast`).
|
|
52
74
|
|
|
53
75
|
The footer (0.7.0, `templates/footer.html`) is the same for every brand; a brand fills it. Its family row lists the
|
|
54
76
|
brand's siblings (a GemPages site: GemBoss, GemX, GemUpsells), its Ask AI row asks about the brand, and its wordmark
|
|
@@ -106,8 +128,9 @@ button filled from site CSS (`accent-button`).
|
|
|
106
128
|
|
|
107
129
|
`check` also reports: colours outside the tokens, a redefined kit token (`token-redefined`, including the radius and
|
|
108
130
|
action tokens), overrides of `.gw-*` classes, an unknown profile or one set on another element than `<html>`
|
|
109
|
-
(`profile-unknown`), `.gw-header--float` anywhere but the header itself (`header-float-markup`),
|
|
110
|
-
`data-gw-action="accent"`, accent button words below 4.5:1 (`action-contrast`)
|
|
131
|
+
(`profile-unknown`), `.gw-header--float` anywhere but the header itself (`header-float-markup`), on a page with
|
|
132
|
+
`data-gw-action="accent"`, accent button words below 4.5:1 (`action-contrast`), a brand's own announcement bar or
|
|
133
|
+
Stage with words below 4.5:1 (`brand-contrast`), and a one-file logo without its markup (`brand-markup`).
|
|
111
134
|
|
|
112
135
|
## A full example
|
|
113
136
|
|
|
@@ -136,7 +159,7 @@ action tokens), overrides of `.gw-*` classes, an unknown profile or one set on a
|
|
|
136
159
|
…
|
|
137
160
|
<header class="gw-header gw-header--float" data-gw-header>
|
|
138
161
|
<div class="gw-wrap gw-header__bar">
|
|
139
|
-
<a class="gw-brand" href="/"><img class="gw-brand__mark" src="/logo-mark.svg" alt="" width="
|
|
162
|
+
<a class="gw-brand" href="/"><img class="gw-brand__mark" src="/logo-mark.svg" alt="" width="34" height="34"><span class="gw-brand__name">GemPages</span></a>
|
|
140
163
|
```
|
|
141
164
|
|
|
142
165
|
## Check the contrast before you ship
|
|
@@ -145,8 +168,10 @@ The accent family is read as text: the kit draws links, step numbers, active men
|
|
|
145
168
|
`--gw-accent-strong`, and the label square, focus ring, check marks and the featured plan's hairline in `--gw-accent`.
|
|
146
169
|
So:
|
|
147
170
|
|
|
148
|
-
1. **Accent text on white at least 4.5:1**: `--gw-accent-light` and `--gw-
|
|
149
|
-
|
|
171
|
+
1. **Accent text on white at least 4.5:1**: `--gw-accent-strong-light` on `#ffffff` and on `--gw-bg-soft`
|
|
172
|
+
(`#f2f3f5`), where links sit in soft sections. The plain `--gw-accent-light` is never text (icons, marks, the focus
|
|
173
|
+
ring, the featured plan's hairline): **3:1** on `#ffffff` is enough (WCAG 1.4.11), so a gold or a light green can
|
|
174
|
+
stay bright there (0.13.0; this line asked 4.5:1 of it before, which no gold passes).
|
|
150
175
|
2. `--gw-accent-strong-light` on `--gw-accent-soft-light`: at least 4.5:1 (the accent badge, the active chip and the
|
|
151
176
|
chosen option of a segmented control).
|
|
152
177
|
3. `--gw-accent-dark` and `--gw-accent-strong-dark` on `#0d0d12` (dark page) and on `#0c0e14` (night band): at
|
|
@@ -158,7 +183,9 @@ So:
|
|
|
158
183
|
build does the same for its own colours.
|
|
159
184
|
|
|
160
185
|
Any WCAG contrast checker works (paste the two hex values). A bright brand colour (yellow, light green, cyan) almost
|
|
161
|
-
never passes on white: darken it for `-light
|
|
186
|
+
never passes as text on white: darken it for `-strong-light`, keep it near the brand for `-light` (3:1) and bright
|
|
187
|
+
for `-dark`. A colour that needs dark words on it (gold, yellow) cannot be the button's fill and its text colour at
|
|
188
|
+
once: keep the ink button (Gemians does). If you cannot get 4.5:1 without
|
|
162
189
|
changing the brand colour beyond recognition, keep the kit's accent and use the brand colour only in the logo and
|
|
163
190
|
illustrations, and keep the ink button.
|
|
164
191
|
|
package/js/website.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/*! @gemboss/website-ui 0.
|
|
1
|
+
/*! @gemboss/website-ui 0.13.0 website.js | GENERATED by scripts/build.mjs from js/website.mjs (the source, with its comments): edit that file | spec: docs/markup-contract.md | load with defer */
|
|
2
2
|
(function () {
|
|
3
3
|
'use strict';
|
|
4
4
|
if (window.gwWebsite) return;
|
|
5
5
|
|
|
6
|
-
var version = '0.
|
|
6
|
+
var version = '0.13.0';
|
|
7
7
|
var api = { version: version, init: init, destroy: destroy };
|
|
8
8
|
var doc, root, mine, fine, sys, wide, desk, mid, live = [], queued = false;
|
|
9
9
|
var still, lights;
|
package/js/website.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// the export line and the // comments, plus auto-init): edit this file, then run the build. Keep comments on lines of
|
|
7
7
|
// their own, so the build can leave them out.
|
|
8
8
|
|
|
9
|
-
var version = '0.
|
|
9
|
+
var version = '0.13.0';
|
|
10
10
|
var api = { version: version, init: init, destroy: destroy };
|
|
11
11
|
// Page state. Set up by the first init(), never at import: a server can import this file.
|
|
12
12
|
var doc, root, mine, fine, sys, wide, desk, mid, live = [], queued = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemboss/website-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "The GemBoss website kit: tokens, header, footer, sections and the rules (AGENTS.md) that GemCommerce public websites share, plus a CLI that checks a site, reports kit gaps and upgrades.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
package/templates/page.html
CHANGED
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
<div class="gw-wrap">
|
|
135
135
|
<div class="gw-footer__grid">
|
|
136
136
|
<div class="gw-footer__brand">
|
|
137
|
-
<a class="gw-brand" href="/"><img class="gw-brand__mark" src="[/logo.svg]" alt="" width="
|
|
137
|
+
<a class="gw-brand" href="/"><img class="gw-brand__mark" src="[/logo.svg]" alt="" width="34" height="34"><span class="gw-brand__name">[Brand]</span></a>
|
|
138
138
|
<p>[One sentence on what the brand does for the reader.]</p>
|
|
139
139
|
</div>
|
|
140
140
|
<nav class="gw-footer__col" aria-label="[Column heading]">
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
role="status" line ("Sent. We'll reply to name@example.com.").
|
|
12
12
|
- Consent boxes start unchecked, say exactly what the reader gets, and are never required to get something else.
|
|
13
13
|
- One field and its button (a newsletter, "email me the guide"): the .gw-form-inline row, see templates/footer.html.
|
|
14
|
+
As a page's first screen (a contact page, 0.13.0): the heading is the page's one <h1>, drawn at the section size
|
|
15
|
+
(<h1 class="gw-h2">), and the page needs no hero above it.
|
|
14
16
|
Limits: three fields, one .gw-btn; no field that the team does not read.
|
|
15
17
|
-->
|
|
16
18
|
<section class="gw-section" aria-labelledby="[form-h]">
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Variants: .gw-hero--center (centred, for short promises), .gw-hero--compact (subpages: less height, h2-sized h1).
|
|
4
4
|
Add .gw-band--night for a dark hero (it counts as one of the page's three night bands).
|
|
5
5
|
Limits: a short label, a two-part headline (muted first sentence, ink second), at most two lines under it,
|
|
6
|
-
one .gw-btn and one secondary action.
|
|
6
|
+
one .gw-btn and one secondary action. Instead of the window, a photo: <figure class="gw-figure"> with its caption.
|
|
7
|
+
The optional window holds a real product screen (img with alt, width, height)
|
|
7
8
|
or a drawn mock-up. Numbers in it that are not real get the caption "Illustrative data." (vi: "Dữ liệu minh hoạ.",
|
|
8
9
|
zh: "示意数据。"); real ones get their source there. Never tuck the hero under the header.
|
|
9
10
|
-->
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Split: one idea told in words beside a figure (a real screen, or a drawn mock-up of the product).
|
|
3
3
|
.gw-split--reverse puts the figure on the left on wide screens; on phones the words always come first.
|
|
4
4
|
Alternate --reverse between consecutive splits. Limits: label, h2, at most two lines, a check list of three to
|
|
5
|
-
five items, one action (a link or one button). The figure is a .gw-window or a <figure> with alt text; numbers in
|
|
5
|
+
five items, one action (a link or one button). The figure is a .gw-window (a screen) or a <figure class="gw-figure"> (a photo, with alt text and a caption); numbers in
|
|
6
6
|
a window that are not real get the caption "Illustrative data.", real ones their source.
|
|
7
7
|
-->
|
|
8
8
|
<section class="gw-section">
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Every static HTML page, served byte for byte from the built file and prerendered at build time, so each page is a
|
|
2
|
+
// file on the CDN. A more specific route (app/blog/[slug]/page.js…) wins over this catch-all. Unknown URL: 404.html, 404.
|
|
3
|
+
// Written by `gemboss-website init --site` (@gemboss/website-ui); the site owns it from here.
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { ROOT, pages } from "../../lib/pages.mjs";
|
|
7
|
+
|
|
8
|
+
export const dynamic = "force-static";
|
|
9
|
+
export const dynamicParams = true;
|
|
10
|
+
|
|
11
|
+
export function generateStaticParams() {
|
|
12
|
+
return [...pages().keys()].map((route) => ({ path: route === "/" ? [] : route.slice(1).split("/") }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const HTML = { "content-type": "text/html; charset=utf-8" };
|
|
16
|
+
|
|
17
|
+
export async function GET(_req, { params }) {
|
|
18
|
+
const { path } = await params;
|
|
19
|
+
const route = "/" + (path || []).join("/");
|
|
20
|
+
const file = pages().get(route);
|
|
21
|
+
if (file) return new Response(await readFile(join(/*turbopackIgnore: true*/ ROOT, file)), { status: 200, headers: HTML });
|
|
22
|
+
return new Response(await readFile(join(/*turbopackIgnore: true*/ ROOT, "404.html")), { status: 404, headers: HTML });
|
|
23
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/* Stopgaps for what @gemboss/website-ui does not have yet (AGENTS.md section 9). Each: classes prefixed kitgap-, the
|
|
2
|
+
kit's tokens and scales only, never a .gw-* override, and a comment naming its gap (gap #N, from
|
|
3
|
+
npx gemboss-website gap). npx gemboss-website upgrade says when the kit ships it: then delete the stopgap. */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* The site's own CSS, linked after the kit (website.css). Two jobs only (AGENTS.md sections 7 and 8):
|
|
2
|
+
1. the brand's choices, in ONE :root block: its accent family and wash, and if it has its own, its announcement bar
|
|
3
|
+
and Stage colours (docs/theming.md). Delete the lines the brand keeps from the kit; GemBoss sets none.
|
|
4
|
+
2. what the kit cannot know: an illustration, a drawn mock-up of the product. Tokens and scales only, classes with
|
|
5
|
+
the site's own prefix, never .gw-*. Something reusable the kit lacks is a gap: kit-gaps.css. */
|
|
6
|
+
:root {
|
|
7
|
+
/* --gw-accent-light: #5661f5; the accent on light pages: links' marks, focus ring, label square, check marks */
|
|
8
|
+
/* --gw-accent-strong-light: #3a44de; accent text: 4.5:1 on --gw-bg and --gw-bg-soft */
|
|
9
|
+
/* --gw-accent-soft-light: #eef0fe; its tint */
|
|
10
|
+
/* --gw-on-accent-light: #ffffff; words on it (the button, under data-gw-action="accent") */
|
|
11
|
+
/* --gw-accent-dark: #7c84ff; the same four in dark mode and night bands */
|
|
12
|
+
/* --gw-accent-strong-dark: #a3a9ff; */
|
|
13
|
+
/* --gw-accent-soft-dark: #1f2140; */
|
|
14
|
+
/* --gw-on-accent-dark: #0d0d12; */
|
|
15
|
+
/* --gw-wash-pink: #ff6f5e; --gw-wash-violet: #7c8af7; --gw-wash-sky: #43b0ff; --gw-wash-mint: #1fcf9f; */
|
|
16
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The static HTML pages this site serves, and at which URL: the Next route (app/[[...path]]/route.js) serves them, and
|
|
2
|
+
// `npx gemboss-website build` writes the header and footer into them. An allow-list, not "every .html": partials/ and
|
|
3
|
+
// node_modules/ are full of HTML that is not a page. A new folder of pages goes into DIRS on purpose, e.g. ["blog", true].
|
|
4
|
+
// Written by `gemboss-website init --site` (@gemboss/website-ui); the site owns it from here.
|
|
5
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { join, relative, sep } from "node:path";
|
|
7
|
+
|
|
8
|
+
export const ROOT = process.cwd();
|
|
9
|
+
// [folder, recursive?]
|
|
10
|
+
const DIRS = [["", false]];
|
|
11
|
+
|
|
12
|
+
// turbopackIgnore: these reads only run at BUILD (prerendering every page); without it Next traces the whole project
|
|
13
|
+
// into the function. At runtime the function only needs 404.html (next.config.mjs).
|
|
14
|
+
function htmlIn(dir, deep) {
|
|
15
|
+
const abs = join(/*turbopackIgnore: true*/ ROOT, dir);
|
|
16
|
+
if (!existsSync(/*turbopackIgnore: true*/ abs)) return [];
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const name of readdirSync(/*turbopackIgnore: true*/ abs)) {
|
|
19
|
+
const full = join(/*turbopackIgnore: true*/ abs, name);
|
|
20
|
+
if (statSync(/*turbopackIgnore: true*/ full).isDirectory()) {
|
|
21
|
+
if (deep) out.push(...htmlIn(join(dir, name), true));
|
|
22
|
+
} else if (name.endsWith(".html")) {
|
|
23
|
+
out.push(relative(ROOT, full).split(sep).join("/"));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** file → clean URL, like Vercel's cleanUrls: index.html → /, pricing.html → /pricing. */
|
|
30
|
+
export function routeOf(file) {
|
|
31
|
+
const stem = file.slice(0, -".html".length);
|
|
32
|
+
if (stem === "index") return "/";
|
|
33
|
+
return "/" + stem.replace(/\/index$/, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** URL → file for every served page (404.html is the error page, not a page). */
|
|
37
|
+
export function pages() {
|
|
38
|
+
const map = new Map();
|
|
39
|
+
for (const [dir, deep] of DIRS) for (const f of htmlIn(dir, deep)) if (f !== "404.html") map.set(routeOf(f), f);
|
|
40
|
+
return map;
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Clean URLs: /pricing.html and /index.html redirect to the clean URL, /pricing/ to /pricing.
|
|
2
|
+
// Written by `gemboss-website init --site` (@gemboss/website-ui); the site owns it from here.
|
|
3
|
+
/** @type {import('next').NextConfig} */
|
|
4
|
+
export default {
|
|
5
|
+
trailingSlash: false,
|
|
6
|
+
poweredByHeader: false,
|
|
7
|
+
async redirects() {
|
|
8
|
+
return [
|
|
9
|
+
{ source: "/index.html", destination: "/", permanent: true },
|
|
10
|
+
{ source: "/:path(.*)\\.html", destination: "/:path", permanent: true },
|
|
11
|
+
];
|
|
12
|
+
},
|
|
13
|
+
// an unknown URL runs the route at request time and reads 404.html: ship that file with the function
|
|
14
|
+
outputFileTracingIncludes: { "/*": ["./404.html"] },
|
|
15
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copy the site's static files (not its HTML pages) into public/ before `next build`: Next serves only public/ and the
|
|
3
|
+
// routes in app/. An ALLOW-LIST: only what is listed here becomes a public URL. Written by
|
|
4
|
+
// `gemboss-website init --site` (@gemboss/website-ui); the site owns it from here.
|
|
5
|
+
// node stage-public.mjs # rebuild public/ (npm run build runs it)
|
|
6
|
+
// node stage-public.mjs --check # CI: public/ has what it needs and nothing internal
|
|
7
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
8
|
+
import { join, relative, sep } from "node:path";
|
|
9
|
+
|
|
10
|
+
const ROOT = process.cwd();
|
|
11
|
+
const OUT = join(ROOT, "public");
|
|
12
|
+
const ROOT_FILES = ["favicon.svg", "robots.txt", "sitemap.xml"];
|
|
13
|
+
const DIRS = ["assets"];
|
|
14
|
+
const SKIP = (name) => name.endsWith(".html") || name.endsWith(".md") || name === ".DS_Store";
|
|
15
|
+
|
|
16
|
+
function walk(dir) {
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const name of readdirSync(dir)) {
|
|
19
|
+
const full = join(dir, name);
|
|
20
|
+
if (statSync(full).isDirectory()) out.push(...walk(full));
|
|
21
|
+
else out.push(relative(OUT, full).split(sep).join("/"));
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stage() {
|
|
27
|
+
rmSync(OUT, { recursive: true, force: true });
|
|
28
|
+
mkdirSync(OUT, { recursive: true });
|
|
29
|
+
for (const f of ROOT_FILES) if (existsSync(join(ROOT, f))) cpSync(join(ROOT, f), join(OUT, f));
|
|
30
|
+
for (const d of DIRS) if (existsSync(join(ROOT, d))) cpSync(join(ROOT, d), join(OUT, d), { recursive: true, filter: (src) => !SKIP(src.split(sep).pop()) });
|
|
31
|
+
const files = walk(OUT);
|
|
32
|
+
console.log(`public/: ${files.length} files`);
|
|
33
|
+
return files;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function check() {
|
|
37
|
+
const files = stage();
|
|
38
|
+
const bad = [];
|
|
39
|
+
for (const need of ["favicon.svg", "assets/kit/website.css", "assets/kit/website.js", "assets/site.css"]) {
|
|
40
|
+
if (!files.includes(need)) bad.push(`missing public file: ${need}`);
|
|
41
|
+
}
|
|
42
|
+
for (const f of files) if (/\.(html|md|py|mjs|sh)$/.test(f) || /(^|\/)(package(-lock)?\.json|vercel\.json)$/.test(f)) bad.push(`internal file in public/: ${f}`);
|
|
43
|
+
for (const b of bad) console.error(" " + b);
|
|
44
|
+
console.log(bad.length ? `stage-public: RED, ${bad.length}` : "stage-public: green");
|
|
45
|
+
return bad.length ? 1 : 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (process.argv.includes("--check")) process.exit(check());
|
|
49
|
+
stage();
|
package/website.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @gemboss/website-ui 0.
|
|
1
|
+
/*! @gemboss/website-ui 0.13.0 | generated by scripts/build.mjs from the files in css/. Edit those and not this file */
|
|
2
2
|
/* @gemboss/website-ui tokens. GENERATED from system.json by scripts/build.mjs: do not edit by hand. */
|
|
3
3
|
:root {
|
|
4
4
|
color-scheme: light;
|
|
@@ -740,6 +740,13 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
|
|
|
740
740
|
}
|
|
741
741
|
.gw-window__body:has(> img:only-child) + .gw-window__caption { padding-top: 12px; }
|
|
742
742
|
|
|
743
|
+
/* figure (0.13.0, Gemians): a photo and its caption where a section shows one outside the prose (beside words in a
|
|
744
|
+
split, under a hero's actions): the picture at the card radius, the caption 14px, muted, 12px under it, as a figure
|
|
745
|
+
in .gw-prose. A screen of the product goes in .gw-window instead. */
|
|
746
|
+
.gw-figure > :is(img, video, picture) { display: block; width: 100%; height: auto; border-radius: var(--gw-radius-card); }
|
|
747
|
+
.gw-figure > picture > img { display: block; width: 100%; height: auto; border-radius: inherit; }
|
|
748
|
+
.gw-figure > figcaption { margin-top: 12px; color: var(--gw-muted); font-size: 14px; line-height: 1.5; }
|
|
749
|
+
|
|
743
750
|
/* icon: 20px inline stroke SVG (1.7 to 2px stroke, currentColor) */
|
|
744
751
|
.gw-icon { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 20px; height: 20px; }
|
|
745
752
|
.gw-icon svg { width: 20px; height: 20px; }
|
|
@@ -1541,6 +1548,19 @@ html.is-menu-open .gw-header:has(> .gw-mobile-menu.is-drill) { position: sticky;
|
|
|
1541
1548
|
:root:not([data-theme="light"]) .gw-theme-toggle__moon { display: none; }
|
|
1542
1549
|
}
|
|
1543
1550
|
|
|
1551
|
+
/* ---- 0.13.0: the brand's logo as one drawing (Gemians): the mark and the wordmark in one file, in place of the mark and
|
|
1552
|
+
the name, 26px tall, its width the file's. Its alt is the brand's name. A logo drawn for dark grounds only (a white
|
|
1553
|
+
wordmark) comes as a second file, .gw-brand__logo--dark, right after the first: it shows in dark (the system's or the
|
|
1554
|
+
toggle's) and the first one hides, the toggle icons' rule. One file that reads on both: no --dark. */
|
|
1555
|
+
.gw-brand__logo { display: block; flex: none; width: auto; height: 26px; }
|
|
1556
|
+
.gw-brand__logo--dark { display: none; }
|
|
1557
|
+
:root[data-theme="dark"] .gw-brand__logo--dark { display: block; }
|
|
1558
|
+
:root[data-theme="dark"] .gw-brand__logo:has(+ .gw-brand__logo--dark) { display: none; }
|
|
1559
|
+
@media (prefers-color-scheme: dark) {
|
|
1560
|
+
:root:not([data-theme="light"]) .gw-brand__logo--dark { display: block; }
|
|
1561
|
+
:root:not([data-theme="light"]) .gw-brand__logo:has(+ .gw-brand__logo--dark) { display: none; }
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1544
1564
|
/* footer: brand block, link columns, bottom row. One hairline on top, white like the page. */
|
|
1545
1565
|
.gw-footer {
|
|
1546
1566
|
padding-block: 64px 32px;
|
|
@@ -1807,7 +1827,7 @@ html.is-menu-open .gw-header:has(> .gw-mobile-menu.is-drill) { position: sticky;
|
|
|
1807
1827
|
radial-gradient(420px 320px at 52% 118%, color-mix(in srgb, var(--gw-wash-mint) 10%, transparent), transparent 62%);
|
|
1808
1828
|
}
|
|
1809
1829
|
.gw-hero h1 .gw-dim { display: block; }
|
|
1810
|
-
.gw-hero > .gw-wrap > .gw-window { margin-top: 48px; }
|
|
1830
|
+
.gw-hero > .gw-wrap > :is(.gw-window, .gw-figure) { margin-top: 48px; }
|
|
1811
1831
|
.gw-hero--center { text-align: center; }
|
|
1812
1832
|
.gw-hero--center > .gw-wrap > * { margin-inline: auto; }
|
|
1813
1833
|
.gw-hero--center .gw-actions { justify-content: center; }
|
|
@@ -1840,7 +1860,7 @@ html.is-menu-open .gw-header:has(> .gw-mobile-menu.is-drill) { position: sticky;
|
|
|
1840
1860
|
.gw-logos img { width: auto; height: 28px; }
|
|
1841
1861
|
|
|
1842
1862
|
/* stats: three or four facts, each with its source */
|
|
1843
|
-
.gw-stats { display: grid; grid-template-columns: minmax(0, 1fr); gap: 24px;
|
|
1863
|
+
.gw-stats { display: grid; grid-template-columns: minmax(0, 1fr); gap: 24px; padding: 0; list-style: none; }
|
|
1844
1864
|
.gw-stats__value { font-size: var(--gw-display-h2); font-weight: 600; letter-spacing: -0.03em; line-height: 1.1; font-variant-numeric: tabular-nums; }
|
|
1845
1865
|
.gw-stats__label { margin-top: 8px; max-width: 28ch; color: var(--gw-muted); font-size: 14px; }
|
|
1846
1866
|
.gw-stats__source { margin-top: 8px; color: var(--gw-muted); font-size: 12px; }
|
|
@@ -1873,7 +1893,6 @@ html.is-menu-open .gw-header:has(> .gw-mobile-menu.is-drill) { position: sticky;
|
|
|
1873
1893
|
display: grid;
|
|
1874
1894
|
grid-template-columns: minmax(0, 1fr);
|
|
1875
1895
|
gap: 32px 24px;
|
|
1876
|
-
margin: 0;
|
|
1877
1896
|
padding: 0;
|
|
1878
1897
|
list-style: none;
|
|
1879
1898
|
counter-reset: gw-step;
|
|
@@ -2305,7 +2324,7 @@ html.is-menu-open .gw-header:has(> .gw-mobile-menu.is-drill) { position: sticky;
|
|
|
2305
2324
|
.gw-card__media first, a .gw-label for the category, the title (its link covers the card, so the card is one target
|
|
2306
2325
|
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
|
|
2307
2326
|
column on phones, two from 768px, three from 1080px. --compact: one column of rows, no card and no picture. */
|
|
2308
|
-
.gw-post-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px;
|
|
2327
|
+
.gw-post-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; padding: 0; list-style: none; }
|
|
2309
2328
|
.gw-post-list > li { display: flex; min-width: 0; }
|
|
2310
2329
|
.gw-post { position: relative; display: flex; flex: 1 1 auto; flex-direction: column; min-width: 0; }
|
|
2311
2330
|
.gw-post:hover { border-color: var(--gw-line-strong); }
|
|
@@ -2521,7 +2540,6 @@ html.is-menu-open .gw-header:has(> .gw-mobile-menu.is-drill) { position: sticky;
|
|
|
2521
2540
|
display: grid;
|
|
2522
2541
|
grid-template-columns: minmax(0, 1fr);
|
|
2523
2542
|
gap: 16px;
|
|
2524
|
-
margin: 0;
|
|
2525
2543
|
padding: 0;
|
|
2526
2544
|
list-style: none;
|
|
2527
2545
|
}
|