levrops-contracts 1.3.1 → 1.3.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 +83 -0
- package/contracts/backend/schema/migration_discipline.schema.yaml +220 -0
- package/contracts/content/heirloom/lead-capture.ts +212 -0
- package/contracts/content/index.ts +8 -0
- package/creative/README.md +21 -0
- package/creative/creative_cluster.schema.json +44 -0
- package/creative/creative_edge_alignment.schema.json +59 -0
- package/creative/creative_signal_profile.schema.json +71 -0
- package/creative/creative_whisper.schema.json +70 -0
- package/creative/models.py +86 -0
- package/creative/requirements.txt +2 -0
- package/package.json +6 -4
- package/sanity/schema/generated/blocks/cta.ts +68 -0
- package/sanity/schema/generated/blocks/editorial_grid.ts +58 -0
- package/sanity/schema/generated/blocks/hero.ts +72 -0
- package/sanity/schema/generated/blocks/product.ts +66 -0
- package/sanity/schema/generated/blocks/story.ts +59 -0
- package/sanity/schema/generated/index.ts +16 -0
- package/sanity/schema/generated/page.ts +74 -0
- package/sanity/schema/levropsStructure/blocks/cta.ts +77 -0
- package/sanity/schema/levropsStructure/blocks/editorialGrid.ts +66 -0
- package/sanity/schema/levropsStructure/blocks/hero.ts +71 -0
- package/sanity/schema/levropsStructure/blocks/product.ts +69 -0
- package/sanity/schema/levropsStructure/blocks/ritual.ts +59 -0
- package/sanity/schema/levropsStructure/blocks/story.ts +57 -0
- package/sanity/schema/levropsStructure/index.ts +21 -0
- package/sanity/schema/levropsStructure/page.ts +118 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://contracts.levrops.com/creative/creative_signal_profile.schema.json",
|
|
4
|
+
"title": "CreativeSignalProfile",
|
|
5
|
+
"description": "Extracted creative signal profile from a single asset. Captures emotional, thematic, and narrative dimensions for Creative Whisperer synthesis.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["asset_id", "asset_type", "energy"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"asset_id": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Unique identifier of the source asset",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"maxLength": 255
|
|
14
|
+
},
|
|
15
|
+
"asset_type": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Type of creative asset",
|
|
18
|
+
"enum": ["note", "audio", "url", "lyric", "image", "other"]
|
|
19
|
+
},
|
|
20
|
+
"energy": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Perceived energy level of the creative content",
|
|
23
|
+
"enum": ["low", "mid", "high"]
|
|
24
|
+
},
|
|
25
|
+
"emotions": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"description": "Detected emotional tones",
|
|
28
|
+
"items": { "type": "string", "maxLength": 100 },
|
|
29
|
+
"maxItems": 20,
|
|
30
|
+
"default": []
|
|
31
|
+
},
|
|
32
|
+
"themes": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"description": "Recurring themes or topics",
|
|
35
|
+
"items": { "type": "string", "maxLength": 100 },
|
|
36
|
+
"maxItems": 20,
|
|
37
|
+
"default": []
|
|
38
|
+
},
|
|
39
|
+
"narrative_posture": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Stance or posture of the narrative voice",
|
|
42
|
+
"maxLength": 200
|
|
43
|
+
},
|
|
44
|
+
"intensity": {
|
|
45
|
+
"type": "number",
|
|
46
|
+
"description": "Intensity score (0–1)",
|
|
47
|
+
"minimum": 0,
|
|
48
|
+
"maximum": 1
|
|
49
|
+
},
|
|
50
|
+
"vulnerability": {
|
|
51
|
+
"type": "number",
|
|
52
|
+
"description": "Vulnerability/openness score (0–1)",
|
|
53
|
+
"minimum": 0,
|
|
54
|
+
"maximum": 1
|
|
55
|
+
},
|
|
56
|
+
"certainty": {
|
|
57
|
+
"type": "number",
|
|
58
|
+
"description": "Certainty/conviction score (0–1)",
|
|
59
|
+
"minimum": 0,
|
|
60
|
+
"maximum": 1
|
|
61
|
+
},
|
|
62
|
+
"imagery": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"description": "Evocative imagery or visual motifs",
|
|
65
|
+
"items": { "type": "string", "maxLength": 200 },
|
|
66
|
+
"maxItems": 30,
|
|
67
|
+
"default": []
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"additionalProperties": false
|
|
71
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://contracts.levrops.com/creative/creative_whisper.schema.json",
|
|
4
|
+
"title": "CreativeWhisper",
|
|
5
|
+
"description": "Synthesis output from Creative Whisperer. Summarizes clusters, patterns, tensions, and suggests follow-up exploration with citations.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["cluster_summary", "confidence"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"cluster_summary": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Summary of the creative cluster(s) analyzed",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"maxLength": 2000
|
|
14
|
+
},
|
|
15
|
+
"detected_pattern": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Detected creative pattern or recurring motif",
|
|
18
|
+
"maxLength": 500
|
|
19
|
+
},
|
|
20
|
+
"creative_tension": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Identified creative tension or dialectic",
|
|
23
|
+
"maxLength": 500
|
|
24
|
+
},
|
|
25
|
+
"suggested_exploration": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "Suggested direction for further creative exploration",
|
|
28
|
+
"maxLength": 1000
|
|
29
|
+
},
|
|
30
|
+
"confidence": {
|
|
31
|
+
"type": "number",
|
|
32
|
+
"description": "Confidence in the synthesis (0–1)",
|
|
33
|
+
"minimum": 0,
|
|
34
|
+
"maximum": 1
|
|
35
|
+
},
|
|
36
|
+
"followup_questions": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"description": "Suggested follow-up questions to deepen exploration",
|
|
39
|
+
"items": { "type": "string", "maxLength": 300 },
|
|
40
|
+
"maxItems": 10,
|
|
41
|
+
"default": []
|
|
42
|
+
},
|
|
43
|
+
"citations": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"description": "Citations referencing asset IDs with short quotes",
|
|
46
|
+
"items": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"required": ["asset_id", "quote"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"asset_id": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "ID of the cited asset",
|
|
53
|
+
"minLength": 1,
|
|
54
|
+
"maxLength": 255
|
|
55
|
+
},
|
|
56
|
+
"quote": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "Short excerpt (≤140 chars)",
|
|
59
|
+
"minLength": 1,
|
|
60
|
+
"maxLength": 140
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"additionalProperties": false
|
|
64
|
+
},
|
|
65
|
+
"maxItems": 20,
|
|
66
|
+
"default": []
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"additionalProperties": false
|
|
70
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for Creative Whisperer synthesis contracts.
|
|
3
|
+
|
|
4
|
+
Mirrors the JSON schemas in creative/*.schema.json.
|
|
5
|
+
Install: pip install pydantic
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AssetType(str, Enum):
|
|
15
|
+
"""Creative asset type."""
|
|
16
|
+
|
|
17
|
+
NOTE = "note"
|
|
18
|
+
AUDIO = "audio"
|
|
19
|
+
URL = "url"
|
|
20
|
+
LYRIC = "lyric"
|
|
21
|
+
IMAGE = "image"
|
|
22
|
+
OTHER = "other"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Energy(str, Enum):
|
|
26
|
+
"""Perceived energy level."""
|
|
27
|
+
|
|
28
|
+
LOW = "low"
|
|
29
|
+
MID = "mid"
|
|
30
|
+
HIGH = "high"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CreativeSignalProfile(BaseModel):
|
|
34
|
+
"""Extracted creative signal profile from a single asset."""
|
|
35
|
+
|
|
36
|
+
asset_id: str = Field(..., min_length=1, max_length=255)
|
|
37
|
+
asset_type: AssetType
|
|
38
|
+
energy: Energy
|
|
39
|
+
emotions: list[str] = Field(default_factory=list, max_length=20)
|
|
40
|
+
themes: list[str] = Field(default_factory=list, max_length=20)
|
|
41
|
+
narrative_posture: str | None = Field(None, max_length=200)
|
|
42
|
+
intensity: float | None = Field(None, ge=0.0, le=1.0)
|
|
43
|
+
vulnerability: float | None = Field(None, ge=0.0, le=1.0)
|
|
44
|
+
certainty: float | None = Field(None, ge=0.0, le=1.0)
|
|
45
|
+
imagery: list[str] = Field(default_factory=list, max_length=30)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CreativeEdgeAlignment(BaseModel):
|
|
49
|
+
"""Alignment metrics between two creative signal profiles."""
|
|
50
|
+
|
|
51
|
+
source_asset_id: str = Field(..., min_length=1, max_length=255)
|
|
52
|
+
target_asset_id: str = Field(..., min_length=1, max_length=255)
|
|
53
|
+
semantic_score: float = Field(..., ge=0.0, le=1.0)
|
|
54
|
+
emotional_score: float = Field(..., ge=0.0, le=1.0)
|
|
55
|
+
theme_overlap_count: int = Field(..., ge=0)
|
|
56
|
+
motif_overlap_count: int = Field(..., ge=0)
|
|
57
|
+
resonance_score: float = Field(..., ge=0.0, le=1.0)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CreativeCluster(BaseModel):
|
|
61
|
+
"""Cluster of related creative signal profiles."""
|
|
62
|
+
|
|
63
|
+
cluster_id: str = Field(..., min_length=1, max_length=255)
|
|
64
|
+
asset_ids: list[str] = Field(..., min_length=1, max_length=100)
|
|
65
|
+
dominant_emotions: list[str] = Field(default_factory=list, max_length=10)
|
|
66
|
+
dominant_themes: list[str] = Field(default_factory=list, max_length=10)
|
|
67
|
+
cohesion_score: float | None = Field(None, ge=0.0, le=1.0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CreativeWhisperCitation(BaseModel):
|
|
71
|
+
"""Citation referencing an asset with a short quote."""
|
|
72
|
+
|
|
73
|
+
asset_id: str = Field(..., min_length=1, max_length=255)
|
|
74
|
+
quote: str = Field(..., min_length=1, max_length=140)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CreativeWhisper(BaseModel):
|
|
78
|
+
"""Synthesis output from Creative Whisperer."""
|
|
79
|
+
|
|
80
|
+
cluster_summary: str = Field(..., min_length=1, max_length=2000)
|
|
81
|
+
detected_pattern: str | None = Field(None, max_length=500)
|
|
82
|
+
creative_tension: str | None = Field(None, max_length=500)
|
|
83
|
+
suggested_exploration: str | None = Field(None, max_length=1000)
|
|
84
|
+
confidence: float = Field(..., ge=0.0, le=1.0)
|
|
85
|
+
followup_questions: list[str] = Field(default_factory=list, max_length=10)
|
|
86
|
+
citations: list[CreativeWhisperCitation] = Field(default_factory=list, max_length=20)
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "levrops-contracts",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "LevrOps API contracts, schemas, code generators, and Sanity content contracts",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/jackmckracken/levrops-contracts.git"
|
|
8
|
+
"url": "git+https://github.com/jackmckracken/levrops-contracts.git"
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://github.com/jackmckracken/levrops-contracts#readme",
|
|
11
11
|
"type": "module",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"contracts/",
|
|
26
|
+
"creative/",
|
|
26
27
|
"sanity/",
|
|
27
28
|
"index.ts",
|
|
28
29
|
"README.md",
|
|
@@ -31,13 +32,15 @@
|
|
|
31
32
|
],
|
|
32
33
|
"scripts": {
|
|
33
34
|
"sanity:codegen": "tsx tools/levrops-sanity-codegen.ts",
|
|
34
|
-
"sanity:check": "tsx tools/sanity-check.ts"
|
|
35
|
+
"sanity:check": "tsx tools/sanity-check.ts",
|
|
36
|
+
"test:creative": "node --test tests/creative/validation.test.mjs"
|
|
35
37
|
},
|
|
36
38
|
"dependencies": {
|
|
37
39
|
"sanity": "^3.56.0"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@types/node": "^20.11.24",
|
|
43
|
+
"ajv": "^8.12.0",
|
|
41
44
|
"tsx": "^4.7.0",
|
|
42
45
|
"typescript": "^5.4.0"
|
|
43
46
|
},
|
|
@@ -48,4 +51,3 @@
|
|
|
48
51
|
"access": "public"
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
|
-
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { defineType, defineField, string, text, url, image, boolean, number, array, reference } from "sanity";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CTA
|
|
5
|
+
*
|
|
6
|
+
* Generated from Structure DSL block: "cta"
|
|
7
|
+
* Sanity type: "block.cta"
|
|
8
|
+
* Call to action block
|
|
9
|
+
*/
|
|
10
|
+
export const blockCTA = defineType({
|
|
11
|
+
name: "block.cta",
|
|
12
|
+
title: "CTA",
|
|
13
|
+
type: "object",
|
|
14
|
+
description: "Call to action block",
|
|
15
|
+
fields: [
|
|
16
|
+
defineField({
|
|
17
|
+
name: "_key",
|
|
18
|
+
type: "string",
|
|
19
|
+
title: "Key",
|
|
20
|
+
description: "Unique key for array item (auto-generated)",
|
|
21
|
+
readOnly: true,
|
|
22
|
+
}),
|
|
23
|
+
defineField({
|
|
24
|
+
name: "headline",
|
|
25
|
+
type: string(),
|
|
26
|
+
title: "Headline",
|
|
27
|
+
description: "Constraints: Max 100 characters",
|
|
28
|
+
validation: (Rule) => Rule.required().max(100),
|
|
29
|
+
}),
|
|
30
|
+
defineField({
|
|
31
|
+
name: "description",
|
|
32
|
+
type: string(),
|
|
33
|
+
title: "Description",
|
|
34
|
+
description: "Constraints: Max 300 characters",
|
|
35
|
+
validation: (Rule) => Rule.max(300),
|
|
36
|
+
}),
|
|
37
|
+
defineField({
|
|
38
|
+
name: "buttonText",
|
|
39
|
+
type: string(),
|
|
40
|
+
title: "Button Text",
|
|
41
|
+
description: "Constraints: Max 50 characters",
|
|
42
|
+
validation: (Rule) => Rule.required().max(50),
|
|
43
|
+
}),
|
|
44
|
+
defineField({
|
|
45
|
+
name: "buttonLink",
|
|
46
|
+
type: url(),
|
|
47
|
+
title: "Button Link",
|
|
48
|
+
validation: (Rule) => Rule.required(),
|
|
49
|
+
}),
|
|
50
|
+
defineField({
|
|
51
|
+
name: "variant",
|
|
52
|
+
type: string(),
|
|
53
|
+
title: "Variant",
|
|
54
|
+
initialValue: "primary",
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
preview: {
|
|
58
|
+
select: {
|
|
59
|
+
headline: "headline",
|
|
60
|
+
},
|
|
61
|
+
prepare({ headline }) {
|
|
62
|
+
return {
|
|
63
|
+
title: headline || "CTA",
|
|
64
|
+
subtitle: "Call to action block",
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineType, defineField, string, text, url, image, boolean, number, array, reference } from "sanity";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Editorial Grid
|
|
5
|
+
*
|
|
6
|
+
* Generated from Structure DSL block: "editorial_grid"
|
|
7
|
+
* Sanity type: "block.editorial_grid"
|
|
8
|
+
* Grid of editorial content
|
|
9
|
+
*/
|
|
10
|
+
export const blockEditorialGrid = defineType({
|
|
11
|
+
name: "block.editorial_grid",
|
|
12
|
+
title: "Editorial Grid",
|
|
13
|
+
type: "object",
|
|
14
|
+
description: "Grid of editorial content",
|
|
15
|
+
fields: [
|
|
16
|
+
defineField({
|
|
17
|
+
name: "_key",
|
|
18
|
+
type: "string",
|
|
19
|
+
title: "Key",
|
|
20
|
+
description: "Unique key for array item (auto-generated)",
|
|
21
|
+
readOnly: true,
|
|
22
|
+
}),
|
|
23
|
+
defineField({
|
|
24
|
+
name: "title",
|
|
25
|
+
type: string(),
|
|
26
|
+
title: "Title",
|
|
27
|
+
description: "Constraints: Max 200 characters",
|
|
28
|
+
validation: (Rule) => Rule.required().max(200),
|
|
29
|
+
}),
|
|
30
|
+
defineField({
|
|
31
|
+
name: "items",
|
|
32
|
+
type: array({
|
|
33
|
+
of: [{ type: "reference", to: [{ type: "blog-post" }] }],
|
|
34
|
+
}),
|
|
35
|
+
title: "Items",
|
|
36
|
+
validation: (Rule) => Rule.required().min(1),
|
|
37
|
+
}),
|
|
38
|
+
defineField({
|
|
39
|
+
name: "columns",
|
|
40
|
+
type: number(),
|
|
41
|
+
title: "Columns",
|
|
42
|
+
description: "Constraints: Min value: 1, Max value: 4",
|
|
43
|
+
validation: (Rule) => Rule.min(1).max(4),
|
|
44
|
+
initialValue: 3,
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
47
|
+
preview: {
|
|
48
|
+
select: {
|
|
49
|
+
title: "title",
|
|
50
|
+
},
|
|
51
|
+
prepare({ title }) {
|
|
52
|
+
return {
|
|
53
|
+
title: title || "Editorial Grid",
|
|
54
|
+
subtitle: "Grid of editorial content",
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { defineType, defineField, string, text, url, image, boolean, number, array, reference } from "sanity";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hero
|
|
5
|
+
*
|
|
6
|
+
* Generated from Structure DSL block: "hero"
|
|
7
|
+
* Sanity type: "block.hero"
|
|
8
|
+
* Large headline with supporting text and CTA
|
|
9
|
+
*/
|
|
10
|
+
export const blockHero = defineType({
|
|
11
|
+
name: "block.hero",
|
|
12
|
+
title: "Hero",
|
|
13
|
+
type: "object",
|
|
14
|
+
description: "Large headline with supporting text and CTA",
|
|
15
|
+
fields: [
|
|
16
|
+
defineField({
|
|
17
|
+
name: "_key",
|
|
18
|
+
type: "string",
|
|
19
|
+
title: "Key",
|
|
20
|
+
description: "Unique key for array item (auto-generated)",
|
|
21
|
+
readOnly: true,
|
|
22
|
+
}),
|
|
23
|
+
defineField({
|
|
24
|
+
name: "headline",
|
|
25
|
+
type: string(),
|
|
26
|
+
title: "Headline",
|
|
27
|
+
description: "Constraints: Max 100 characters",
|
|
28
|
+
validation: (Rule) => Rule.required().max(100),
|
|
29
|
+
}),
|
|
30
|
+
defineField({
|
|
31
|
+
name: "subheadline",
|
|
32
|
+
type: string(),
|
|
33
|
+
title: "Subheadline",
|
|
34
|
+
description: "Constraints: Max 200 characters",
|
|
35
|
+
validation: (Rule) => Rule.max(200),
|
|
36
|
+
}),
|
|
37
|
+
defineField({
|
|
38
|
+
name: "backgroundImage",
|
|
39
|
+
type: image({
|
|
40
|
+
options: {
|
|
41
|
+
hotspot: true,
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
title: "Background Image",
|
|
45
|
+
description: "Constraints: Allowed formats: jpg, png, webp, Max size: 5.0MB",
|
|
46
|
+
validation: (Rule) => Rule.required(),
|
|
47
|
+
}),
|
|
48
|
+
defineField({
|
|
49
|
+
name: "ctaText",
|
|
50
|
+
type: string(),
|
|
51
|
+
title: "CTA Button Text",
|
|
52
|
+
description: "Constraints: Max 50 characters",
|
|
53
|
+
validation: (Rule) => Rule.max(50),
|
|
54
|
+
}),
|
|
55
|
+
defineField({
|
|
56
|
+
name: "ctaLink",
|
|
57
|
+
type: url(),
|
|
58
|
+
title: "CTA Link",
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
preview: {
|
|
62
|
+
select: {
|
|
63
|
+
headline: "headline",
|
|
64
|
+
},
|
|
65
|
+
prepare({ headline }) {
|
|
66
|
+
return {
|
|
67
|
+
title: headline || "Hero",
|
|
68
|
+
subtitle: "Large headline with supporting text and CTA",
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { defineType, defineField, string, text, url, image, boolean, number, array, reference } from "sanity";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Product Block
|
|
5
|
+
*
|
|
6
|
+
* Generated from Structure DSL block: "product"
|
|
7
|
+
* Sanity type: "block.product"
|
|
8
|
+
* Product showcase block
|
|
9
|
+
*/
|
|
10
|
+
export const blockProductBlock = defineType({
|
|
11
|
+
name: "block.product",
|
|
12
|
+
title: "Product Block",
|
|
13
|
+
type: "object",
|
|
14
|
+
description: "Product showcase block",
|
|
15
|
+
fields: [
|
|
16
|
+
defineField({
|
|
17
|
+
name: "_key",
|
|
18
|
+
type: "string",
|
|
19
|
+
title: "Key",
|
|
20
|
+
description: "Unique key for array item (auto-generated)",
|
|
21
|
+
readOnly: true,
|
|
22
|
+
}),
|
|
23
|
+
defineField({
|
|
24
|
+
name: "title",
|
|
25
|
+
type: string(),
|
|
26
|
+
title: "Title",
|
|
27
|
+
description: "Constraints: Max 200 characters",
|
|
28
|
+
validation: (Rule) => Rule.required().max(200),
|
|
29
|
+
}),
|
|
30
|
+
defineField({
|
|
31
|
+
name: "description",
|
|
32
|
+
type: string(),
|
|
33
|
+
title: "Description",
|
|
34
|
+
description: "Constraints: Max 500 characters",
|
|
35
|
+
validation: (Rule) => Rule.max(500),
|
|
36
|
+
}),
|
|
37
|
+
defineField({
|
|
38
|
+
name: "products",
|
|
39
|
+
type: array({
|
|
40
|
+
of: [{ type: "reference", to: [{ type: "product" }] }],
|
|
41
|
+
}),
|
|
42
|
+
title: "Products",
|
|
43
|
+
validation: (Rule) => Rule.required().min(1).max(10),
|
|
44
|
+
}),
|
|
45
|
+
defineField({
|
|
46
|
+
name: "image",
|
|
47
|
+
type: image({
|
|
48
|
+
options: {
|
|
49
|
+
hotspot: true,
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
title: "Image",
|
|
53
|
+
}),
|
|
54
|
+
],
|
|
55
|
+
preview: {
|
|
56
|
+
select: {
|
|
57
|
+
title: "title",
|
|
58
|
+
},
|
|
59
|
+
prepare({ title }) {
|
|
60
|
+
return {
|
|
61
|
+
title: title || "Product Block",
|
|
62
|
+
subtitle: "Product showcase block",
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { defineType, defineField, string, text, url, image, boolean, number, array, reference } from "sanity";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Story Section
|
|
5
|
+
*
|
|
6
|
+
* Generated from Structure DSL block: "story"
|
|
7
|
+
* Sanity type: "block.story"
|
|
8
|
+
* Narrative content block
|
|
9
|
+
*/
|
|
10
|
+
export const blockStorySection = defineType({
|
|
11
|
+
name: "block.story",
|
|
12
|
+
title: "Story Section",
|
|
13
|
+
type: "object",
|
|
14
|
+
description: "Narrative content block",
|
|
15
|
+
fields: [
|
|
16
|
+
defineField({
|
|
17
|
+
name: "_key",
|
|
18
|
+
type: "string",
|
|
19
|
+
title: "Key",
|
|
20
|
+
description: "Unique key for array item (auto-generated)",
|
|
21
|
+
readOnly: true,
|
|
22
|
+
}),
|
|
23
|
+
defineField({
|
|
24
|
+
name: "title",
|
|
25
|
+
type: string(),
|
|
26
|
+
title: "Title",
|
|
27
|
+
description: "Constraints: Max 200 characters",
|
|
28
|
+
validation: (Rule) => Rule.required().max(200),
|
|
29
|
+
}),
|
|
30
|
+
defineField({
|
|
31
|
+
name: "content",
|
|
32
|
+
type: array({
|
|
33
|
+
of: [{ type: "block" }],
|
|
34
|
+
}),
|
|
35
|
+
title: "Content",
|
|
36
|
+
validation: (Rule) => Rule.required(),
|
|
37
|
+
}),
|
|
38
|
+
defineField({
|
|
39
|
+
name: "image",
|
|
40
|
+
type: image({
|
|
41
|
+
options: {
|
|
42
|
+
hotspot: true,
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
title: "Image",
|
|
46
|
+
}),
|
|
47
|
+
],
|
|
48
|
+
preview: {
|
|
49
|
+
select: {
|
|
50
|
+
title: "title",
|
|
51
|
+
},
|
|
52
|
+
prepare({ title }) {
|
|
53
|
+
return {
|
|
54
|
+
title: title || "Story Section",
|
|
55
|
+
subtitle: "Narrative content block",
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated Sanity Schema Index
|
|
3
|
+
*
|
|
4
|
+
* This file is auto-generated from Structure DSL.
|
|
5
|
+
* DO NOT EDIT THIS FILE DIRECTLY.
|
|
6
|
+
*
|
|
7
|
+
* To regenerate:
|
|
8
|
+
* node tools/codegen/structure-to-sanity/index.js --input <path> --out <dir>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { page } from './page';
|
|
12
|
+
export { blockHero } from './blocks/hero';
|
|
13
|
+
export { blockStorySection } from './blocks/story';
|
|
14
|
+
export { blockProductBlock } from './blocks/product';
|
|
15
|
+
export { blockEditorialGrid } from './blocks/editorial_grid';
|
|
16
|
+
export { blockCTA } from './blocks/cta';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { defineType, defineField, string, slug, array } from "sanity";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Page Document
|
|
5
|
+
*
|
|
6
|
+
* Generated from Structure DSL page templates.
|
|
7
|
+
* Single document type with pageType discriminant.
|
|
8
|
+
*/
|
|
9
|
+
export const page = defineType({
|
|
10
|
+
name: "page",
|
|
11
|
+
title: "Page",
|
|
12
|
+
type: "document",
|
|
13
|
+
description: "A page composed of ordered blocks, discriminated by pageType",
|
|
14
|
+
fields: [
|
|
15
|
+
defineField({
|
|
16
|
+
name: "title",
|
|
17
|
+
type: string(),
|
|
18
|
+
title: "Title",
|
|
19
|
+
validation: (Rule) => Rule.required().max(200),
|
|
20
|
+
description: "Page title",
|
|
21
|
+
}),
|
|
22
|
+
defineField({
|
|
23
|
+
name: "slug",
|
|
24
|
+
type: slug({
|
|
25
|
+
source: "title",
|
|
26
|
+
}),
|
|
27
|
+
title: "Slug",
|
|
28
|
+
validation: (Rule) => Rule.required(),
|
|
29
|
+
description: "URL-friendly identifier",
|
|
30
|
+
}),
|
|
31
|
+
defineField({
|
|
32
|
+
name: "pageType",
|
|
33
|
+
type: string(),
|
|
34
|
+
title: "Page Type",
|
|
35
|
+
validation: (Rule) => Rule.required(),
|
|
36
|
+
description: "Template type (maps to Structure DSL page template ID)",
|
|
37
|
+
options: {
|
|
38
|
+
list: [
|
|
39
|
+
{ title: "Homepage", value: "homepage" },
|
|
40
|
+
{ title: "Product Page", value: "product_page" },
|
|
41
|
+
{ title: "Editorial Page", value: "editorial_page" },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
defineField({
|
|
46
|
+
name: "content",
|
|
47
|
+
type: array({
|
|
48
|
+
of: [
|
|
49
|
+
{ type: "block.hero" },
|
|
50
|
+
{ type: "block.story" },
|
|
51
|
+
{ type: "block.product" },
|
|
52
|
+
{ type: "block.editorial_grid" },
|
|
53
|
+
{ type: "block.cta" },
|
|
54
|
+
],
|
|
55
|
+
}),
|
|
56
|
+
title: "Content Blocks",
|
|
57
|
+
description: "Ordered composition of content blocks",
|
|
58
|
+
validation: (Rule) => Rule.required().min(1),
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
preview: {
|
|
62
|
+
select: {
|
|
63
|
+
title: "title",
|
|
64
|
+
pageType: "pageType",
|
|
65
|
+
slug: "slug.current",
|
|
66
|
+
},
|
|
67
|
+
prepare({ title, pageType, slug }) {
|
|
68
|
+
return {
|
|
69
|
+
title: title || "Untitled Page",
|
|
70
|
+
subtitle: `${pageType || "unknown"} • /${slug || ""}`,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|