@taprootio/site-authoring 0.1.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taprootio/site-authoring",
3
- "version": "0.1.1",
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.6.0"
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.1.1";
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";
@@ -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
- ...["fontWeightBody", "fontWeightHeadings", "fontWeightBrand", "fontWeightMonospace"].map((name) =>
205
- themeField(name, "CSS font-weight: 1–1000, normal/bold/lighter/bolder, or a CSS-wide keyword.")
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 }),
@@ -330,6 +340,18 @@ export function getAppearanceReference() {
330
340
  "Use site-owned image ids retained from pull or returned by media upload. URLs are server projections, never authored inputs.",
331
341
  logoContract:
332
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.",
333
355
  mutationOrder: [
334
356
  "fresh footer read plus concurrency-protected ten-color overlay",
335
357
  "default scheme, brand fonts and assets, favicon, and header scalars",
@@ -577,6 +599,8 @@ export function formatPresentationReference(reference) {
577
599
  + "\n\nRead-only server projections:\n"
578
600
  + reference.readOnlyProjections.map((field) => " " + field.file + " :: " + field.path).join("\n")
579
601
  + "\n\nImages: " + reference.imageReferences + "\nLogos: " + reference.logoContract
602
+ + "\nHeader width: " + reference.headerWidthContract
603
+ + "\nMobile menu and menu font: " + reference.menuContract
580
604
  + "\nFooter: " + reference.footerBoundary
581
605
  + "\nFooter content guard: " + reference.footerContentGuard + "\n\nMutation order (non-atomic):\n"
582
606
  + reference.mutationOrder.map((step, index) => " " + (index + 1) + ". " + step).join("\n")
@@ -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",
@@ -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",