@taprootio/site-authoring 0.1.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/LICENSE +15 -0
- package/README.md +239 -0
- package/bin/taproot-site.js +5 -0
- package/package.json +40 -0
- package/src/api.js +1507 -0
- package/src/appearance-contract.js +311 -0
- package/src/atomic-file.js +136 -0
- package/src/bounded-file.js +59 -0
- package/src/capabilities.js +29 -0
- package/src/cli.js +872 -0
- package/src/config.js +412 -0
- package/src/constants.js +173 -0
- package/src/content/components.js +870 -0
- package/src/content/free-form-section-registry.json +524 -0
- package/src/content/free-form-sections.js +582 -0
- package/src/content/index.js +29 -0
- package/src/content/inline-facts.js +97 -0
- package/src/content/markdown.js +1625 -0
- package/src/content/tiptap-prosemirror.ts +1266 -0
- package/src/content/validate-document.js +1079 -0
- package/src/content/vocabulary.js +397 -0
- package/src/credentials.js +544 -0
- package/src/errors.js +133 -0
- package/src/footer-contract.js +687 -0
- package/src/footer-draft-hash.js +203 -0
- package/src/footer-workspace.js +136 -0
- package/src/image-metadata.js +169 -0
- package/src/index.js +16 -0
- package/src/json-guard.js +111 -0
- package/src/output.js +157 -0
- package/src/presentation-reference.js +600 -0
- package/src/reference-help.js +927 -0
- package/src/session.js +316 -0
- package/src/settings-catalog.js +187 -0
- package/src/settings.js +157 -0
- package/src/theme-validation.js +344 -0
- package/src/transport.js +777 -0
- package/src/verbs/approve.js +178 -0
- package/src/verbs/deploy.js +327 -0
- package/src/verbs/env.js +89 -0
- package/src/verbs/footer-push.js +115 -0
- package/src/verbs/index.js +125 -0
- package/src/verbs/login.js +529 -0
- package/src/verbs/logout.js +39 -0
- package/src/verbs/media-upload.js +319 -0
- package/src/verbs/nav-push.js +333 -0
- package/src/verbs/pages-push.js +990 -0
- package/src/verbs/preview-page.js +107 -0
- package/src/verbs/preview-revoke.js +56 -0
- package/src/verbs/pull.js +727 -0
- package/src/verbs/sites.js +57 -0
- package/src/verbs/status.js +94 -0
- package/src/verbs/theme-push.js +197 -0
- package/src/verbs/use.js +129 -0
- package/src/verbs/validate.js +477 -0
- package/src/verbs/whoami.js +108 -0
- package/src/workspace.js +1085 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DATA_SERIES_KEYS,
|
|
5
|
+
DEFAULT_DATA_RAMP_STEPS,
|
|
6
|
+
MAX_DATA_RAMP_STEPS,
|
|
7
|
+
MIN_DATA_RAMP_STEPS,
|
|
8
|
+
} from "@taprootio/espalier/shared/data-colors";
|
|
9
|
+
import {
|
|
10
|
+
COLOR_SOURCES,
|
|
11
|
+
DEFAULT_DARK_THEME,
|
|
12
|
+
DEFAULT_LIGHT_THEME,
|
|
13
|
+
LIGHTNESS_KEYS,
|
|
14
|
+
NESTED_THEME_KEYS,
|
|
15
|
+
ROLE_NAMES,
|
|
16
|
+
SEMANTIC_COLOR_NAMES,
|
|
17
|
+
STATUS_COLOR_SOURCES,
|
|
18
|
+
VARIANT_COLOR_SOURCES,
|
|
19
|
+
} from "@taprootio/espalier/shared/theme";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
APPEARANCE_FILES,
|
|
23
|
+
APPEARANCE_FOOTER_COLOR_FIELDS,
|
|
24
|
+
APPEARANCE_READ_ONLY_FIELDS,
|
|
25
|
+
APPEARANCE_SCALAR_FIELDS,
|
|
26
|
+
} from "./appearance-contract.js";
|
|
27
|
+
import { CLI_BINARY_NAME } from "./constants.js";
|
|
28
|
+
import {
|
|
29
|
+
FOOTER_BACKGROUND_PRESENTATIONS,
|
|
30
|
+
FOOTER_EXAMPLE,
|
|
31
|
+
FOOTER_FADE_MODES,
|
|
32
|
+
FOOTER_FIELD_INVENTORY,
|
|
33
|
+
FOOTER_LIMITS,
|
|
34
|
+
FOOTER_READ_ONLY_FIELDS,
|
|
35
|
+
validateFooterDocument,
|
|
36
|
+
} from "./footer-contract.js";
|
|
37
|
+
import {
|
|
38
|
+
APPEARANCE_COLOR_CONSTRAINT,
|
|
39
|
+
MAXIMUM_THEME_OPEN_MAP_ENTRIES,
|
|
40
|
+
MAXIMUM_THEME_OPEN_MAP_KEY_SCALARS,
|
|
41
|
+
REQUIRED_THEME_PROPERTIES,
|
|
42
|
+
validateAndEncodeThemePair,
|
|
43
|
+
} from "./theme-validation.js";
|
|
44
|
+
import {
|
|
45
|
+
SETTINGS_TYPE_SITE_PUBLISHING_PREFERENCES,
|
|
46
|
+
SETTINGS_TYPE_TAPROOT_STYLES,
|
|
47
|
+
} from "./settings-catalog.js";
|
|
48
|
+
|
|
49
|
+
let espalierVersion;
|
|
50
|
+
|
|
51
|
+
function getEspalierVersion() {
|
|
52
|
+
espalierVersion ??= JSON.parse(
|
|
53
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
54
|
+
).dependencies["@taprootio/espalier"];
|
|
55
|
+
return espalierVersion;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const OPTIONAL_THEME_FIELDS = Object.freeze([
|
|
59
|
+
"explicitMappingTokens",
|
|
60
|
+
"boxBackgroundImage",
|
|
61
|
+
"boxBackgroundImageOpacity",
|
|
62
|
+
"vellumOpacity",
|
|
63
|
+
"vellumBackgroundImage",
|
|
64
|
+
"vellumBackgroundImageOpacity",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
export const THEME_FIELD_NAMES = Object.freeze([
|
|
68
|
+
...REQUIRED_THEME_PROPERTIES,
|
|
69
|
+
...OPTIONAL_THEME_FIELDS,
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const THEME_FIELD_TYPES = Object.freeze({
|
|
73
|
+
explicitMappingTokens: "semantic-token[]",
|
|
74
|
+
stylesheets: "string[]",
|
|
75
|
+
pageBackgroundImage: "string",
|
|
76
|
+
pageBackgroundImageOpacity: "number",
|
|
77
|
+
boxBackgroundImage: "string",
|
|
78
|
+
boxBackgroundImageOpacity: "number",
|
|
79
|
+
vellumOpacity: "number",
|
|
80
|
+
vellumBackgroundImage: "string",
|
|
81
|
+
vellumBackgroundImageOpacity: "number",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
function exampleTheme(defaults, scheme) {
|
|
85
|
+
return {
|
|
86
|
+
...structuredClone(defaults),
|
|
87
|
+
seedColor: scheme === "light" ? "oklch(0.58 0.14 305)" : "oklch(0.72 0.11 305)",
|
|
88
|
+
pageBackgroundImage: "",
|
|
89
|
+
pageBackgroundImageOpacity: 0,
|
|
90
|
+
anchors: {
|
|
91
|
+
plum: { color: "#6f2a78", text: "#4b1553", hover: "#8a3d92" },
|
|
92
|
+
paper: "#fffaf4",
|
|
93
|
+
midnight: "#21172b",
|
|
94
|
+
},
|
|
95
|
+
roles: {
|
|
96
|
+
canvas: "primary",
|
|
97
|
+
ink: { color: "primary", heading: "anchor:midnight" },
|
|
98
|
+
accent: { color: "anchor:plum", text: "anchor:plum.text", hover: "anchor:plum.hover" },
|
|
99
|
+
action: { color: "anchor:plum", ink: "anchor:paper" },
|
|
100
|
+
structure: "primary",
|
|
101
|
+
},
|
|
102
|
+
contexts: {
|
|
103
|
+
inverted: {
|
|
104
|
+
canvas: "anchor:midnight",
|
|
105
|
+
ink: { color: "anchor:paper", heading: "anchor:paper" },
|
|
106
|
+
accent: { color: "anchor:plum", text: "anchor:paper", hover: "anchor:plum.hover" },
|
|
107
|
+
action: { color: "anchor:paper", ink: "anchor:midnight" },
|
|
108
|
+
structure: "anchor:paper",
|
|
109
|
+
lightness: scheme === "light"
|
|
110
|
+
? { surface: 0.16, raised1: 0.2, raised2: 0.25, raised3: 0.3, raised4: 0.36, text: 0.96, ink: 0.99 }
|
|
111
|
+
: { surface: 0.12, raised1: 0.16, raised2: 0.21, raised3: 0.27, raised4: 0.34, text: 0.96, ink: 0.99 },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
intents: { info: "#1467a8", danger: "#9c2433" },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const THEME_EXAMPLE = Object.freeze({
|
|
119
|
+
lightTheme: Object.freeze(exampleTheme(DEFAULT_LIGHT_THEME, "light")),
|
|
120
|
+
darkTheme: Object.freeze(exampleTheme(DEFAULT_DARK_THEME, "dark")),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
function themeField(name, description, constraints = {}) {
|
|
124
|
+
const defaultValue = Object.hasOwn(DEFAULT_LIGHT_THEME, name)
|
|
125
|
+
? structuredClone(DEFAULT_LIGHT_THEME[name])
|
|
126
|
+
: undefined;
|
|
127
|
+
return Object.freeze({
|
|
128
|
+
name,
|
|
129
|
+
type: THEME_FIELD_TYPES[name]
|
|
130
|
+
?? (Array.isArray(defaultValue) ? "array" : typeof defaultValue === "object" ? "object" : typeof defaultValue),
|
|
131
|
+
description,
|
|
132
|
+
...(defaultValue === undefined ? { inheritedWhenOmitted: true } : { default: defaultValue }),
|
|
133
|
+
...constraints,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const THEME_GROUPS = Object.freeze([
|
|
138
|
+
Object.freeze({
|
|
139
|
+
name: "brand-color-model",
|
|
140
|
+
summary: "Declare brand anchors, assign designer-facing roles, and retune fixed-meaning status families.",
|
|
141
|
+
fields: Object.freeze([
|
|
142
|
+
themeField("seedColor", "Base CSS color for geometric sources; accepts hex, rgb(), hsl(), and oklch()."),
|
|
143
|
+
themeField("anchors", "Open map of named absolute colors and optional named slots.", {
|
|
144
|
+
additionalProperties: true,
|
|
145
|
+
maximumEntries: MAXIMUM_THEME_OPEN_MAP_ENTRIES,
|
|
146
|
+
namePattern: "lowercase slug beginning with a letter",
|
|
147
|
+
maximumNameScalars: MAXIMUM_THEME_OPEN_MAP_KEY_SCALARS,
|
|
148
|
+
}),
|
|
149
|
+
themeField("roles", "Closed map of designer-facing functional color roles.", { keys: ROLE_NAMES }),
|
|
150
|
+
themeField("contexts", "Open map of named zone-level role, lightness, tone, and mapping rebindings.", {
|
|
151
|
+
additionalProperties: true,
|
|
152
|
+
maximumEntries: MAXIMUM_THEME_OPEN_MAP_ENTRIES,
|
|
153
|
+
}),
|
|
154
|
+
themeField("angles", "Closed geometric hue-angle map; finite values outside 0–360 produce warnings.", {
|
|
155
|
+
keys: ["analogous", "complementary", "splitComplementary", "triadic"],
|
|
156
|
+
}),
|
|
157
|
+
themeField("semanticHues", "Closed hue-angle map for danger, success, warning, and info.", {
|
|
158
|
+
keys: STATUS_COLOR_SOURCES,
|
|
159
|
+
}),
|
|
160
|
+
themeField("intents", "Closed optional color overrides for the four fixed-meaning status families.", {
|
|
161
|
+
keys: STATUS_COLOR_SOURCES,
|
|
162
|
+
}),
|
|
163
|
+
]),
|
|
164
|
+
}),
|
|
165
|
+
Object.freeze({
|
|
166
|
+
name: "semantic-engine",
|
|
167
|
+
summary: "Use roles first; pin individual semantic mappings only when roles cannot express the intent.",
|
|
168
|
+
fields: Object.freeze([
|
|
169
|
+
themeField("lightness", "Closed 0–1 lightness ramp.", { keys: LIGHTNESS_KEYS, minimum: 0, maximum: 1 }),
|
|
170
|
+
themeField("tones", "Open map of named 0–1 lightness values for explicit mappings.", {
|
|
171
|
+
additionalProperties: true,
|
|
172
|
+
maximumEntries: MAXIMUM_THEME_OPEN_MAP_ENTRIES,
|
|
173
|
+
maximumNameScalars: MAXIMUM_THEME_OPEN_MAP_KEY_SCALARS,
|
|
174
|
+
minimum: 0,
|
|
175
|
+
maximum: 1,
|
|
176
|
+
}),
|
|
177
|
+
themeField("chroma", "Closed semantic-token map of { min, max }; min ≥ 0, max ≤ 0.4, min ≤ max.", {
|
|
178
|
+
keys: SEMANTIC_COLOR_NAMES,
|
|
179
|
+
minimum: 0,
|
|
180
|
+
maximum: 0.4,
|
|
181
|
+
}),
|
|
182
|
+
themeField("semanticMappings", "Closed semantic-token map of { source, lightness }.", {
|
|
183
|
+
keys: SEMANTIC_COLOR_NAMES,
|
|
184
|
+
sourceValues: COLOR_SOURCES,
|
|
185
|
+
lightnessValues: LIGHTNESS_KEYS,
|
|
186
|
+
}),
|
|
187
|
+
themeField("explicitMappingTokens", "Espalier-maintained list of mappings intentionally pinned.", {
|
|
188
|
+
values: SEMANTIC_COLOR_NAMES,
|
|
189
|
+
}),
|
|
190
|
+
themeField("variantChroma", "Closed optional 0–0.4 base-chroma overrides for non-primary sources.", {
|
|
191
|
+
keys: VARIANT_COLOR_SOURCES,
|
|
192
|
+
minimum: 0,
|
|
193
|
+
maximum: 0.4,
|
|
194
|
+
}),
|
|
195
|
+
]),
|
|
196
|
+
}),
|
|
197
|
+
Object.freeze({
|
|
198
|
+
name: "typography-and-scale",
|
|
199
|
+
summary: "Choose families and weights, then tune type, spacing, radius, and fluid viewport scales together.",
|
|
200
|
+
fields: Object.freeze([
|
|
201
|
+
...["fontBody", "fontHeadings", "fontBrand", "fontMonospace"].map((name) =>
|
|
202
|
+
themeField(name, "CSS font-family; empty inherits the consuming surface's fallback.")
|
|
203
|
+
),
|
|
204
|
+
...["fontWeightBody", "fontWeightHeadings", "fontWeightBrand", "fontWeightMonospace"].map((name) =>
|
|
205
|
+
themeField(name, "CSS font-weight: 1–1000, normal/bold/lighter/bolder, or a CSS-wide keyword.")
|
|
206
|
+
),
|
|
207
|
+
themeField("rootFontSize", "Root font size in px.", { minimum: 1, warningMaximum: 100 }),
|
|
208
|
+
themeField("typeRatio", "Modular type-scale ratio.", { exclusiveMinimum: 1, warningMaximum: 1.3 }),
|
|
209
|
+
themeField("spaceRatio", "Modular spacing-scale ratio.", { exclusiveMinimum: 1, warningMaximum: 2 }),
|
|
210
|
+
themeField("borderRadius", "Global border radius in rem.", { minimum: 0, warningMaximum: 10 }),
|
|
211
|
+
themeField("viewportMin", "Minimum fluid-interpolation viewport in px.", { minimum: 100 }),
|
|
212
|
+
themeField("viewportMax", "Maximum viewport in px; must exceed viewportMin.", { minimum: 200 }),
|
|
213
|
+
]),
|
|
214
|
+
}),
|
|
215
|
+
Object.freeze({
|
|
216
|
+
name: "data-color",
|
|
217
|
+
summary: "Keep categorical series distinguishable and use named ramps for ordered or diverging data.",
|
|
218
|
+
fields: Object.freeze([
|
|
219
|
+
themeField("dataPalette", "Closed eight-series map of CSS colors or anchor references.", {
|
|
220
|
+
keys: DATA_SERIES_KEYS,
|
|
221
|
+
}),
|
|
222
|
+
themeField("dataRamps", "Open map of sequential or diverging ramp declarations.", {
|
|
223
|
+
additionalProperties: true,
|
|
224
|
+
maximumEntries: MAXIMUM_THEME_OPEN_MAP_ENTRIES,
|
|
225
|
+
stepMinimum: MIN_DATA_RAMP_STEPS,
|
|
226
|
+
stepMaximum: MAX_DATA_RAMP_STEPS,
|
|
227
|
+
stepDefault: DEFAULT_DATA_RAMP_STEPS,
|
|
228
|
+
}),
|
|
229
|
+
]),
|
|
230
|
+
}),
|
|
231
|
+
Object.freeze({
|
|
232
|
+
name: "surface-assets",
|
|
233
|
+
summary: "Preserve pulled asset fields; use appearance image ids for site-owned canvas art.",
|
|
234
|
+
fields: Object.freeze([
|
|
235
|
+
themeField("stylesheets", "Agent-authored site themes must keep this exact array empty.", { exact: [] }),
|
|
236
|
+
themeField(
|
|
237
|
+
"pageBackgroundImage",
|
|
238
|
+
"CSS background-image; the complete Taproot workspace includes an empty value.",
|
|
239
|
+
),
|
|
240
|
+
themeField("pageBackgroundImageOpacity", "Page background opacity.", { minimum: 0, maximum: 1 }),
|
|
241
|
+
themeField("boxBackgroundImage", "Optional inherited box-surface background image."),
|
|
242
|
+
themeField("boxBackgroundImageOpacity", "Optional box background opacity.", { minimum: 0, maximum: 1 }),
|
|
243
|
+
themeField("vellumOpacity", "Optional modal-vellum opacity.", { minimum: 0, maximum: 1 }),
|
|
244
|
+
themeField("vellumBackgroundImage", "Optional inherited vellum background image."),
|
|
245
|
+
themeField("vellumBackgroundImageOpacity", "Optional vellum image opacity.", { minimum: 0, maximum: 1 }),
|
|
246
|
+
]),
|
|
247
|
+
}),
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
export function getThemeReference() {
|
|
251
|
+
const version = getEspalierVersion();
|
|
252
|
+
return {
|
|
253
|
+
title: "Espalier " + version + " complete site-theme contract",
|
|
254
|
+
referenceKind: "theme",
|
|
255
|
+
espalierVersion: version,
|
|
256
|
+
workspaceFile: APPEARANCE_FILES[SETTINGS_TYPE_TAPROOT_STYLES],
|
|
257
|
+
themePaths: ["settings.lightTheme", "settings.darkTheme"],
|
|
258
|
+
completeBaselineRequired: true,
|
|
259
|
+
nestedMergeGroups: NESTED_THEME_KEYS,
|
|
260
|
+
groups: THEME_GROUPS,
|
|
261
|
+
vocabulary: {
|
|
262
|
+
roles: ROLE_NAMES,
|
|
263
|
+
colorSources: COLOR_SOURCES,
|
|
264
|
+
statusFamilies: STATUS_COLOR_SOURCES,
|
|
265
|
+
lightnessStops: LIGHTNESS_KEYS,
|
|
266
|
+
semanticColors: SEMANTIC_COLOR_NAMES,
|
|
267
|
+
dataSeries: DATA_SERIES_KEYS,
|
|
268
|
+
},
|
|
269
|
+
workflow: [
|
|
270
|
+
"Run taproot-site pull and edit the complete light/dark pair it writes; never build a sparse theme from memory.",
|
|
271
|
+
"Define brand anchors first, then assign canvas, ink, accent, action, and structure roles in both schemes.",
|
|
272
|
+
"Add named contexts for whole zones, including inverted zones, and rebind lightness when the zone changes brightness.",
|
|
273
|
+
"Tune typography, type/space ratios, radii, and viewport interpolation as one layout system.",
|
|
274
|
+
"Retune danger/success/warning/info through intents without changing their meanings; design data palettes separately.",
|
|
275
|
+
"Use semanticMappings only for meanings roles and contexts cannot express; scattered pins shadow the coherent model.",
|
|
276
|
+
"Run theme push, inspect warnings, then verify text, actions, focus states, and both schemes in authoring previews.",
|
|
277
|
+
],
|
|
278
|
+
example: THEME_EXAMPLE,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function appearanceColorReference(definition) {
|
|
283
|
+
return definition.type === "appearance-color"
|
|
284
|
+
? {
|
|
285
|
+
allowedTokens: [...definition.allowedTokens],
|
|
286
|
+
customForms: APPEARANCE_COLOR_CONSTRAINT.customForms,
|
|
287
|
+
maximumLength: APPEARANCE_COLOR_CONSTRAINT.maximumLength,
|
|
288
|
+
}
|
|
289
|
+
: {};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function getAppearanceReference() {
|
|
293
|
+
const scalarFields = APPEARANCE_SCALAR_FIELDS.map((field) => ({
|
|
294
|
+
file: APPEARANCE_FILES[field.settingsType],
|
|
295
|
+
path: "settings." + field.name,
|
|
296
|
+
type: field.type,
|
|
297
|
+
...(field.values ? { values: field.values } : {}),
|
|
298
|
+
...(field.minimum !== undefined ? { minimum: field.minimum, maximum: field.maximum } : {}),
|
|
299
|
+
...(field.maximumLength !== undefined ? { maximumLength: field.maximumLength } : {}),
|
|
300
|
+
...appearanceColorReference(field),
|
|
301
|
+
default: field.default,
|
|
302
|
+
}));
|
|
303
|
+
return {
|
|
304
|
+
title: "Site appearance workspace and theme-push contract",
|
|
305
|
+
referenceKind: "appearance",
|
|
306
|
+
usage: CLI_BINARY_NAME + " theme push",
|
|
307
|
+
fields: [
|
|
308
|
+
{
|
|
309
|
+
file: APPEARANCE_FILES[SETTINGS_TYPE_TAPROOT_STYLES],
|
|
310
|
+
path: "settings.lightTheme",
|
|
311
|
+
type: "complete-espalier-theme",
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
file: APPEARANCE_FILES[SETTINGS_TYPE_TAPROOT_STYLES],
|
|
315
|
+
path: "settings.darkTheme",
|
|
316
|
+
type: "complete-espalier-theme",
|
|
317
|
+
},
|
|
318
|
+
...scalarFields,
|
|
319
|
+
...APPEARANCE_FOOTER_COLOR_FIELDS.map((field) => ({
|
|
320
|
+
file: field.file,
|
|
321
|
+
path: field.path,
|
|
322
|
+
type: field.type,
|
|
323
|
+
...appearanceColorReference(field),
|
|
324
|
+
default: field.default,
|
|
325
|
+
})),
|
|
326
|
+
],
|
|
327
|
+
readOnlyProjections: APPEARANCE_READ_ONLY_FIELDS,
|
|
328
|
+
imageReferences:
|
|
329
|
+
"Use site-owned image ids retained from pull or returned by media upload. URLs are server projections, never authored inputs.",
|
|
330
|
+
logoContract:
|
|
331
|
+
"lightLogoId and darkLogoId are the current scheme-specific logo fields; there is no separate compact-logo setting.",
|
|
332
|
+
mutationOrder: [
|
|
333
|
+
"fresh footer read plus concurrency-protected ten-color overlay",
|
|
334
|
+
"default scheme, brand fonts and assets, favicon, and header scalars",
|
|
335
|
+
"complete light theme, then complete dark theme",
|
|
336
|
+
],
|
|
337
|
+
nonAtomic: true,
|
|
338
|
+
recovery:
|
|
339
|
+
"A failure reports completedWrites. Pull, compare the remote result with the intended workspace, reconcile, and retry.",
|
|
340
|
+
footerBoundary:
|
|
341
|
+
"theme push changes only five colors per footer scheme. Use footer push for prose, links, layout, and imagery.",
|
|
342
|
+
footerContentGuard:
|
|
343
|
+
"theme push refuses while settings.footerSettings carries unpushed content edits: "
|
|
344
|
+
+ "theme.unpushed_footer_content names " + CLI_BINARY_NAME + " footer push to save them "
|
|
345
|
+
+ "(" + CLI_BINARY_NAME + " pull discards them). A workspace pulled before this baseline "
|
|
346
|
+
+ "fails theme.pull_required; footer push records it non-destructively, while pull "
|
|
347
|
+
+ "discards local edits and re-records it.",
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const FOOTER_FIELD_DETAILS = Object.freeze({
|
|
352
|
+
"FooterSettings.enabled": { type: "boolean", default: false },
|
|
353
|
+
"FooterSettings.showBrand": { type: "boolean", default: true },
|
|
354
|
+
"FooterSettings.showBrandText": { type: "boolean", default: true },
|
|
355
|
+
"FooterSettings.linkColumns": { type: "FooterLinkColumn[]", default: [], maximumItems: FOOTER_LIMITS.linkColumns },
|
|
356
|
+
"FooterSettings.asideHeadingContent": {
|
|
357
|
+
type: "FooterRichText",
|
|
358
|
+
default: { paragraphs: [] },
|
|
359
|
+
maximumCharacters: FOOTER_LIMITS.asideHeadingLength,
|
|
360
|
+
maximumParagraphs: 1,
|
|
361
|
+
},
|
|
362
|
+
"FooterSettings.asideBodyContent": {
|
|
363
|
+
type: "FooterRichText",
|
|
364
|
+
default: { paragraphs: [] },
|
|
365
|
+
maximumCharacters: FOOTER_LIMITS.asideBodyLength,
|
|
366
|
+
maximumParagraphs: FOOTER_LIMITS.richTextParagraphs,
|
|
367
|
+
},
|
|
368
|
+
"FooterSettings.asideCta": { type: "FooterLink|null", default: null },
|
|
369
|
+
"FooterSettings.bottomContent": {
|
|
370
|
+
type: "FooterRichText",
|
|
371
|
+
default: { paragraphs: [] },
|
|
372
|
+
maximumCharacters: FOOTER_LIMITS.bottomTextLength,
|
|
373
|
+
maximumParagraphs: FOOTER_LIMITS.richTextParagraphs,
|
|
374
|
+
},
|
|
375
|
+
"FooterSettings.bottomLinks": { type: "FooterLink[]", default: [], maximumItems: FOOTER_LIMITS.bottomLinks },
|
|
376
|
+
"FooterSettings.light": { type: "FooterSchemeSettings", default: "scheme defaults" },
|
|
377
|
+
"FooterSettings.dark": { type: "FooterSchemeSettings", default: "scheme defaults" },
|
|
378
|
+
"FooterSettings.featureImage": { type: "FooterFeatureImageSettings|null", default: null },
|
|
379
|
+
"FooterLinkColumn.id": { type: "canonical-lowercase-uuid", required: true },
|
|
380
|
+
"FooterLinkColumn.groups": { type: "FooterLinkGroup[]", default: [], maximumItems: FOOTER_LIMITS.groupsPerColumn },
|
|
381
|
+
"FooterLinkGroup.id": { type: "canonical-lowercase-uuid", required: true },
|
|
382
|
+
"FooterLinkGroup.heading": {
|
|
383
|
+
type: "plain-text",
|
|
384
|
+
requiredWhen: "links is nonempty",
|
|
385
|
+
maximumLength: FOOTER_LIMITS.groupHeadingLength,
|
|
386
|
+
},
|
|
387
|
+
"FooterLinkGroup.links": { type: "FooterLink[]", default: [], maximumItems: FOOTER_LIMITS.linksPerGroup },
|
|
388
|
+
"FooterLink.id": { type: "canonical-lowercase-uuid", required: true },
|
|
389
|
+
"FooterLink.label": { type: "plain-text", required: true, maximumLength: FOOTER_LIMITS.linkLabelLength },
|
|
390
|
+
"FooterLink.pageResourceId": { type: "tracked-page-resource-uuid", exclusiveWith: "externalUrl" },
|
|
391
|
+
"FooterLink.externalUrl": {
|
|
392
|
+
type: "absolute-http-or-https-url",
|
|
393
|
+
exclusiveWith: "pageResourceId",
|
|
394
|
+
maximumLength: FOOTER_LIMITS.externalUrlLength,
|
|
395
|
+
},
|
|
396
|
+
"FooterRichText.paragraphs": { type: "FooterRichTextParagraph[]", default: [] },
|
|
397
|
+
"FooterRichTextParagraph.runs": { type: "FooterRichTextRun[]", default: [] },
|
|
398
|
+
"FooterRichTextRun.text": { type: "string", required: true },
|
|
399
|
+
"FooterRichTextRun.bold": { type: "boolean", default: false },
|
|
400
|
+
"FooterRichTextRun.italic": { type: "boolean", default: false },
|
|
401
|
+
"FooterRichTextRun.underline": { type: "boolean", default: false },
|
|
402
|
+
"FooterRichTextRun.link": { type: "FooterInlineLink|null", default: null },
|
|
403
|
+
"FooterInlineLink.pageResourceId": { type: "tracked-page-resource-uuid", exclusiveWith: "externalUrl" },
|
|
404
|
+
"FooterInlineLink.externalUrl": {
|
|
405
|
+
type: "absolute-http-or-https-url",
|
|
406
|
+
exclusiveWith: "pageResourceId",
|
|
407
|
+
maximumLength: FOOTER_LIMITS.externalUrlLength,
|
|
408
|
+
},
|
|
409
|
+
"FooterSchemeSettings.backgroundColor": {
|
|
410
|
+
type: "appearance-color",
|
|
411
|
+
...appearanceColorReference(APPEARANCE_FOOTER_COLOR_FIELDS.find((field) => field.name === "backgroundColor")),
|
|
412
|
+
default: "",
|
|
413
|
+
},
|
|
414
|
+
"FooterSchemeSettings.textColor": {
|
|
415
|
+
type: "appearance-color",
|
|
416
|
+
...appearanceColorReference(APPEARANCE_FOOTER_COLOR_FIELDS.find((field) => field.name === "textColor")),
|
|
417
|
+
default: "",
|
|
418
|
+
},
|
|
419
|
+
"FooterSchemeSettings.headingColor": {
|
|
420
|
+
type: "appearance-color",
|
|
421
|
+
...appearanceColorReference(APPEARANCE_FOOTER_COLOR_FIELDS.find((field) => field.name === "headingColor")),
|
|
422
|
+
default: "",
|
|
423
|
+
},
|
|
424
|
+
"FooterSchemeSettings.linkColor": {
|
|
425
|
+
type: "appearance-color",
|
|
426
|
+
...appearanceColorReference(APPEARANCE_FOOTER_COLOR_FIELDS.find((field) => field.name === "linkColor")),
|
|
427
|
+
default: "",
|
|
428
|
+
},
|
|
429
|
+
"FooterSchemeSettings.linkHoverColor": {
|
|
430
|
+
type: "appearance-color",
|
|
431
|
+
...appearanceColorReference(APPEARANCE_FOOTER_COLOR_FIELDS.find((field) => field.name === "linkHoverColor")),
|
|
432
|
+
default: "",
|
|
433
|
+
},
|
|
434
|
+
"FooterSchemeSettings.backgroundImageId": { type: "site-owned-image-uuid", default: "" },
|
|
435
|
+
"FooterSchemeSettings.backgroundImageUrl": { type: "url", readOnly: true },
|
|
436
|
+
"FooterSchemeSettings.backgroundImageOpacity": { type: "number", default: 1, minimum: 0, maximum: 1 },
|
|
437
|
+
"FooterSchemeSettings.backgroundPresentation": {
|
|
438
|
+
type: "enum",
|
|
439
|
+
default: FOOTER_BACKGROUND_PRESENTATIONS[0],
|
|
440
|
+
values: FOOTER_BACKGROUND_PRESENTATIONS,
|
|
441
|
+
},
|
|
442
|
+
"FooterSchemeSettings.backgroundFade": { type: "enum", default: FOOTER_FADE_MODES[0], values: FOOTER_FADE_MODES },
|
|
443
|
+
"FooterSchemeSettings.backgroundRepeatHeightPx": {
|
|
444
|
+
type: "integer",
|
|
445
|
+
default: FOOTER_LIMITS.backgroundRepeatHeightDefault,
|
|
446
|
+
minimum: FOOTER_LIMITS.backgroundRepeatHeightMinimum,
|
|
447
|
+
maximum: FOOTER_LIMITS.backgroundRepeatHeightMaximum,
|
|
448
|
+
step: FOOTER_LIMITS.backgroundRepeatHeightStep,
|
|
449
|
+
},
|
|
450
|
+
"FooterSchemeSettings.additionalTopPaddingRem": {
|
|
451
|
+
type: "number",
|
|
452
|
+
default: 0,
|
|
453
|
+
minimum: 0,
|
|
454
|
+
maximum: FOOTER_LIMITS.additionalTopPaddingRemMaximum,
|
|
455
|
+
},
|
|
456
|
+
"FooterFeatureImageSettings.imageId": { type: "site-owned-image-uuid", requiredWith: "alt" },
|
|
457
|
+
"FooterFeatureImageSettings.imageUrl": { type: "url", readOnly: true },
|
|
458
|
+
"FooterFeatureImageSettings.alt": {
|
|
459
|
+
type: "plain-text",
|
|
460
|
+
requiredWith: "imageId",
|
|
461
|
+
maximumLength: FOOTER_LIMITS.featureImageAltLength,
|
|
462
|
+
},
|
|
463
|
+
"FooterFeatureImageSettings.responsiveUrls": { type: "FooterFeatureResponsiveImageUrl[]", readOnly: true },
|
|
464
|
+
"FooterFeatureResponsiveImageUrl.minWidth": { type: "integer", readOnly: true },
|
|
465
|
+
"FooterFeatureResponsiveImageUrl.url": { type: "url", readOnly: true },
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
function footerMessages() {
|
|
469
|
+
return Object.entries(FOOTER_FIELD_INVENTORY).map(([name, fields]) => ({
|
|
470
|
+
name,
|
|
471
|
+
additionalProperties: false,
|
|
472
|
+
fields: fields.map((field) => ({ name: field, ...FOOTER_FIELD_DETAILS[name + "." + field] })),
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function getFooterReference() {
|
|
477
|
+
return {
|
|
478
|
+
title: "Complete footer workspace and save contract",
|
|
479
|
+
referenceKind: "footer",
|
|
480
|
+
usage: CLI_BINARY_NAME + " footer push",
|
|
481
|
+
workspaceFile: APPEARANCE_FILES[SETTINGS_TYPE_SITE_PUBLISHING_PREFERENCES],
|
|
482
|
+
workspacePath: "settings.footerSettings",
|
|
483
|
+
messages: footerMessages(),
|
|
484
|
+
totalGroupMaximum: FOOTER_LIMITS.linkGroups,
|
|
485
|
+
totalRichTextRunsMaximum: FOOTER_LIMITS.richTextRuns,
|
|
486
|
+
targetRule:
|
|
487
|
+
"Each link has exactly one target: a tracked pageResourceId, or an absolute credential-free http/https externalUrl.",
|
|
488
|
+
imageRule:
|
|
489
|
+
"Image ids come from pull or media upload. Feature images require alt text; background art is decorative.",
|
|
490
|
+
readOnlyProjections: FOOTER_READ_ONLY_FIELDS,
|
|
491
|
+
concurrency:
|
|
492
|
+
"pull records expectedDraftHash. A conflict returns footer.concurrent_modification; pull, reconcile, and retry.",
|
|
493
|
+
themeInteraction:
|
|
494
|
+
"theme push rewrites this file from a fresh server read before its ten-color overlay. It refuses while "
|
|
495
|
+
+ "unpushed footer-content edits exist (theme.unpushed_footer_content); run " + CLI_BINARY_NAME
|
|
496
|
+
+ " footer push first, or " + CLI_BINARY_NAME + " pull to discard the local edit.",
|
|
497
|
+
workflow: [
|
|
498
|
+
CLI_BINARY_NAME + " pull",
|
|
499
|
+
"edit settings/site-publishing-preferences.json at settings.footerSettings",
|
|
500
|
+
CLI_BINARY_NAME + " footer push",
|
|
501
|
+
CLI_BINARY_NAME + " pull to verify the semantic round trip",
|
|
502
|
+
],
|
|
503
|
+
example: FOOTER_EXAMPLE,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function assertPresentationExamples() {
|
|
508
|
+
validateAndEncodeThemePair(THEME_EXAMPLE.lightTheme, THEME_EXAMPLE.darkTheme);
|
|
509
|
+
validateFooterDocument(FOOTER_EXAMPLE);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function fieldConstraint(field) {
|
|
513
|
+
const details = [];
|
|
514
|
+
if (field.readOnly) details.push("read-only");
|
|
515
|
+
if (field.required) details.push("required");
|
|
516
|
+
if (field.requiredWhen) details.push(`required when ${field.requiredWhen}`);
|
|
517
|
+
if (field.requiredWith) details.push(`required with ${field.requiredWith}`);
|
|
518
|
+
if (field.exclusiveWith) details.push(`exclusive with ${field.exclusiveWith}`);
|
|
519
|
+
if (Object.hasOwn(field, "default")) details.push(`default ${JSON.stringify(field.default)}`);
|
|
520
|
+
if (field.inheritedWhenOmitted) details.push("inherits when omitted");
|
|
521
|
+
if (field.additionalProperties === true) details.push("open named map");
|
|
522
|
+
if (field.additionalProperties === false) details.push("unlisted keys rejected");
|
|
523
|
+
if (field.values) details.push(`values ${field.values.join(", ")}`);
|
|
524
|
+
if (field.allowedTokens) details.push(`tokens ${field.allowedTokens.join(", ")}`);
|
|
525
|
+
if (field.customForms) {
|
|
526
|
+
details.push(
|
|
527
|
+
"custom colors " + field.customForms.map((form) =>
|
|
528
|
+
form.lightness
|
|
529
|
+
? `${form.syntax} with lightness ${form.lightness.minimum}..${form.lightness.maximum}, `
|
|
530
|
+
+ `chroma ${form.chroma.minimum}..${form.chroma.maximum}, hue ${form.hue.minimum}..${form.hue.maximum}`
|
|
531
|
+
: form.syntax
|
|
532
|
+
).join(" or "),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (field.keys) details.push(`keys ${field.keys.join(", ")}`);
|
|
536
|
+
if (field.minimum !== undefined) details.push(`minimum ${field.minimum}`);
|
|
537
|
+
if (field.exclusiveMinimum !== undefined) details.push(`greater than ${field.exclusiveMinimum}`);
|
|
538
|
+
if (field.maximum !== undefined) details.push(`maximum ${field.maximum}`);
|
|
539
|
+
if (field.warningMaximum !== undefined) details.push(`warning above ${field.warningMaximum}`);
|
|
540
|
+
if (field.maximumItems !== undefined) details.push(`maximum ${field.maximumItems} items`);
|
|
541
|
+
if (field.maximumEntries !== undefined) details.push(`maximum ${field.maximumEntries} entries`);
|
|
542
|
+
if (field.maximumLength !== undefined) details.push(`maximum ${field.maximumLength} characters`);
|
|
543
|
+
if (field.maximumCharacters !== undefined) details.push(`maximum ${field.maximumCharacters} characters total`);
|
|
544
|
+
if (field.maximumParagraphs !== undefined) details.push(`maximum ${field.maximumParagraphs} paragraphs`);
|
|
545
|
+
if (field.maximumNameScalars !== undefined) details.push(`maximum ${field.maximumNameScalars} name characters`);
|
|
546
|
+
if (field.namePattern) details.push(`names are ${field.namePattern}`);
|
|
547
|
+
if (field.sourceValues) details.push(`sources ${field.sourceValues.join(", ")}`);
|
|
548
|
+
if (field.lightnessValues) details.push(`lightness ${field.lightnessValues.join(", ")}`);
|
|
549
|
+
if (field.step !== undefined) details.push(`step ${field.step}`);
|
|
550
|
+
if (field.stepMinimum !== undefined) {
|
|
551
|
+
details.push(`steps ${field.stepMinimum}..${field.stepMaximum}; default ${field.stepDefault}`);
|
|
552
|
+
}
|
|
553
|
+
if (field.exact !== undefined) details.push(`exactly ${JSON.stringify(field.exact)}`);
|
|
554
|
+
return (field.type ?? "object") + (details.length > 0 ? `; ${details.join("; ")}` : "");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export function formatPresentationReference(reference) {
|
|
558
|
+
if (reference.referenceKind === "theme") {
|
|
559
|
+
const groups = reference.groups.map((group) =>
|
|
560
|
+
"\n" + group.name + ": " + group.summary + "\n"
|
|
561
|
+
+ group.fields.map((field) => " " + field.name.padEnd(30) + field.description + " " + fieldConstraint(field))
|
|
562
|
+
.join("\n")
|
|
563
|
+
).join("\n");
|
|
564
|
+
return reference.title + "\nWorkspace: " + reference.workspaceFile + " ("
|
|
565
|
+
+ reference.themePaths.join(", ") + ")\nComplete pulled baseline required: yes\nNested merge groups: "
|
|
566
|
+
+ reference.nestedMergeGroups.join(", ") + "\n\nDesign workflow:\n"
|
|
567
|
+
+ reference.workflow.map((step, index) => " " + (index + 1) + ". " + step).join("\n")
|
|
568
|
+
+ "\n\nTheme groups:\n" + groups + "\n\nValid complete pair:\n"
|
|
569
|
+
+ JSON.stringify(reference.example, null, 2) + "\n";
|
|
570
|
+
}
|
|
571
|
+
if (reference.referenceKind === "appearance") {
|
|
572
|
+
return reference.title + "\nUsage: " + reference.usage + "\n\nWritable fields:\n"
|
|
573
|
+
+ reference.fields.map((field) => " " + field.file + " :: " + field.path + " — " + fieldConstraint(field)).join(
|
|
574
|
+
"\n",
|
|
575
|
+
)
|
|
576
|
+
+ "\n\nRead-only server projections:\n"
|
|
577
|
+
+ reference.readOnlyProjections.map((field) => " " + field.file + " :: " + field.path).join("\n")
|
|
578
|
+
+ "\n\nImages: " + reference.imageReferences + "\nLogos: " + reference.logoContract
|
|
579
|
+
+ "\nFooter: " + reference.footerBoundary
|
|
580
|
+
+ "\nFooter content guard: " + reference.footerContentGuard + "\n\nMutation order (non-atomic):\n"
|
|
581
|
+
+ reference.mutationOrder.map((step, index) => " " + (index + 1) + ". " + step).join("\n")
|
|
582
|
+
+ "\nRecovery: " + reference.recovery + "\n";
|
|
583
|
+
}
|
|
584
|
+
if (reference.referenceKind === "footer") {
|
|
585
|
+
const messages = reference.messages.map((message) =>
|
|
586
|
+
message.name + " (unlisted fields rejected)\n"
|
|
587
|
+
+ message.fields.map((field) => " " + field.name.padEnd(30) + fieldConstraint(field)).join("\n")
|
|
588
|
+
).join("\n\n");
|
|
589
|
+
return reference.title + "\nUsage: " + reference.usage + "\nWorkspace: "
|
|
590
|
+
+ reference.workspaceFile + " :: " + reference.workspacePath + "\n\nClosed schema:\n"
|
|
591
|
+
+ messages + "\n\nTargets: " + reference.targetRule + "\nImages: " + reference.imageRule
|
|
592
|
+
+ "\nConcurrency: " + reference.concurrency
|
|
593
|
+
+ "\nTheme interaction: " + reference.themeInteraction + "\nRead-only projections: "
|
|
594
|
+
+ reference.readOnlyProjections.join(", ") + "\n\nWorkflow:\n"
|
|
595
|
+
+ reference.workflow.map((step, index) => " " + (index + 1) + ". " + step).join("\n")
|
|
596
|
+
+ "\n\nValid marketing-site example:\n"
|
|
597
|
+
+ JSON.stringify(reference.example, null, 2) + "\n";
|
|
598
|
+
}
|
|
599
|
+
throw new TypeError("Unknown presentation reference kind '" + reference.referenceKind + "'.");
|
|
600
|
+
}
|