@taprootio/site-authoring 0.13.0 → 0.14.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
@@ -362,8 +362,10 @@ file that is not an MP4 with its index first as one (copying the tracks, never
362
362
  re-encoding), declares its codecs, and prints the server's one-line refusal as
363
363
  it came for anything it will not accept (a non-H.264 or non-AAC file, one over
364
364
  the licence's upload cap). A video is ready when its upload confirms; the CLI sets no poster (Node has no video decoder), so
365
- choose one on the Videos page, where an upload made there gets one automatically; the command
366
- reports its `videoId` for a `video` component (`help component video`), and
365
+ choose one on the Videos page, where an upload made there gets one automatically, and set its
366
+ title and caption there too: the poster, caption and shape are the video's own and show wherever
367
+ it is placed. The command reports its `videoId`, title, caption and file name for a `video`
368
+ component (`help component video`), and
367
369
  `pages push` refuses a page that places a video the site does not have. `nav push`, `theme push`, and `footer push` replace the whole
368
370
  navigation tree, the complete light/dark theme pair with its appearance
369
371
  settings, and the closed footer document. `pull` writes each scheme's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taprootio/site-authoring",
3
- "version": "0.13.0",
3
+ "version": "0.14.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",
package/src/api.js CHANGED
@@ -841,6 +841,8 @@ export function normalizeVideo(value) {
841
841
  return {
842
842
  videoId: requireCanonicalUuid(video.videoId, "api.video_contract", "video.videoId"),
843
843
  title: text(video.title),
844
+ caption: text(video.caption),
845
+ fileName: text(video.fileName),
844
846
  bytes: safeCount(video.bytes),
845
847
  durationMilliseconds: safeCount(video.durationMs),
846
848
  width: safeCount(video.width),
@@ -1025,6 +1027,10 @@ export async function getPublishingReadiness(client, siteId, selection = {}) {
1025
1027
  blockedPageCount: safeCount(response.blockedPageCount),
1026
1028
  hasCandidateChanges: response.hasCandidateChanges === true,
1027
1029
  hasSuccessfulStagingDeployment: response.hasSuccessfulStagingDeployment === true,
1030
+ // Site-wide inputs frozen with every release that differ from production's.
1031
+ redirectsChanged: response.redirectsChanged === true,
1032
+ formsChanged: response.formsChanged === true,
1033
+ videosChanged: response.videosChanged === true,
1028
1034
  blockers,
1029
1035
  };
1030
1036
  }
@@ -297,17 +297,12 @@ const MEDIA_PRESENTATION_HELP =
297
297
  * `VideoData`. The page names a library video by id and nothing about where it
298
298
  * is served: the generator resolves the id at publish time and refuses a video
299
299
  * that is not `ready` or not this site's own. Delivery URLs are not authorable
300
- * here, so a `delivery` key is refused like any other unknown field.
300
+ * here, so a `delivery` key is refused like any other unknown field. The poster,
301
+ * caption and ratio are the video's own (TR01184), so a block carrying one is
302
+ * refused the same way.
301
303
  */
302
304
  const VIDEO = Object.freeze({
303
305
  videoId: Object.freeze({ kind: "uuid", required: true }),
304
- poster: image(),
305
- caption: boundedStr(300),
306
- aspectRatio: str({
307
- pattern: VIDEO_ASPECT_RATIO_RE,
308
- patternSource: VIDEO_ASPECT_RATIO_PATTERN,
309
- patternDescription: "two whole numbers of at most three digits joined by a colon, such as 16:9",
310
- }),
311
306
  maxHeight: cssLength({ allowNone: true }),
312
307
  borderWidth: cssLength(),
313
308
  });
@@ -639,17 +634,14 @@ const COMPONENT_DEFINITIONS = Object.freeze({
639
634
  "video": component(
640
635
  VIDEO,
641
636
  "Video",
642
- "A video from the site's library that plays when the visitor presses play; nothing loads before then.",
637
+ "A video from the site's library that plays when the visitor presses play; the page reads only the start of the file before then.",
643
638
  {},
644
639
  [
645
- "Add a caption that tells a visitor what the video is about; the caption is also the player's accessible name.",
640
+ "The poster, the caption and the shape are the video's own and show wherever it is placed: set the title, caption and poster on the site's Videos page. The caption tells a visitor what the video is about and is also the player's accessible name.",
646
641
  "A video with spoken content needs captions or a transcript in the page text; the player does not generate them.",
647
- "Leave aspectRatio out to follow the video's own shape; set it only to crop the frame to a different one.",
648
642
  ],
649
643
  {
650
644
  videoId: "7c5e2b1a-9d3f-4a68-b0c4-1e2f3a4b5c6d",
651
- caption: "A walk through the studio before your first class.",
652
- aspectRatio: "16:9",
653
645
  },
654
646
  { notes: [MEDIA_PRESENTATION_HELP] },
655
647
  ),
@@ -71,7 +71,7 @@ import {
71
71
 
72
72
  export { getAppearanceReference, getFooterReference, getThemeReference };
73
73
 
74
- export const REFERENCE_VERSION = 32;
74
+ export const REFERENCE_VERSION = 33;
75
75
 
76
76
  // The date-limit examples `help forms` prints; tests run them through the form validator.
77
77
  export const FORM_DATE_WINDOW_FIELD = Object.freeze({
@@ -838,8 +838,8 @@ const WORKFLOW_REFERENCES = Object.freeze({
838
838
  "Prepare variants locally with any image tool before uploading: a transparent logo, its @2x copy, a scheme-specific dark logo when one asset does not read on both headers, and a square favicon. The CLI uploads what you give it; it has no recolor, crop, or SVG upload command.",
839
839
  "Assign both schemes after uploading: lightLogoId and darkLogoId in settings/taproot-styles.json and brand.faviconId in settings/brand.json take the returned image ids; theme push writes them. See help walkthrough for the full design pass.",
840
840
  "Each result item includes media: { imageId, src, urls, width, height, alt }.",
841
- "MP4, MOV, and WebM files are uploaded as videos (up to 250 MB each; the site's licence sets the cap, and the container is read from the file's bytes, not its name). The CLI reads each file with mediabunny. A file that is H.264 and AAC but not already an MP4 with its index first is rewritten as one without re-encoding; anything else is sent as it is and judged by the server. When the server refuses a file, its one-line message is printed as media.video_refused. A video is ready to play as soon as the upload is confirmed. The CLI sets no poster, because Node has no video decoder: the Videos page picks one automatically for a video uploaded there, and lets the owner choose another frame, so set a CLI-uploaded video's poster there. The command reports videos.items: { file, videoId, contentType, byteLength, deduplicated, durationMilliseconds, component }, where component.markdown is the component:video fence and component.block the componentBlock node that place it. Needs the site's licence to include video.",
842
- "Place an uploaded video on a page with a video component: a component:video fence in Markdown or a componentBlock of type video, with the returned videoId and optional poster, caption, and aspectRatio. See help component video. pages push refuses a video that is not in the site's library; validate checks it against fixture.videoIds.",
841
+ "MP4, MOV, and WebM files are uploaded as videos (up to 250 MB each; the site's licence sets the cap, and the container is read from the file's bytes, not its name). The CLI reads each file with mediabunny. A file that is H.264 and AAC but not already an MP4 with its index first is rewritten as one without re-encoding; anything else is sent as it is and judged by the server. When the server refuses a file, its one-line message is printed as media.video_refused. A video is ready to play as soon as the upload is confirmed. The CLI sets no poster, because Node has no video decoder: the Videos page picks one automatically for a video uploaded there, and lets the owner choose another frame, so set a CLI-uploaded video's poster there, with its title and caption. The command reports videos.items: { file, videoId, title, caption, fileName, contentType, byteLength, deduplicated, durationMilliseconds, component }, where component.markdown is the component:video fence and component.block the componentBlock node that place it. Needs the site's licence to include video.",
842
+ "Place an uploaded video on a page with a video component: a component:video fence in Markdown or a componentBlock of type video, with the returned videoId and optional maxHeight and borderWidth; the poster, caption and shape are the video's own. See help component video. pages push refuses a video that is not in the site's library; validate checks it against fixture.videoIds.",
843
843
  "The same src/urls delivery fields are saved in .taproot-site-media.json for page and component authoring.",
844
844
  ]),
845
845
  example: Object.freeze({
@@ -132,6 +132,19 @@ async function resolveStagingDeploymentId(client, siteId, invocation, manifest,
132
132
  return completed.id;
133
133
  }
134
134
 
135
+ /**
136
+ * Names the site-wide changes a staging with nothing selected publishes, the way the
137
+ * Deployments page does (TR01184): an edited redirect map, form, or video caption or
138
+ * poster, each frozen with every release.
139
+ */
140
+ export function siteWideChanges(readiness) {
141
+ return [
142
+ readiness.redirectsChanged ? "the redirect map" : "",
143
+ readiness.formsChanged ? "a form" : "",
144
+ readiness.videosChanged ? "a video's caption or poster" : "",
145
+ ].filter(Boolean);
146
+ }
147
+
135
148
  function reportReadiness(readiness) {
136
149
  const blockers = boundedList(readiness.blockers, MAXIMUM_REPORTED_BLOCKERS);
137
150
  return {
@@ -141,6 +154,9 @@ function reportReadiness(readiness) {
141
154
  blockedPageCount: readiness.blockedPageCount,
142
155
  hasCandidateChanges: readiness.hasCandidateChanges,
143
156
  hasSuccessfulStagingDeployment: readiness.hasSuccessfulStagingDeployment,
157
+ redirectsChanged: readiness.redirectsChanged,
158
+ formsChanged: readiness.formsChanged,
159
+ videosChanged: readiness.videosChanged,
144
160
  blockers: blockers.items,
145
161
  ...(blockers.truncated ? { blockersTruncated: true } : {}),
146
162
  };
@@ -321,7 +337,8 @@ export async function deploy(invocation) {
321
337
  const nothingSelected = stagedPageIds.length === 0 && selectedSettingsTypes.length === 0 && !includeNavigation;
322
338
  // A managed Docs site stages settings only, so an empty selection is final.
323
339
  // Any other site may still have a publishable change no selection names: an
324
- // edited redirect map. Readiness below says whether it does.
340
+ // edited redirect map, form, or video caption or poster. Readiness below says
341
+ // whether it does.
325
342
  const emptySelectionError = () =>
326
343
  new SiteAuthoringError(
327
344
  "deploy.empty_selection",
@@ -329,7 +346,8 @@ export async function deploy(invocation) {
329
346
  ? "A staging deployment on a managed Docs site needs at least one settings change; it stages settings "
330
347
  + "only. Change a theme, brand, header, or footer setting first."
331
348
  : "A staging deployment needs at least one approved page, settings group, or navigation change, or an "
332
- + "edited redirect map. Run 'taproot-site approve' or change staged settings/navigation first.",
349
+ + "edited redirect map, form, or video caption or poster. Run 'taproot-site approve' or change "
350
+ + "staged settings/navigation first.",
333
351
  { field: "Candidate" },
334
352
  );
335
353
  if (nothingSelected && presentationOnly) {
@@ -362,6 +380,15 @@ export async function deploy(invocation) {
362
380
  if (nothingSelected && !readiness.hasCandidateChanges) {
363
381
  throw emptySelectionError();
364
382
  }
383
+ const siteWide = siteWideChanges(readiness);
384
+ if (nothingSelected && readiness.hasCandidateChanges) {
385
+ // An API that names none of them still knows the site changed; say so, as the web dialog does.
386
+ onProgress(
387
+ siteWide.length > 0
388
+ ? `Staging publishes what changed since the last release: ${siteWide.join(", ")}.`
389
+ : "Your site has changed since the last release. Staging publishes the change.",
390
+ );
391
+ }
365
392
  if (!readiness.hasCandidateChanges) {
366
393
  onProgress("Taproot reports this candidate contains no changes; deploying it anyway would be a no-op.");
367
394
  }
@@ -497,6 +497,9 @@ export async function mediaUpload(invocation) {
497
497
  uploadedVideos.push({
498
498
  file,
499
499
  videoId: video.videoId,
500
+ title: video.title,
501
+ caption: video.caption,
502
+ fileName: video.fileName,
500
503
  contentType,
501
504
  byteLength,
502
505
  deduplicated,
@@ -619,6 +622,10 @@ export async function mediaUpload(invocation) {
619
622
  uploadedVideos.map((entry) => ({
620
623
  file: entry.file,
621
624
  videoId: entry.videoId,
625
+ // Set on the Videos page; the placement shows them wherever the video goes.
626
+ title: entry.title,
627
+ caption: entry.caption,
628
+ fileName: entry.fileName,
622
629
  contentType: entry.contentType,
623
630
  byteLength: entry.byteLength,
624
631
  deduplicated: entry.deduplicated,
@@ -198,6 +198,9 @@ export async function status(invocation) {
198
198
  blockedPageCount: readiness.blockedPageCount,
199
199
  hasCandidateChanges: readiness.hasCandidateChanges,
200
200
  hasSuccessfulStagingDeployment: readiness.hasSuccessfulStagingDeployment,
201
+ redirectsChanged: readiness.redirectsChanged,
202
+ formsChanged: readiness.formsChanged,
203
+ videosChanged: readiness.videosChanged,
201
204
  blockers: blockers.items,
202
205
  ...(blockers.truncated ? { blockersTruncated: true } : {}),
203
206
  },