@taprootio/site-authoring 0.8.0 → 0.8.2
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 +19 -1
- package/package.json +2 -2
- package/src/cli.js +18 -1
- package/src/constants.js +1 -1
- package/src/content/components.js +29 -0
- package/src/content/free-form-section-registry.json +28 -1
- package/src/content/markdown.js +3 -1
- package/src/content/tiptap-prosemirror.ts +9 -0
- package/src/content/validate-document.js +2 -0
- package/src/presentation-reference.js +6 -0
- package/src/reference-help.js +313 -1
package/README.md
CHANGED
|
@@ -79,16 +79,34 @@ upgrade available to anyone the deploy refuses.
|
|
|
79
79
|
|
|
80
80
|
`taproot-site --help` lists every verb and the configuration contract.
|
|
81
81
|
`taproot-site help` is the offline reference family agents read before
|
|
82
|
-
authoring: page types, components, navigation, media, previews, themes,
|
|
82
|
+
authoring: page types, composable design blueprints, components, navigation, media, previews, themes,
|
|
83
83
|
appearance, the footer, and the offline fixture contract, each with `--json`
|
|
84
84
|
for stable machine-readable output.
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
87
|
taproot-site help
|
|
88
|
+
taproot-site help designs
|
|
89
|
+
taproot-site help design professional-portfolio
|
|
88
90
|
taproot-site help page free-form
|
|
89
91
|
taproot-site help component hero-section --json
|
|
90
92
|
```
|
|
91
93
|
|
|
94
|
+
Design help separates three choices that agents should make deliberately:
|
|
95
|
+
site purpose, visual language, and interaction intensity. The six blueprints
|
|
96
|
+
cover professional portfolios, bold consumer brands, editorial publications,
|
|
97
|
+
local services, cultural events, and community directories. Each describes a
|
|
98
|
+
content model, representative pages, navigation shape, theme direction,
|
|
99
|
+
component recipes, a motion ceiling, media guidance, integration boundaries,
|
|
100
|
+
and accessibility cautions. They are composable starting points—not templates
|
|
101
|
+
or permission to copy another site's protected assets or trade dress. Use
|
|
102
|
+
`--json` when an agent needs the stable recipe structure.
|
|
103
|
+
|
|
104
|
+
Before pushing appearance, use `taproot-site help appearance` and preview both
|
|
105
|
+
schemes. A light logo must remain distinct on the rendered light header and a
|
|
106
|
+
dark logo on the rendered dark header at desktop and compact/mobile widths;
|
|
107
|
+
transparent canvas pixels do not count as contrast. Supply separate
|
|
108
|
+
scheme-specific assets when one visible mark cannot satisfy both surfaces.
|
|
109
|
+
|
|
92
110
|
`taproot-site validate <fixture-directory>` checks a complete offline fixture
|
|
93
111
|
with no credential, no network, and no write: a directory laid out like a
|
|
94
112
|
pulled workspace — `pages/`, `nav.json`, `redirects.json`, and the four
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
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.13.0"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"test": "node --test",
|
package/src/cli.js
CHANGED
|
@@ -49,12 +49,15 @@ import {
|
|
|
49
49
|
formatReferenceResult,
|
|
50
50
|
getAppearanceReference,
|
|
51
51
|
getComponentReference,
|
|
52
|
+
getDesignBlueprint,
|
|
52
53
|
getFooterReference,
|
|
53
54
|
getPageTypeReference,
|
|
54
55
|
getThemeReference,
|
|
55
56
|
getWorkflowReference,
|
|
56
57
|
listComponentTypeReferences,
|
|
58
|
+
listDesignBlueprints,
|
|
57
59
|
listPageTypeReferences,
|
|
60
|
+
DESIGN_TYPES,
|
|
58
61
|
PAGE_TYPES,
|
|
59
62
|
REFERENCE_TOPICS,
|
|
60
63
|
REFERENCE_VERSION,
|
|
@@ -628,6 +631,7 @@ function parseReferenceArguments(arguments_) {
|
|
|
628
631
|
(
|
|
629
632
|
topic === "pages"
|
|
630
633
|
|| topic === "components"
|
|
634
|
+
|| topic === "designs"
|
|
631
635
|
|| topic === "nav"
|
|
632
636
|
|| topic === "media"
|
|
633
637
|
|| topic === "preview"
|
|
@@ -641,7 +645,7 @@ function parseReferenceArguments(arguments_) {
|
|
|
641
645
|
) {
|
|
642
646
|
throw usageError("help.usage", `The '${topic}' topic does not accept a name.`);
|
|
643
647
|
}
|
|
644
|
-
if ((topic === "page" || topic === "component") && (subject === undefined || extra.length > 0)) {
|
|
648
|
+
if ((topic === "page" || topic === "component" || topic === "design") && (subject === undefined || extra.length > 0)) {
|
|
645
649
|
throw usageError("help.usage", `The '${topic}' topic requires exactly one name.`);
|
|
646
650
|
}
|
|
647
651
|
return { mode: "reference", topic, subject, json };
|
|
@@ -662,6 +666,8 @@ function referenceResult(parsed) {
|
|
|
662
666
|
return { ...result, topic: "page-types", pageTypes: listPageTypeReferences() };
|
|
663
667
|
case "components":
|
|
664
668
|
return { ...result, topic: "component-types", components: listComponentTypeReferences() };
|
|
669
|
+
case "designs":
|
|
670
|
+
return { ...result, topic: "design-types", designs: listDesignBlueprints() };
|
|
665
671
|
case "page": {
|
|
666
672
|
const page = getPageTypeReference(parsed.subject);
|
|
667
673
|
if (!page) {
|
|
@@ -685,6 +691,17 @@ function referenceResult(parsed) {
|
|
|
685
691
|
}
|
|
686
692
|
return { ...result, topic: "component", component };
|
|
687
693
|
}
|
|
694
|
+
case "design": {
|
|
695
|
+
const design = getDesignBlueprint(parsed.subject);
|
|
696
|
+
if (!design) {
|
|
697
|
+
throw usageError(
|
|
698
|
+
"help.design_type_unknown",
|
|
699
|
+
`Unknown design type. Expected one of: ${DESIGN_TYPES.join(", ")}.`,
|
|
700
|
+
{ alternatives: DESIGN_TYPES },
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
return { ...result, topic: "design", design };
|
|
704
|
+
}
|
|
688
705
|
case "nav":
|
|
689
706
|
case "redirects":
|
|
690
707
|
case "media":
|
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.2";
|
|
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";
|
|
@@ -226,6 +226,9 @@ const CARD_GRID = Object.freeze({
|
|
|
226
226
|
{ kind: "value", value: Object.freeze([]) },
|
|
227
227
|
),
|
|
228
228
|
columns: num({ values: Object.freeze([2, 3]) }),
|
|
229
|
+
presentation: enumOf("cards", "editorial", "featured", "poster"),
|
|
230
|
+
imageAspect: enumOf("auto", "landscape", "square", "portrait"),
|
|
231
|
+
interaction: enumOf("none", "lift", "zoom", "caption-reveal"),
|
|
229
232
|
borderWidth: num({
|
|
230
233
|
integer: true,
|
|
231
234
|
minimum: 0,
|
|
@@ -257,6 +260,7 @@ const IMAGE_BANNER = Object.freeze({
|
|
|
257
260
|
// A built-in or application-registered Espalier texture name.
|
|
258
261
|
texture: textureName(),
|
|
259
262
|
textureScale: enumOf("fine", "medium", "coarse"),
|
|
263
|
+
imageMotion: enumOf("none", "slow-zoom", "drift-start", "drift-end"),
|
|
260
264
|
});
|
|
261
265
|
|
|
262
266
|
const SPACER = Object.freeze({
|
|
@@ -444,6 +448,9 @@ const COMPONENT_DEFINITIONS = Object.freeze({
|
|
|
444
448
|
{
|
|
445
449
|
cards: [{ image: null, title: "", description: "", linkUrl: "" }],
|
|
446
450
|
columns: 3,
|
|
451
|
+
presentation: "cards",
|
|
452
|
+
imageAspect: "landscape",
|
|
453
|
+
interaction: "lift",
|
|
447
454
|
borderWidth: 0,
|
|
448
455
|
},
|
|
449
456
|
[
|
|
@@ -467,6 +474,9 @@ const COMPONENT_DEFINITIONS = Object.freeze({
|
|
|
467
474
|
},
|
|
468
475
|
],
|
|
469
476
|
columns: 2,
|
|
477
|
+
presentation: "featured",
|
|
478
|
+
imageAspect: "landscape",
|
|
479
|
+
interaction: "lift",
|
|
470
480
|
borderWidth: 1,
|
|
471
481
|
},
|
|
472
482
|
),
|
|
@@ -490,6 +500,7 @@ const COMPONENT_DEFINITIONS = Object.freeze({
|
|
|
490
500
|
textShadow: "none",
|
|
491
501
|
texture: "none",
|
|
492
502
|
textureScale: "medium",
|
|
503
|
+
imageMotion: "none",
|
|
493
504
|
},
|
|
494
505
|
[
|
|
495
506
|
"Set altText to describe meaningful banner imagery; leave it empty only when the image is decorative.",
|
|
@@ -505,6 +516,7 @@ const COMPONENT_DEFINITIONS = Object.freeze({
|
|
|
505
516
|
scrim: "bottom",
|
|
506
517
|
scrimStrength: "medium",
|
|
507
518
|
texture: "paper",
|
|
519
|
+
imageMotion: "slow-zoom",
|
|
508
520
|
},
|
|
509
521
|
),
|
|
510
522
|
});
|
|
@@ -829,6 +841,23 @@ export function validateComponentBlock(componentType, componentData, basePath) {
|
|
|
829
841
|
const fields = isComponentType(componentType) ? COMPONENT_SHAPES[componentType] : undefined;
|
|
830
842
|
if (!fields) return errors;
|
|
831
843
|
validateFields(fields, parsed, dataPath, errors);
|
|
844
|
+
if (componentType === "card-grid" && Array.isArray(parsed.cards)) {
|
|
845
|
+
for (const [index, card] of parsed.cards.entries()) {
|
|
846
|
+
if (
|
|
847
|
+
isPlainObject(card)
|
|
848
|
+
&& typeof card.linkUrl === "string"
|
|
849
|
+
&& card.linkUrl.trim().length > 0
|
|
850
|
+
&& isSafeUrl(card.linkUrl)
|
|
851
|
+
&& (typeof card.title !== "string" || card.title.trim().length === 0)
|
|
852
|
+
) {
|
|
853
|
+
errors.push(contentError(
|
|
854
|
+
`${dataPath}/cards/${index}/title`,
|
|
855
|
+
CODES.componentData,
|
|
856
|
+
"A linked card must have a visible title.",
|
|
857
|
+
));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
832
861
|
return errors;
|
|
833
862
|
}
|
|
834
863
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 6,
|
|
3
3
|
"section": {
|
|
4
4
|
"nodeType": "section",
|
|
5
5
|
"label": "Section",
|
|
@@ -38,6 +38,33 @@
|
|
|
38
38
|
"elevated"
|
|
39
39
|
]
|
|
40
40
|
},
|
|
41
|
+
"entrance": {
|
|
42
|
+
"label": "Entrance",
|
|
43
|
+
"help": "Optional CSS-only reveal for the section content. Unsupported browsers render the content statically and reduced-motion preferences disable the effect.",
|
|
44
|
+
"type": "enum",
|
|
45
|
+
"required": false,
|
|
46
|
+
"default": "none",
|
|
47
|
+
"values": [
|
|
48
|
+
"none",
|
|
49
|
+
"fade",
|
|
50
|
+
"rise",
|
|
51
|
+
"scale",
|
|
52
|
+
"slide-start",
|
|
53
|
+
"slide-end"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"entranceStagger": {
|
|
57
|
+
"label": "Entrance stagger",
|
|
58
|
+
"help": "Optional bounded delay between the section's direct content blocks. It has no effect when entrance is none and becomes static under reduced-motion preferences.",
|
|
59
|
+
"type": "enum",
|
|
60
|
+
"required": false,
|
|
61
|
+
"default": "none",
|
|
62
|
+
"values": [
|
|
63
|
+
"none",
|
|
64
|
+
"tight",
|
|
65
|
+
"relaxed"
|
|
66
|
+
]
|
|
67
|
+
},
|
|
41
68
|
"background": {
|
|
42
69
|
"label": "Photo background",
|
|
43
70
|
"help": "Optional site-owned responsive photograph behind the section band, with normalized focal points and a context-derived readability scrim.",
|
package/src/content/markdown.js
CHANGED
|
@@ -656,7 +656,7 @@ function sectionHeaderAttrs(text, line) {
|
|
|
656
656
|
);
|
|
657
657
|
}
|
|
658
658
|
|
|
659
|
-
for (const name of ["contentPadding", "surface"]) {
|
|
659
|
+
for (const name of ["contentPadding", "surface", "entrance", "entranceStagger"]) {
|
|
660
660
|
const definition = definitions[name];
|
|
661
661
|
const value = parsed[name];
|
|
662
662
|
if (value !== undefined && (typeof value !== "string" || !definition.values.includes(value))) {
|
|
@@ -691,6 +691,8 @@ function sectionHeaderAttrs(text, line) {
|
|
|
691
691
|
...(typeof context === "string" ? { context } : {}),
|
|
692
692
|
contentPadding: parsed.contentPadding ?? definitions.contentPadding.default,
|
|
693
693
|
surface: parsed.surface ?? definitions.surface.default,
|
|
694
|
+
...(parsed.entrance === undefined ? {} : { entrance: parsed.entrance }),
|
|
695
|
+
...(parsed.entranceStagger === undefined ? {} : { entranceStagger: parsed.entranceStagger }),
|
|
694
696
|
...(normalizedBackground.background ? { background: normalizedBackground.background } : {}),
|
|
695
697
|
...(normalizedDecoration.decoration ? { decoration: normalizedDecoration.decoration } : {}),
|
|
696
698
|
};
|
|
@@ -624,6 +624,13 @@ function renderSection(
|
|
|
624
624
|
const surface = attrs.surface === "raised" || attrs.surface === "elevated"
|
|
625
625
|
? attrs.surface
|
|
626
626
|
: definitions.surface.default;
|
|
627
|
+
const entrance = typeof attrs.entrance === "string" && definitions.entrance.values.includes(attrs.entrance as never)
|
|
628
|
+
? attrs.entrance
|
|
629
|
+
: definitions.entrance.default;
|
|
630
|
+
const entranceStagger = typeof attrs.entranceStagger === "string"
|
|
631
|
+
&& definitions.entranceStagger.values.includes(attrs.entranceStagger as never)
|
|
632
|
+
? attrs.entranceStagger
|
|
633
|
+
: definitions.entranceStagger.default;
|
|
627
634
|
const contextValue = kind === "explicit" ? stringAttr(attrs.context) : undefined;
|
|
628
635
|
const context = contextValue
|
|
629
636
|
&& countUnicodeScalars(contextValue) <= definitions.context.maximumLength
|
|
@@ -672,6 +679,8 @@ function renderSection(
|
|
|
672
679
|
"data-taproot-section": kind,
|
|
673
680
|
"data-content-padding": contentPadding,
|
|
674
681
|
"data-surface": surface,
|
|
682
|
+
"data-entrance": entrance,
|
|
683
|
+
"data-entrance-stagger": entranceStagger,
|
|
675
684
|
context,
|
|
676
685
|
style: sectionStyle,
|
|
677
686
|
},
|
|
@@ -227,6 +227,8 @@ const SECTION_ATTRS = Object.freeze({
|
|
|
227
227
|
}),
|
|
228
228
|
contentPadding: optionalEnum(FREE_FORM_SECTION_REGISTRY.section.attrs.contentPadding.values),
|
|
229
229
|
surface: optionalEnum(FREE_FORM_SECTION_REGISTRY.section.attrs.surface.values),
|
|
230
|
+
entrance: optionalEnum(FREE_FORM_SECTION_REGISTRY.section.attrs.entrance.values),
|
|
231
|
+
entranceStagger: optionalEnum(FREE_FORM_SECTION_REGISTRY.section.attrs.entranceStagger.values),
|
|
230
232
|
// Nested field diagnostics are owned by
|
|
231
233
|
// `normalizeFreeFormSectionBackground` below.
|
|
232
234
|
background: spec(() => null),
|
|
@@ -334,6 +334,11 @@ export function getAppearanceReference() {
|
|
|
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
336
|
"lightLogoId and darkLogoId are the current scheme-specific logo fields; there is no separate compact-logo setting.",
|
|
337
|
+
logoContrastContract:
|
|
338
|
+
"Evaluate lightLogoId against the rendered light-scheme header and darkLogoId against the rendered dark-scheme "
|
|
339
|
+
+ "header at desktop and compact/mobile widths. Transparent pixels provide no contrast: inspect the visible mark, "
|
|
340
|
+
+ "not its canvas bounds. When one asset does not remain distinct in both schemes, supply scheme-specific logo "
|
|
341
|
+
+ "assets rather than relying on a shadow, outline, or header color that has not been previewed.",
|
|
337
342
|
headerWidthContract:
|
|
338
343
|
"headerWidth 'contained' keeps the brand and the header buttons at the page content edges; "
|
|
339
344
|
+ "'wide' moves them to the viewport edges. Pages with root-band components (image-banner) span the viewport, "
|
|
@@ -596,6 +601,7 @@ export function formatPresentationReference(reference) {
|
|
|
596
601
|
+ "\n\nRead-only server projections:\n"
|
|
597
602
|
+ reference.readOnlyProjections.map((field) => " " + field.file + " :: " + field.path).join("\n")
|
|
598
603
|
+ "\n\nImages: " + reference.imageReferences + "\nLogos: " + reference.logoContract
|
|
604
|
+
+ "\nLogo contrast: " + reference.logoContrastContract
|
|
599
605
|
+ "\nHeader width: " + reference.headerWidthContract
|
|
600
606
|
+ "\nMobile menu and menu font: " + reference.menuContract
|
|
601
607
|
+ "\nFooter: " + reference.footerBoundary
|
package/src/reference-help.js
CHANGED
|
@@ -51,8 +51,290 @@ import {
|
|
|
51
51
|
|
|
52
52
|
export { getAppearanceReference, getFooterReference, getThemeReference };
|
|
53
53
|
|
|
54
|
-
export const REFERENCE_VERSION =
|
|
54
|
+
export const REFERENCE_VERSION = 23;
|
|
55
55
|
export const PAGE_TYPES = Object.freeze(["free-form"]);
|
|
56
|
+
|
|
57
|
+
function deepFreeze(value) {
|
|
58
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
59
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
60
|
+
return Object.freeze(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const DESIGN_TYPES = Object.freeze([
|
|
64
|
+
"professional-portfolio",
|
|
65
|
+
"bold-consumer-brand",
|
|
66
|
+
"editorial-publication",
|
|
67
|
+
"local-service",
|
|
68
|
+
"cultural-events",
|
|
69
|
+
"community-directory",
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const DESIGN_BLUEPRINTS = deepFreeze([
|
|
73
|
+
{
|
|
74
|
+
type: "professional-portfolio",
|
|
75
|
+
displayName: "Professional portfolio",
|
|
76
|
+
summary: "A focused body of selected work, credibility, and a clear route to start a conversation.",
|
|
77
|
+
sitePurpose: "Present an individual or small practice's work and make its expertise easy to evaluate.",
|
|
78
|
+
visualLanguage: "Quiet editorial spacing, confident typography, and a restrained image rhythm that lets work lead.",
|
|
79
|
+
interactionIntensity: "Low: direct reading and deliberate links, with modest affordances only where they clarify a destination.",
|
|
80
|
+
contentModel: { primary: ["selected projects", "case studies", "profile and approach"], supporting: ["services", "testimonials", "contact details"] },
|
|
81
|
+
representativePages: [
|
|
82
|
+
{ title: "Home", purpose: "State the practice and foreground a small selection of work." },
|
|
83
|
+
{ title: "Work", purpose: "Browse projects by discipline, sector, or outcome." },
|
|
84
|
+
{ title: "Case study", purpose: "Explain the brief, process, collaboration, and result for one project." },
|
|
85
|
+
{ title: "About", purpose: "Introduce the practitioner, perspective, and capabilities." },
|
|
86
|
+
{ title: "Contact", purpose: "Offer one obvious, accessible way to begin an enquiry." },
|
|
87
|
+
],
|
|
88
|
+
navigationShape: "A compact primary set: Work, About, Services, Contact; case studies remain one level below Work.",
|
|
89
|
+
themeDirection: "A calm light/dark pair with generous contrast, one measured accent, and roomy display/body type scale.",
|
|
90
|
+
recipes: [
|
|
91
|
+
{
|
|
92
|
+
name: "Selected-work opening",
|
|
93
|
+
purpose: "Introduce the practice, then make the first few destinations tangible.",
|
|
94
|
+
section: { entrance: "rise", entranceStagger: "tight" },
|
|
95
|
+
components: [
|
|
96
|
+
{ type: "hero-section", properties: { titleSize: "display", alignment: "start", mediaArrangement: "split", mediaWidth: "wide" } },
|
|
97
|
+
{ type: "card-grid", properties: { columns: 2, presentation: "editorial", imageAspect: "landscape", interaction: "lift" } },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "Capability proof",
|
|
102
|
+
purpose: "Pair concise service language with independently readable client evidence.",
|
|
103
|
+
section: { entrance: "fade", entranceStagger: "relaxed" },
|
|
104
|
+
components: [
|
|
105
|
+
{ type: "feature-grid", properties: { columns: 3, iconSize: "small", borderWidth: 0 } },
|
|
106
|
+
{ type: "testimonial", properties: { columns: 2, carousel: false, borderWidth: 0 } },
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
motionCeiling: "One entrance family per section; no autoplaying carousels, parallax, or motion that competes with the work.",
|
|
111
|
+
mediaBrief: "Use original project imagery, credited photography, and outcome-oriented captions; keep decorative media clearly optional.",
|
|
112
|
+
integrationBoundaries: "Link to an external booking, portfolio-hosting, or contact service when needed. This blueprint does not provide custom code or built-in commerce, search, scheduling, or synchronization.",
|
|
113
|
+
accessibilityCautions: ["Make each card title identify its destination without relying on its image.", "Keep case-study headings logical and give meaningful work images useful alt text."],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: "bold-consumer-brand",
|
|
117
|
+
displayName: "Bold consumer brand",
|
|
118
|
+
summary: "A product- and campaign-led presence that builds recognition while keeping the next step unmistakable.",
|
|
119
|
+
sitePurpose: "Introduce a consumer offering, its point of view, and paths people can take to learn or buy elsewhere.",
|
|
120
|
+
visualLanguage: "Large type, high-contrast fields, assertive image crops, and a small, consistent set of branded shapes.",
|
|
121
|
+
interactionIntensity: "Medium: purposeful hover and reveal feedback, never required to understand the product or complete a task.",
|
|
122
|
+
contentModel: { primary: ["product or collection stories", "brand promise", "campaign landing pages"], supporting: ["stockist links", "press", "frequently asked questions"] },
|
|
123
|
+
representativePages: [
|
|
124
|
+
{ title: "Home", purpose: "Lead with a proposition, featured collection, and one primary action." },
|
|
125
|
+
{ title: "Products", purpose: "Orient visitors to product families without claiming a built-in shop." },
|
|
126
|
+
{ title: "Story", purpose: "Explain origins, materials, values, or makers." },
|
|
127
|
+
{ title: "Stockists", purpose: "Link people to approved places to purchase or visit." },
|
|
128
|
+
],
|
|
129
|
+
navigationShape: "A short, campaign-aware primary nav; collection and story pages use local in-page links instead of deep menus.",
|
|
130
|
+
themeDirection: "A distinctive accent against neutral surfaces, strong semantic contrast, and display typography balanced by practical body text.",
|
|
131
|
+
recipes: [
|
|
132
|
+
{
|
|
133
|
+
name: "Campaign landing",
|
|
134
|
+
purpose: "Make a single product story legible before offering supporting choices.",
|
|
135
|
+
section: { entrance: "scale", entranceStagger: "none" },
|
|
136
|
+
components: [
|
|
137
|
+
{ type: "image-banner", properties: { ratio: "3/1", heightMode: "viewport", contentPosition: "bottom-start", scrim: "bottom", imageMotion: "slow-zoom" } },
|
|
138
|
+
{ type: "cta", properties: { variant: "primary", imagePosition: "right", borderWidth: 0 } },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "Collection browse",
|
|
143
|
+
purpose: "Offer product families as clear destinations with visible labels before interaction.",
|
|
144
|
+
section: { entrance: "rise", entranceStagger: "tight" },
|
|
145
|
+
components: [
|
|
146
|
+
{ type: "card-grid", properties: { columns: 3, presentation: "featured", imageAspect: "square", interaction: "zoom" } },
|
|
147
|
+
{ type: "feature-grid", properties: { columns: 3, iconSize: "medium", borderWidth: 0 } },
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
motionCeiling: "Use one slow image movement or short hover response per view; never conceal names, price context, or actions behind motion.",
|
|
152
|
+
mediaBrief: "Commission or license product, material, and in-use photography; document crop-safe banner areas and supply alt text for informative images.",
|
|
153
|
+
integrationBoundaries: "Use outbound links for retail, commerce, search, or booking providers. This blueprint includes none of those systems and no custom code.",
|
|
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."],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
type: "editorial-publication",
|
|
158
|
+
displayName: "Editorial publication",
|
|
159
|
+
summary: "A reading-first publication that organizes recurring coverage, authorship, and archives without turning discovery into noise.",
|
|
160
|
+
sitePurpose: "Publish a coherent body of reporting, criticism, essays, or institutional editorial work.",
|
|
161
|
+
visualLanguage: "Measured typographic hierarchy, generous reading measure, structured metadata, and images that support rather than interrupt reading.",
|
|
162
|
+
interactionIntensity: "Low: browsing aids and clear links, with no kinetic treatment that interrupts sustained reading.",
|
|
163
|
+
contentModel: { primary: ["articles", "series or sections", "author bylines"], supporting: ["issue introductions", "archive pathways", "about and submission information"] },
|
|
164
|
+
representativePages: [
|
|
165
|
+
{ title: "Home", purpose: "Feature current coverage and point to durable sections." },
|
|
166
|
+
{ title: "Section", purpose: "Collect related work with short editorial framing." },
|
|
167
|
+
{ title: "Article", purpose: "Support long-form reading with attribution and related destinations." },
|
|
168
|
+
{ title: "Archive", purpose: "Provide chronological or thematic browse paths without promising built-in search." },
|
|
169
|
+
{ title: "About", purpose: "Explain editorial purpose, contributors, and contact routes." },
|
|
170
|
+
],
|
|
171
|
+
navigationShape: "Sections at the top level, with Archive and About as utilities; article-level navigation stays contextual and modest.",
|
|
172
|
+
themeDirection: "Reading-optimized light and dark contexts, durable link contrast, restrained accents, and distinct display and body faces.",
|
|
173
|
+
recipes: [
|
|
174
|
+
{
|
|
175
|
+
name: "Lead story and desk",
|
|
176
|
+
purpose: "Set a lead story beside a calm selection of current work.",
|
|
177
|
+
section: { entrance: "fade", entranceStagger: "tight" },
|
|
178
|
+
components: [
|
|
179
|
+
{ type: "image-banner", properties: { ratio: "2/1", heightMode: "ratio", contentPosition: "bottom-start", scrim: "bottom", imageMotion: "none" } },
|
|
180
|
+
{ type: "latest-posts", properties: { count: 6, columns: 3, highlightFirst: false, showDescription: true, showDate: true } },
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "Section archive",
|
|
185
|
+
purpose: "Make recurring coverage browsable without a fabricated search interface.",
|
|
186
|
+
section: { entrance: "none", entranceStagger: "none" },
|
|
187
|
+
components: [{ type: "card-grid", properties: { columns: 3, presentation: "editorial", imageAspect: "landscape", interaction: "none" } }],
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
motionCeiling: "No motion within article reading; entrances may be brief and nonessential on index pages only.",
|
|
191
|
+
mediaBrief: "Use licensed or commissioned lead images with accurate captions and credits; optimize portraits and illustrations for editorial role, not decoration.",
|
|
192
|
+
integrationBoundaries: "Link to an external newsletter, membership, submission, or search provider where appropriate. This blueprint does not add custom code or built-in commerce, search, scheduling, or event synchronization.",
|
|
193
|
+
accessibilityCautions: ["Maintain readable line length, heading order, and visible links in both theme modes.", "Do not encode section, author, or date only by color, position, or image treatment."],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
type: "local-service",
|
|
197
|
+
displayName: "Local service",
|
|
198
|
+
summary: "A practical local presence that explains services, establishes trust, and points visitors to an appropriate external next step.",
|
|
199
|
+
sitePurpose: "Help nearby people understand a service business, its coverage, and how to make contact or arrange service.",
|
|
200
|
+
visualLanguage: "Warm, legible, place-aware imagery with plain-language hierarchy and reassuring, lightly structured surfaces.",
|
|
201
|
+
interactionIntensity: "Low: fast orientation, stable calls to action, and no flourishes that delay contact information.",
|
|
202
|
+
contentModel: { primary: ["services", "service areas", "trust signals"], supporting: ["team", "frequently asked questions", "contact and external booking links"] },
|
|
203
|
+
representativePages: [
|
|
204
|
+
{ title: "Home", purpose: "State who the business serves, where, and how to get help." },
|
|
205
|
+
{ title: "Services", purpose: "Explain services, scope, and suitable next steps." },
|
|
206
|
+
{ title: "Service area", purpose: "Clarify locations, travel expectations, or local context." },
|
|
207
|
+
{ title: "About", purpose: "Introduce people, qualifications, and operating principles." },
|
|
208
|
+
{ title: "Contact", purpose: "Surface phone, email, hours, and external scheduling or enquiry destinations." },
|
|
209
|
+
],
|
|
210
|
+
navigationShape: "Service-led primary nav with a persistent Contact action; service-area detail stays shallow and predictable.",
|
|
211
|
+
themeDirection: "High-clarity neutral surfaces, a trustworthy accent, comfortable body text, and a light/dark pair tested for contact content.",
|
|
212
|
+
recipes: [
|
|
213
|
+
{
|
|
214
|
+
name: "Service reassurance",
|
|
215
|
+
purpose: "State the offer, locality, and first action before presenting proof points.",
|
|
216
|
+
section: { entrance: "rise", entranceStagger: "tight" },
|
|
217
|
+
components: [
|
|
218
|
+
{ type: "hero-section", properties: { titleSize: "display", alignment: "start", mediaArrangement: "split", mediaWidth: "equal" } },
|
|
219
|
+
{ type: "feature-grid", properties: { columns: 3, iconSize: "medium", borderWidth: 1 } },
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "Service finder",
|
|
224
|
+
purpose: "Direct people to the right service without pretending to provide a booking engine.",
|
|
225
|
+
section: { entrance: "fade", entranceStagger: "relaxed" },
|
|
226
|
+
components: [
|
|
227
|
+
{ type: "card-grid", properties: { columns: 3, presentation: "cards", imageAspect: "landscape", interaction: "lift" } },
|
|
228
|
+
{ type: "cta", properties: { variant: "primary", imagePosition: "left", borderWidth: 0 } },
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
motionCeiling: "Keep motion nearly absent; service, phone, hours, and access information remain immediately available.",
|
|
233
|
+
mediaBrief: "Prioritize real people, local context, and service-in-progress photography; avoid generic imagery that could misrepresent the service or area.",
|
|
234
|
+
integrationBoundaries: "Use direct links to an external scheduler, map, payments, or service portal if the business has one. This blueprint does not include those capabilities, custom code, or synchronization.",
|
|
235
|
+
accessibilityCautions: ["Expose contact information as real text and use tel/mailto only where destination is clear.", "Do not rely on a map image alone to communicate service area or physical access information."],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
type: "cultural-events",
|
|
239
|
+
displayName: "Cultural events",
|
|
240
|
+
summary: "A program-led cultural presence that foregrounds what is happening, why it matters, and where people can learn more or reserve elsewhere.",
|
|
241
|
+
sitePurpose: "Present a venue, festival, institution, or program with clear event context and durable cultural framing.",
|
|
242
|
+
visualLanguage: "Expressive but disciplined poster energy, flexible image crops, and strong date/place typography.",
|
|
243
|
+
interactionIntensity: "Medium: responsive cards and subtle image movement add vitality while core event information stays static and complete.",
|
|
244
|
+
contentModel: { primary: ["program highlights", "event or exhibition pages", "artist or participant context"], supporting: ["visit information", "institutional story", "external ticket links"] },
|
|
245
|
+
representativePages: [
|
|
246
|
+
{ title: "Home", purpose: "Introduce the current season, program, or headline event." },
|
|
247
|
+
{ title: "Program", purpose: "Collect upcoming and recurring activity as labelled destinations." },
|
|
248
|
+
{ title: "Event", purpose: "Explain dates, venue, access, participants, and external reservation options." },
|
|
249
|
+
{ title: "Visit", purpose: "Share access, location, and practical preparation information." },
|
|
250
|
+
{ title: "About", purpose: "Frame the organization's mission and cultural context." },
|
|
251
|
+
],
|
|
252
|
+
navigationShape: "Program and Visit remain top-level; event pages are reached from program groupings rather than a deep date-only hierarchy.",
|
|
253
|
+
themeDirection: "A flexible, high-contrast light/dark pair that supports poster-like color while reserving reliable semantic colors for text, links, and status.",
|
|
254
|
+
recipes: [
|
|
255
|
+
{
|
|
256
|
+
name: "Season opening",
|
|
257
|
+
purpose: "Give a headline program visual presence with readable event context.",
|
|
258
|
+
section: { entrance: "slide-start", entranceStagger: "none" },
|
|
259
|
+
components: [
|
|
260
|
+
{ type: "image-banner", properties: { ratio: "2/1", heightMode: "ratio", contentPosition: "bottom-end", scrim: "radial", imageMotion: "drift-end" } },
|
|
261
|
+
{ type: "cta", properties: { variant: "primary", imagePosition: "top", borderWidth: 1 } },
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "Program cards",
|
|
266
|
+
purpose: "Make event destinations readable by name, date, and place before visual interaction.",
|
|
267
|
+
section: { entrance: "rise", entranceStagger: "tight" },
|
|
268
|
+
components: [
|
|
269
|
+
{ type: "card-grid", properties: { columns: 3, presentation: "poster", imageAspect: "portrait", interaction: "caption-reveal" } },
|
|
270
|
+
{ type: "feature-grid", properties: { columns: 3, iconSize: "small", borderWidth: 0 } },
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
motionCeiling: "Use one restrained banner drift or card reveal at a time; dates, venue, access, and reservation links never depend on it.",
|
|
275
|
+
mediaBrief: "Use commissioned or cleared event imagery, poster art, and artist-approved materials with credits, rights notes, and legible crops.",
|
|
276
|
+
integrationBoundaries: "Point to an external ticketing, calendar, reservation, or streaming service where needed. This blueprint offers no built-in commerce, scheduling, event synchronization, search, or custom code.",
|
|
277
|
+
accessibilityCautions: ["Put event date, time, venue, and access information in text, not only poster imagery.", "Ensure caption-reveal content is also available without hover, and reduce decorative motion when requested."],
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
type: "community-directory",
|
|
281
|
+
displayName: "Community directory",
|
|
282
|
+
summary: "A trustworthy guide to people, places, and resources, designed for clear browsing rather than an implied live database.",
|
|
283
|
+
sitePurpose: "Help a defined community discover organizations, resources, and ways to participate.",
|
|
284
|
+
visualLanguage: "Friendly utility, distinct category cues, calm card rhythm, and readable metadata over decorative spectacle.",
|
|
285
|
+
interactionIntensity: "Low to medium: useful card feedback and filters represented as navigation, not as a claim of built-in search.",
|
|
286
|
+
contentModel: { primary: ["directory entries", "category landing pages", "resource guides"], supporting: ["community story", "contribution guidance", "contact and external services"] },
|
|
287
|
+
representativePages: [
|
|
288
|
+
{ title: "Home", purpose: "Explain who the directory is for and highlight useful routes in." },
|
|
289
|
+
{ title: "Categories", purpose: "Offer a stable, readable way to browse resource groups." },
|
|
290
|
+
{ title: "Directory entry", purpose: "Describe one organization, service, or place with clear ownership and contact context." },
|
|
291
|
+
{ title: "Guides", purpose: "Publish curated orientation for common community needs." },
|
|
292
|
+
{ title: "Contribute", purpose: "Explain how people can suggest updates through a contact route." },
|
|
293
|
+
],
|
|
294
|
+
navigationShape: "Category-led browsing with Guides and Contribute as utilities; keep entry pages shallow and avoid pretending categories are live filters.",
|
|
295
|
+
themeDirection: "Accessible category accents against dependable neutral layers, a high-legibility type scale, and dark-mode states tested for links and card boundaries.",
|
|
296
|
+
recipes: [
|
|
297
|
+
{
|
|
298
|
+
name: "Directory orientation",
|
|
299
|
+
purpose: "Give newcomers a clear explanation and several approachable browse routes.",
|
|
300
|
+
section: { entrance: "fade", entranceStagger: "tight" },
|
|
301
|
+
components: [
|
|
302
|
+
{ type: "hero-section", properties: { titleSize: "normal", alignment: "center", mediaArrangement: "stacked", mediaWidth: "narrow" } },
|
|
303
|
+
{ type: "feature-grid", properties: { columns: 3, iconSize: "large", borderWidth: 1 } },
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: "Resource categories",
|
|
308
|
+
purpose: "Present curated categories as explicit destinations, with enough text to distinguish them.",
|
|
309
|
+
section: { entrance: "slide-end", entranceStagger: "relaxed" },
|
|
310
|
+
components: [
|
|
311
|
+
{ type: "card-grid", properties: { columns: 3, presentation: "cards", imageAspect: "auto", interaction: "lift" } },
|
|
312
|
+
{ type: "cta", properties: { variant: "primary", imagePosition: "right", borderWidth: 0 } },
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
motionCeiling: "Limit feedback to short card affordances; never represent category navigation as a live filter or conceal names in animation.",
|
|
317
|
+
mediaBrief: "Use community-supplied or properly licensed images only with permission, contextual captions, and alternatives for entries without photography.",
|
|
318
|
+
integrationBoundaries: "Link out to external maps, forms, search, calendars, or member systems if the community operates them. This blueprint includes no custom code, built-in search, commerce, scheduling, or synchronization.",
|
|
319
|
+
accessibilityCautions: ["Do not use color alone to identify a category or resource type.", "Keep each entry's name, purpose, contact route, and eligibility information available as semantic text."],
|
|
320
|
+
},
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
export function listDesignBlueprints() {
|
|
324
|
+
return DESIGN_BLUEPRINTS.map((blueprint) => Object.freeze({
|
|
325
|
+
type: blueprint.type,
|
|
326
|
+
displayName: blueprint.displayName,
|
|
327
|
+
summary: blueprint.summary,
|
|
328
|
+
sitePurpose: blueprint.sitePurpose,
|
|
329
|
+
visualLanguage: blueprint.visualLanguage,
|
|
330
|
+
interactionIntensity: blueprint.interactionIntensity,
|
|
331
|
+
helpCommand: `${CLI_BINARY_NAME} help design ${blueprint.type}`,
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function getDesignBlueprint(type) {
|
|
336
|
+
return DESIGN_BLUEPRINTS.find((blueprint) => blueprint.type === type);
|
|
337
|
+
}
|
|
56
338
|
export const REFERENCE_TOPICS = Object.freeze([
|
|
57
339
|
Object.freeze({ name: "pages", usage: `${CLI_BINARY_NAME} help pages`, summary: "List authorable page types." }),
|
|
58
340
|
Object.freeze({
|
|
@@ -70,6 +352,12 @@ export const REFERENCE_TOPICS = Object.freeze([
|
|
|
70
352
|
usage: `${CLI_BINARY_NAME} help component <component-type>`,
|
|
71
353
|
summary: "Describe one component schema and show a valid example.",
|
|
72
354
|
}),
|
|
355
|
+
Object.freeze({ name: "designs", usage: `${CLI_BINARY_NAME} help designs`, summary: "List composable design blueprints." }),
|
|
356
|
+
Object.freeze({
|
|
357
|
+
name: "design",
|
|
358
|
+
usage: `${CLI_BINARY_NAME} help design <design-type>`,
|
|
359
|
+
summary: "Describe one composable design blueprint.",
|
|
360
|
+
}),
|
|
73
361
|
Object.freeze({ name: "nav", usage: `${CLI_BINARY_NAME} help nav`, summary: "Describe nav.json item shapes." }),
|
|
74
362
|
Object.freeze({
|
|
75
363
|
name: "redirects",
|
|
@@ -1132,6 +1420,14 @@ function formatPlacement(row) {
|
|
|
1132
1420
|
}; ${row.contentPadding}; ${row.well}; measure ${row.measure}`;
|
|
1133
1421
|
}
|
|
1134
1422
|
|
|
1423
|
+
function formatDesignRecipe(recipe) {
|
|
1424
|
+
return ` ${recipe.name}: ${recipe.purpose}\n section entrance ${recipe.section.entrance}; stagger ${recipe.section.entranceStagger}\n${
|
|
1425
|
+
recipe.components.map((component) =>
|
|
1426
|
+
` ${component.type}: ${Object.entries(component.properties).map(([name, value]) => `${name}=${JSON.stringify(value)}`).join(", ")}`
|
|
1427
|
+
).join("\n")
|
|
1428
|
+
}`;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1135
1431
|
export function formatReferenceResult(result) {
|
|
1136
1432
|
switch (result.topic) {
|
|
1137
1433
|
case "topics":
|
|
@@ -1150,6 +1446,22 @@ export function formatReferenceResult(result) {
|
|
|
1150
1446
|
` ${component.type.padEnd(16)} ${component.summary}\n${" ".repeat(19)}Help: ${component.helpCommand}`
|
|
1151
1447
|
).join("\n")
|
|
1152
1448
|
}\n`;
|
|
1449
|
+
case "design-types":
|
|
1450
|
+
return `Composable design blueprints:\nThese are starting points, not templates or instructions to copy protected assets or trade dress. Site purpose, visual language, and interaction intensity are intentionally orthogonal decisions.\n\n${
|
|
1451
|
+
result.designs.map((design) =>
|
|
1452
|
+
` ${design.type.padEnd(24)} ${design.summary}\n${" ".repeat(27)}Purpose: ${design.sitePurpose}\n${" ".repeat(27)}Visual language: ${design.visualLanguage}\n${" ".repeat(27)}Interaction intensity: ${design.interactionIntensity}\n${" ".repeat(27)}Help: ${design.helpCommand}`
|
|
1453
|
+
).join("\n")
|
|
1454
|
+
}\n`;
|
|
1455
|
+
case "design": {
|
|
1456
|
+
const design = result.design;
|
|
1457
|
+
return `${design.displayName} (${design.type})\n${design.summary}\n\nThese are composable starting points, not templates or instructions to copy protected assets or trade dress.\n\nSite purpose: ${design.sitePurpose}\nVisual language: ${design.visualLanguage}\nInteraction intensity: ${design.interactionIntensity}\n\nContent model:\n Primary: ${design.contentModel.primary.join("; ")}\n Supporting: ${design.contentModel.supporting.join("; ")}\n\nRepresentative pages:\n${
|
|
1458
|
+
design.representativePages.map((page) => ` ${page.title}: ${page.purpose}`).join("\n")
|
|
1459
|
+
}\n\nNavigation shape: ${design.navigationShape}\nTheme direction: ${design.themeDirection}\n\nComponent and section recipes:\n${
|
|
1460
|
+
design.recipes.map(formatDesignRecipe).join("\n")
|
|
1461
|
+
}\n\nMotion ceiling: ${design.motionCeiling}\nMedia brief: ${design.mediaBrief}\nIntegration boundaries: ${design.integrationBoundaries}\n\nAccessibility cautions:\n${
|
|
1462
|
+
design.accessibilityCautions.map((caution) => ` - ${caution}`).join("\n")
|
|
1463
|
+
}\n\nUse --json for the stable structured recipes.\n`;
|
|
1464
|
+
}
|
|
1153
1465
|
case "page": {
|
|
1154
1466
|
const page = result.page;
|
|
1155
1467
|
const markdownFormat = page.workspace.formats.find((format) => format.extension === ".md");
|