@cparkerwebm/webmonterey 1.0.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/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/agents/.gitkeep +0 -0
- package/dist/webm.mjs +2381 -0
- package/hooks/.gitkeep +0 -0
- package/package.json +101 -0
- package/schema/design.json +118 -0
- package/skills/launch/SKILL.md +183 -0
- package/skills/new-component/SKILL.md +85 -0
- package/skills/start/SKILL.md +117 -0
- package/skills/traps/SKILL.md +333 -0
- package/skills/upgrade/SKILL.md +42 -0
- package/src/actions/index.ts +348 -0
- package/src/cli/checks.test.ts +711 -0
- package/src/cli/checks.ts +822 -0
- package/src/cli/codemods.test.ts +42 -0
- package/src/cli/codemods.ts +51 -0
- package/src/cli/compare.test.ts +144 -0
- package/src/cli/compare.ts +222 -0
- package/src/cli/design-extract.test.ts +96 -0
- package/src/cli/design-extract.ts +229 -0
- package/src/cli/doctor.ts +187 -0
- package/src/cli/mcp.test.ts +57 -0
- package/src/cli/mcp.ts +110 -0
- package/src/cli/new.ts +148 -0
- package/src/cli/package-root.ts +58 -0
- package/src/cli/scaffold.test.ts +230 -0
- package/src/cli/scaffold.ts +424 -0
- package/src/cli/seed.ts +133 -0
- package/src/cli/slug.test.ts +70 -0
- package/src/cli/slug.ts +109 -0
- package/src/cli/sync.test.ts +137 -0
- package/src/cli/sync.ts +266 -0
- package/src/cli/upgrade.ts +93 -0
- package/src/design/__fixtures__/tokens-v1.4.1.css +227 -0
- package/src/design/brand.ts +49 -0
- package/src/design/compile.test.ts +98 -0
- package/src/design/compile.ts +155 -0
- package/src/design/defaults.ts +315 -0
- package/src/design/index.ts +18 -0
- package/src/design/resolve.test.ts +80 -0
- package/src/design/resolve.ts +108 -0
- package/src/design/types.ts +96 -0
- package/src/emails/autoresponse.test.ts +82 -0
- package/src/emails/autoresponse.ts +83 -0
- package/src/emails/footer.test.ts +102 -0
- package/src/emails/footer.ts +91 -0
- package/src/emails/index.ts +24 -0
- package/src/emails/subject.test.ts +66 -0
- package/src/emails/subject.ts +80 -0
- package/src/emails/submission-notification.ts +76 -0
- package/src/env.d.ts +12 -0
- package/src/includes/cloudflare/d1/client.ts +86 -0
- package/src/includes/cloudflare/r2/README.md +68 -0
- package/src/includes/cloudflare/r2/media.ts +41 -0
- package/src/includes/cloudflare/r2/url.test.ts +44 -0
- package/src/includes/cloudflare/r2/url.ts +34 -0
- package/src/includes/cloudflare/turnstile/Turnstile.astro +161 -0
- package/src/includes/cloudflare/turnstile/verify.ts +157 -0
- package/src/includes/cloudflare/workers/env.ts +56 -0
- package/src/includes/google/tag-manager/TagManager.astro +73 -0
- package/src/includes/sinch/mailgun/redirect.test.ts +96 -0
- package/src/includes/sinch/mailgun/redirect.ts +96 -0
- package/src/includes/sinch/mailgun/send.ts +186 -0
- package/src/includes/webmonterey/animations/animations.css +347 -0
- package/src/includes/webmonterey/animations/observe.test.ts +88 -0
- package/src/includes/webmonterey/animations/observe.ts +209 -0
- package/src/includes/webmonterey/compliance/ConsentInit.astro +74 -0
- package/src/includes/webmonterey/compliance/CookieConsent.astro +558 -0
- package/src/includes/webmonterey/compliance/consent-styles.test.ts +83 -0
- package/src/includes/webmonterey/compliance/consent.ts +142 -0
- package/src/includes/webmonterey/config.test.ts +94 -0
- package/src/includes/webmonterey/config.ts +346 -0
- package/src/includes/webmonterey/copy-defaults.ts +148 -0
- package/src/includes/webmonterey/copy.ts +13 -0
- package/src/includes/webmonterey/credits/Credit.astro +80 -0
- package/src/includes/webmonterey/credits/credit.test.ts +111 -0
- package/src/includes/webmonterey/credits/credit.ts +59 -0
- package/src/includes/webmonterey/forms/honeypot.test.ts +40 -0
- package/src/includes/webmonterey/forms/honeypot.ts +66 -0
- package/src/includes/webmonterey/prose/inline.test.ts +94 -0
- package/src/includes/webmonterey/prose/inline.ts +71 -0
- package/src/includes/webmonterey/scroll-top/ScrollTop.astro +209 -0
- package/src/includes/webmonterey/site.ts +136 -0
- package/src/includes/webmonterey/structured-data/nodes.ts +315 -0
- package/src/includes/webmonterey/zoned-hour.test.ts +49 -0
- package/src/integration/adapter.ts +53 -0
- package/src/integration/app-middleware.ts +43 -0
- package/src/integration/config.ts +96 -0
- package/src/integration/content.ts +66 -0
- package/src/integration/image-size.test.ts +83 -0
- package/src/integration/image-size.ts +100 -0
- package/src/integration/index.ts +386 -0
- package/src/integration/virtual.d.ts +101 -0
- package/src/layouts/base.astro +402 -0
- package/src/package.test.ts +121 -0
- package/src/pages/404.astro +33 -0
- package/src/pages/[...slug].astro +126 -0
- package/src/pages/robots.txt.ts +41 -0
- package/src/pages/webm.astro +23 -0
- package/src/scripts/.gitkeep +0 -0
- package/src/styles/base.css +185 -0
- package/src/styles/global.css +47 -0
- package/src/styles/layers.test.ts +32 -0
- package/src/styles/layers.ts +21 -0
- package/src/styles/layout.css +235 -0
- package/src/styles/reset.css +135 -0
- package/src/styles/utilities.css +90 -0
- package/src/worker.ts +76 -0
- package/template/assets/logo.svg +14 -0
- package/template/assets/open-graph.png +0 -0
- package/template/migrations/0001_create_submissions.sql +40 -0
- package/template/migrations/README.md +41 -0
- package/template/public/android-chrome-192x192.png +0 -0
- package/template/public/android-chrome-512x512.png +0 -0
- package/template/public/apple-touch-icon.png +0 -0
- package/template/public/favicon-16x16.png +0 -0
- package/template/public/favicon-32x32.png +0 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/favicon.svg +4 -0
- package/template/public/open-graph.png +0 -0
- package/template/scripts/check-node.mjs +72 -0
- package/template/scripts/test-hooks.mjs +96 -0
- package/template/site/.editorconfig +22 -0
- package/template/site/.prettierignore +15 -0
- package/template/site/.prettierrc.json +17 -0
- package/template/site/CLAUDE.md +245 -0
- package/template/site/CONTENT.md +135 -0
- package/template/site/PRIVACY-POLICY-DRAFT.md +81 -0
- package/template/site/public/_headers +70 -0
- package/template/site/src/forms/contact.json +45 -0
- package/template/workflows/ci.yml +52 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* robots.txt, GENERATED — not a file in public/.
|
|
3
|
+
*
|
|
4
|
+
* It used to live in public/, which is copied verbatim and therefore cannot read
|
|
5
|
+
* webmonterey.json. That left the `Sitemap:` line commented out with an instruction to write
|
|
6
|
+
* the domain in by hand at launch: one manual step, on one line, in a file nobody looks at
|
|
7
|
+
* again, whose omission is completely silent. Every site that forgot it simply had no sitemap
|
|
8
|
+
* submitted.
|
|
9
|
+
*
|
|
10
|
+
* As an endpoint it is correct by construction. `output: 'static'` prerenders this at build,
|
|
11
|
+
* so the deployed result is still a plain static file with no runtime cost — check
|
|
12
|
+
* dist/client/robots.txt after a build.
|
|
13
|
+
*
|
|
14
|
+
* The Sitemap line is emitted ONLY when `domain` is set, because that is exactly the
|
|
15
|
+
* condition under which @astrojs/sitemap generates the file at all (it needs an absolute URL).
|
|
16
|
+
* Advertising a sitemap that was never built is worse than advertising none.
|
|
17
|
+
*
|
|
18
|
+
* WORTH KNOWING WHEN THIS FILE LOOKS WRONG IN PRODUCTION: if AI/search signals are configured
|
|
19
|
+
* on the Cloudflare zone, Cloudflare PREPENDS its own managed block to the response and keeps
|
|
20
|
+
* this content below it. Both are served; neither replaces the other. Read past the header
|
|
21
|
+
* before concluding the file has been overwritten.
|
|
22
|
+
*/
|
|
23
|
+
import type { APIRoute } from 'astro';
|
|
24
|
+
|
|
25
|
+
export const GET: APIRoute = ({ site }) => {
|
|
26
|
+
const lines = ['User-agent: *', 'Allow: /'];
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* Deliberately no Disallow for noindex routes. Blocking a path here stops crawlers fetching
|
|
30
|
+
* it, which means they never see its noindex tag — and an inbound link can then get the URL
|
|
31
|
+
* indexed anyway, with no description. To exclude a page you must let it be crawled. The
|
|
32
|
+
* sitemap `filter` in astro.config.mjs is what keeps those routes out.
|
|
33
|
+
*/
|
|
34
|
+
if (site) {
|
|
35
|
+
lines.push('', `Sitemap: ${new URL('sitemap-index.xml', site).href}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return new Response(`${lines.join('\n')}\n`, {
|
|
39
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
40
|
+
});
|
|
41
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
/*
|
|
3
|
+
* WORKSPACE — a scratch page for building components.
|
|
4
|
+
*
|
|
5
|
+
* Drop a component in here to look at it in isolation before wiring it into a real page.
|
|
6
|
+
* Nothing here ships to visitors: the page is `noindex, nofollow` and excluded from the
|
|
7
|
+
* sitemap (see NOINDEX_ROUTES in astro.config.mjs).
|
|
8
|
+
*
|
|
9
|
+
* It is NOT linked from anywhere. Reach it directly at /webm.
|
|
10
|
+
*
|
|
11
|
+
* Leave it empty when you are done — anything left behind renders on every deploy, including
|
|
12
|
+
* production.
|
|
13
|
+
*/
|
|
14
|
+
import Base from '../layouts/base.astro';
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<Base title="WebMonterey Workspace" noindex>
|
|
18
|
+
<section class="webm-section">
|
|
19
|
+
<div class="webm-container">
|
|
20
|
+
<!-- Build here. -->
|
|
21
|
+
</div>
|
|
22
|
+
</section>
|
|
23
|
+
</Base>
|
|
File without changes
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* webm.base — element defaults and page-level primitives.
|
|
3
|
+
* Everything here is driven by tokens. No literal colors, sizes, or durations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
@layer webm.base {
|
|
7
|
+
:root {
|
|
8
|
+
color-scheme: light;
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Focus Not Obscured (WCAG 2.4.11) and anchor targets.
|
|
12
|
+
*
|
|
13
|
+
* Once a site has a sticky header region, anything the browser scrolls into view — an
|
|
14
|
+
* element receiving focus via Tab, a #hash target, the skip link — would otherwise land
|
|
15
|
+
* underneath it and be invisible while focused.
|
|
16
|
+
*
|
|
17
|
+
* A header component sets --webm-header-height itself. The fallback below keeps the
|
|
18
|
+
* behavior sane before one exists.
|
|
19
|
+
*/
|
|
20
|
+
--webm-header-height: 0px;
|
|
21
|
+
|
|
22
|
+
scroll-padding-block-start: calc(var(--webm-header-height) + var(--webm-space-sm));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
body {
|
|
26
|
+
background-color: var(--webm-surface);
|
|
27
|
+
color: var(--webm-text);
|
|
28
|
+
font-family: var(--webm-font-sans);
|
|
29
|
+
font-size: var(--webm-font-size-sm);
|
|
30
|
+
line-height: var(--webm-leading-normal);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* --- type scale ---------------------------------------------------------- */
|
|
34
|
+
|
|
35
|
+
h1,
|
|
36
|
+
.webm-h1 {
|
|
37
|
+
font-size: var(--webm-font-size-3xl);
|
|
38
|
+
line-height: var(--webm-leading-tight);
|
|
39
|
+
letter-spacing: var(--webm-tracking-tight);
|
|
40
|
+
font-weight: var(--webm-weight-bold);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
h2,
|
|
44
|
+
.webm-h2 {
|
|
45
|
+
font-size: var(--webm-font-size-2xl);
|
|
46
|
+
line-height: var(--webm-leading-tight);
|
|
47
|
+
letter-spacing: var(--webm-tracking-tight);
|
|
48
|
+
font-weight: var(--webm-weight-bold);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
h3,
|
|
52
|
+
.webm-h3 {
|
|
53
|
+
font-size: var(--webm-font-size-xl);
|
|
54
|
+
line-height: var(--webm-leading-snug);
|
|
55
|
+
font-weight: var(--webm-weight-semibold);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
h4,
|
|
59
|
+
.webm-h4 {
|
|
60
|
+
font-size: var(--webm-font-size-lg);
|
|
61
|
+
line-height: var(--webm-leading-snug);
|
|
62
|
+
font-weight: var(--webm-weight-semibold);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
h5,
|
|
66
|
+
.webm-h5 {
|
|
67
|
+
font-size: var(--webm-font-size-md);
|
|
68
|
+
line-height: var(--webm-leading-snug);
|
|
69
|
+
font-weight: var(--webm-weight-semibold);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
h6,
|
|
73
|
+
.webm-h6 {
|
|
74
|
+
font-size: var(--webm-font-size-sm);
|
|
75
|
+
line-height: var(--webm-leading-normal);
|
|
76
|
+
font-weight: var(--webm-weight-semibold);
|
|
77
|
+
letter-spacing: var(--webm-tracking-wide);
|
|
78
|
+
text-transform: uppercase;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
small,
|
|
82
|
+
.webm-small {
|
|
83
|
+
font-size: var(--webm-font-size-xs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
code,
|
|
87
|
+
kbd,
|
|
88
|
+
samp,
|
|
89
|
+
pre {
|
|
90
|
+
font-family: var(--webm-font-mono);
|
|
91
|
+
font-size: 0.9em;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* --- links --------------------------------------------------------------- */
|
|
95
|
+
|
|
96
|
+
a {
|
|
97
|
+
color: var(--webm-link);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
a:hover {
|
|
101
|
+
color: var(--webm-link-hover);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* --- focus --------------------------------------------------------------- */
|
|
105
|
+
|
|
106
|
+
:focus-visible {
|
|
107
|
+
/*
|
|
108
|
+
* COMPOSED FROM THE PARTS, not from --webm-focus-ring.
|
|
109
|
+
*
|
|
110
|
+
* A custom property containing var() is resolved where it is DECLARED. --webm-focus-ring is
|
|
111
|
+
* declared on :root, so it resolves to "3px solid <action>" there and inherits down already
|
|
112
|
+
* resolved - a component setting --webm-focus-color further down the tree changed nothing at
|
|
113
|
+
* all. Any component on a colored surface was therefore stuck with the site-wide action
|
|
114
|
+
* color, which is an accessibility failure with no symptom in code review.
|
|
115
|
+
*
|
|
116
|
+
* friendsofthemarinalibrary.org fixed this inside its own copy of base.css; every other site
|
|
117
|
+
* still had it.
|
|
118
|
+
*
|
|
119
|
+
* --webm-focus-ring stays defined in the token set: it is a reasonable thing for a client to
|
|
120
|
+
* reference, and removing it would break any site that does. It is simply not consumed here.
|
|
121
|
+
*/
|
|
122
|
+
outline: var(--webm-focus-width) solid var(--webm-focus-color);
|
|
123
|
+
outline-offset: var(--webm-focus-offset);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* --- landmarks ----------------------------------------------------------- */
|
|
127
|
+
|
|
128
|
+
/* Required for the skip link to actually move focus in Safari. */
|
|
129
|
+
#webm-main:focus {
|
|
130
|
+
outline: none;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* --- skip link ----------------------------------------------------------- */
|
|
134
|
+
|
|
135
|
+
.webm-skip-link {
|
|
136
|
+
position: absolute;
|
|
137
|
+
inset-inline-start: var(--webm-space-sm);
|
|
138
|
+
inset-block-start: var(--webm-space-sm);
|
|
139
|
+
z-index: var(--webm-z-skiplink);
|
|
140
|
+
padding: var(--webm-space-xs) var(--webm-space-md);
|
|
141
|
+
background-color: var(--webm-surface);
|
|
142
|
+
color: var(--webm-text);
|
|
143
|
+
border-radius: var(--webm-radius-sm);
|
|
144
|
+
box-shadow: var(--webm-shadow-md);
|
|
145
|
+
translate: 0 -200%;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.webm-skip-link:focus-visible {
|
|
149
|
+
translate: 0 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* --- horizontal rule ----------------------------------------------------- */
|
|
153
|
+
|
|
154
|
+
hr {
|
|
155
|
+
border: 0;
|
|
156
|
+
border-block-start: var(--webm-border-width-1) solid var(--webm-border-subtle);
|
|
157
|
+
margin-block: var(--webm-space-lg);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* --- selection ----------------------------------------------------------- */
|
|
161
|
+
|
|
162
|
+
::selection {
|
|
163
|
+
background-color: var(--webm-surface-accent);
|
|
164
|
+
color: var(--webm-text-strong);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* --- motion -------------------------------------------------------------- */
|
|
168
|
+
|
|
169
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
170
|
+
html {
|
|
171
|
+
scroll-behavior: smooth;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@media (prefers-reduced-motion: reduce) {
|
|
176
|
+
*,
|
|
177
|
+
*::before,
|
|
178
|
+
*::after {
|
|
179
|
+
animation-duration: 0.01ms !important;
|
|
180
|
+
animation-iteration-count: 1 !important;
|
|
181
|
+
transition-duration: 0.01ms !important;
|
|
182
|
+
scroll-behavior: auto !important;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The package's stylesheet. Everything package-owned, in cascade order.
|
|
3
|
+
*
|
|
4
|
+
* LAYER ORDER. Later layers win.
|
|
5
|
+
*
|
|
6
|
+
* webm.reset browser normalization
|
|
7
|
+
* webm.tokens custom properties - COMPILED FROM design.json, see below
|
|
8
|
+
* webm.base element defaults, a11y primitives
|
|
9
|
+
* webm.layout section/container/grid primitives
|
|
10
|
+
* webm.components .core then .custom <- the client override seam
|
|
11
|
+
* webm.utilities single-purpose helpers
|
|
12
|
+
* webm.overrides per-client escape hatch, highest priority
|
|
13
|
+
*
|
|
14
|
+
* THE BARE @layer STATEMENT BELOW DOES NOT SURVIVE THE BUILD. Verified in generation 2: it is
|
|
15
|
+
* absent from dist/client/_astro/*.css. Wrapping each file in `@layer name { ... }` preserves
|
|
16
|
+
* each layer's MEMBERSHIP but says nothing about the ORDER of the layers - and order is the
|
|
17
|
+
* entire point. With the statement gone, order falls back to first appearance in whatever the
|
|
18
|
+
* bundler emitted. A component's scoped style landing ahead of this file was enough to put
|
|
19
|
+
* webm.components.core before webm.reset, at which point `img { max-inline-size: 100% }` beat a
|
|
20
|
+
* component's `max-inline-size: 11.25rem` and rendered a 180px logo at 883px.
|
|
21
|
+
*
|
|
22
|
+
* Correctness therefore comes from base.astro, which emits this same statement as an inline
|
|
23
|
+
* <style is:inline> ahead of every stylesheet, where nothing can bundle it away.
|
|
24
|
+
*
|
|
25
|
+
* >> THE TWO LISTS MUST MATCH. Change this one and you must change base.astro's. <<
|
|
26
|
+
* webm doctor checks that they do.
|
|
27
|
+
*
|
|
28
|
+
* WHAT CHANGED FROM GENERATION 2: tokens.css is gone. The token layer is compiled from the
|
|
29
|
+
* client's design.json into a virtual module, which base.astro imports. There is no tokens.css
|
|
30
|
+
* to @import here and nothing to edit by hand - `webm design:extract` migrates an existing one.
|
|
31
|
+
*
|
|
32
|
+
* custom/_index.css is also absent, because it lives in the CLIENT repo and a package cannot
|
|
33
|
+
* reach it. The client's own src/styles/index.css imports this file and then theirs, which is
|
|
34
|
+
* what keeps webm.components.custom beating webm.components.core without !important.
|
|
35
|
+
*
|
|
36
|
+
* An EMPTY @layer block is dropped by the minifier, which is why webm.components.custom and
|
|
37
|
+
* webm.overrides are absent from built CSS until a client writes a rule.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
@layer webm.reset, webm.tokens, webm.base, webm.layout, webm.components.core,
|
|
41
|
+
webm.components.custom, webm.utilities, webm.overrides;
|
|
42
|
+
|
|
43
|
+
@import url('./reset.css');
|
|
44
|
+
@import url('./base.css');
|
|
45
|
+
@import url('./layout.css');
|
|
46
|
+
@import url('./utilities.css');
|
|
47
|
+
@import url('../includes/webmonterey/animations/animations.css');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { LAYERS, LAYER_STATEMENT } from './layers.ts';
|
|
5
|
+
|
|
6
|
+
/** The @layer statement in global.css, normalized to a single line. */
|
|
7
|
+
function statementFrom(path: string): string {
|
|
8
|
+
const css = readFileSync(new URL(path, import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
9
|
+
const m = css.match(/@layer\s+([^;]+);/);
|
|
10
|
+
assert.ok(m, `no @layer statement found in ${path}`);
|
|
11
|
+
return m[1]!
|
|
12
|
+
.split(',')
|
|
13
|
+
.map((s) => s.trim())
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(', ');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test('global.css declares exactly the layers in layers.ts, in order', () => {
|
|
19
|
+
assert.equal(statementFrom('./global.css'), LAYERS.join(', '));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('the statement puts tokens above reset and below components', () => {
|
|
23
|
+
const i = (n: string) => LAYERS.indexOf(n as (typeof LAYERS)[number]);
|
|
24
|
+
assert.ok(i('webm.reset') < i('webm.tokens'), 'reset must not beat tokens');
|
|
25
|
+
assert.ok(i('webm.tokens') < i('webm.components.core'), 'tokens must lose to components');
|
|
26
|
+
assert.ok(i('webm.components.core') < i('webm.components.custom'), 'the client seam must win');
|
|
27
|
+
assert.ok(i('webm.overrides') === LAYERS.length - 1, 'overrides is the last word');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('LAYER_STATEMENT is a single valid statement', () => {
|
|
31
|
+
assert.match(LAYER_STATEMENT, /^@layer [\w., ]+;$/);
|
|
32
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The layer order, as data.
|
|
3
|
+
*
|
|
4
|
+
* Declared in exactly one place so global.css, base.astro's inline statement and `webm doctor`
|
|
5
|
+
* cannot drift apart. In generation 2 the two lists were kept in step by a comment saying they
|
|
6
|
+
* must be - which is a convention, not a mechanism, and the failure is invisible: a mismatched
|
|
7
|
+
* order produces no error, just a cascade that inverts on some pages and not others.
|
|
8
|
+
*/
|
|
9
|
+
export const LAYERS = [
|
|
10
|
+
'webm.reset',
|
|
11
|
+
'webm.tokens',
|
|
12
|
+
'webm.base',
|
|
13
|
+
'webm.layout',
|
|
14
|
+
'webm.components.core',
|
|
15
|
+
'webm.components.custom',
|
|
16
|
+
'webm.utilities',
|
|
17
|
+
'webm.overrides',
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
/** The `@layer a, b, c;` statement. base.astro emits this inline, ahead of every stylesheet. */
|
|
21
|
+
export const LAYER_STATEMENT = `@layer ${LAYERS.join(', ')};`;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* webm.layout — the structural primitives every page block is built from.
|
|
3
|
+
*
|
|
4
|
+
* MODEL: every block is a full-bleed row (.webm-section) wrapping a constrained inner
|
|
5
|
+
* (.webm-container). --webm-width-max is the SITE bound; prose additionally constrains to
|
|
6
|
+
* --webm-width-text or line length becomes unreadable.
|
|
7
|
+
*
|
|
8
|
+
* Variants are data attributes, not modifier classes, so a component can pass them straight
|
|
9
|
+
* through from its schema: <section class="webm-section" data-tone={tone}>
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
@layer webm.layout {
|
|
13
|
+
/* --- full-bleed row ------------------------------------------------------ */
|
|
14
|
+
|
|
15
|
+
.webm-section {
|
|
16
|
+
inline-size: 100%;
|
|
17
|
+
padding-block: var(--webm-section-md);
|
|
18
|
+
background-color: var(--webm-surface);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.webm-section:where([data-tone='alt']) {
|
|
22
|
+
background-color: var(--webm-surface-alt);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.webm-section:where([data-tone='inverse']) {
|
|
26
|
+
background-color: var(--webm-surface-inverse);
|
|
27
|
+
color: var(--webm-text-inverse);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.webm-section:where([data-tone='accent']) {
|
|
31
|
+
background-color: var(--webm-surface-accent);
|
|
32
|
+
color: var(--webm-text-strong);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.webm-section:where([data-space='none']) {
|
|
36
|
+
padding-block: 0;
|
|
37
|
+
}
|
|
38
|
+
.webm-section:where([data-space='sm']) {
|
|
39
|
+
padding-block: var(--webm-section-sm);
|
|
40
|
+
}
|
|
41
|
+
.webm-section:where([data-space='lg']) {
|
|
42
|
+
padding-block: var(--webm-section-lg);
|
|
43
|
+
}
|
|
44
|
+
.webm-section:where([data-space='xl']) {
|
|
45
|
+
padding-block: var(--webm-section-xl);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* --- constrained inner --------------------------------------------------- */
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* `inline-size: min(100% - 2*gutter, MAX)` rather than `max-width + padding-inline`:
|
|
52
|
+
* the CONTENT box is then exactly MAX at large viewports regardless of box-sizing,
|
|
53
|
+
* and no calc() on max-width is needed.
|
|
54
|
+
*/
|
|
55
|
+
.webm-container {
|
|
56
|
+
inline-size: min(
|
|
57
|
+
100% - (var(--webm-gutter) * 2),
|
|
58
|
+
var(--webm-container-width, var(--webm-width-max))
|
|
59
|
+
);
|
|
60
|
+
margin-inline: auto;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.webm-container:where([data-width='wide']) {
|
|
64
|
+
--webm-container-width: var(--webm-width-wide);
|
|
65
|
+
}
|
|
66
|
+
.webm-container:where([data-width='content']) {
|
|
67
|
+
--webm-container-width: var(--webm-width-content);
|
|
68
|
+
}
|
|
69
|
+
.webm-container:where([data-width='narrow']) {
|
|
70
|
+
--webm-container-width: var(--webm-width-narrow);
|
|
71
|
+
}
|
|
72
|
+
.webm-container:where([data-width='text']) {
|
|
73
|
+
--webm-container-width: var(--webm-width-text);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* --- stack: vertical rhythm ---------------------------------------------- */
|
|
77
|
+
|
|
78
|
+
.webm-stack {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: var(--webm-stack-gap, var(--webm-space-md));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.webm-stack:where([data-gap='2xs']) {
|
|
85
|
+
--webm-stack-gap: var(--webm-space-2xs);
|
|
86
|
+
}
|
|
87
|
+
.webm-stack:where([data-gap='xs']) {
|
|
88
|
+
--webm-stack-gap: var(--webm-space-xs);
|
|
89
|
+
}
|
|
90
|
+
.webm-stack:where([data-gap='sm']) {
|
|
91
|
+
--webm-stack-gap: var(--webm-space-sm);
|
|
92
|
+
}
|
|
93
|
+
.webm-stack:where([data-gap='lg']) {
|
|
94
|
+
--webm-stack-gap: var(--webm-space-lg);
|
|
95
|
+
}
|
|
96
|
+
.webm-stack:where([data-gap='xl']) {
|
|
97
|
+
--webm-stack-gap: var(--webm-space-xl);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* --- cluster: horizontal group that wraps -------------------------------- */
|
|
101
|
+
|
|
102
|
+
.webm-cluster {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-wrap: wrap;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: var(--webm-cluster-gap, var(--webm-space-sm));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.webm-cluster:where([data-justify='between']) {
|
|
110
|
+
justify-content: space-between;
|
|
111
|
+
}
|
|
112
|
+
.webm-cluster:where([data-justify='center']) {
|
|
113
|
+
justify-content: center;
|
|
114
|
+
}
|
|
115
|
+
.webm-cluster:where([data-justify='end']) {
|
|
116
|
+
justify-content: flex-end;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.webm-cluster:where([data-gap='2xs']) {
|
|
120
|
+
--webm-cluster-gap: var(--webm-space-2xs);
|
|
121
|
+
}
|
|
122
|
+
.webm-cluster:where([data-gap='xs']) {
|
|
123
|
+
--webm-cluster-gap: var(--webm-space-xs);
|
|
124
|
+
}
|
|
125
|
+
.webm-cluster:where([data-gap='md']) {
|
|
126
|
+
--webm-cluster-gap: var(--webm-space-md);
|
|
127
|
+
}
|
|
128
|
+
.webm-cluster:where([data-gap='lg']) {
|
|
129
|
+
--webm-cluster-gap: var(--webm-space-lg);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* --- grid: auto-fitting columns ------------------------------------------ */
|
|
133
|
+
|
|
134
|
+
/* No breakpoints — the grid reflows on available width, not viewport width. */
|
|
135
|
+
.webm-grid {
|
|
136
|
+
display: grid;
|
|
137
|
+
gap: var(--webm-grid-gap, var(--webm-space-lg));
|
|
138
|
+
grid-template-columns: repeat(auto-fit, minmax(min(var(--webm-grid-min, 18rem), 100%), 1fr));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.webm-grid:where([data-columns='2']) {
|
|
142
|
+
--webm-grid-min: 24rem;
|
|
143
|
+
}
|
|
144
|
+
.webm-grid:where([data-columns='3']) {
|
|
145
|
+
--webm-grid-min: 18rem;
|
|
146
|
+
}
|
|
147
|
+
.webm-grid:where([data-columns='4']) {
|
|
148
|
+
--webm-grid-min: 14rem;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.webm-grid:where([data-gap='sm']) {
|
|
152
|
+
--webm-grid-gap: var(--webm-space-sm);
|
|
153
|
+
}
|
|
154
|
+
.webm-grid:where([data-gap='md']) {
|
|
155
|
+
--webm-grid-gap: var(--webm-space-md);
|
|
156
|
+
}
|
|
157
|
+
.webm-grid:where([data-gap='xl']) {
|
|
158
|
+
--webm-grid-gap: var(--webm-space-xl);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* --- breakout grid (opt-in, editorial pages) ----------------------------- */
|
|
162
|
+
|
|
163
|
+
/*
|
|
164
|
+
* Named grid lines let a child pick its own width with one declaration:
|
|
165
|
+
* <div class="webm-breakout">
|
|
166
|
+
* <p>…</p> <- defaults to `content`
|
|
167
|
+
* <figure data-span="full">…</figure>
|
|
168
|
+
* </div>
|
|
169
|
+
*/
|
|
170
|
+
.webm-breakout {
|
|
171
|
+
display: grid;
|
|
172
|
+
grid-template-columns:
|
|
173
|
+
[full-start] minmax(var(--webm-gutter), 1fr)
|
|
174
|
+
[wide-start] minmax(0, calc((var(--webm-width-max) - var(--webm-width-wide)) / 2))
|
|
175
|
+
[content-start] minmax(0, calc((var(--webm-width-wide) - var(--webm-width-content)) / 2))
|
|
176
|
+
[text-start] min(100% - (var(--webm-gutter) * 2), var(--webm-width-text)) [text-end]
|
|
177
|
+
minmax(0, calc((var(--webm-width-wide) - var(--webm-width-content)) / 2))
|
|
178
|
+
[content-end] minmax(0, calc((var(--webm-width-max) - var(--webm-width-wide)) / 2))
|
|
179
|
+
[wide-end] minmax(var(--webm-gutter), 1fr) [full-end];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.webm-breakout > * {
|
|
183
|
+
grid-column: content;
|
|
184
|
+
}
|
|
185
|
+
.webm-breakout > :where([data-span='text']) {
|
|
186
|
+
grid-column: text;
|
|
187
|
+
}
|
|
188
|
+
.webm-breakout > :where([data-span='wide']) {
|
|
189
|
+
grid-column: wide;
|
|
190
|
+
}
|
|
191
|
+
.webm-breakout > :where([data-span='full']) {
|
|
192
|
+
grid-column: full;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* --- media bleed: image edge-to-edge behind constrained content ---------- */
|
|
196
|
+
|
|
197
|
+
.webm-section:where([data-media]) {
|
|
198
|
+
position: relative;
|
|
199
|
+
isolation: isolate;
|
|
200
|
+
padding-block: var(--webm-section-lg);
|
|
201
|
+
color: var(--webm-text-inverse);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.webm-section__bleed {
|
|
205
|
+
position: absolute;
|
|
206
|
+
inset: 0;
|
|
207
|
+
z-index: var(--webm-z-below);
|
|
208
|
+
inline-size: 100%;
|
|
209
|
+
block-size: 100%;
|
|
210
|
+
object-fit: cover;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Scrim over the bleed image, so overlaid text keeps its contrast ratio. */
|
|
214
|
+
.webm-section:where([data-media])::before {
|
|
215
|
+
content: '';
|
|
216
|
+
position: absolute;
|
|
217
|
+
inset: 0;
|
|
218
|
+
z-index: var(--webm-z-below);
|
|
219
|
+
background-color: color-mix(
|
|
220
|
+
in oklch,
|
|
221
|
+
var(--webm-base-900) var(--webm-media-scrim, 55%),
|
|
222
|
+
transparent
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.webm-section:where([data-scrim='light']) {
|
|
227
|
+
--webm-media-scrim: 30%;
|
|
228
|
+
}
|
|
229
|
+
.webm-section:where([data-scrim='heavy']) {
|
|
230
|
+
--webm-media-scrim: 75%;
|
|
231
|
+
}
|
|
232
|
+
.webm-section:where([data-scrim='none']) {
|
|
233
|
+
--webm-media-scrim: 0%;
|
|
234
|
+
}
|
|
235
|
+
}
|