@taprootio/site-authoring 0.1.0 → 0.2.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 +12 -6
- package/package.json +2 -2
- package/src/appearance-contract.js +39 -1
- package/src/constants.js +1 -1
- package/src/content/components.js +5 -0
- package/src/presentation-reference.js +27 -2
- package/src/reference-help.js +1 -1
- package/src/settings-catalog.js +5 -0
- package/src/verbs/validate.js +34 -1
package/README.md
CHANGED
|
@@ -223,12 +223,18 @@ Actions output.
|
|
|
223
223
|
|
|
224
224
|
## Public release
|
|
225
225
|
|
|
226
|
-
Private Taproot is the source authority.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
allowlist-exports only
|
|
231
|
-
|
|
226
|
+
Private Taproot is the source authority. Merging a version bump to private
|
|
227
|
+
`main` releases this package: the release workflow verifies that the commit
|
|
228
|
+
belongs to `main`, that `site-authoring-v<version>` names the version
|
|
229
|
+
`package.json` declares, and that `@taprootio/espalier` is pinned to the exact
|
|
230
|
+
version the release manifest records, then allowlist-exports only
|
|
231
|
+
`packages/site-authoring/` to public
|
|
232
|
+
[`taprootio/trunk`](https://github.com/taprootio/trunk), whose commit and
|
|
233
|
+
`site-authoring-v<version>` tag are the release record and name the private
|
|
234
|
+
source commit. A pull request that changes this package's public files
|
|
235
|
+
without raising the version fails CI, so the bump is never an afterthought;
|
|
236
|
+
pushing a private `site-authoring-v<version>` tag by hand remains the manual
|
|
237
|
+
path and runs the same verification. Trunk's separate
|
|
232
238
|
`publish-site-authoring.yml` workflow tests and publishes those exact public
|
|
233
239
|
tag bytes with npm trusted-publishing provenance, under its own concurrency
|
|
234
240
|
group, GitHub Environment, tag prefix, and npm identity.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@taprootio/espalier": "4.
|
|
30
|
+
"@taprootio/espalier": "4.7.0"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"test": "node --test",
|
|
@@ -40,6 +40,20 @@ export const APPEARANCE_SCALAR_FIELDS = Object.freeze([
|
|
|
40
40
|
type: "string",
|
|
41
41
|
default: "",
|
|
42
42
|
}),
|
|
43
|
+
Object.freeze({
|
|
44
|
+
settingsType: SETTINGS_TYPE_TAPROOT_STYLES,
|
|
45
|
+
name: "fontMenu",
|
|
46
|
+
path: "taproot-styles.fontMenu",
|
|
47
|
+
type: "string",
|
|
48
|
+
default: "",
|
|
49
|
+
}),
|
|
50
|
+
Object.freeze({
|
|
51
|
+
settingsType: SETTINGS_TYPE_TAPROOT_STYLES,
|
|
52
|
+
name: "fontWeightMenu",
|
|
53
|
+
path: "taproot-styles.fontWeightMenu",
|
|
54
|
+
type: "string",
|
|
55
|
+
default: "",
|
|
56
|
+
}),
|
|
43
57
|
...["lightLogoId", "darkLogoId", "lightCanvasImageId", "darkCanvasImageId"].map((name) =>
|
|
44
58
|
Object.freeze({
|
|
45
59
|
settingsType: SETTINGS_TYPE_TAPROOT_STYLES,
|
|
@@ -72,9 +86,33 @@ export const APPEARANCE_SCALAR_FIELDS = Object.freeze([
|
|
|
72
86
|
name: "headerLayout",
|
|
73
87
|
path: "site-header.headerLayout",
|
|
74
88
|
type: "enum",
|
|
75
|
-
values: Object.freeze(["standard", "centered-brand", "minimal"]),
|
|
89
|
+
values: Object.freeze(["standard", "centered-brand", "minimal", "centered-menu"]),
|
|
76
90
|
default: "standard",
|
|
77
91
|
}),
|
|
92
|
+
Object.freeze({
|
|
93
|
+
settingsType: SETTINGS_TYPE_SITE_HEADER,
|
|
94
|
+
name: "headerWidth",
|
|
95
|
+
path: "site-header.headerWidth",
|
|
96
|
+
type: "enum",
|
|
97
|
+
values: Object.freeze(["contained", "wide"]),
|
|
98
|
+
default: "contained",
|
|
99
|
+
}),
|
|
100
|
+
Object.freeze({
|
|
101
|
+
settingsType: SETTINGS_TYPE_SITE_HEADER,
|
|
102
|
+
name: "navDrawerStyle",
|
|
103
|
+
path: "site-header.navDrawerStyle",
|
|
104
|
+
type: "enum",
|
|
105
|
+
values: Object.freeze(["panel", "full-screen"]),
|
|
106
|
+
default: "full-screen",
|
|
107
|
+
}),
|
|
108
|
+
Object.freeze({
|
|
109
|
+
settingsType: SETTINGS_TYPE_SITE_HEADER,
|
|
110
|
+
name: "navDrawerTransition",
|
|
111
|
+
path: "site-header.navDrawerTransition",
|
|
112
|
+
type: "enum",
|
|
113
|
+
values: Object.freeze(["fade", "slide-down", "slide-up", "slide-left", "slide-right"]),
|
|
114
|
+
default: "fade",
|
|
115
|
+
}),
|
|
78
116
|
Object.freeze({
|
|
79
117
|
settingsType: SETTINGS_TYPE_SITE_HEADER,
|
|
80
118
|
name: "brandText",
|
package/src/constants.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const CLI_NAME = "@taprootio/site-authoring";
|
|
2
|
-
export const CLI_VERSION = "0.
|
|
2
|
+
export const CLI_VERSION = "0.2.0";
|
|
3
3
|
export const CLI_BINARY_NAME = "taproot-site";
|
|
4
4
|
export const RESULT_SCHEMA_VERSION = 1;
|
|
5
5
|
export const CONFIG_FILE_NAME = "taproot-site.json";
|
|
@@ -227,6 +227,9 @@ const IMAGE_BANNER = Object.freeze({
|
|
|
227
227
|
}),
|
|
228
228
|
ratio: enumOf("4/1", "3/1", "2/1"),
|
|
229
229
|
compactRatio: enumOf("", "2/1", "3/2", "1/1"),
|
|
230
|
+
// `ratio` keeps the wide/compact shape, capped at the viewport space below
|
|
231
|
+
// the site header; `viewport` fills exactly that space (TR00413).
|
|
232
|
+
heightMode: enumOf("ratio", "viewport"),
|
|
230
233
|
contentPosition: enumOf("bottom-start", "bottom", "bottom-end", "center", "top-start", "top", "top-end"),
|
|
231
234
|
scrim: enumOf("auto", "none", "flat", "top", "bottom", "left", "right", "radial"),
|
|
232
235
|
scrimStrength: enumOf("soft", "medium", "strong"),
|
|
@@ -457,6 +460,7 @@ const COMPONENT_DEFINITIONS = Object.freeze({
|
|
|
457
460
|
focus: { x: 0.5, y: 0.5 },
|
|
458
461
|
ratio: "3/1",
|
|
459
462
|
compactRatio: "3/2",
|
|
463
|
+
heightMode: "ratio",
|
|
460
464
|
contentPosition: "center",
|
|
461
465
|
scrim: "auto",
|
|
462
466
|
scrimStrength: "medium",
|
|
@@ -473,6 +477,7 @@ const COMPONENT_DEFINITIONS = Object.freeze({
|
|
|
473
477
|
altText: "",
|
|
474
478
|
overlayText: "Train together",
|
|
475
479
|
focus: { x: 0.5, y: 0.35 },
|
|
480
|
+
heightMode: "ratio",
|
|
476
481
|
contentPosition: "bottom-start",
|
|
477
482
|
scrim: "bottom",
|
|
478
483
|
scrimStrength: "medium",
|
|
@@ -57,6 +57,12 @@ function getEspalierVersion() {
|
|
|
57
57
|
|
|
58
58
|
const OPTIONAL_THEME_FIELDS = Object.freeze([
|
|
59
59
|
"explicitMappingTokens",
|
|
60
|
+
// Espalier 4.7.0 menu typography: documented so the theme help covers the
|
|
61
|
+
// installed contract, optional so pulled baselines that predate it stay
|
|
62
|
+
// complete (the server seeds and requires the fields listed in
|
|
63
|
+
// DefaultSiteTheme.RequiredProperties only).
|
|
64
|
+
"fontMenu",
|
|
65
|
+
"fontWeightMenu",
|
|
60
66
|
"boxBackgroundImage",
|
|
61
67
|
"boxBackgroundImageOpacity",
|
|
62
68
|
"vellumOpacity",
|
|
@@ -201,8 +207,12 @@ const THEME_GROUPS = Object.freeze([
|
|
|
201
207
|
...["fontBody", "fontHeadings", "fontBrand", "fontMonospace"].map((name) =>
|
|
202
208
|
themeField(name, "CSS font-family; empty inherits the consuming surface's fallback.")
|
|
203
209
|
),
|
|
204
|
-
|
|
205
|
-
|
|
210
|
+
themeField(
|
|
211
|
+
"fontMenu",
|
|
212
|
+
"CSS font-family for navigation menu items and group labels; empty falls back to fontBody, never to headings.",
|
|
213
|
+
),
|
|
214
|
+
...["fontWeightBody", "fontWeightHeadings", "fontWeightBrand", "fontWeightMonospace", "fontWeightMenu"].map(
|
|
215
|
+
(name) => themeField(name, "CSS font-weight: 1–1000, normal/bold/lighter/bolder, or a CSS-wide keyword."),
|
|
206
216
|
),
|
|
207
217
|
themeField("rootFontSize", "Root font size in px.", { minimum: 1, warningMaximum: 100 }),
|
|
208
218
|
themeField("typeRatio", "Modular type-scale ratio.", { exclusiveMinimum: 1, warningMaximum: 1.3 }),
|
|
@@ -271,6 +281,7 @@ export function getThemeReference() {
|
|
|
271
281
|
"Define brand anchors first, then assign canvas, ink, accent, action, and structure roles in both schemes.",
|
|
272
282
|
"Add named contexts for whole zones, including inverted zones, and rebind lightness when the zone changes brightness.",
|
|
273
283
|
"Tune typography, type/space ratios, radii, and viewport interpolation as one layout system.",
|
|
284
|
+
"Typography is per scheme on purpose: light and dark set their own moods, so a heading, body, or brand face that differs between them is a design choice, and theme push never warns about it.",
|
|
274
285
|
"Retune danger/success/warning/info through intents without changing their meanings; design data palettes separately.",
|
|
275
286
|
"Use semanticMappings only for meanings roles and contexts cannot express; scattered pins shadow the coherent model.",
|
|
276
287
|
"Run theme push, inspect warnings, then verify text, actions, focus states, and both schemes in authoring previews.",
|
|
@@ -329,6 +340,18 @@ export function getAppearanceReference() {
|
|
|
329
340
|
"Use site-owned image ids retained from pull or returned by media upload. URLs are server projections, never authored inputs.",
|
|
330
341
|
logoContract:
|
|
331
342
|
"lightLogoId and darkLogoId are the current scheme-specific logo fields; there is no separate compact-logo setting.",
|
|
343
|
+
headerWidthContract:
|
|
344
|
+
"headerWidth 'contained' keeps the brand and the header buttons at the page content edges; "
|
|
345
|
+
+ "'wide' moves them to the viewport edges. Pages with root-band components (image-banner) span the viewport, "
|
|
346
|
+
+ "so full-bleed designs usually pair headerWidth 'wide' with headerLayout 'centered-menu', which keeps the "
|
|
347
|
+
+ "brand left, the buttons right, and the navigation centered. validate prints a hint, never a failure, when a "
|
|
348
|
+
+ "root-band page ships with a contained header.",
|
|
349
|
+
menuContract:
|
|
350
|
+
"navDrawerStyle 'full-screen' (the published default) opens the mobile menu over the whole viewport with the "
|
|
351
|
+
+ "brand centered above large centered items and navDrawerTransition choosing how it appears (fade, slide-down, "
|
|
352
|
+
+ "slide-up, slide-left, or slide-right); 'panel' keeps the side panel, which exists for application-style "
|
|
353
|
+
+ "menus with many groups. fontMenu and fontWeightMenu set the navigation face the way fontBrand sets the "
|
|
354
|
+
+ "brand; an empty fontMenu falls back to the body font, never to headings.",
|
|
332
355
|
mutationOrder: [
|
|
333
356
|
"fresh footer read plus concurrency-protected ten-color overlay",
|
|
334
357
|
"default scheme, brand fonts and assets, favicon, and header scalars",
|
|
@@ -576,6 +599,8 @@ export function formatPresentationReference(reference) {
|
|
|
576
599
|
+ "\n\nRead-only server projections:\n"
|
|
577
600
|
+ reference.readOnlyProjections.map((field) => " " + field.file + " :: " + field.path).join("\n")
|
|
578
601
|
+ "\n\nImages: " + reference.imageReferences + "\nLogos: " + reference.logoContract
|
|
602
|
+
+ "\nHeader width: " + reference.headerWidthContract
|
|
603
|
+
+ "\nMobile menu and menu font: " + reference.menuContract
|
|
579
604
|
+ "\nFooter: " + reference.footerBoundary
|
|
580
605
|
+ "\nFooter content guard: " + reference.footerContentGuard + "\n\nMutation order (non-atomic):\n"
|
|
581
606
|
+ reference.mutationOrder.map((step, index) => " " + (index + 1) + ". " + step).join("\n")
|
package/src/reference-help.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
|
|
28
28
|
export { getAppearanceReference, getFooterReference, getThemeReference };
|
|
29
29
|
|
|
30
|
-
export const REFERENCE_VERSION =
|
|
30
|
+
export const REFERENCE_VERSION = 15;
|
|
31
31
|
export const PAGE_TYPES = Object.freeze(["free-form"]);
|
|
32
32
|
export const REFERENCE_TOPICS = Object.freeze([
|
|
33
33
|
Object.freeze({ name: "pages", usage: `${CLI_BINARY_NAME} help pages`, summary: "List authorable page types." }),
|
package/src/settings-catalog.js
CHANGED
|
@@ -58,6 +58,8 @@ export const SETTINGS_GROUPS = Object.freeze([
|
|
|
58
58
|
"defaultScheme",
|
|
59
59
|
"fontBrand",
|
|
60
60
|
"fontWeightBrand",
|
|
61
|
+
"fontMenu",
|
|
62
|
+
"fontWeightMenu",
|
|
61
63
|
"lightLogoId",
|
|
62
64
|
"lightLogoUrl",
|
|
63
65
|
"darkLogoId",
|
|
@@ -86,6 +88,9 @@ export const SETTINGS_GROUPS = Object.freeze([
|
|
|
86
88
|
fields: Object.freeze([
|
|
87
89
|
...stringFields(
|
|
88
90
|
"headerLayout",
|
|
91
|
+
"headerWidth",
|
|
92
|
+
"navDrawerStyle",
|
|
93
|
+
"navDrawerTransition",
|
|
89
94
|
"brandText",
|
|
90
95
|
"brandColor",
|
|
91
96
|
"logoAlt",
|
package/src/verbs/validate.js
CHANGED
|
@@ -2,7 +2,7 @@ import { lstat, realpath } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import { NAVIGATION_MAXIMUM_DEPTH, TEMPLATE_TYPE_FREE_FORM } from "../api.js";
|
|
5
|
-
import { sharedThemeContextNames } from "../content/free-form-sections.js";
|
|
5
|
+
import { freeFormRootPresentation, sharedThemeContextNames } from "../content/free-form-sections.js";
|
|
6
6
|
import { VERB_VALIDATE } from "../constants.js";
|
|
7
7
|
import { isCanonicalUuid, SiteAuthoringError } from "../errors.js";
|
|
8
8
|
import { FOOTER_SETTINGS_FILE } from "../footer-workspace.js";
|
|
@@ -340,6 +340,34 @@ function validatePageImageReferences(value, field, knownImageIds, deliveryOrigin
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
/** Whether a validated page document places a root-band component at its root. */
|
|
344
|
+
function pageUsesRootBand(document) {
|
|
345
|
+
return Array.isArray(document?.content)
|
|
346
|
+
&& document.content.some((node) => freeFormRootPresentation(node)?.rootPlacement === "root-band");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Advisory only: a root-band component spans the viewport, so a header whose
|
|
351
|
+
* brand and buttons stop at the content edges usually reads as misaligned
|
|
352
|
+
* beside it. The wide header with the centered menu is the recommended pair,
|
|
353
|
+
* never a requirement, so this is a hint in the result and on stderr, not a
|
|
354
|
+
* failure (TR00697).
|
|
355
|
+
*/
|
|
356
|
+
function headerWidthHints(headerWidth, rootBandPages) {
|
|
357
|
+
if (rootBandPages.length === 0 || headerWidth === "wide") return [];
|
|
358
|
+
const pages = boundedList(rootBandPages, MAXIMUM_REPORTED);
|
|
359
|
+
return [{
|
|
360
|
+
code: "header.width_contained_with_root_band",
|
|
361
|
+
message: `${rootBandPages.length} page(s) use a full-bleed root-band component while site-header.headerWidth is `
|
|
362
|
+
+ `'${headerWidth}'. Full-bleed designs usually read better with headerWidth 'wide' (brand and buttons at the `
|
|
363
|
+
+ "viewport edges) paired with headerLayout 'centered-menu'.",
|
|
364
|
+
setting: "site-header.headerWidth",
|
|
365
|
+
suggested: { headerWidth: "wide", headerLayout: "centered-menu" },
|
|
366
|
+
pages: pages.items,
|
|
367
|
+
...(pages.truncated ? { pagesTruncated: true } : {}),
|
|
368
|
+
}];
|
|
369
|
+
}
|
|
370
|
+
|
|
343
371
|
export async function validateFixture(invocation = {}) {
|
|
344
372
|
const onProgress = typeof invocation.onProgress === "function" ? invocation.onProgress : () => {};
|
|
345
373
|
const fixtureRoot = await resolveFixtureRoot(invocation.cwd ?? process.cwd(), invocation.fixturePath);
|
|
@@ -377,6 +405,7 @@ export async function validateFixture(invocation = {}) {
|
|
|
377
405
|
fail("fixture.page_untracked", `Page source '${untracked}' is not bound by ${FIXTURE_MANIFEST_FILE_NAME}.`, untracked);
|
|
378
406
|
}
|
|
379
407
|
const validatedPages = [];
|
|
408
|
+
const rootBandPages = [];
|
|
380
409
|
for (const file of pageFiles) {
|
|
381
410
|
onProgress(`Validating '${file}'.`);
|
|
382
411
|
const entry = manifestByFile.get(file);
|
|
@@ -404,7 +433,10 @@ export async function validateFixture(invocation = {}) {
|
|
|
404
433
|
}
|
|
405
434
|
validatePageImageReferences(page.document, file, imageIds, deliveryOrigins);
|
|
406
435
|
validatedPages.push({ file, path: page.pagePath });
|
|
436
|
+
if (pageUsesRootBand(page.document)) rootBandPages.push(file);
|
|
407
437
|
}
|
|
438
|
+
const hints = headerWidthHints(presentation.header.headerWidth, rootBandPages);
|
|
439
|
+
for (const hint of hints) onProgress(`Hint: ${hint.message}`);
|
|
408
440
|
|
|
409
441
|
onProgress("Validating navigation shape and local PAGE targets.");
|
|
410
442
|
const navigation = validateNavigationWorkspaceDocument(
|
|
@@ -459,6 +491,7 @@ export async function validateFixture(invocation = {}) {
|
|
|
459
491
|
count: presentation.themes.warningCount,
|
|
460
492
|
...(presentation.themes.warningsTruncated ? { truncated: true } : {}),
|
|
461
493
|
},
|
|
494
|
+
hints,
|
|
462
495
|
proves: [
|
|
463
496
|
"fixture structure and bounded files",
|
|
464
497
|
"page content and named theme contexts",
|