minutework 0.1.55 → 0.1.57
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/assets/claude-local/CLAUDE.md.template +19 -0
- package/assets/claude-local/skills/README.md +2 -0
- package/assets/claude-local/skills/landing-page-assets/SKILL.md +153 -0
- package/assets/claude-local/skills/runtime-capability-inventory/SKILL.md +3 -0
- package/assets/claude-local/skills/runtime-capability-inventory/primitive-catalog.json +19 -0
- package/assets/claude-local/skills/visualize-context/SKILL.md +97 -0
- package/assets/claude-local/skills/workspace-guidance-refresh/SKILL.md +6 -0
- package/assets/templates/vuilder-public-site/src/app/globals.css +95 -0
- package/assets/templates/vuilder-public-site/src/app/page.tsx +214 -13
- package/assets/templates/vuilder-public-site/src/lib/public-dj.server.ts +48 -2
- package/dist/developer-client.d.ts +5 -0
- package/dist/developer-client.js +8 -0
- package/dist/developer-client.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/reporting.js +1 -1
- package/dist/reporting.js.map +1 -1
- package/dist/sandbox.js +209 -3
- package/dist/sandbox.js.map +1 -1
- package/package.json +2 -2
- package/vendor/workspace-mcp/types.d.ts +28 -0
|
@@ -73,6 +73,25 @@ When the task involves event-driven behavior or reacting to external/reference
|
|
|
73
73
|
datasets, read `skills/event-bus/SKILL.md` and
|
|
74
74
|
`skills/cross-server-subscriptions/SKILL.md`.
|
|
75
75
|
|
|
76
|
+
## Authored Workspace Overlays
|
|
77
|
+
|
|
78
|
+
Everything under `skills/` plus `CLAUDE.md` and `AGENTS.md` is **managed**:
|
|
79
|
+
`minutework workspace sync-assets` regenerates it from the MinuteWork baseline.
|
|
80
|
+
This workspace may also carry **authored overlays** that capture intent specific
|
|
81
|
+
to this build and are **never** overwritten by sync:
|
|
82
|
+
|
|
83
|
+
- `CLAUDE.workspace.md` / `AGENTS.workspace.md` — authored guidance. Read the one
|
|
84
|
+
your engine uses (`CLAUDE.workspace.md` in Claude Code; `AGENTS.workspace.md`
|
|
85
|
+
in Codex/Cursor/other agents). They typically mirror each other.
|
|
86
|
+
- `skills.workspace/<name>/SKILL.md` — authored skills. Browse this directory the
|
|
87
|
+
same way you browse `skills/`, and read a `SKILL.md` when its topic matches.
|
|
88
|
+
|
|
89
|
+
When an authored overlay and the managed baseline disagree for this workspace,
|
|
90
|
+
the authored overlay wins — it was written for this specific product. Treat the
|
|
91
|
+
overlays as your own to edit; do not move authored content into the managed
|
|
92
|
+
`CLAUDE.md`, `AGENTS.md`, or `skills/` files, because the next
|
|
93
|
+
`minutework workspace sync-assets` would discard it.
|
|
94
|
+
|
|
76
95
|
## Capability Gaps
|
|
77
96
|
|
|
78
97
|
When a request exposes missing shared MinuteWork substrate, read
|
|
@@ -54,6 +54,8 @@ Generated-workspace-first guidance should live here, especially:
|
|
|
54
54
|
- `vuilder-workspace-architecture/SKILL.md`
|
|
55
55
|
- `vuilder-public-site-authoring/SKILL.md`
|
|
56
56
|
- `vuilder-mode-b-hosted-and-spawn/SKILL.md`
|
|
57
|
+
- `visualize-context/SKILL.md`
|
|
58
|
+
- `landing-page-assets/SKILL.md`
|
|
57
59
|
- `workspace-guidance-refresh/SKILL.md`
|
|
58
60
|
- `shell-architecture/SKILL.md`
|
|
59
61
|
- `runtime-capability-inventory/SKILL.md`
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: landing-page-assets
|
|
3
|
+
description: "Generate website-ready hero images, backgrounds, section visuals, textures, and OG assets, then wire selected images into public-site surfaces."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Landing Page Assets
|
|
7
|
+
|
|
8
|
+
Use this skill when the user asks for website-bound imagery, for example:
|
|
9
|
+
|
|
10
|
+
- "generate hero image"
|
|
11
|
+
- "background image"
|
|
12
|
+
- "section below it"
|
|
13
|
+
- "website asset"
|
|
14
|
+
- "landing page visual"
|
|
15
|
+
- "use this in the site"
|
|
16
|
+
- "OG image"
|
|
17
|
+
- "texture"
|
|
18
|
+
|
|
19
|
+
Default v1 output: generate an actual image, store it, add alt text, and wire
|
|
20
|
+
the selected asset into the target site/page/manifest when a target surface is
|
|
21
|
+
available.
|
|
22
|
+
|
|
23
|
+
## Asset Roles
|
|
24
|
+
|
|
25
|
+
Choose one role before prompting:
|
|
26
|
+
|
|
27
|
+
- `hero_background`: wide atmospheric or metaphor image behind/around HTML text.
|
|
28
|
+
- `hero_visual`: primary product/domain visual next to or within the hero.
|
|
29
|
+
- `section_illustration`: visual supporting a feature/content section.
|
|
30
|
+
- `background_texture`: text-free repeatable or full-bleed texture/motif.
|
|
31
|
+
- `cta_visual`: closing section image that reinforces action.
|
|
32
|
+
- `og_image`: social preview image; text is allowed only when explicitly needed.
|
|
33
|
+
|
|
34
|
+
Strong rule: prefer HTML text over text baked into images. Only generate a
|
|
35
|
+
poster/graphic with text when the user explicitly asks for a graphic image or
|
|
36
|
+
OG card.
|
|
37
|
+
|
|
38
|
+
## Workflow
|
|
39
|
+
|
|
40
|
+
1. Infer the asset role and target section.
|
|
41
|
+
- Use the latest user request, current page JSON, section id, and prior
|
|
42
|
+
visual direction.
|
|
43
|
+
- If a target page/manifest is available, note where the asset should land.
|
|
44
|
+
- If the user only wants exploration, generate a preview artifact and do not
|
|
45
|
+
mutate the site.
|
|
46
|
+
|
|
47
|
+
2. Preserve brand and layout inputs.
|
|
48
|
+
- Keep palette, typography mood, material style, photo/illustration style,
|
|
49
|
+
and metaphor constraints.
|
|
50
|
+
- For verticals like trucking, healthcare, legal, or finance, use grounded
|
|
51
|
+
domain visuals and avoid generic SaaS abstractions.
|
|
52
|
+
|
|
53
|
+
3. Write an asset prompt.
|
|
54
|
+
- Include role, section purpose, subject, composition, palette, lighting,
|
|
55
|
+
texture, crop safety, responsive layout intent, and negative constraints.
|
|
56
|
+
- For backgrounds, explicitly request no text, no UI, no logos, and safe
|
|
57
|
+
empty space where page HTML will sit.
|
|
58
|
+
- For section images, avoid fake product screenshots unless the user asks
|
|
59
|
+
for an interface mockup.
|
|
60
|
+
|
|
61
|
+
4. Generate through `media.generate_image`.
|
|
62
|
+
- `use_case`: `landing_page_asset`.
|
|
63
|
+
- Use 16:9 or 21:9 for hero/backgrounds, 4:3 for section visuals, and
|
|
64
|
+
1200x630 for OG images.
|
|
65
|
+
- Attach generated outputs to the thread for preview.
|
|
66
|
+
|
|
67
|
+
5. Promote the selected asset when it belongs in a public site.
|
|
68
|
+
- Use the public-dj media asset upsert endpoint/service to create or update
|
|
69
|
+
`MediaAsset` by `vertical + asset_key`.
|
|
70
|
+
- Store durable metadata: `asset_key`, `digest`, `url`, `alt_text`, `role`,
|
|
71
|
+
dimensions, mime type, and prompt summary.
|
|
72
|
+
- Do not store long-lived presigned URLs or storage secrets in manifest
|
|
73
|
+
metadata.
|
|
74
|
+
|
|
75
|
+
6. Wire the page/manifest.
|
|
76
|
+
- Add or update manifest `payload.media[]` entries:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"asset_key": "fleet-hero-owner-operator",
|
|
81
|
+
"digest": "sha256:...",
|
|
82
|
+
"url": "/api/v1/media/fleetrun/fleet-hero-owner-operator/sha256:...",
|
|
83
|
+
"alt_text": "Owner-operator reviewing an insurance quote beside a semi truck",
|
|
84
|
+
"role": "hero_background",
|
|
85
|
+
"metadata": {
|
|
86
|
+
"width": 1600,
|
|
87
|
+
"height": 900,
|
|
88
|
+
"mime_type": "image/png"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- In section payloads, reference images by `asset_key` when possible:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"type": "hero_with_cta",
|
|
98
|
+
"headline": "Instant quote. Instant answers.",
|
|
99
|
+
"image": {
|
|
100
|
+
"asset_key": "fleet-hero-owner-operator",
|
|
101
|
+
"role": "hero_background"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- Supported section image fields for v1:
|
|
107
|
+
`hero_with_cta.image`, `feature_grid.items[].image`,
|
|
108
|
+
`content_markdown.image`, and `cta_banner.image`.
|
|
109
|
+
- For workspace-only generated assets, place selected files under the
|
|
110
|
+
site's public asset directory and reference them directly.
|
|
111
|
+
|
|
112
|
+
## Prompt Patterns
|
|
113
|
+
|
|
114
|
+
Hero/background:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
Create a website hero_background for [product/page] serving [audience].
|
|
118
|
+
Subject: [domain-specific scene or metaphor].
|
|
119
|
+
Composition: wide crop, responsive-safe, clear negative space for HTML text on [side].
|
|
120
|
+
Style: [brand style, palette, lighting, realism level].
|
|
121
|
+
No text, no UI, no logos, no fake brand marks.
|
|
122
|
+
Aspect ratio: 16:9 or 21:9.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Section visual:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
Create a section_illustration for the section "[section headline]".
|
|
129
|
+
Show [specific concept/user/workflow] in a grounded way.
|
|
130
|
+
Composition: uncluttered, works beside HTML text, no tiny labels.
|
|
131
|
+
Style: [brand cues].
|
|
132
|
+
Avoid generic SaaS dashboards unless a product UI is explicitly requested.
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
OG image:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
Create an og_image for [product/page].
|
|
139
|
+
Use the brand/product as the first-viewport signal.
|
|
140
|
+
Readable text only if explicitly requested; otherwise leave text to HTML/social metadata.
|
|
141
|
+
Aspect ratio: 1200x630.
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Guardrails
|
|
145
|
+
|
|
146
|
+
- Do not bake landing page body copy into imagery.
|
|
147
|
+
- Do not generate fake customer logos, certifications, insurance carrier logos,
|
|
148
|
+
DOT records, prices, or legal/compliance claims.
|
|
149
|
+
- Do not use dark, blurred, cropped, stock-like imagery when the asset needs to
|
|
150
|
+
communicate the actual product, buyer, or domain.
|
|
151
|
+
- Do not promote an asset into a site unless the user asked to use it or the
|
|
152
|
+
current task is site generation.
|
|
153
|
+
- Always provide alt text for promoted assets.
|
|
@@ -135,6 +135,9 @@ generated Builder workspace.
|
|
|
135
135
|
action capability; preflight fail-closes on connection/secret availability, and the
|
|
136
136
|
delivery row is idempotent on the ActionRun id with a SENT short-circuit. Not
|
|
137
137
|
agent-exposed yet: exposure is a separate per-definition mw.core.agent_actions flip.
|
|
138
|
+
- `media.generate_image` -- Runtime media action capability wraps the existing
|
|
139
|
+
generate_image service, stores outputs in runtime_storage, and attaches
|
|
140
|
+
generated_image thread artifacts when thread context is present.
|
|
138
141
|
- `consent.check_channel` -- Service seam consumed by other executors and sidecars as
|
|
139
142
|
a preflight; not an agent-callable tool.
|
|
140
143
|
- `discovery.request_proposal` -- Effectful; outside the default READ_RING, granted
|
|
@@ -217,6 +217,25 @@
|
|
|
217
217
|
"approval_required": false,
|
|
218
218
|
"interim_path": ""
|
|
219
219
|
},
|
|
220
|
+
{
|
|
221
|
+
"primitive_id": "media.generate_image",
|
|
222
|
+
"family": "media",
|
|
223
|
+
"display_name": "Generate Image",
|
|
224
|
+
"description": "Generate image assets through the runtime media provider and store outputs as runtime files.",
|
|
225
|
+
"runtime_app": "runtime_media",
|
|
226
|
+
"dispatch_path": "",
|
|
227
|
+
"status": "active",
|
|
228
|
+
"requires_integration": false,
|
|
229
|
+
"requires_secret": true,
|
|
230
|
+
"metered": true,
|
|
231
|
+
"executor_ready": "live",
|
|
232
|
+
"executor_ref": "capability:media.generate_image",
|
|
233
|
+
"executor_notes": "Runtime media action capability wraps the existing generate_image service, stores outputs in runtime_storage, and attaches generated_image thread artifacts when thread context is present.",
|
|
234
|
+
"agent_exposable": true,
|
|
235
|
+
"agent_exposure_path": "app_pack_action",
|
|
236
|
+
"approval_required": false,
|
|
237
|
+
"interim_path": ""
|
|
238
|
+
},
|
|
220
239
|
{
|
|
221
240
|
"primitive_id": "consent.check_channel",
|
|
222
241
|
"family": "consent",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: visualize-context
|
|
3
|
+
description: "Generate strategy, product, system, PLG, and concept visuals from messy context, then attach generated images to the Builder thread."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Visualize Context
|
|
7
|
+
|
|
8
|
+
Use this skill when the user asks to visualize strategy/product context, for
|
|
9
|
+
example:
|
|
10
|
+
|
|
11
|
+
- "visualize this"
|
|
12
|
+
- "generate an image"
|
|
13
|
+
- "show the flow"
|
|
14
|
+
- "make this easier to see"
|
|
15
|
+
- "PLG loop"
|
|
16
|
+
- "system map"
|
|
17
|
+
- "architecture visual"
|
|
18
|
+
- "background metaphor"
|
|
19
|
+
|
|
20
|
+
Default v1 output: an actual generated image attached back to the thread as a
|
|
21
|
+
runtime-stored `generated_image` artifact. Do not stop at a prompt unless image
|
|
22
|
+
generation is unavailable.
|
|
23
|
+
|
|
24
|
+
## Workflow
|
|
25
|
+
|
|
26
|
+
1. Extract the decision context.
|
|
27
|
+
- Identify the product, audience, buyer, loop, system, or strategic argument.
|
|
28
|
+
- Preserve named entities, constraints, data flow, and any stated design style.
|
|
29
|
+
- Separate verified facts from interpretation. Do not invent traction,
|
|
30
|
+
customers, logos, certifications, or claims.
|
|
31
|
+
|
|
32
|
+
2. Choose the visual type.
|
|
33
|
+
- `product_flow_map`: activation, value moment, retention, conversion.
|
|
34
|
+
- `plg_loop`: acquisition, free value, habit, share/expand, conversion.
|
|
35
|
+
- `system_diagram`: components, data movement, approvals, runtime boundaries.
|
|
36
|
+
- `strategy_map`: thesis, unfair advantage, risks, market timing, proof.
|
|
37
|
+
- `concept_visual`: metaphor image that makes an abstract idea graspable.
|
|
38
|
+
- `landing_hero_mockup`: first-screen marketing visual preview only. If the
|
|
39
|
+
user wants assets used in a site, switch to `landing-page-assets`.
|
|
40
|
+
|
|
41
|
+
3. Write a structured image prompt.
|
|
42
|
+
- Put the core visual in the first sentence.
|
|
43
|
+
- Include layout, hierarchy, style, palette, target aspect ratio, and what
|
|
44
|
+
should be legible.
|
|
45
|
+
- Use labels sparingly. Prefer large, readable labels over dense tiny text.
|
|
46
|
+
- If the image is meant to explain a complex flow, ask for a clean diagram
|
|
47
|
+
composition, not a photorealistic scene.
|
|
48
|
+
|
|
49
|
+
4. Generate through `media.generate_image`.
|
|
50
|
+
- Use `use_case`: `visualize_context`.
|
|
51
|
+
- Use 16:9 or 4:3 for strategy diagrams; 1:1 only when the user asks for a
|
|
52
|
+
square artifact.
|
|
53
|
+
- Set `request_source_ref` to the current thread/run context when available.
|
|
54
|
+
- Attach the generated output as a thread artifact. The action capability
|
|
55
|
+
does this automatically when thread context is present.
|
|
56
|
+
|
|
57
|
+
5. Respond with the result.
|
|
58
|
+
- Explain the visual in one or two sentences only if needed.
|
|
59
|
+
- Do not claim the image proves anything. It is a visualization aid.
|
|
60
|
+
|
|
61
|
+
## Prompt Contract
|
|
62
|
+
|
|
63
|
+
For visual prompts, include:
|
|
64
|
+
|
|
65
|
+
- `visual_type`
|
|
66
|
+
- `main_subject`
|
|
67
|
+
- `audience`
|
|
68
|
+
- `flow_or_components`
|
|
69
|
+
- `style_reference`
|
|
70
|
+
- `brand_constraints`
|
|
71
|
+
- `must_show`
|
|
72
|
+
- `must_avoid`
|
|
73
|
+
- `aspect_ratio`
|
|
74
|
+
|
|
75
|
+
Use this shape internally:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
Create a [visual_type] showing [main_subject] for [audience].
|
|
79
|
+
Composition: [layout and hierarchy].
|
|
80
|
+
Content: [flow/components, 4-7 max].
|
|
81
|
+
Style: [brand/design cues].
|
|
82
|
+
Readable text: [short labels only].
|
|
83
|
+
Avoid: [fake logos, claims, tiny text, generic stock visuals].
|
|
84
|
+
Aspect ratio: [16:9 / 4:3 / square].
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Guardrails
|
|
88
|
+
|
|
89
|
+
- Do not invent fake customer names, fake logos, fake metrics, fake compliance,
|
|
90
|
+
or market claims.
|
|
91
|
+
- Do not create tiny unreadable UI text. Use simple labels and preserve detail
|
|
92
|
+
in the thread answer or source content.
|
|
93
|
+
- Do not bake long paragraphs into images.
|
|
94
|
+
- Do not use generic SaaS stock visuals when the user supplied a specific
|
|
95
|
+
domain, metaphor, or brand style.
|
|
96
|
+
- If the user asks for a website asset or says "use it in the site", switch to
|
|
97
|
+
`landing-page-assets` because that needs asset role, alt text, and promotion.
|
|
@@ -23,6 +23,12 @@ does not match the current MinuteWork CLI export.
|
|
|
23
23
|
- `mcp/claude-desktop.sample.json`
|
|
24
24
|
- It does not regenerate tenant code, sidecar code, schema files, or arbitrary
|
|
25
25
|
user-owned files outside the managed export set.
|
|
26
|
+
- It never touches the authored overlays: `CLAUDE.workspace.md`,
|
|
27
|
+
`AGENTS.workspace.md`, and `skills.workspace/**`. These are deliberately
|
|
28
|
+
outside the managed manifest, so authored guidance and authored skills survive
|
|
29
|
+
every `sync-assets` (including `--force`) and are never reported as conflicts.
|
|
30
|
+
Keep workspace-specific guidance and skills in the overlays, not in the managed
|
|
31
|
+
`CLAUDE.md` / `AGENTS.md` / `skills/` files.
|
|
26
32
|
- If the workspace uses an older exported skill set, prefer refreshing assets
|
|
27
33
|
before concluding that a Builder capability or instruction is missing.
|
|
28
34
|
- If the workspace lacks sandbox/tier guidance for `minutework sandbox create`,
|
|
@@ -53,6 +53,38 @@ a {
|
|
|
53
53
|
max-width: 760px;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
.hero-with-media {
|
|
57
|
+
grid-template-columns: minmax(0, 0.95fr) minmax(280px, 0.85fr);
|
|
58
|
+
max-width: none;
|
|
59
|
+
align-items: center;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.hero-copy {
|
|
63
|
+
display: grid;
|
|
64
|
+
gap: 24px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.hero-media,
|
|
68
|
+
.section-image,
|
|
69
|
+
.feature-image {
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
border: 1px solid var(--line);
|
|
72
|
+
background: var(--surface);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.hero-media {
|
|
76
|
+
aspect-ratio: 4 / 3;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.hero-media img,
|
|
80
|
+
.section-image img,
|
|
81
|
+
.feature-image img {
|
|
82
|
+
display: block;
|
|
83
|
+
width: 100%;
|
|
84
|
+
height: 100%;
|
|
85
|
+
object-fit: cover;
|
|
86
|
+
}
|
|
87
|
+
|
|
56
88
|
.eyebrow {
|
|
57
89
|
margin: 0;
|
|
58
90
|
color: var(--accent);
|
|
@@ -96,8 +128,71 @@ p {
|
|
|
96
128
|
padding: 24px;
|
|
97
129
|
}
|
|
98
130
|
|
|
131
|
+
.content-section,
|
|
132
|
+
.cta-banner {
|
|
133
|
+
display: grid;
|
|
134
|
+
grid-template-columns: minmax(0, 0.9fr) minmax(240px, 0.7fr);
|
|
135
|
+
gap: 32px;
|
|
136
|
+
align-items: center;
|
|
137
|
+
margin-top: 56px;
|
|
138
|
+
padding-top: 56px;
|
|
139
|
+
border-top: 1px solid var(--line);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.content-section h2,
|
|
143
|
+
.cta-banner h2 {
|
|
144
|
+
margin: 0;
|
|
145
|
+
font-size: clamp(2rem, 5vw, 4rem);
|
|
146
|
+
line-height: 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.section-image {
|
|
150
|
+
aspect-ratio: 4 / 3;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.feature-grid {
|
|
154
|
+
display: grid;
|
|
155
|
+
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
|
156
|
+
gap: 16px;
|
|
157
|
+
margin-top: 28px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.feature-item {
|
|
161
|
+
border: 1px solid var(--line);
|
|
162
|
+
background: var(--surface);
|
|
163
|
+
padding: 18px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.feature-item h3 {
|
|
167
|
+
margin: 14px 0 0;
|
|
168
|
+
font-size: 1rem;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.feature-item p {
|
|
172
|
+
margin-bottom: 0;
|
|
173
|
+
font-size: 0.95rem;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.feature-image {
|
|
177
|
+
aspect-ratio: 16 / 10;
|
|
178
|
+
}
|
|
179
|
+
|
|
99
180
|
.site-footer {
|
|
100
181
|
padding: 32px 0;
|
|
101
182
|
border-top: 1px solid var(--line);
|
|
102
183
|
color: var(--muted);
|
|
103
184
|
}
|
|
185
|
+
|
|
186
|
+
@media (max-width: 760px) {
|
|
187
|
+
.site-header {
|
|
188
|
+
align-items: flex-start;
|
|
189
|
+
gap: 16px;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.hero-with-media,
|
|
194
|
+
.content-section,
|
|
195
|
+
.cta-banner {
|
|
196
|
+
grid-template-columns: 1fr;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -1,15 +1,206 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
|
|
3
3
|
import { appRoutes } from "@/lib/routes";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getHomePage,
|
|
6
|
+
getSiteManifest,
|
|
7
|
+
type PageSection,
|
|
8
|
+
type PublicSiteManifest,
|
|
9
|
+
type PublicSiteMediaAsset,
|
|
10
|
+
} from "@/lib/public-dj.server";
|
|
11
|
+
|
|
12
|
+
type UnknownRecord = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
function asRecord(value: unknown): UnknownRecord | null {
|
|
15
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
16
|
+
? (value as UnknownRecord)
|
|
17
|
+
: null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function asRecordArray(value: unknown): UnknownRecord[] {
|
|
21
|
+
return Array.isArray(value)
|
|
22
|
+
? value.map(asRecord).filter((item): item is UnknownRecord => item !== null)
|
|
23
|
+
: [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function textValue(value: unknown): string {
|
|
27
|
+
return typeof value === "string" ? value.trim() : "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function firstText(...values: unknown[]): string {
|
|
31
|
+
for (const value of values) {
|
|
32
|
+
const text = textValue(value);
|
|
33
|
+
if (text) {
|
|
34
|
+
return text;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mediaByAssetKey(
|
|
41
|
+
manifest: PublicSiteManifest,
|
|
42
|
+
assetKey: string,
|
|
43
|
+
): PublicSiteMediaAsset | null {
|
|
44
|
+
return (
|
|
45
|
+
manifest.payload.media.find(
|
|
46
|
+
(item: PublicSiteMediaAsset) => item.asset_key === assetKey,
|
|
47
|
+
) ?? null
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mediaByRole(
|
|
52
|
+
manifest: PublicSiteManifest,
|
|
53
|
+
roles: string[],
|
|
54
|
+
): PublicSiteMediaAsset | null {
|
|
55
|
+
return (
|
|
56
|
+
manifest.payload.media.find((item: PublicSiteMediaAsset) =>
|
|
57
|
+
roles.includes(item.role),
|
|
58
|
+
) ?? null
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mediaFromImageField(
|
|
63
|
+
manifest: PublicSiteManifest,
|
|
64
|
+
value: unknown,
|
|
65
|
+
): PublicSiteMediaAsset | null {
|
|
66
|
+
const image = asRecord(value);
|
|
67
|
+
if (!image) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const assetKey = firstText(image.asset_key, image.assetKey);
|
|
71
|
+
if (assetKey) {
|
|
72
|
+
return mediaByAssetKey(manifest, assetKey);
|
|
73
|
+
}
|
|
74
|
+
const url = firstText(image.url, image.src);
|
|
75
|
+
if (!url) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
asset_key: firstText(image.asset_key, image.assetKey, `inline-${url}`),
|
|
80
|
+
digest: firstText(image.digest, "inline"),
|
|
81
|
+
url,
|
|
82
|
+
alt_text: firstText(image.alt_text, image.altText, image.alt),
|
|
83
|
+
role: firstText(image.role, "section_illustration"),
|
|
84
|
+
metadata: {},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderImage(asset: PublicSiteMediaAsset, className: string) {
|
|
89
|
+
return (
|
|
90
|
+
<div className={className}>
|
|
91
|
+
<img src={asset.url} alt={asset.alt_text || ""} loading="lazy" />
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sectionImage(
|
|
97
|
+
manifest: PublicSiteManifest,
|
|
98
|
+
section: UnknownRecord,
|
|
99
|
+
): PublicSiteMediaAsset | null {
|
|
100
|
+
return (
|
|
101
|
+
mediaFromImageField(manifest, section.image) ??
|
|
102
|
+
mediaFromImageField(manifest, section.visual) ??
|
|
103
|
+
null
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderSection(
|
|
108
|
+
section: PageSection,
|
|
109
|
+
manifest: PublicSiteManifest,
|
|
110
|
+
index: number,
|
|
111
|
+
) {
|
|
112
|
+
const record = asRecord(section) ?? {};
|
|
113
|
+
const type = firstText(record.type, "section");
|
|
114
|
+
const headline = firstText(record.headline, record.title);
|
|
115
|
+
const body = firstText(record.body, record.subhead, record.summary, record.markdown);
|
|
116
|
+
const image = sectionImage(manifest, record);
|
|
117
|
+
|
|
118
|
+
if (type === "feature_grid") {
|
|
119
|
+
const items = asRecordArray(record.items);
|
|
120
|
+
return (
|
|
121
|
+
<section className="content-section" key={`${type}-${index}`}>
|
|
122
|
+
{image ? renderImage(image, "section-image") : null}
|
|
123
|
+
<div>
|
|
124
|
+
{headline ? <h2>{headline}</h2> : null}
|
|
125
|
+
{body ? <p>{body}</p> : null}
|
|
126
|
+
{items.length ? (
|
|
127
|
+
<div className="feature-grid">
|
|
128
|
+
{items.map((item, itemIndex) => {
|
|
129
|
+
const itemImage = sectionImage(manifest, item);
|
|
130
|
+
return (
|
|
131
|
+
<article className="feature-item" key={`${type}-${index}-${itemIndex}`}>
|
|
132
|
+
{itemImage ? renderImage(itemImage, "feature-image") : null}
|
|
133
|
+
<h3>{firstText(item.headline, item.title)}</h3>
|
|
134
|
+
<p>{firstText(item.body, item.summary)}</p>
|
|
135
|
+
</article>
|
|
136
|
+
);
|
|
137
|
+
})}
|
|
138
|
+
</div>
|
|
139
|
+
) : null}
|
|
140
|
+
</div>
|
|
141
|
+
</section>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (type === "cta_banner") {
|
|
146
|
+
const href = firstText(asRecord(record.cta)?.href) || appRoutes.onboarding();
|
|
147
|
+
const ctaLabel = firstText(asRecord(record.cta)?.label, "Start onboarding");
|
|
148
|
+
return (
|
|
149
|
+
<section className="cta-banner" key={`${type}-${index}`}>
|
|
150
|
+
<div>
|
|
151
|
+
{headline ? <h2>{headline}</h2> : null}
|
|
152
|
+
{body ? <p>{body}</p> : null}
|
|
153
|
+
<Link className="button" href={href}>
|
|
154
|
+
{ctaLabel}
|
|
155
|
+
</Link>
|
|
156
|
+
</div>
|
|
157
|
+
{image ? renderImage(image, "section-image") : null}
|
|
158
|
+
</section>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (type === "content_markdown" || headline || body || image) {
|
|
163
|
+
return (
|
|
164
|
+
<section className="content-section" key={`${type}-${index}`}>
|
|
165
|
+
<div>
|
|
166
|
+
{headline ? <h2>{headline}</h2> : null}
|
|
167
|
+
{body ? <p>{body}</p> : null}
|
|
168
|
+
</div>
|
|
169
|
+
{image ? renderImage(image, "section-image") : null}
|
|
170
|
+
</section>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
5
176
|
|
|
6
177
|
export default async function HomePage() {
|
|
7
|
-
const [manifest, homePage] = await Promise.all([getSiteManifest(),
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
178
|
+
const [manifest, homePage] = await Promise.all([getSiteManifest(), getHomePage()]);
|
|
179
|
+
const sections: PageSection[] = homePage?.payload.sections ?? [];
|
|
180
|
+
const heroSection =
|
|
181
|
+
sections.find((section: PageSection) => firstText(section.type) === "hero_with_cta") ??
|
|
182
|
+
null;
|
|
183
|
+
const heroRecord = asRecord(heroSection) ?? {};
|
|
184
|
+
const heroAsset =
|
|
185
|
+
sectionImage(manifest, heroRecord) ??
|
|
186
|
+
mediaByRole(manifest, ["hero_background", "hero_visual"]);
|
|
187
|
+
const headline =
|
|
188
|
+
firstText(
|
|
189
|
+
heroRecord.headline,
|
|
190
|
+
manifest.payload.metadata.headline,
|
|
191
|
+
homePage?.title,
|
|
192
|
+
manifest.display_name,
|
|
193
|
+
) || manifest.display_name;
|
|
194
|
+
const summary =
|
|
195
|
+
firstText(
|
|
196
|
+
heroRecord.subhead,
|
|
197
|
+
heroRecord.summary,
|
|
198
|
+
manifest.payload.metadata.summary,
|
|
11
199
|
"A custom Vuilder public site rendered from pinned public-dj content.",
|
|
12
|
-
|
|
200
|
+
) || "A custom Vuilder public site rendered from pinned public-dj content.";
|
|
201
|
+
const cta = asRecord(heroRecord.cta);
|
|
202
|
+
const ctaLabel = firstText(cta?.label, "Start onboarding");
|
|
203
|
+
const ctaHref = firstText(cta?.href) || appRoutes.onboarding();
|
|
13
204
|
|
|
14
205
|
return (
|
|
15
206
|
<div className="site-shell">
|
|
@@ -22,14 +213,24 @@ export default async function HomePage() {
|
|
|
22
213
|
</nav>
|
|
23
214
|
</header>
|
|
24
215
|
<main className="site-main">
|
|
25
|
-
<section className="hero">
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
216
|
+
<section className={heroAsset ? "hero hero-with-media" : "hero"}>
|
|
217
|
+
<div className="hero-copy">
|
|
218
|
+
<p className="eyebrow">{manifest.vertical_slug}</p>
|
|
219
|
+
<h1>{headline}</h1>
|
|
220
|
+
<p>{summary}</p>
|
|
221
|
+
<Link className="button" href={ctaHref}>
|
|
222
|
+
{ctaLabel}
|
|
223
|
+
</Link>
|
|
224
|
+
</div>
|
|
225
|
+
{heroAsset ? renderImage(heroAsset, "hero-media") : null}
|
|
32
226
|
</section>
|
|
227
|
+
{sections
|
|
228
|
+
.filter(
|
|
229
|
+
(section: PageSection) => firstText(section.type) !== "hero_with_cta",
|
|
230
|
+
)
|
|
231
|
+
.map((section: PageSection, index: number) =>
|
|
232
|
+
renderSection(section, manifest, index),
|
|
233
|
+
)}
|
|
33
234
|
<section className="panel">
|
|
34
235
|
<p>
|
|
35
236
|
Release manifest: {manifest.content_ref} / {manifest.digest}
|