@taprootio/site-authoring 0.30.0 → 0.32.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/README.md +87 -12
- package/examples/riverbend-wellness/settings/taproot-styles.json +1 -0
- package/package.json +2 -2
- package/src/api.js +110 -3
- package/src/appearance-contract.js +11 -0
- package/src/block-flow.js +117 -0
- package/src/changelog.json +45 -0
- package/src/cli.js +102 -6
- package/src/constants.js +3 -0
- package/src/content/components.js +28 -15
- package/src/content/external-link.js +77 -0
- package/src/content/free-form-section-registry.json +30 -4
- package/src/content/free-form-sections.js +29 -1
- package/src/content/image-description-document.js +108 -0
- package/src/content/image-description-validation.js +70 -0
- package/src/content/tiptap-prosemirror.ts +85 -27
- package/src/content/validate-document.js +2 -0
- package/src/image-description-contract.js +84 -0
- package/src/navigation-readiness.js +47 -0
- package/src/output.js +8 -0
- package/src/page-metadata.js +10 -10
- package/src/page-structure-checks.js +50 -45
- package/src/presentation-reference.js +9 -0
- package/src/reference-help.js +23 -10
- package/src/setting-reference.js +9 -0
- package/src/settings-catalog.js +1 -0
- package/src/site-check-findings.js +18 -12
- package/src/site-check-rules.json +10 -6
- package/src/typed-pages.js +25 -9
- package/src/verbs/deploy.js +11 -0
- package/src/verbs/index.js +10 -0
- package/src/verbs/media-images.js +67 -0
- package/src/verbs/pages-push.js +2 -1
- package/src/verbs/status.js +19 -5
package/README.md
CHANGED
|
@@ -157,12 +157,20 @@ banner title or hero title is the page's `<h1>`; `seo.h1_demoted` when a `#` hea
|
|
|
157
157
|
publishes as an `<h2>` because the banner's overlay text, a hero title or an earlier
|
|
158
158
|
`#` heading already is the `<h1>` (`detail` says which); `seo.h1_multiple` for a hero
|
|
159
159
|
title after the `<h1>`; `seo.heading_renumbered` and `seo.heading_before_title` for
|
|
160
|
-
headings published at another level; and `a11y.
|
|
161
|
-
`a11y.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
headings published at another level; and `a11y.link_text_empty`. Image-default
|
|
161
|
+
completeness (`a11y.image_default_missing`) comes from the stored deployment
|
|
162
|
+
report, using frozen image defaults. Placement alt strings and decorative choices
|
|
163
|
+
do not fill a missing reusable default. Known author-entered metadata receives missing, short and
|
|
164
|
+
long title/description advice, plus `seo.description_matches_title` when the description
|
|
165
|
+
repeats its title. The writing guidance is 15–70 characters for titles and 50–160 for
|
|
166
|
+
descriptions, counted as Unicode characters after outer trimming. These suggestions
|
|
167
|
+
never require padding or truncation and never refuse a save, push or deploy.
|
|
168
|
+
Generated custom values are checked locally. Generated defaults, home-page settings
|
|
169
|
+
and unknown fields defer to deployment in `siteChecks.deferredToDeployment`.
|
|
170
|
+
`siteChecks.checkedInEditor` names rules only the page editor evaluates, such as a selected cover. `siteChecks.checkedBySite` names the rules that need the site's own
|
|
171
|
+
records, such as referenced images and forms: `status` reads the stored deployment
|
|
172
|
+
report under `siteHealth` without scanning current drafts or compiling a new report;
|
|
173
|
+
`siteChecks.checkedAtDeploy` names the ones only a deploy applies. A redirect source the site would refuse also names its rule in
|
|
166
174
|
`error.problems[].rule`.
|
|
167
175
|
|
|
168
176
|
`taproot-site validate <fixture-directory>` checks a complete offline fixture
|
|
@@ -585,6 +593,49 @@ not atomic together: a failure reports in `error.completedWrites` which steps
|
|
|
585
593
|
completed, which failed, and which did not run, and the next `plan` shows
|
|
586
594
|
finished steps as nothing to do. Redirects, forms, approval, and deployment
|
|
587
595
|
stay separate commands.
|
|
596
|
+
Image defaults belong to the reusable image. Use the selected site's ordinary
|
|
597
|
+
library, without rewriting page bodies:
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
taproot-site media images list --missing-description --page-size 25 --json
|
|
601
|
+
taproot-site media images get <image-id> --json
|
|
602
|
+
taproot-site media images describe <image-id> --description 'A red bird on a branch.' --expected-revision 0 --if-missing --json
|
|
603
|
+
taproot-site media images describe <image-id> --clear --expected-revision 1 --json
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
List returns one page in `images.items`, `images.totalImages` for the filtered
|
|
607
|
+
library, and `images.nextPageToken`; pass a nonempty cursor unchanged with
|
|
608
|
+
`--page-token` to continue. `--page-size` accepts 1–100. Filtering happens on the
|
|
609
|
+
server before pagination, and reads make no per-image requests. If a page's
|
|
610
|
+
text and previews exceed the output bound, repeat the same cursor with a smaller
|
|
611
|
+
page size. No rows are silently omitted. Zero matches is a successful empty list.
|
|
612
|
+
|
|
613
|
+
Each image exposes `imageId`, `uploadedName`, nullable `defaultDescription`,
|
|
614
|
+
`descriptionRevision`, dimensions, processing state, `previewUrl` and
|
|
615
|
+
`responsivePreviewUrls`. Revisions are decimal strings so JavaScript cannot
|
|
616
|
+
round an int64 value; pass them unchanged as `--expected-revision`. Preview URLs
|
|
617
|
+
are validated HTTPS addresses for inspection, may expire, and stay out of
|
|
618
|
+
`GITHUB_OUTPUT`. No image metadata is written to a workspace file by these verbs.
|
|
619
|
+
|
|
620
|
+
Describe requires exactly one of `--description` and `--clear`. The server trims
|
|
621
|
+
outer whitespace, turns empty/whitespace input into null, and rejects text longer
|
|
622
|
+
than 1000 Unicode scalar values without truncation. Unicode and newlines are
|
|
623
|
+
preserved in JSON. `--if-missing` is checked atomically at the write and cannot
|
|
624
|
+
clear a default. A stale revision reports `media.image_description_conflict`; an
|
|
625
|
+
existing default under the condition reports `media.image_description_exists`.
|
|
626
|
+
Both exit 1 and leave the stored text alone. Read the current image, reconcile,
|
|
627
|
+
and use its revision for a new intentional save; mutations never retry
|
|
628
|
+
automatically. Invalid input exits 2. Permission and credential refusals keep the
|
|
629
|
+
ordinary API error codes. Every verb requires `site.media.manage`, and images
|
|
630
|
+
from another site, profile/account scope, or an installation remain inaccessible.
|
|
631
|
+
|
|
632
|
+
Defaults apply to inherited usages after the next selected deployment. Current
|
|
633
|
+
public output and in-flight releases retain their frozen text; custom overrides,
|
|
634
|
+
decorative choices and unrelated album captions remain independent. Missing
|
|
635
|
+
means the image's default is absent, even when a placement has contextual text.
|
|
636
|
+
These commands populate no production content automatically. `help media` has
|
|
637
|
+
the full read/write contract.
|
|
638
|
+
|
|
588
639
|
`media upload [path...]` uploads raster files and records each one in
|
|
589
640
|
`.taproot-site-media.json`; Markdown then names an image by its path (such as
|
|
590
641
|
`media/photo.webp`) everywhere an image goes, from body images to component and
|
|
@@ -704,12 +755,17 @@ It stages; nothing reaches an audience until `deploy --staging` publishes the
|
|
|
704
755
|
candidate and `deploy --production` promotes that completed staging
|
|
705
756
|
deployment. `status` reports the platform authoring switch, the CLI release,
|
|
706
757
|
deployments, readiness, image processing, and site health under Content
|
|
707
|
-
permission. `siteHealth`
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
findings
|
|
712
|
-
|
|
758
|
+
permission. `siteHealth` reads the last successful production deployment's stored
|
|
759
|
+
report. It includes the deployment and output identities, input hash, checked time,
|
|
760
|
+
rule version, coverage, assessed page count and status. Unpublished edits are
|
|
761
|
+
outside that report. Page findings identify the rule, severity, page and value;
|
|
762
|
+
image findings group each missing default description with its affected pages.
|
|
763
|
+
Known reports include `covered: true`, `errors`, `warnings`, `pages` and `findings`.
|
|
764
|
+
Only a complete report with zero findings establishes that the assessed candidate
|
|
765
|
+
has no warnings. Missing, unsupported and incomplete assessments are explicit.
|
|
766
|
+
Output is bounded to 100 page findings, with an explicit `findingsTruncated` flag.
|
|
767
|
+
Preview URLs appear only in normal output and are omitted from GitHub output.
|
|
768
|
+
A denied, malformed or failed read fails `status` instead of implying a clean site.
|
|
713
769
|
|
|
714
770
|
## Output contract
|
|
715
771
|
|
|
@@ -916,6 +972,25 @@ targets are refused (`content.link_rel`, `content.link_target`). The editor's
|
|
|
916
972
|
old default, `noopener noreferrer nofollow`, reads as no choice. See
|
|
917
973
|
`taproot-site help page free-form`.
|
|
918
974
|
|
|
975
|
+
### External links open in a new tab in 0.32.0
|
|
976
|
+
|
|
977
|
+
A link to another site opens in a new tab with nothing to set. On the next
|
|
978
|
+
render of a page, every absolute `http:` or `https:` address whose host is not
|
|
979
|
+
one of the site's own hosts publishes `target="_blank"` with `noopener
|
|
980
|
+
noreferrer` added after any `rel` tokens the link already carries. This covers
|
|
981
|
+
link marks in page text and tables, and component links: feature-grid,
|
|
982
|
+
image-row, card-grid, product-grid, hero-section, cta, image-banner and
|
|
983
|
+
social-links, plus the footer and header navigation. Each of these links also
|
|
984
|
+
says "(opens in a new tab)" to a screen reader, with no change on screen.
|
|
985
|
+
`mailto:`, `tel:`, relative paths, fragments,
|
|
986
|
+
queries and the site's own addresses stay in the same tab. A subdomain of the
|
|
987
|
+
site is another host, so it opens in a new tab; `www.` is ignored. To keep a
|
|
988
|
+
link mark in the same tab, write `target="_self"` after it in Markdown or set
|
|
989
|
+
`attrs.target` to `"_self"` in `.pm.json`. Component links have no setting for
|
|
990
|
+
this. Existing pages change when a publish renders the whole site, such as a
|
|
991
|
+
settings or navigation change; pages an incremental publish copies forward keep
|
|
992
|
+
their old links until then.
|
|
993
|
+
|
|
919
994
|
### What changed between releases
|
|
920
995
|
|
|
921
996
|
Taproot accepts only the latest CLI, so an upgrade can be forced at any time.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Taproot site authoring CLI: sign in once in the browser, then pull, validate, push, preview, approve, and deploy a site from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@taprootio/espalier": "6.
|
|
31
|
+
"@taprootio/espalier": "6.9.0",
|
|
32
32
|
"mediabunny": "1.61.1"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
package/src/api.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { normalizeNavigationReadiness } from "./navigation-readiness.js";
|
|
1
2
|
import { normalizeLatestCliVersion } from "./cli-release.js";
|
|
3
|
+
import { imageDescriptionRevision, isImageId, safeImagePreviewUrl } from "./image-description-contract.js";
|
|
2
4
|
import {
|
|
3
5
|
BANDWIDTH_ALLOWANCE_REFUSAL_REASON,
|
|
4
6
|
CANONICAL_TIMESTAMP,
|
|
@@ -1153,6 +1155,65 @@ export async function listSiteImages(client, siteId, requestOptions = {}, { imag
|
|
|
1153
1155
|
return { images, summary, truncated: true };
|
|
1154
1156
|
}
|
|
1155
1157
|
|
|
1158
|
+
function normalizeDescriptionImage(value, siteId, expectedImageId) {
|
|
1159
|
+
const code = "api.image_description_contract";
|
|
1160
|
+
const record = requireObject(value, code, "image library entry");
|
|
1161
|
+
const wire = requireObject(record.image, code, "image");
|
|
1162
|
+
const image = normalizeImage(wire);
|
|
1163
|
+
if (!isImageId(image.imageId) || wire.siteId !== siteId
|
|
1164
|
+
|| wire.ownershipScope !== IMAGE_OWNERSHIP_SCOPE_SITE
|
|
1165
|
+
|| (expectedImageId !== undefined && image.imageId !== expectedImageId)) {
|
|
1166
|
+
throw new SiteAuthoringError(code, "Taproot returned an image outside the requested site library.");
|
|
1167
|
+
}
|
|
1168
|
+
const descriptionRevision = imageDescriptionRevision(wire.descriptionRevision ?? "0");
|
|
1169
|
+
const defaultDescription = wire.defaultDescription === undefined || wire.defaultDescription === null || wire.defaultDescription === ""
|
|
1170
|
+
? null : wire.defaultDescription;
|
|
1171
|
+
if (descriptionRevision === undefined || (defaultDescription !== null
|
|
1172
|
+
&& (typeof defaultDescription !== "string" || !defaultDescription.isWellFormed()))) {
|
|
1173
|
+
throw new SiteAuthoringError(code, "Taproot returned invalid image description metadata.");
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
imageId: image.imageId,
|
|
1177
|
+
uploadedName: image.uploadedName,
|
|
1178
|
+
defaultDescription,
|
|
1179
|
+
descriptionRevision,
|
|
1180
|
+
width: image.width,
|
|
1181
|
+
height: image.height,
|
|
1182
|
+
processingState: image.processingState,
|
|
1183
|
+
previewUrl: safeImagePreviewUrl(image.url),
|
|
1184
|
+
responsivePreviewUrls: image.responsiveUrls.slice(0, 8)
|
|
1185
|
+
.map((url) => ({ minWidth: url.minWidth, url: safeImagePreviewUrl(url.url) }))
|
|
1186
|
+
.filter((url) => url.url !== ""),
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** One bounded page, filtered by the server before its cursor is applied. */
|
|
1191
|
+
export async function listSiteImageDescriptions(client, siteId, { pageSize, pageToken, missingDescription }) {
|
|
1192
|
+
const response = requireObject(await client.request(sitePath(siteId, `images${query([
|
|
1193
|
+
["pageSize", pageSize], ["pageToken", pageToken], ["missingDescription", missingDescription || undefined],
|
|
1194
|
+
])}`)), "api.image_description_contract", "site images");
|
|
1195
|
+
if (response.images !== undefined && !Array.isArray(response.images))
|
|
1196
|
+
throw new SiteAuthoringError("api.image_description_contract", "Taproot returned an invalid image list.");
|
|
1197
|
+
if (response.nextPageToken !== undefined && typeof response.nextPageToken !== "string")
|
|
1198
|
+
throw new SiteAuthoringError("api.image_description_contract", "Taproot returned an invalid image cursor.");
|
|
1199
|
+
return {
|
|
1200
|
+
items: (response.images ?? []).map((entry) => normalizeDescriptionImage(entry, siteId)),
|
|
1201
|
+
nextPageToken: response.nextPageToken ?? "",
|
|
1202
|
+
totalImages: safeCount(response.totalImages),
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
export async function getSiteImageDescription(client, siteId, imageId) {
|
|
1207
|
+
return normalizeDescriptionImage(
|
|
1208
|
+
await client.request(sitePath(siteId, `images/${encodeURIComponent(imageId)}`)), siteId, imageId);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
export async function updateSiteImageDescription(client, siteId, imageId, body) {
|
|
1212
|
+
return normalizeDescriptionImage(await client.request(sitePath(siteId, `images/${encodeURIComponent(imageId)}/description`), {
|
|
1213
|
+
method: "PATCH", body,
|
|
1214
|
+
}), siteId, imageId);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1156
1217
|
/** The site checks' findings over the latest editable page bodies, authorized by Content. */
|
|
1157
1218
|
export async function getSiteHealth(client, siteId) {
|
|
1158
1219
|
const code = "api.site_health_contract";
|
|
@@ -1191,10 +1252,55 @@ export async function getSiteHealth(client, siteId) {
|
|
|
1191
1252
|
detail: text(finding.detail),
|
|
1192
1253
|
};
|
|
1193
1254
|
});
|
|
1255
|
+
const statuses = new Set(["not_checked", "complete", "incomplete", "unsupported"]);
|
|
1256
|
+
const status = statuses.has(response.status) ? response.status : "unknown";
|
|
1257
|
+
const optionalId = (value, field) => value === undefined || value === "" ? undefined : requireCanonicalUuid(value, code, field);
|
|
1258
|
+
const deploymentId = optionalId(response.deploymentId, "deploymentId");
|
|
1259
|
+
const outputReleaseId = optionalId(response.outputReleaseId, "outputReleaseId");
|
|
1260
|
+
const claimsAssessment = status === "complete" || status === "incomplete" && Boolean(deploymentId || outputReleaseId);
|
|
1261
|
+
if (claimsAssessment && (!deploymentId || !outputReleaseId || !CANONICAL_TIMESTAMP.test(response.checkedAt ?? "")
|
|
1262
|
+
|| typeof response.inputHash !== "string" || !/^[0-9a-f]{64}$/u.test(response.inputHash)
|
|
1263
|
+
|| typeof response.ruleVersion !== "string" || response.ruleVersion === ""
|
|
1264
|
+
|| !Array.isArray(response.coverage) || response.coverage.length === 0)) {
|
|
1265
|
+
throw new SiteAuthoringError(code, "Taproot returned an assessed report without its immutable deployment identity.");
|
|
1266
|
+
}
|
|
1267
|
+
if (response.coverage !== undefined && (!Array.isArray(response.coverage) || response.coverage.some(area => typeof area !== "string"))) {
|
|
1268
|
+
throw new SiteAuthoringError(code, "Taproot returned invalid health coverage.", { field: "coverage" });
|
|
1269
|
+
}
|
|
1270
|
+
if (response.images !== undefined && !Array.isArray(response.images)) {
|
|
1271
|
+
throw new SiteAuthoringError(code, "Taproot returned invalid image findings.", { field: "images" });
|
|
1272
|
+
}
|
|
1273
|
+
const images = (response.images ?? []).map(value => {
|
|
1274
|
+
const image = requireObject(value, code, "image finding");
|
|
1275
|
+
if (!Array.isArray(image.affectedPages) || image.affectedPages.length === 0) {
|
|
1276
|
+
throw new SiteAuthoringError(code, "Taproot returned an image finding without affected pages.");
|
|
1277
|
+
}
|
|
1278
|
+
return {
|
|
1279
|
+
imageId: requireCanonicalUuid(image.imageId, code, "imageId"), name: text(image.name),
|
|
1280
|
+
previewUrl: safeImagePreviewUrl(image.previewUrl),
|
|
1281
|
+
affectedPages: image.affectedPages.map(page => ({
|
|
1282
|
+
pageId: requireCanonicalUuid(page?.pageId, code, "affectedPages.pageId"),
|
|
1283
|
+
title: text(page?.title), path: text(page?.path),
|
|
1284
|
+
})),
|
|
1285
|
+
};
|
|
1286
|
+
});
|
|
1287
|
+
const errors = count(response.errorCount, "errorCount");
|
|
1288
|
+
const warnings = count(response.warningCount, "warningCount");
|
|
1289
|
+
const keptErrors = findings.filter(finding => finding.severity === "error").length;
|
|
1290
|
+
const keptWarnings = findings.length - keptErrors + images.length;
|
|
1291
|
+
if (claimsAssessment && (errors < keptErrors || warnings < keptWarnings ||
|
|
1292
|
+
response.truncated !== true && (errors !== keptErrors || warnings !== keptWarnings))) {
|
|
1293
|
+
throw new SiteAuthoringError(code, "Taproot returned health totals that disagree with the stored findings.");
|
|
1294
|
+
}
|
|
1194
1295
|
return {
|
|
1195
|
-
findings,
|
|
1196
|
-
|
|
1197
|
-
|
|
1296
|
+
findings, images, status, assessmentKnown: status !== "unknown",
|
|
1297
|
+
...(deploymentId ? { deploymentId } : {}), ...(outputReleaseId ? { outputReleaseId } : {}),
|
|
1298
|
+
inputHash: text(response.inputHash), checkedAt: text(response.checkedAt), ruleVersion: text(response.ruleVersion),
|
|
1299
|
+
coverage: response.coverage ?? [], assessedPageCount: count(response.assessedPageCount, "assessedPageCount"),
|
|
1300
|
+
environment: response.environment === "staging" ? "staging" : "production",
|
|
1301
|
+
stagedDeploymentId: optionalId(response.stagedDeploymentId, "stagedDeploymentId"),
|
|
1302
|
+
errorCount: errors,
|
|
1303
|
+
warningCount: warnings,
|
|
1198
1304
|
pageCount: count(response.pageCount, "pageCount"),
|
|
1199
1305
|
truncated: response.truncated === true,
|
|
1200
1306
|
};
|
|
@@ -1257,6 +1363,7 @@ export async function getPublishingReadiness(client, siteId, selection = {}) {
|
|
|
1257
1363
|
}));
|
|
1258
1364
|
return {
|
|
1259
1365
|
state: typeof response.state === "string" ? response.state : "PAGE_PUBLISHING_READINESS_STATE_UNKNOWN",
|
|
1366
|
+
navigationReadiness: normalizeNavigationReadiness(response.navigationReadiness),
|
|
1260
1367
|
approvedPageCount: safeCount(response.approvedPageCount),
|
|
1261
1368
|
selectedPageCount: safeCount(response.selectedPageCount),
|
|
1262
1369
|
blockedPageCount: safeCount(response.blockedPageCount),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BLOCK_SPACINGS, DEFAULT_BLOCK_SPACING } from "./block-flow.js";
|
|
1
2
|
import { hasControlCharacter, isCanonicalUuid, SiteAuthoringError } from "./errors.js";
|
|
2
3
|
import {
|
|
3
4
|
SETTINGS_TYPE_BRAND,
|
|
@@ -26,6 +27,16 @@ export const APPEARANCE_SCALAR_FIELDS = Object.freeze([
|
|
|
26
27
|
values: Object.freeze(["system", "light", "dark"]),
|
|
27
28
|
default: "light",
|
|
28
29
|
}),
|
|
30
|
+
// The names come from the synced block-flow rules, so a spacing the page
|
|
31
|
+
// has no scale for can never pass validation (TR01328).
|
|
32
|
+
Object.freeze({
|
|
33
|
+
settingsType: SETTINGS_TYPE_TAPROOT_STYLES,
|
|
34
|
+
name: "blockSpacing",
|
|
35
|
+
path: "taproot-styles.blockSpacing",
|
|
36
|
+
type: "enum",
|
|
37
|
+
values: Object.freeze(Object.keys(BLOCK_SPACINGS)),
|
|
38
|
+
default: DEFAULT_BLOCK_SPACING,
|
|
39
|
+
}),
|
|
29
40
|
...["lightLogoId", "darkLogoId", "lightCanvasImageId", "darkCanvasImageId"].map((name) =>
|
|
30
41
|
Object.freeze({
|
|
31
42
|
settingsType: SETTINGS_TYPE_TAPROOT_STYLES,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Generated from shared/block-flow.ts by scripts/sync-field-validation.mjs. Do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* The one owner of how page blocks space themselves (TR01328): the three
|
|
4
|
+
* site-wide spacing choices and the stylesheet that turns them into margins.
|
|
5
|
+
*
|
|
6
|
+
* Text used to get its space from a browser default and components got none.
|
|
7
|
+
* Now every direct child of a flow container takes one gap, a token that
|
|
8
|
+
* follows the site's text size and the chosen spacing. The published page
|
|
9
|
+
* shell inlines `BLOCK_FLOW_CSS` in its core section and the editor's page
|
|
10
|
+
* preview, which has no shell, applies the same string, so both agree.
|
|
11
|
+
*
|
|
12
|
+
* The generator and the site-authoring CLI cannot import a repo-root file, so
|
|
13
|
+
* `node scripts/sync-field-validation.mjs` writes their copies; edit only this
|
|
14
|
+
* file and re-run it. The editor imports it by relative path.
|
|
15
|
+
*/
|
|
16
|
+
/** The scale each choice puts on the gap and on a heading's top margin. */
|
|
17
|
+
export const BLOCK_SPACINGS = {
|
|
18
|
+
compact: 0.75,
|
|
19
|
+
standard: 1,
|
|
20
|
+
roomy: 1.5,
|
|
21
|
+
};
|
|
22
|
+
/** What a site that never chose gets; it is also the value the page leaves unwritten. */
|
|
23
|
+
export const DEFAULT_BLOCK_SPACING = "standard";
|
|
24
|
+
// A flow container holds a page's blocks as direct children. This is the
|
|
25
|
+
// shell's older list with `.taproot-free-form` swapped for the section
|
|
26
|
+
// content: the free-form element's own children are sections and root bands,
|
|
27
|
+
// which must not gain a margin.
|
|
28
|
+
const FLOW_CONTAINERS = [
|
|
29
|
+
"[data-section-content]",
|
|
30
|
+
"[slot=\"body\"]",
|
|
31
|
+
"[slot=\"introduction\"]",
|
|
32
|
+
".recipe-story",
|
|
33
|
+
".taproot-legal__body",
|
|
34
|
+
].join(", ");
|
|
35
|
+
// The end room needs only the text elements whose bottom margin is 0; the
|
|
36
|
+
// unpadded edge leaves text, lists and rules alone, as they are today.
|
|
37
|
+
const TEXT_AND_LISTS = "p, h1, h2, h3, h4, h5, h6, figure, blockquote, dl, ul, ol, pre, hr";
|
|
38
|
+
const TEXT_WITHOUT_END_MARGIN = "p, h1, h2, h3, h4, h5, h6, figure, blockquote, dl";
|
|
39
|
+
const SCALE_MAPPING = Object.entries(BLOCK_SPACINGS)
|
|
40
|
+
.map(([name, scale]) => `[data-block-spacing="${name}"] {\n --taproot-block-scale: ${scale};\n}`)
|
|
41
|
+
.join("\n");
|
|
42
|
+
/**
|
|
43
|
+
* The block flow rules, compact enough to inline in every page. No CSS comments
|
|
44
|
+
* ship, so each rule's reason is here:
|
|
45
|
+
*
|
|
46
|
+
* - The gap is a token, not em, so a heading and a paragraph resolve the same
|
|
47
|
+
* length and it stays fluid on phones. The 1em fallback holds the browser's
|
|
48
|
+
* text margin when the runtime never ran and set `--esp-type-normal`.
|
|
49
|
+
* - The block rule and its text reset have zero specificity (`:where()`), so any
|
|
50
|
+
* rule a component, a banner or an author writes for its own margin wins with
|
|
51
|
+
* no ordering fight. The reset repeats `esp-reset.css` because the page preview
|
|
52
|
+
* has no reset.
|
|
53
|
+
* - Components load lazily and an undefined element is inline, where a vertical
|
|
54
|
+
* margin does nothing, so the listed hosts match their `:host` from first paint.
|
|
55
|
+
* Add every new lazily loaded component host to that list.
|
|
56
|
+
* - A heading's top margin scales with the setting. At (0,1,0) it beats Espalier's
|
|
57
|
+
* root heading rule and loses to the banner resets, in any section order.
|
|
58
|
+
* - A spacer adds its height to the gap instead of a second margin.
|
|
59
|
+
* - The last text block of a padded section gets room, so a section closes as it
|
|
60
|
+
* opens. The descendant combinator reaches past a surface's `esp-box`.
|
|
61
|
+
* - An unpadded section's first and last component stay flush. Text, lists, rules
|
|
62
|
+
* and anything with a margin rule of its own keep what they have.
|
|
63
|
+
* - Banners stay flush with their neighbors. Those rules live here, not in the
|
|
64
|
+
* banner section, because the page preview has no shell.
|
|
65
|
+
*/
|
|
66
|
+
export const BLOCK_FLOW_CSS = `
|
|
67
|
+
:where(${FLOW_CONTAINERS}) {
|
|
68
|
+
--taproot-block-gap: calc(var(--esp-type-normal, 1em) * var(--taproot-block-scale, 1));
|
|
69
|
+
}
|
|
70
|
+
${SCALE_MAPPING}
|
|
71
|
+
:where(${FLOW_CONTAINERS}) > * {
|
|
72
|
+
margin-block: var(--taproot-block-gap);
|
|
73
|
+
}
|
|
74
|
+
:where(${FLOW_CONTAINERS}) > :where(p, h1, h2, h3, h4, figure, blockquote, dl) {
|
|
75
|
+
margin-block-end: 0;
|
|
76
|
+
}
|
|
77
|
+
taproot-hero,
|
|
78
|
+
taproot-cta,
|
|
79
|
+
taproot-testimonial,
|
|
80
|
+
taproot-feature-grid,
|
|
81
|
+
taproot-latest-posts,
|
|
82
|
+
taproot-card-grid,
|
|
83
|
+
taproot-product-grid {
|
|
84
|
+
display: block;
|
|
85
|
+
}
|
|
86
|
+
:is(${FLOW_CONTAINERS}) > :where(h1, h2, h3, h4, h5) {
|
|
87
|
+
margin-block-start: calc(var(--esp-size-small-to-normal, 0.83em) * var(--taproot-block-scale, 1));
|
|
88
|
+
}
|
|
89
|
+
:where(${FLOW_CONTAINERS}) > taproot-spacer {
|
|
90
|
+
margin-block: 0;
|
|
91
|
+
}
|
|
92
|
+
esp-section[data-content-padding="standard"] [data-section-content] > :is(${TEXT_WITHOUT_END_MARGIN}):last-child {
|
|
93
|
+
margin-block-end: var(--taproot-block-gap);
|
|
94
|
+
}
|
|
95
|
+
:where(esp-section[data-content-padding="none"] [data-section-content]) > :where(:first-child:not(${TEXT_AND_LISTS})) {
|
|
96
|
+
margin-block-start: 0;
|
|
97
|
+
}
|
|
98
|
+
:where(esp-section[data-content-padding="none"] [data-section-content]) > :where(:last-child:not(${TEXT_AND_LISTS})) {
|
|
99
|
+
margin-block-end: 0;
|
|
100
|
+
}
|
|
101
|
+
:is(${FLOW_CONTAINERS}) taproot-image-banner {
|
|
102
|
+
margin: 0;
|
|
103
|
+
}
|
|
104
|
+
:is(${FLOW_CONTAINERS}) esp-image[banner] {
|
|
105
|
+
margin: 0;
|
|
106
|
+
}
|
|
107
|
+
:is(${FLOW_CONTAINERS}) *:has(+ taproot-image-banner) {
|
|
108
|
+
margin-block-end: 0;
|
|
109
|
+
}
|
|
110
|
+
:is(${FLOW_CONTAINERS}) taproot-image-banner + * {
|
|
111
|
+
margin-block-start: 0;
|
|
112
|
+
}
|
|
113
|
+
[data-taproot-section][data-following-spacing="compact"] > [data-section-content] > :first-child,
|
|
114
|
+
[data-taproot-section][data-following-spacing="compact"] > esp-box > [data-section-content] > :first-child {
|
|
115
|
+
margin-block-start: 0;
|
|
116
|
+
}
|
|
117
|
+
`.trim();
|
package/src/changelog.json
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"entries": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.32.0",
|
|
5
|
+
"note": "Links to other sites open in a new tab on published pages with nothing to set; target=\"_self\" keeps a link mark in the same tab; page blocks get real default spacing with a site-wide setting.",
|
|
6
|
+
"behavior": [
|
|
7
|
+
"An absolute http or https link to a host other than the site's own now publishes target=\"_blank\" and rel with noopener noreferrer added, with nothing to set. This covers link marks in page text and tables, inline facts, and the links of feature-grid, image-row, card-grid, product-grid, hero-section, cta, image-banner and social-links components, and the footer. mailto and tel links, relative paths, fragments, queries and the site's own addresses (including www.) stay in the same tab; a subdomain counts as another host. Component links have no setting to keep them in the tab.",
|
|
8
|
+
"A link mark keeps its own choice: target=\"_blank\" opens a new tab for any link, and target=\"_self\" keeps even an external link in the same tab. In Markdown write {target=\"_self\"} after the link; in .pm.json set attrs.target to \"_self\". A mark with a null target, or with the editor's old default rel, follows the default. See help page free-form.",
|
|
9
|
+
"Header navigation follows the same rule: an item or linked group with an http or https address on another host opens in a new tab. mailto and tel items, page items and the site's own addresses stay in the same tab.",
|
|
10
|
+
"A link that opens a new tab now says \"(opens in a new tab)\" to a screen reader, with no change on screen. This covers component links, link marks, navigation and the footer.",
|
|
11
|
+
"Help for component links and the image-row and product-grid notes no longer say links open in the same tab.",
|
|
12
|
+
"Existing pages change at the site's next whole-site render, such as a settings or navigation change. Pages an incremental publish copies forward keep their old links until then.",
|
|
13
|
+
"Page blocks get real default spacing. Text, tables and every component sit one gap apart, where components used to have no space from the text before them; the last text block of a padded section gets the same gap as room before the section ends; a heading's top margin scales with it. Banners stay flush with their neighbors.",
|
|
14
|
+
"New required setting blockSpacing in settings/taproot-styles.json (Brand & style > Block spacing): compact, standard or roomy scales the gap to 0.75, 1 or 1.5 times, in both color schemes. Standard keeps text where it is today.",
|
|
15
|
+
"spacer blocks add their height to the new gap instead of replacing it, so review hand-placed spacers and remove the ones that only worked around tight spacing.",
|
|
16
|
+
"blockSpacing is required on theme push like every other appearance field. A workspace pulled before this version does not hold it: run pull again before theme push, or add \"blockSpacing\": \"standard\" to settings/taproot-styles.json.",
|
|
17
|
+
"A site shows the new spacing after its next whole-site render, such as a settings or navigation change. Pages an incremental publish copies forward keep their old spacing until then.",
|
|
18
|
+
"A section decoration can be sized by height. Set blockSizePercent (an integer from 10 to 200) for \"auto N%\" in place of inlineSizePercent; the two cannot be combined, and neither set keeps the width default of 40. See help page free-form.",
|
|
19
|
+
"The help reference version is 51."
|
|
20
|
+
],
|
|
21
|
+
"help": [
|
|
22
|
+
"page",
|
|
23
|
+
"components",
|
|
24
|
+
"component",
|
|
25
|
+
"settings",
|
|
26
|
+
"appearance"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"version": "0.31.0",
|
|
31
|
+
"note": "Image defaults can be listed, read and changed safely through media images; metadata advice uses consistent title and description guidance.",
|
|
32
|
+
"behavior": [
|
|
33
|
+
"New verbs: media images list [--missing-description] [--page-size <1-100>] [--page-token <cursor>], media images get <image-id>, and media images describe <image-id> --description <text> (or --clear) --expected-revision <n> [--if-missing]. List returns one bounded page and its nextPageToken; the server filters missing defaults before pagination, without scanning page placements or fetching every image separately.",
|
|
34
|
+
"Get/list/describe report stable imageId, uploadedName, nullable defaultDescription, descriptionRevision as an exact decimal string, dimensions, processing state and validated preview URLs. Preview URLs are for inspection and may expire; they are omitted from GitHub Actions output and never written into a description or workspace manifest.",
|
|
35
|
+
"Description writes require the selected site's media permission and the expected revision from list/get. --if-missing is checked atomically by the server and cannot be combined with --clear or blank text. A stale revision or existing default is a refusal, with no automatic overwrite or mutation retry. The server trims outer whitespace, clears blank descriptions to null, and rejects more than 1000 Unicode scalar values without truncation. Existing published output keeps its frozen text until deployment; custom/decorative placement choices stay independent.",
|
|
36
|
+
"Local metadata advice checks known author-entered values consistently: a nonempty title outside 15–70 Unicode scalar values, a description outside 50–160, missing text, or a description matching the title after whitespace and case normalization. Warnings do not block saves. Fallback-dependent metadata remains deferred to the deployment evaluator.",
|
|
37
|
+
"The coordinated release replaces local a11y.image_alt_missing placement advice with a11y.image_default_missing in the stored deployment report. Completeness uses only the frozen image-owned default: contextual alt text, album captions and decorative choices do not waive a missing reusable description. status reads the report and never scans current drafts or triggers compilation.",
|
|
38
|
+
"Image Row and Product Grid image records now follow the shared description contract: descriptionMode selects inherit, custom or decorative, while defaultDescription and descriptionRevision are read-only hydrated metadata and are omitted from authored workspace sources. An Image Row image with descriptionMode inherit may omit alt locally, but its description must resolve to non-blank alt text before deployment; custom and legacy Image Row alt text remains required. See help component image-row and help component product-grid.",
|
|
39
|
+
"The help reference version is 50. Taproot accepts only this latest CLI after the API deploy; upgrade with npm install -g @taprootio/site-authoring@latest."
|
|
40
|
+
],
|
|
41
|
+
"help": [
|
|
42
|
+
"media",
|
|
43
|
+
"pages",
|
|
44
|
+
"page",
|
|
45
|
+
"component"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
3
48
|
{
|
|
4
49
|
"version": "0.30.0",
|
|
5
50
|
"note": "A card-grid can show each card's image whole at a set height, aligned and unframed, with chosen space on any side of the image, so a logo no longer needs a wide padded file.",
|