I always liked callout solutions from MCC pack. However I rarely used it, because I was forgetting the syntax and was too lazy to take a look in the .css file.
Today I decided to make a templater script that will insert any callout I want, including floating and multicolumn options from the pack.
<%*
/*
Prereqs: MCC Multi Column CSS pack + Templater plugin; this wizard inserts regular, float, and multi‑column callouts with pipe‑style metadata.
Workflow: pick callout type → (for multi) choose parent metadata → lock a width family → pick per‑column value → then choose callout names; float picks side/size first.
Tip: Replace the default callout list below with your own (e.g., from ~/.obsidian/plugins/callout-manager/data.json) if using custom callouts.
*/
// ---------- Default Obsidian callouts (replace with your own if desired) ----------
const callouts = [
"note","abstract","info","todo","tip","success","question",
"warning","failure","danger","bug","example","quote","summary"
]; // replace with custom names if using a snippet/plugin that defines them (see note above). [web:24]
// ---------- Suggester helper ----------
const choose = async (labels, values, placeholder) =>
await tp.system.suggester(labels, values, false, placeholder); // Templater suggester. [web:76]
// ---------- Percentage map and dynamic filtering (pw family) ----------
const pwMap = { pw1:10, pw2:18, pw3:28, pw4:38, pw5:48, pw6:58, pw7:68, pw8:78, pw9:88 }; // from MCC width tokens. [web:24]
const PW_BUDGET = 88; // leave headroom to avoid overflow in wrapped layouts. [web:24]
function allowedPwKeys(remaining){ return Object.entries(pwMap).filter(([k,p])=>p<=remaining).map(([k])=>k); } // [web:24]
function pwLabel(k){ return `${k} — about ${pwMap[k]}% width`; } // [web:24]
function sumPwTokens(tokens){ return tokens.filter(t=>t.startsWith("pw")).reduce((s,t)=>s+(pwMap[t]||0),0); } // [web:24]
// ---------- Single-choice width pickers (family lock uses these) ----------
async function pickWide(colIdx){
return await choose(
["wide-2 — grows 2×","wide-3 — grows 3×","wide-4 — grows 4×","wide-5 — grows 5×"],
["wide-2","wide-3","wide-4","wide-5"],
`Column ${colIdx}: choose wide value`
); // wide-x maps to flex-grow in MCC. [web:24]
}
async function pickDw(colIdx, partsSoFar){
const labels = Array.from({length:8},(_,i)=>`${i+2} parts (dw${i+2}) — parts so far: ${partsSoFar}`);
const vals = Array.from({length:8},(_,i)=>`dw${i+2}`);
return await choose(labels, vals, `Column ${colIdx}: choose parts (dwN)`); // dwN maps to discrete flex ratios. [web:24]
}
async function pickPw(colIdx, usedPwTotalSoFar){
const remaining = Math.max(0, PW_BUDGET - usedPwTotalSoFar);
const keys = allowedPwKeys(remaining);
if (keys.length === 0) {
await choose([`No percentage options left (remaining ≈${remaining}%) — press ENTER`],[null],`Column ${colIdx}: pw`);
return null;
}
return await choose(keys.map(k=>pwLabel(k)), keys, `Column ${colIdx}: choose percentage (remaining ≈${remaining}%)`); // pwN basis steps. [web:24]
}
async function pickFw(colIdx){
const pairs = [
["fw1 — 100px","fw1"],["fw2 — 200px","fw2"],["fw3 — 300px","fw3"],
["fw4 — 400px","fw4"],["fw5 — 500px","fw5"],["fw6 — 600px","fw6"],
["fw7 — 700px","fw7"],["fw8 — 800px","fw8"],["fw9 — 900px","fw9"]
];
return await choose(pairs.map(x=>x[0]), pairs.map(x=>x[1]), `Column ${colIdx}: choose fixed width (fwN)`); // fwN fixed basis. [web:24]
}
// ---------- Header helper ----------
const header = (level, kind, metaPipe, title) => {
const t = title && title.trim() ? `+ ${title.trim()}` : "";
const chev = level === 2 ? ">>" : ">";
return `${chev} [!${kind}${metaPipe}]${t}`; // pipe-style metadata per MCC docs. [web:24]
};
// ---------- Top-level ----------
const type = await choose(
[
"regular — single custom callout",
"multi column — [!multi-column] with children",
"float — aside left/right",
"blank — minimal container"
],
["regular","multi","float","blank"],
"Callout type"
); // primary modes covered by MCC + float. [web:24]
// ---------- Regular ----------
if (type === "regular") {
const kind = await choose(callouts, callouts, "Regular callout type"); // default set. [web:24]
const title = await tp.system.prompt("Title (optional)", ""); // no special metadata for regular here. [web:24]
tR += `${header(1, kind, "", title)}\n> \n`; // emit block. [web:24]
}
// ---------- Multi-column ----------
if (type === "multi") {
// Parent metadata: one width/flow + optional flex-h; pipes in output. [web:24]
const optPairs = [
["none — standard flow","none"],
["center-fixed — fixed width, centered","center-fixed"],
["left-fixed — fixed width, left","left-fixed"],
["right-fixed — fixed width, right","right-fixed"],
["no-wrap — single row with scrollbar (disables width controls)","no-wrap"],
["flex-h — children keep natural height (combine with one above)","flex-h"]
];
let picked = [];
while (true) {
const labels = optPairs.map(([lbl,val]) => picked.includes(val) ? `✓ ${lbl}` : lbl);
const vals = optPairs.map(([,val])=>val);
const choice = await choose(labels, vals, "Parent metadata — pick width/flow and optionally add flex-h; ESC to finish");
if (!choice) break;
if (choice === "none") { picked = []; break; }
if (choice === "flex-h") {
if (!picked.includes("flex-h")) picked.push("flex-h");
} else {
picked = picked.filter(x => !["center-fixed","left-fixed","right-fixed","no-wrap"].includes(x));
if (!picked.includes(choice)) picked.push(choice);
}
}
const parentPipe = picked.length ? `|${picked.join("|")}` : ""; // pipe separators. [web:24]
const parentHasNoWrap = picked.includes("no-wrap"); // width controls invalid under no-wrap per docs. [web:24]
const nCols = parseInt(await choose(["2","3","4","5"],["2","3","4","5"],"Number of columns")); // practical range. [web:24]
// Family lock (skip when no-wrap). [web:24]
let family = "none";
if (!parentHasNoWrap) {
family = await choose(
[
"none — no width metadata for columns",
"wide — relative growth",
"dw — proportional parts",
"pw — percentage targets",
"fw — fixed pixels"
],
["none","wide","dw","pw","fw"],
"Choose one width family for all columns"
);
}
let out = `${header(1, "multi-column", parentPipe, "")}\n`;
// Track running totals for pw/dw hints. [web:24]
let usedPwTotal = 0;
let dwPartsSoFar = 0;
for (let i = 0; i < nCols; i++) {
if (i > 0) out += ">\n";
// WIDTH FIRST per column (unless no-wrap or family none). [web:24]
let colTokens = [];
if (!parentHasNoWrap && family !== "none") {
if (family === "wide") {
const tok = await pickWide(i+1); if (tok) colTokens.push(tok);
} else if (family === "dw") {
const tok = await pickDw(i+1, dwPartsSoFar);
if (tok) { colTokens.push(tok); dwPartsSoFar += parseInt(tok.replace("dw","")) || 0; }
} else if (family === "pw") {
const tok = await pickPw(i+1, usedPwTotal);
if (tok) { colTokens.push(tok); usedPwTotal += pwMap[tok] || 0; }
} else if (family === "fw") {
const tok = await pickFw(i+1); if (tok) colTokens.push(tok);
}
}
const metaPipe = colTokens.length ? `|${colTokens.join("|")}` : ""; // pipe join. [web:24]
// THEN column callout type (regular or blank); blank never prompts title. [web:24]
const ctype = await choose(["regular child","blank"],["regular","blank"],`Column ${i+1}: content type`);
const kind = ctype === "regular"
? await choose(callouts, callouts, `Column ${i+1}: regular type`)
: "blank";
const title = kind === "blank" ? "" : await tp.system.prompt(`Column ${i+1} title (optional)`, "");
out += `${header(2, kind, metaPipe, title)}\n`;
out += `>> \n`;
}
tR += out;
}
// ---------- Float ----------
if (type === "float") {
// Choose width BEFORE callout kind. [web:24]
const sideMode = await choose(["Reading mode (left/right)","Live Preview (float-left/float-right)"],["lr","flr"],"Float side mode");
const sideList = sideMode === "flr" ? ["float-left","float-right"] : ["left","right"];
const side = await choose(sideList, sideList, "Side");
const widthPath = await choose(["Preset size — small/medium/large","Granular — choose width metadata"],["preset","granular"],"Float width mode");
let metaTokens = [];
if (widthPath === "preset") {
const size = await choose(["small — about 300px","medium — about 400px","large — about 600px"],["small","medium","large"],"Preset size");
metaTokens.push(`${side}-${size}`); // legacy preset sizes align with MCC float vars. [web:24]
} else {
metaTokens.push(side);
const fam = await choose(["wide","dw","pw","fw","none"],["wide","dw","pw","fw","none"],"Float: choose width type");
let tok = null;
if (fam === "wide") tok = await pickWide("float");
else if (fam === "dw") tok = await pickDw("float", 0);
else if (fam === "pw") tok = await pickPw("float", 0);
else if (fam === "fw") tok = await pickFw("float");
if (tok && tok !== "none") metaTokens.push(tok);
}
const metaPipe = metaTokens.length ? `|${metaTokens.join("|")}` : ""; // [web:24]
// THEN callout kind; blank has no title prompt. [web:24]
const contentType = await choose(["regular","blank"],["regular","blank"],"Content type");
const kind = contentType === "regular"
? await choose(callouts, callouts, "Regular callout type")
: "blank";
const title = kind === "blank" ? "" : await tp.system.prompt("Title (optional)", "");
tR += `${header(1, kind, metaPipe, title)}\n> \n`;
}
// ---------- Blank ----------
if (type === "blank") {
tR += `${header(1, "blank", "", "")}\n> \n`; // standalone blank; no title. [web:24]
}
%>
This is not the perfect and exhaustive version, but it covers all of my use cases and a little more. I think it should be sufficient for the most part of users, but I’d be glad to see how you further tweak it.