@taprootio/site-authoring 0.8.2 → 0.8.4
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 +2 -2
- package/src/constants.js +1 -1
- package/src/content/components.js +18 -2
- package/src/content/tiptap-prosemirror.ts +144 -21
- package/src/presentation-reference.js +12 -1
- package/src/reference-help.js +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@taprootio/espalier": "4.
|
|
31
|
+
"@taprootio/espalier": "4.14.1"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"test": "node --test",
|
package/src/constants.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const CLI_NAME = "@taprootio/site-authoring";
|
|
2
|
-
export const CLI_VERSION = "0.8.
|
|
2
|
+
export const CLI_VERSION = "0.8.4";
|
|
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";
|
|
@@ -252,6 +252,8 @@ const IMAGE_BANNER = Object.freeze({
|
|
|
252
252
|
// the site header; `viewport` fills exactly that space (TR00413).
|
|
253
253
|
heightMode: enumOf("ratio", "viewport"),
|
|
254
254
|
contentPosition: enumOf("bottom-start", "bottom", "bottom-end", "center", "top-start", "top", "top-end"),
|
|
255
|
+
contentGutter: enumOf("frame", "page"),
|
|
256
|
+
followingSpacing: enumOf("standard", "compact"),
|
|
255
257
|
scrim: enumOf("auto", "none", "flat", "top", "bottom", "left", "right", "radial"),
|
|
256
258
|
scrimStrength: enumOf("soft", "medium", "strong"),
|
|
257
259
|
bannerScheme: enumOf("auto", "light", "dark"),
|
|
@@ -260,7 +262,7 @@ const IMAGE_BANNER = Object.freeze({
|
|
|
260
262
|
// A built-in or application-registered Espalier texture name.
|
|
261
263
|
texture: textureName(),
|
|
262
264
|
textureScale: enumOf("fine", "medium", "coarse"),
|
|
263
|
-
imageMotion: enumOf("none", "slow-zoom", "drift-start", "drift-end"),
|
|
265
|
+
imageMotion: enumOf("none", "slow-zoom", "drift-start", "drift-end", "parallax-subtle", "parallax"),
|
|
264
266
|
});
|
|
265
267
|
|
|
266
268
|
const SPACER = Object.freeze({
|
|
@@ -371,6 +373,8 @@ const COMPONENT_DEFINITIONS = Object.freeze({
|
|
|
371
373
|
"Identify the speaker with authorName; use authorTitle when it gives useful context.",
|
|
372
374
|
"Author images need descriptive alt text unless they are purely decorative.",
|
|
373
375
|
"Keep carousel intervals long enough for the full quotation to be read.",
|
|
376
|
+
"Set carousel:true for directional previous/next arrows, dot navigation and a play/pause control. Navigation or keyboard focus pauses rotation; reduced-motion users get manual navigation without sliding or automatic rotation.",
|
|
377
|
+
"Use real attributed reviews, or clearly label fictional demo reviews. Never imply invented testimonials are genuine customer endorsements.",
|
|
374
378
|
],
|
|
375
379
|
{
|
|
376
380
|
items: [{
|
|
@@ -494,6 +498,8 @@ const COMPONENT_DEFINITIONS = Object.freeze({
|
|
|
494
498
|
compactRatio: "3/2",
|
|
495
499
|
heightMode: "ratio",
|
|
496
500
|
contentPosition: "center",
|
|
501
|
+
contentGutter: "frame",
|
|
502
|
+
followingSpacing: "standard",
|
|
497
503
|
scrim: "auto",
|
|
498
504
|
scrimStrength: "medium",
|
|
499
505
|
bannerScheme: "auto",
|
|
@@ -505,6 +511,16 @@ const COMPONENT_DEFINITIONS = Object.freeze({
|
|
|
505
511
|
[
|
|
506
512
|
"Set altText to describe meaningful banner imagery; leave it empty only when the image is decorative.",
|
|
507
513
|
"Choose a scrim and scheme that keep overlay text readable across the image.",
|
|
514
|
+
"For a full-bleed photograph with text aligned to the surrounding page well, set contentGutter to page. followingSpacing: compact reduces the top padding of an immediately following section so a bottom banner heading reads with its supporting paragraph. Use a banner-only contentPadding: none section followed by a standard text section; keep their theme contexts consistent. Defaults preserve frame-edge text and standard section spacing.",
|
|
515
|
+
"Compose around the actual photograph: identify the meaningful subject and a quiet area for copy. Do not cover faces, product details, or high-detail edges. A focal point controls cropping, not text placement; inspect wide and compact crops separately. Keep the subject's face, ears, and other identifying details inside the compact frame, including at both ends of parallax travel. Adjust focus toward the subject rather than reusing a generic center; reduce motion or choose a roomier crop if preserving the subject requires it.",
|
|
516
|
+
"Use contentPosition to place copy in that quiet area, not automatically in the center. Keep one dominant message and nearby supporting copy/actions; if the photo has no safe text area, use a split hero or a separate text section instead of forcing an overlay.",
|
|
517
|
+
"Use the rule of thirds as a composition guide: consider the subject near one third and a compact text group in quiet space on the opposite side. Golden-ratio proportions are optional alternatives, not a proven engagement formula. Neither guide overrides legibility, the actual subject, or the compact crop. Current contentPosition values are edge/center anchors, not exact third or golden-ratio coordinates; do not invent unsupported positioning properties.",
|
|
518
|
+
"Check contrast behind the actual letters at both motion endpoints and both themes: WCAG AA requires 4.5:1 for ordinary text and 3:1 for qualifying large text. A text shadow alone is not a contrast guarantee. Keep text and actions stationary; recheck at mobile widths and enlarged text.",
|
|
519
|
+
"Prefer parallax-subtle or parallax when motion is useful: motion follows the reader's scrolling and stops when they stop. Infinite ambient loops are an explicit art-direction choice, not the default recommendation.",
|
|
520
|
+
"imageMotion is optional and defaults to none. slow-zoom is a cinematic 18% zoom over 6 seconds, alternating; drift-start and drift-end travel horizontally over 8 seconds with a 14% safety crop.",
|
|
521
|
+
"These retuned ambient presets affect existing published banners when their shared runtime updates, without a per-site republish. Preview existing sites before rolling out the runtime.",
|
|
522
|
+
"parallax-subtle and parallax are scroll-linked image motion, not autoplay: scroll to see them. Published pages prefer CSS view timelines and use a platform-owned JavaScript fallback in unsupported browsers, including Firefox; authors need no custom code. Both paths respect reduced motion. They travel vertically by +/-4% and +/-8% of image height with 12% and 22% overscan. Keep focal subjects away from edges; overlay text remains stationary in its frame.",
|
|
523
|
+
"All image motion is disabled for prefers-reduced-motion. Without native view timelines and JavaScript, parallax remains static. Choose one motion per banner; never put essential text in the raster image.",
|
|
508
524
|
],
|
|
509
525
|
{
|
|
510
526
|
image: null,
|
|
@@ -516,7 +532,7 @@ const COMPONENT_DEFINITIONS = Object.freeze({
|
|
|
516
532
|
scrim: "bottom",
|
|
517
533
|
scrimStrength: "medium",
|
|
518
534
|
texture: "paper",
|
|
519
|
-
imageMotion: "
|
|
535
|
+
imageMotion: "parallax-subtle",
|
|
520
536
|
},
|
|
521
537
|
),
|
|
522
538
|
});
|
|
@@ -68,6 +68,16 @@ export interface RenderProseMirrorOptions {
|
|
|
68
68
|
/** The page shell emits this default once so rich-text formats inherit it. */
|
|
69
69
|
maxHeightVh?: TiptapImageMaxHeightVh | number | null;
|
|
70
70
|
};
|
|
71
|
+
/** Generator-only render hooks for structured nodes that need page context. */
|
|
72
|
+
nodeRenderers?: {
|
|
73
|
+
componentBlock?: (node: ProseMirrorNode) => string | undefined;
|
|
74
|
+
taprootImage?: (node: ProseMirrorNode) => string | undefined;
|
|
75
|
+
rawHtml?: (html: string, node: ProseMirrorNode) => string | undefined;
|
|
76
|
+
};
|
|
77
|
+
/** Records visible document-order content after each node is rendered. */
|
|
78
|
+
onRenderedNode?: (node: ProseMirrorNode, html: string) => void;
|
|
79
|
+
/** Claims the page's one LCP slot before rendering a section photo. */
|
|
80
|
+
claimSectionBackgroundImage?: () => boolean;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
export function createEmptyProseMirrorDocument(): ProseMirrorDocument {
|
|
@@ -97,11 +107,62 @@ export function renderRichTextBodyToHtml(
|
|
|
97
107
|
body: unknown,
|
|
98
108
|
options: RenderProseMirrorOptions = {},
|
|
99
109
|
): string {
|
|
100
|
-
if (typeof body === "string") return body;
|
|
101
110
|
if (!isProseMirrorDocument(body)) return "";
|
|
102
111
|
return renderProseMirrorDocumentToHtml(body, options);
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Extracts the reader-visible text from a ProseMirror document for metadata
|
|
116
|
+
* that only accepts plain text, such as Schema.org recipe instructions.
|
|
117
|
+
*/
|
|
118
|
+
export function extractRichTextBodyText(body: unknown): string {
|
|
119
|
+
if (!isProseMirrorDocument(body)) return "";
|
|
120
|
+
const parts: string[] = [];
|
|
121
|
+
collectText(body, parts);
|
|
122
|
+
return parts.join("").replace(/\s+/gu, " ").trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function collectText(node: ProseMirrorNode, parts: string[]): void {
|
|
126
|
+
if (node.type === "text") {
|
|
127
|
+
if (node.text) parts.push(node.text);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// rawHtml is an intentional, already-sanitized node in the JSON document
|
|
132
|
+
// contract. It is not the retired outer HTML-body transport. Preserve its
|
|
133
|
+
// reader-visible text when a consumer (for example Recipe JSON-LD) needs a
|
|
134
|
+
// plain-text representation of the document.
|
|
135
|
+
if (node.type === "rawHtml") {
|
|
136
|
+
const html = stringAttr(node.attrs?.html);
|
|
137
|
+
if (html) parts.push(plainTextFromRawHtml(html));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const child of node.content ?? []) {
|
|
142
|
+
collectText(child, parts);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (node.type === "hardBreak" || isTextBlock(node.type)) {
|
|
146
|
+
parts.push(" ");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isTextBlock(type: string): boolean {
|
|
151
|
+
return type === "paragraph"
|
|
152
|
+
|| type === "heading"
|
|
153
|
+
|| type === "listItem"
|
|
154
|
+
|| type === "blockquote"
|
|
155
|
+
|| type === "codeBlock"
|
|
156
|
+
|| type === "tableCell"
|
|
157
|
+
|| type === "tableHeader";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function plainTextFromRawHtml(html: string): string {
|
|
161
|
+
return html
|
|
162
|
+
.replace(/<(?:br\s*\/?|\/(?:p|div|li|h[1-6]|blockquote|pre|tr|table|section|article))\s*[^>]*>/giu, " ")
|
|
163
|
+
.replace(/<[^>]*>/gu, "");
|
|
164
|
+
}
|
|
165
|
+
|
|
105
166
|
type RenderPlacement = "root" | "section" | "nested";
|
|
106
167
|
|
|
107
168
|
function renderNodes(
|
|
@@ -109,7 +170,11 @@ function renderNodes(
|
|
|
109
170
|
options: RenderProseMirrorOptions,
|
|
110
171
|
placement: RenderPlacement,
|
|
111
172
|
): string {
|
|
112
|
-
return nodes.map((node) =>
|
|
173
|
+
return nodes.map((node) => {
|
|
174
|
+
const html = renderNode(node, options, placement);
|
|
175
|
+
options.onRenderedNode?.(node, html);
|
|
176
|
+
return html;
|
|
177
|
+
}).join("");
|
|
113
178
|
}
|
|
114
179
|
|
|
115
180
|
function renderFreeFormRootNodes(
|
|
@@ -118,16 +183,19 @@ function renderFreeFormRootNodes(
|
|
|
118
183
|
): string {
|
|
119
184
|
const rendered: string[] = [];
|
|
120
185
|
let flow: ProseMirrorNode[] = [];
|
|
186
|
+
let compactFollowing = false;
|
|
121
187
|
const flushFlow = () => {
|
|
122
188
|
if (flow.length === 0) return;
|
|
123
|
-
rendered.push(renderSection(flow, {}, "implicit", options, "root"));
|
|
189
|
+
rendered.push(renderSection(flow, {}, "implicit", options, "root", compactFollowing));
|
|
190
|
+
compactFollowing = false;
|
|
124
191
|
flow = [];
|
|
125
192
|
};
|
|
126
193
|
|
|
127
194
|
for (const node of nodes) {
|
|
128
195
|
if (node.type === freeFormSectionRegistry.section.nodeType || isRootBandComponent(node)) {
|
|
129
196
|
flushFlow();
|
|
130
|
-
rendered.push(renderNode(node, options, "root"));
|
|
197
|
+
rendered.push(renderNode(node, options, "root", compactFollowing));
|
|
198
|
+
compactFollowing = joinsFollowingSection(node);
|
|
131
199
|
continue;
|
|
132
200
|
}
|
|
133
201
|
flow.push(node);
|
|
@@ -136,6 +204,20 @@ function renderFreeFormRootNodes(
|
|
|
136
204
|
return rendered.join("");
|
|
137
205
|
}
|
|
138
206
|
|
|
207
|
+
function joinsFollowingSection(node: ProseMirrorNode): boolean {
|
|
208
|
+
if (node.type === "section") {
|
|
209
|
+
return node.attrs?.contentPadding === "none" && node.content?.length === 1
|
|
210
|
+
&& joinsFollowingSection(node.content[0]);
|
|
211
|
+
}
|
|
212
|
+
if (node.type !== "componentBlock" || node.attrs?.componentType !== "image-banner") return false;
|
|
213
|
+
try {
|
|
214
|
+
const data = JSON.parse(stringAttr(node.attrs?.componentData) ?? "{}");
|
|
215
|
+
return data?.followingSpacing === "compact";
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
139
221
|
function isRootBandComponent(node: ProseMirrorNode): boolean {
|
|
140
222
|
if (node.type !== "componentBlock") return false;
|
|
141
223
|
const componentType = stringAttr(node.attrs?.componentType);
|
|
@@ -150,6 +232,7 @@ function renderNode(
|
|
|
150
232
|
node: ProseMirrorNode,
|
|
151
233
|
options: RenderProseMirrorOptions,
|
|
152
234
|
placement: RenderPlacement,
|
|
235
|
+
compactFollowing = false,
|
|
153
236
|
): string {
|
|
154
237
|
switch (node.type) {
|
|
155
238
|
case "doc":
|
|
@@ -193,11 +276,11 @@ function renderNode(
|
|
|
193
276
|
case "table":
|
|
194
277
|
return placement === "root" || placement === "section" ? renderTable(node, options) : "";
|
|
195
278
|
case "taprootImage":
|
|
196
|
-
return renderTaprootImage(node, options);
|
|
279
|
+
return options.nodeRenderers?.taprootImage?.(node) ?? renderTaprootImage(node, options);
|
|
197
280
|
case "inlineFacts":
|
|
198
281
|
return renderInlineFacts(node, options, placement);
|
|
199
282
|
case "componentBlock":
|
|
200
|
-
return renderComponentBlock(node);
|
|
283
|
+
return options.nodeRenderers?.componentBlock?.(node) ?? renderComponentBlock(node);
|
|
201
284
|
case "integrationPlacement":
|
|
202
285
|
return renderIntegrationPlacement(node, options);
|
|
203
286
|
case "section":
|
|
@@ -207,9 +290,12 @@ function renderNode(
|
|
|
207
290
|
"explicit",
|
|
208
291
|
options,
|
|
209
292
|
placement === "root" ? "section" : "nested",
|
|
293
|
+
compactFollowing,
|
|
210
294
|
);
|
|
211
|
-
case "rawHtml":
|
|
212
|
-
|
|
295
|
+
case "rawHtml": {
|
|
296
|
+
const html = stringAttr(node.attrs?.html) ?? "";
|
|
297
|
+
return options.nodeRenderers?.rawHtml?.(html, node) ?? html;
|
|
298
|
+
}
|
|
213
299
|
default:
|
|
214
300
|
return renderNodes(node.content ?? [], options, "nested");
|
|
215
301
|
}
|
|
@@ -618,6 +704,7 @@ function renderSection(
|
|
|
618
704
|
kind: "explicit" | "implicit",
|
|
619
705
|
options: RenderProseMirrorOptions,
|
|
620
706
|
contentPlacement: RenderPlacement,
|
|
707
|
+
compactFollowing = false,
|
|
621
708
|
): string {
|
|
622
709
|
const definitions = freeFormSectionRegistry.section.attrs;
|
|
623
710
|
const contentPadding = attrs.contentPadding === "none" ? "none" : definitions.contentPadding.default;
|
|
@@ -650,6 +737,9 @@ function renderSection(
|
|
|
650
737
|
if (decorationStyle) sectionStyleParts.push(...decorationStyle);
|
|
651
738
|
}
|
|
652
739
|
const background = kind === "explicit" ? normalizeSectionBackground(attrs.background) : undefined;
|
|
740
|
+
const backgroundPicture = background
|
|
741
|
+
? renderSectionBackgroundPicture(background, options.claimSectionBackgroundImage?.() === true)
|
|
742
|
+
: "";
|
|
653
743
|
if (background) {
|
|
654
744
|
sectionStyleParts.push(
|
|
655
745
|
`--esp-section-background: color-mix(in oklab, var(--esp-color-background) ${background.scrimOpacityPercent}%, transparent)`,
|
|
@@ -678,6 +768,7 @@ function renderSection(
|
|
|
678
768
|
{
|
|
679
769
|
"data-taproot-section": kind,
|
|
680
770
|
"data-content-padding": contentPadding,
|
|
771
|
+
"data-following-spacing": compactFollowing && contentPadding === "standard" ? "compact" : undefined,
|
|
681
772
|
"data-surface": surface,
|
|
682
773
|
"data-entrance": entrance,
|
|
683
774
|
"data-entrance-stagger": entranceStagger,
|
|
@@ -695,7 +786,7 @@ function renderSection(
|
|
|
695
786
|
style:
|
|
696
787
|
`--taproot-section-focus: ${background.focus}; --taproot-section-portrait-focus: ${background.portraitFocus}`,
|
|
697
788
|
},
|
|
698
|
-
`${
|
|
789
|
+
`${backgroundPicture}${section}`,
|
|
699
790
|
);
|
|
700
791
|
}
|
|
701
792
|
|
|
@@ -845,7 +936,7 @@ function normalizeSectionFocus(
|
|
|
845
936
|
return `${x * 100}% ${y * 100}%`;
|
|
846
937
|
}
|
|
847
938
|
|
|
848
|
-
function renderSectionBackgroundPicture(background: NormalizedSectionBackground): string {
|
|
939
|
+
function renderSectionBackgroundPicture(background: NormalizedSectionBackground, hero: boolean): string {
|
|
849
940
|
const portraitSource = background.portraitImage
|
|
850
941
|
? renderVoidElement("source", {
|
|
851
942
|
media: "(max-width: 48rem)",
|
|
@@ -862,7 +953,8 @@ function renderSectionBackgroundPicture(background: NormalizedSectionBackground)
|
|
|
862
953
|
height: background.image.height,
|
|
863
954
|
alt: "",
|
|
864
955
|
decoding: "async",
|
|
865
|
-
loading: "lazy",
|
|
956
|
+
loading: hero ? undefined : "lazy",
|
|
957
|
+
fetchpriority: hero ? "high" : undefined,
|
|
866
958
|
});
|
|
867
959
|
return renderElement(
|
|
868
960
|
"picture",
|
|
@@ -1032,19 +1124,50 @@ function renderComponentBlock(node: ProseMirrorNode): string {
|
|
|
1032
1124
|
);
|
|
1033
1125
|
}
|
|
1034
1126
|
|
|
1035
|
-
|
|
1127
|
+
export interface ResolvedTiptapImage {
|
|
1128
|
+
src: string;
|
|
1129
|
+
urls: Array<{ minWidth: number; url: string; type?: string }>;
|
|
1130
|
+
width: number;
|
|
1131
|
+
height: number;
|
|
1132
|
+
alt: string;
|
|
1133
|
+
imageId: string;
|
|
1134
|
+
preserveStoredSrc: boolean;
|
|
1135
|
+
extraAttributes: Record<string, string | number | undefined>;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
export function resolveTiptapImage(
|
|
1139
|
+
node: ProseMirrorNode,
|
|
1140
|
+
imageDefaults: RenderProseMirrorOptions["imageDefaults"],
|
|
1141
|
+
): ResolvedTiptapImage {
|
|
1036
1142
|
const attrs = node.attrs ?? {};
|
|
1037
|
-
const
|
|
1143
|
+
const crop = jsonAttr(attrs.crop);
|
|
1144
|
+
const resolved = resolveImagePresentation(attrs, imageDefaults);
|
|
1145
|
+
return {
|
|
1146
|
+
src: stringAttr(attrs.src) ?? "",
|
|
1147
|
+
urls: validResponsiveImageUrls(attrs.urls),
|
|
1148
|
+
width: numberAttr(attrs.width) ?? 0,
|
|
1149
|
+
height: numberAttr(attrs.height) ?? 0,
|
|
1150
|
+
alt: stringAttr(attrs.alt) ?? "",
|
|
1151
|
+
imageId: stringAttr(attrs.imageId) ?? "",
|
|
1152
|
+
preserveStoredSrc: Boolean(crop && crop !== "null"),
|
|
1153
|
+
extraAttributes: {
|
|
1154
|
+
...(crop ? { "data-crop": crop } : {}),
|
|
1155
|
+
...resolved,
|
|
1156
|
+
},
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function renderTaprootImage(node: ProseMirrorNode, options: RenderProseMirrorOptions): string {
|
|
1161
|
+
const image = resolveTiptapImage(node, options.imageDefaults);
|
|
1038
1162
|
const imageAttrs: Record<string, string | number | boolean | null | undefined> = {
|
|
1039
|
-
"low-res":
|
|
1040
|
-
"original-width":
|
|
1041
|
-
"original-height":
|
|
1042
|
-
"data-image-id":
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
...resolved,
|
|
1163
|
+
"low-res": image.src,
|
|
1164
|
+
"original-width": image.width,
|
|
1165
|
+
"original-height": image.height,
|
|
1166
|
+
"data-image-id": image.imageId,
|
|
1167
|
+
caption: image.alt,
|
|
1168
|
+
...image.extraAttributes,
|
|
1046
1169
|
};
|
|
1047
|
-
const optionsHtml =
|
|
1170
|
+
const optionsHtml = image.urls
|
|
1048
1171
|
.map((url) =>
|
|
1049
1172
|
renderVoidElement("esp-image-option", {
|
|
1050
1173
|
width: url.minWidth,
|
|
@@ -333,7 +333,17 @@ export function getAppearanceReference() {
|
|
|
333
333
|
imageReferences:
|
|
334
334
|
"Use site-owned image ids retained from pull or returned by media upload. URLs are server projections, never authored inputs.",
|
|
335
335
|
logoContract:
|
|
336
|
-
"lightLogoId and darkLogoId are the current scheme-specific logo fields; there is no separate compact-logo setting."
|
|
336
|
+
"lightLogoId and darkLogoId are the current scheme-specific logo fields; there is no separate compact-logo setting. "
|
|
337
|
+
+ "Prefer transparent PNG or WebP artwork with genuine alpha so the header background shows through, including "
|
|
338
|
+
+ "inside letter counters. Avoid baked-in background rectangles unless the brand intentionally uses a badge. "
|
|
339
|
+
+ "Keep padding tight and verify the delivered image preserves transparency. Logo style and coloration are "
|
|
340
|
+
+ "project-specific: choose monochrome, an accent, or multiple colors to suit the brand, not a universal recipe. "
|
|
341
|
+
+ "Keep the lettering readable and test the visible artwork against each header scheme.",
|
|
342
|
+
faviconContract:
|
|
343
|
+
"Include a favicon when designing a site: upload a square, simplified brand mark and set brand.faviconId "
|
|
344
|
+
+ "in settings/brand.json, then run theme push; faviconUrl is a read-only projection. Do not shrink a full wordmark into a favicon. "
|
|
345
|
+
+ "Inspect at 16 and 32 pixels in light and dark browser tabs; a deliberate contrasting badge can help a favicon "
|
|
346
|
+
+ "remain recognizable independently of the header theme. Verify the published icon loads, not just the setting.",
|
|
337
347
|
logoContrastContract:
|
|
338
348
|
"Evaluate lightLogoId against the rendered light-scheme header and darkLogoId against the rendered dark-scheme "
|
|
339
349
|
+ "header at desktop and compact/mobile widths. Transparent pixels provide no contrast: inspect the visible mark, "
|
|
@@ -602,6 +612,7 @@ export function formatPresentationReference(reference) {
|
|
|
602
612
|
+ reference.readOnlyProjections.map((field) => " " + field.file + " :: " + field.path).join("\n")
|
|
603
613
|
+ "\n\nImages: " + reference.imageReferences + "\nLogos: " + reference.logoContract
|
|
604
614
|
+ "\nLogo contrast: " + reference.logoContrastContract
|
|
615
|
+
+ "\nFavicon: " + reference.faviconContract
|
|
605
616
|
+ "\nHeader width: " + reference.headerWidthContract
|
|
606
617
|
+ "\nMobile menu and menu font: " + reference.menuContract
|
|
607
618
|
+ "\nFooter: " + reference.footerBoundary
|
package/src/reference-help.js
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
|
|
52
52
|
export { getAppearanceReference, getFooterReference, getThemeReference };
|
|
53
53
|
|
|
54
|
-
export const REFERENCE_VERSION =
|
|
54
|
+
export const REFERENCE_VERSION = 24;
|
|
55
55
|
export const PAGE_TYPES = Object.freeze(["free-form"]);
|
|
56
56
|
|
|
57
57
|
function deepFreeze(value) {
|
|
@@ -134,7 +134,7 @@ const DESIGN_BLUEPRINTS = deepFreeze([
|
|
|
134
134
|
purpose: "Make a single product story legible before offering supporting choices.",
|
|
135
135
|
section: { entrance: "scale", entranceStagger: "none" },
|
|
136
136
|
components: [
|
|
137
|
-
{ type: "image-banner", properties: { ratio: "3/1", heightMode: "viewport", contentPosition: "bottom-start", scrim: "bottom", imageMotion: "
|
|
137
|
+
{ type: "image-banner", properties: { ratio: "3/1", heightMode: "viewport", contentPosition: "bottom-start", scrim: "bottom", imageMotion: "parallax" } },
|
|
138
138
|
{ type: "cta", properties: { variant: "primary", imagePosition: "right", borderWidth: 0 } },
|
|
139
139
|
],
|
|
140
140
|
},
|
|
@@ -148,7 +148,7 @@ const DESIGN_BLUEPRINTS = deepFreeze([
|
|
|
148
148
|
],
|
|
149
149
|
},
|
|
150
150
|
],
|
|
151
|
-
motionCeiling: "
|
|
151
|
+
motionCeiling: "Prefer one optional image parallax per view (imageMotion: parallax-subtle or parallax). Published pages use CSS view timelines where supported and a platform-owned scroll-linked JavaScript fallback otherwise; no custom code is needed. Movement stops when scrolling stops, pauses offscreen, and is disabled for reduced motion. Without JavaScript or native timelines the image stays static. Cinematic zoom is an explicit ambient-loop alternative. Never conceal names, price context, or actions behind motion.",
|
|
152
152
|
mediaBrief: "Commission or license product, material, and in-use photography; document crop-safe banner areas and supply alt text for informative images.",
|
|
153
153
|
integrationBoundaries: "Use outbound links for retail, commerce, search, or booking providers. This blueprint includes none of those systems and no custom code.",
|
|
154
154
|
accessibilityCautions: ["Preserve text contrast over imagery with a tested scrim and do not make text depend on hover.", "Respect reduced-motion preferences and give every campaign action a visible text label."],
|
|
@@ -511,6 +511,7 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
511
511
|
details: Object.freeze([
|
|
512
512
|
"Paths are relative to the configured workspace root, not the shell's current directory.",
|
|
513
513
|
"PNG, JPEG, GIF, and WebP are accepted; retina names such as logo@2x.png and logo@3x.png are supported.",
|
|
514
|
+
"For header logos prefer genuine transparent PNG/WebP, check contrast in both themes, and include a simplified square favicon. See help appearance for branding guidance.",
|
|
514
515
|
"Each result item includes media: { imageId, src, urls, width, height, alt }.",
|
|
515
516
|
"The same src/urls delivery fields are saved in .taproot-site-media.json for page and component authoring.",
|
|
516
517
|
]),
|