← Files Creative ProductionARCHIVED FILE
skills/produce/helpers/moodboards/assets/mood-board-app/components/starter-intake/starter-intake.js
9.96 KB · Sep 30, 2026 · 23:20 UTC
export const STARTER_INTAKE_PRESETS = Object.freeze({
branding: {
id: "make-logo-from-scratch",
title: "First things first, what are we branding?",
subtitle: "What's the product? Do you have an existing logo? Any additional art direction you want?",
placeholder: "Type a brief, paste notes, or add a few directions...",
suggestions: [
{
label: "Make 10 logos based on the letter R",
text: "Make 10 distinct logo marks based on the letter R. Explore a wide range of styles: geometric, organic, luxury, playful, tech-forward, editorial, heritage, and minimalist.",
},
{
label: "Make a wordmark for Kairos, a modern wellness spa",
text: "Create a refined wordmark for Kairos, a modern wellness spa. Make it calm, elevated, spacious, and usable as a real brand mark.",
},
{
label: "Create an identity for a sparkling tea brand",
text: "Create an identity for a contemporary sparkling tea brand. Explore logo, color, type, and package-facing directions that feel fresh, crisp, and premium.",
},
],
},
resize: {
id: "template-assets",
title: "Resize assets for different platforms",
subtitle: "What reference asset are we resizing?",
placeholder: "Type a brief, paste notes, or add a few directions...",
suggestions: [
{
label: "Make this fit a YouTube banner",
text: "Make this brand asset fit a YouTube banner. Preserve the main identity, rebuild the composition for the wide safe area, and keep it readable on desktop and mobile.",
},
{
label: "Make this asset for all social platforms",
text: "Make this asset for all major social platforms. Create crops and layout adaptations for feed, story, reel, short-video, and link-preview formats without losing the core idea.",
},
{
label: "Turn this square launch graphic into a Story/Reel",
text: "Turn a square launch graphic into a vertical Story/Reel version with a stronger top-to-bottom layout and room for platform UI.",
},
],
formats: [
"Square 1:1",
"Feed 4:5",
"Story/Reel 9:16",
"Carousel 4:5",
"Landscape 16:9",
"LinkedIn Post",
],
},
});
export function createStarterIntake({
preset = STARTER_INTAKE_PRESETS.branding,
initialStep = "brief",
initialValue = "",
onClose,
onSubmit,
} = {}) {
const root = document.createElement("section");
root.className = "cp-starter-intake";
root.dataset.step = initialStep;
root.setAttribute("aria-label", preset.title);
root.innerHTML = `
<div class="cp-starter-intake__panel">
<div class="cp-starter-intake__copy">
<h2 id="cp-starter-intake-title"></h2>
<p class="cp-starter-intake__subtitle"></p>
</div>
<form class="cp-starter-intake__form" novalidate>
<div class="cp-starter-intake__step" data-intake-step="brief">
<div class="cp-starter-intake__composer">
<div class="cp-starter-intake__attachments" aria-live="polite" hidden></div>
<textarea aria-label="Starter brief"></textarea>
<div class="cp-starter-intake__footer">
<label class="cp-starter-intake__attach" aria-label="Attach files" title="Attach files">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" aria-hidden="true">
<path d="M12 5v14M5 12h14"></path>
</svg>
<input type="file" accept="image/png,image/jpeg,image/webp" multiple />
</label>
</div>
</div>
<div class="cp-starter-intake__suggestions" aria-label="Example use cases"></div>
</div>
<div class="cp-starter-intake__step cp-starter-intake__formats" data-intake-step="formats" hidden>
<div class="cp-starter-intake__format-list" aria-label="Platform formats"></div>
</div>
<div class="cp-starter-intake__actions">
<div class="cp-starter-intake__actions-inner">
<div class="cp-starter-intake__buttons">
<button class="cp-starter-intake__cancel" type="button">Back</button>
<button class="cp-starter-intake__submit" type="submit">Continue</button>
</div>
</div>
</div>
</form>
</div>
`;
const panel = root.querySelector(".cp-starter-intake__panel");
const title = root.querySelector("h2");
const subtitle = root.querySelector(".cp-starter-intake__copy p");
const textarea = root.querySelector("textarea");
const input = root.querySelector("input[type=file]");
const attachments = root.querySelector(".cp-starter-intake__attachments");
const suggestions = root.querySelector(".cp-starter-intake__suggestions");
const formatList = root.querySelector(".cp-starter-intake__format-list");
const cancel = root.querySelector(".cp-starter-intake__cancel");
const submit = root.querySelector(".cp-starter-intake__submit");
title.textContent = preset.title;
subtitle.textContent = preset.subtitle;
textarea.placeholder = preset.placeholder;
textarea.value = initialValue;
for (const suggestion of preset.suggestions || []) {
const suggestionLabel = typeof suggestion === "string" ? suggestion : suggestion.label;
const suggestionText = typeof suggestion === "string" ? suggestion : suggestion.text;
const button = document.createElement("button");
button.className = "cp-starter-intake__suggestion";
button.type = "button";
button.textContent = suggestionLabel;
button.addEventListener("click", () => {
textarea.value = suggestionText;
clearError();
textarea.focus();
});
suggestions.append(button);
}
for (const format of preset.formats || []) {
const button = document.createElement("button");
button.className = "cp-starter-intake__format";
button.type = "button";
button.setAttribute("aria-pressed", "false");
button.innerHTML = `
<span class="cp-starter-intake__format-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 5v14M5 12h14"></path></svg>
</span>
<span>${format}</span>
`;
button.addEventListener("click", () => {
const selected = button.getAttribute("aria-pressed") !== "true";
button.setAttribute("aria-pressed", String(selected));
button.querySelector("path").setAttribute("d", selected ? "m5 12.5 4.2 4.2L19 6.8" : "M12 5v14M5 12h14");
clearError();
});
formatList.append(button);
}
function setStep(step) {
const isFormats = step === "formats" && (preset.formats || []).length > 0;
root.dataset.step = isFormats ? "formats" : "brief";
root.querySelector('[data-intake-step="brief"]').hidden = isFormats;
root.querySelector('[data-intake-step="formats"]').hidden = !isFormats;
title.textContent = isFormats ? "Which sizes are we rebuilding?" : preset.title;
subtitle.textContent = isFormats
? "Choose every platform format you want resized from the reference asset."
: preset.subtitle;
submit.textContent = "Continue";
clearError();
}
function clearError() {
root.classList.remove("has-error");
root.querySelector(".cp-starter-intake__composer")?.classList.remove("is-warning", "is-warning-focus");
subtitle.textContent = root.dataset.step === "formats"
? "Choose every platform format you want resized from the reference asset."
: preset.subtitle;
subtitle.classList.remove("is-warning");
subtitle.removeAttribute("role");
}
function showError(message) {
root.classList.add("has-error");
root.querySelector(".cp-starter-intake__composer")?.classList.add("is-warning");
subtitle.textContent = message;
subtitle.classList.add("is-warning");
subtitle.setAttribute("role", "alert");
}
function renderAttachments() {
attachments.replaceChildren();
const files = [...(input.files || [])];
attachments.hidden = files.length === 0;
for (const file of files) {
const chip = document.createElement("span");
chip.className = "cp-starter-intake__attachment";
chip.textContent = file.name;
attachments.append(chip);
}
}
cancel.addEventListener("click", () => {
if (root.dataset.step === "formats") {
setStep("brief");
textarea.focus();
return;
}
onClose?.();
});
input.addEventListener("change", () => {
clearError();
renderAttachments();
});
textarea.addEventListener("input", clearError);
textarea.addEventListener("focus", () => {
if (root.classList.contains("has-error")) {
root.querySelector(".cp-starter-intake__composer")?.classList.add("is-warning-focus");
}
});
textarea.addEventListener("blur", () => {
root.querySelector(".cp-starter-intake__composer")?.classList.remove("is-warning", "is-warning-focus");
});
root.querySelector("form").addEventListener("submit", (event) => {
event.preventDefault();
const files = [...(input.files || [])];
const notes = textarea.value.trim();
if (root.dataset.step === "brief" && !notes && files.length === 0) {
showError("Add notes or attach a reference to continue");
textarea.focus();
return;
}
if (root.dataset.step === "brief" && preset.formats?.length) {
setStep("formats");
return;
}
const formats = [...formatList.querySelectorAll('[aria-pressed="true"]')].map((node) => node.innerText.trim());
if (root.dataset.step === "formats" && formats.length === 0) {
showError("Choose at least one platform format");
return;
}
onSubmit?.({ preset, notes, files, formats });
});
setStep(initialStep);
return {
element: root,
modal: panel,
setStep,
destroy() {
root.remove();
},
};
}
export function mountStarterIntake(container, options = {}) {
if (!(container instanceof Element)) {
throw new TypeError("mountStarterIntake requires a DOM element container.");
}
const component = createStarterIntake(options);
container.replaceChildren(component.element);
return component;
}
SHA-256: 466c86ea62c03a07cc6570f872ff01250a81271921ca8d4fc67f9735e7b41433