@taprootio/site-authoring 0.29.0 → 0.30.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.29.0",
3
+ "version": "0.30.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",
@@ -1,5 +1,18 @@
1
1
  {
2
2
  "entries": [
3
+ {
4
+ "version": "0.30.0",
5
+ "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.",
6
+ "behavior": [
7
+ "A card-grid takes imageFit (cover, the default, or contain), imageHeight (small 2.5rem/40px, medium 4rem/64px, large 6rem/96px at a 16px root), imageAlign (start, center or end) and imageSpacing (an object with any of top, bottom, start and end, each none, small, medium or text). With contain the whole image shows at imageHeight, never cropped or framed, and an image wider than its card shrinks to fit; each card image needs a nonzero width and height, which media upload records. imageHeight and imageAlign are refused unless imageFit is contain. A side left out of imageSpacing keeps its default for the fit: cover has none on every side; contain has small above, none below and text (the padding around the card's text) at start and end. See help component card-grid.",
8
+ "A card-grid image now sits flush with the top of its card, because the article image margin no longer reaches it. A site gets this on its next publish.",
9
+ "The help reference version is 49."
10
+ ],
11
+ "help": [
12
+ "components",
13
+ "component"
14
+ ]
15
+ },
3
16
  {
4
17
  "version": "0.29.0",
5
18
  "note": "Pages gain an image-row component for linked pictures at a set height and a product-grid component for priced cards with a button each; feature-grid help states its icon sizes and square crop, and published feature-grid icons now render at the documented sizes.",
@@ -247,6 +247,22 @@ const LATEST_POSTS = Object.freeze({
247
247
  bannerScheme: enumOf("auto", "light", "dark"),
248
248
  });
249
249
 
250
+ /** The registry owns the row heights; the help text and the renderer derive from the same map. */
251
+ const IMAGE_ROW_HEIGHTS = registry.components["image-row"].heights;
252
+ const IMAGE_ROW_HEIGHT_SIZES = Object.entries(IMAGE_ROW_HEIGHTS)
253
+ .map(([name, rem]) => `${name} ${rem} (${Number.parseFloat(rem) * 16}px)`)
254
+ .join(", ");
255
+ const IMAGE_ROW_HEIGHT_HELP =
256
+ `Each image's height: ${IMAGE_ROW_HEIGHT_SIZES} at a 16px root; width follows each image's own shape, never cropped or framed.`;
257
+
258
+ /** The registry owns the card image spacings too; each side takes the same four names (TR01326). */
259
+ const CARD_IMAGE_SPACINGS = registry.components["card-grid"].imageSpacings;
260
+ const CARD_IMAGE_SPACING_NAMES = Object.keys(CARD_IMAGE_SPACINGS);
261
+ const CARD_IMAGE_SPACING_VALUES =
262
+ `none (0), small (${CARD_IMAGE_SPACINGS.small}), medium (${CARD_IMAGE_SPACINGS.medium}) or text (the same padding as around the card's text)`;
263
+ const CARD_IMAGE_SPACING_SIDE_HELP = (side, defaults) =>
264
+ `Space on the ${side} of each card image: ${CARD_IMAGE_SPACING_VALUES}. Left out, it is ${defaults}.`;
265
+
250
266
  /** `CardGridData`. `cropState` is the one genuinely optional field in types.ts. */
251
267
  const CARD_GRID = Object.freeze({
252
268
  cards: withOmission(
@@ -262,6 +278,36 @@ const CARD_GRID = Object.freeze({
262
278
  columns: num({ values: Object.freeze([2, 3]) }),
263
279
  presentation: enumOf("cards", "editorial", "featured", "poster"),
264
280
  imageAspect: enumOf("auto", "landscape", "square", "portrait"),
281
+ imageFit: withOmission(
282
+ described(
283
+ enumOf("cover", "contain"),
284
+ "cover fills the card and crops each image to imageAspect; contain shows the whole image at imageHeight, never cropped or framed, and needs each image's width and height.",
285
+ ),
286
+ { kind: "value", value: "cover" },
287
+ ),
288
+ imageHeight: described(
289
+ enumOf(...Object.keys(IMAGE_ROW_HEIGHTS)),
290
+ `${IMAGE_ROW_HEIGHT_HELP} Only with imageFit contain, where it is medium when left out; refused otherwise.`,
291
+ ),
292
+ imageAlign: described(
293
+ enumOf("start", "center", "end"),
294
+ "Where each whole image sits across its card, not the text: start is the left edge and end the right edge in a left-to-right page. Only with imageFit contain, where it is start when left out; refused otherwise.",
295
+ ),
296
+ imageSpacing: described(
297
+ shape({
298
+ top: described(enumOf(...CARD_IMAGE_SPACING_NAMES), CARD_IMAGE_SPACING_SIDE_HELP("top", "none with cover and small with contain")),
299
+ bottom: described(enumOf(...CARD_IMAGE_SPACING_NAMES), CARD_IMAGE_SPACING_SIDE_HELP("bottom", "none")),
300
+ start: described(
301
+ enumOf(...CARD_IMAGE_SPACING_NAMES),
302
+ CARD_IMAGE_SPACING_SIDE_HELP("left edge (start)", "none with cover and text with contain"),
303
+ ),
304
+ end: described(
305
+ enumOf(...CARD_IMAGE_SPACING_NAMES),
306
+ CARD_IMAGE_SPACING_SIDE_HELP("right edge (end)", "none with cover and text with contain"),
307
+ ),
308
+ }),
309
+ "Space around each card image; name only the sides to change, and a side left out keeps its default for the fit. With imageFit contain, start moves an image aligned to start and end moves one aligned to end; otherwise they only limit how wide the image can be.",
310
+ ),
265
311
  interaction: enumOf("none", "lift", "zoom", "caption-reveal"),
266
312
  borderWidth: num({
267
313
  integer: true,
@@ -408,14 +454,6 @@ function imageFieldsFor(spec) {
408
454
  });
409
455
  }
410
456
 
411
- /** The registry owns the row heights; the help text and the renderer derive from the same map. */
412
- const IMAGE_ROW_HEIGHTS = registry.components["image-row"].heights;
413
- const IMAGE_ROW_HEIGHT_HELP = `Each image's height: ${
414
- Object.entries(IMAGE_ROW_HEIGHTS)
415
- .map(([name, rem]) => `${name} ${rem} (${Number.parseFloat(rem) * 16}px)`)
416
- .join(", ")
417
- } at a 16px root; width follows each image's own shape, never cropped or framed.`;
418
-
419
457
  /**
420
458
  * `ImageRowData` (TR01322). A row of pictures carries no other text, so every
421
459
  * image needs alt text and the dimensions the row sizes it by.
@@ -648,7 +686,7 @@ const COMPONENT_DEFINITIONS = Object.freeze({
648
686
  "Icon images need descriptive alt text when they convey information; otherwise use an empty alt value.",
649
687
  "Item title and description are plain text: an item's url makes the whole item the link, and no link can be placed inside the text. Put linked prose in a paragraph beside the grid.",
650
688
  "iconSize sets the icon box: small 3rem (48px), medium 6rem (96px), large 10rem (160px), xlarge 16rem (256px) at a 16px root.",
651
- "Icons are cropped to a square and framed by the site's image border. For badges, logos or any picture that must keep its shape, use the image-row component instead.",
689
+ "Icons are cropped to a square and framed by the site's image border. For badges, logos or any picture that must keep its shape, use the image-row component instead, or a card-grid with imageFit contain when each needs a card.",
652
690
  ],
653
691
  {
654
692
  items: [
@@ -713,6 +751,14 @@ const COMPONENT_DEFINITIONS = Object.freeze({
713
751
  "Every linked card needs a title that describes its destination.",
714
752
  "Card images need descriptive alt text unless they are decorative.",
715
753
  "Card title and description are plain text: a card's linkUrl makes the whole card the link, and no link can be placed inside the text. Put linked prose in a paragraph beside the grid.",
754
+ "Images are cropped when imageFit is cover, the default: each is cut to the fixed imageAspect, while imageAspect auto follows the picture's own shape up to the page's height cap. imageFit contain never crops; it shows the whole image.",
755
+ "For a logo or mark use imageFit contain with a tightly cropped file that has no built-in padding, and set the size with imageHeight and the space around it with imageSpacing, not by padding the file.",
756
+ `imageHeight sets the height of a whole image: ${IMAGE_ROW_HEIGHT_SIZES} at a 16px root. Width follows the image's shape, and an image wider than its card shrinks to fit.`,
757
+ "imageHeight and imageAlign are refused with cover, and when imageFit is left out, which means cover. With contain, imageHeight is medium and imageAlign is start when left out. imageAlign moves the image only, never the text.",
758
+ `imageSpacing is an object with any of top, bottom, start and end, each ${CARD_IMAGE_SPACING_VALUES}. It works with either fit.`,
759
+ "A side left out of imageSpacing keeps its default, so name only the sides to change. Defaults for cover: none on every side. Defaults for contain: top small, bottom none, start and end text.",
760
+ "imageFit contain needs each card image's width and height, which media upload records; a card image with a missing or zero size is refused.",
761
+ "A card with a saved crop shows that crop under contain; replace the image with the whole file to show all of it. The Zoom interaction has no effect on a whole image.",
716
762
  ],
717
763
  {
718
764
  cards: [
@@ -1077,6 +1123,7 @@ function referenceSchema(spec) {
1077
1123
  type: spec.nullable ? ["object", "null"] : "object",
1078
1124
  additionalProperties: false,
1079
1125
  properties: referenceProperties(spec.fields),
1126
+ ...(spec.description ? { description: spec.description } : {}),
1080
1127
  };
1081
1128
  case "image":
1082
1129
  return {
@@ -1341,6 +1388,44 @@ export function expandComponentAuthoringShorthand(componentType, source) {
1341
1388
  return { source: JSON.stringify({ provider: target.provider, videoId: target.videoId, ...rest }) };
1342
1389
  }
1343
1390
 
1391
+ /**
1392
+ * The card grid's image rules that need two fields (TR01326): imageHeight and
1393
+ * imageAlign only mean something with `contain`, and a contained image is
1394
+ * sized from its stored width and height. The published renderer's readiness
1395
+ * check applies the same rules at the same paths.
1396
+ */
1397
+ function validateCardImageFit(parsed, dataPath, errors) {
1398
+ const fit = parsed.imageFit;
1399
+ // An invalid imageFit was already reported by `validateFields`.
1400
+ if (fit !== undefined && fit !== "cover" && fit !== "contain") return;
1401
+ if (fit !== "contain") {
1402
+ for (const field of ["imageHeight", "imageAlign"]) {
1403
+ if (parsed[field] === undefined) continue;
1404
+ errors.push(contentError(
1405
+ `${dataPath}/${field}`,
1406
+ CODES.componentData,
1407
+ `${field} applies only when imageFit is "contain"; remove ${field}, or set imageFit to "contain".`,
1408
+ ));
1409
+ }
1410
+ return;
1411
+ }
1412
+ if (!Array.isArray(parsed.cards)) return;
1413
+ for (const [index, card] of parsed.cards.entries()) {
1414
+ if (!isPlainObject(card) || !isPlainObject(card.image)) continue;
1415
+ for (const dimension of ["width", "height"]) {
1416
+ const value = card.image[dimension];
1417
+ // A wrong-type dimension was already reported by `validateFields`.
1418
+ if (value !== undefined && typeof value !== "number") continue;
1419
+ if (value !== undefined && value !== 0) continue;
1420
+ errors.push(contentError(
1421
+ `${dataPath}/cards/${index}/image/${dimension}`,
1422
+ CODES.componentData,
1423
+ `With imageFit "contain" a card image needs its ${dimension} in pixels; upload the image again with media upload, or set imageFit to "cover".`,
1424
+ ));
1425
+ }
1426
+ }
1427
+ }
1428
+
1344
1429
  /**
1345
1430
  * Validates a `componentBlock`'s `componentType` and `componentData` pair.
1346
1431
  *
@@ -1392,6 +1477,7 @@ export function validateComponentBlock(componentType, componentData, basePath) {
1392
1477
  const fields = isComponentType(componentType) ? COMPONENT_SHAPES[componentType] : undefined;
1393
1478
  if (!fields) return errors;
1394
1479
  validateFields(fields, parsed, dataPath, errors);
1480
+ if (componentType === "card-grid") validateCardImageFit(parsed, dataPath, errors);
1395
1481
  if (componentType === "card-grid" && Array.isArray(parsed.cards)) {
1396
1482
  for (const [index, card] of parsed.cards.entries()) {
1397
1483
  if (
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": 13,
2
+ "version": 14,
3
3
  "section": {
4
4
  "nodeType": "section",
5
5
  "label": "Section",
@@ -548,7 +548,13 @@
548
548
  "card-grid": {
549
549
  "label": "Card Grid",
550
550
  "measure": "wide",
551
- "rootPlacement": "flow"
551
+ "rootPlacement": "flow",
552
+ "imageSpacings": {
553
+ "none": "0rem",
554
+ "small": "1em",
555
+ "medium": "2em",
556
+ "text": "var(--esp-size-small-to-normal, 1rem)"
557
+ }
552
558
  },
553
559
  "image-banner": {
554
560
  "label": "Image Banner",
@@ -95,7 +95,7 @@ import {
95
95
 
96
96
  export { getAppearanceReference, getFooterReference, getSettingsReference, getThemeReference };
97
97
 
98
- export const REFERENCE_VERSION = 48;
98
+ export const REFERENCE_VERSION = 49;
99
99
 
100
100
  // The date-limit examples `help forms` prints; tests run them through the form validator.
101
101
  export const FORM_DATE_WINDOW_FIELD = Object.freeze({