@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 CHANGED
@@ -223,12 +223,18 @@ Actions output.
223
223
 
224
224
  ## Public release
225
225
 
226
- Private Taproot is the source authority. A private `site-authoring-v<version>`
227
- tag verifies this package — the release commit must belong to `main`, the tag
228
- must name the version `package.json` declares, and `@taprootio/espalier` must
229
- be pinned to the exact version the release manifest records — then
230
- allowlist-exports only `packages/site-authoring/` to public
231
- [`taprootio/trunk`](https://github.com/taprootio/trunk). Trunk's separate
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.1.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.4.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.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
- ...["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 }),
@@ -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")
@@ -27,7 +27,7 @@ import {
27
27
 
28
28
  export { getAppearanceReference, getFooterReference, getThemeReference };
29
29
 
30
- export const REFERENCE_VERSION = 13;
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." }),
@@ -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",