Duodeal design quote (HTML, V2 blocks)
🔄 Are these skills current? They are a copy taken from the public repo — there is no git remote behind them and nothing refreshes them on its own, so an install silently stays on the version of the day it was made. Read the line
Skills Duodeal : mises à jour le …inDUODEAL-CONTEXT.md(project root): absent, or more than 7 days old → offer the refresh in one sentence before working. Procedure: ../duodeal-onboarding/references/updating-skills.md. Ask once per session; if the user declines, work with what is installed and drop it.
Guide for building a visually premium quote. The context (sender branding, offers,
prospect) is already known: no research to do — apply these instructions.
Getting a designed block on the page always takes two steps: create it, then write its
full content (connector: add_quotation_block writes default content only, then
update_quotation_block with the complete data, or replace_quotation_block_text
for large html). Read the current block inventory from the quotation itself
(connector: get_quotation → blocks[]; there is no list_quotation_blocks).
(see the duodeal-v2-blocks skill for the technical contract).
Step 0 — New quote, or rework of an existing one?
Both use the same tools and the same rules: an already-delivered quote is edited exactly like one being built, block by block. Nothing here is create-only.
Start by reading the server state (get_quotation {id} → blocks[] with every id and type,
list_quotation_lines {quotation_id}), then touch only what was asked — never rebuild a whole
quote to change a section, and never re-post the full blocks array.
Editing the HTML of a block (same as at creation, on a block that already exists):
- Small change, targeted text:
replace_quotation_block_text{quotation_id,block_id,field("code"for html,"columns.0"for wysiwyg),search+replace} — anchors short and unique, no regex, noreplace_all. - Rewritten block:
update_quotation_blockwith the COMPLETEdata. ⚠️ The merge is shallow at root: a partialdatawipes the rest of the object. Read the block, edit in memory, send it whole — and keep the version you read, it is your only undo. - New section:
add_quotation_block{type,position} (default content) thenupdate_quotation_blockto fill it, thenreorder_quotation_blockswith the complete ordered list of ids if it must move. - The Step 3 golden rules apply to every edit, including a one-line fix: still inline-first,
still
DuoDeal.autoResize()at the end, still no "—".
Editing the price table (native pricing block):
- Rows are not in
block.data(itsdataholds onlydiscountEnabled,discount,discountType,columns): they are quotation-lines. List them first (list_quotation_lines {quotation_id}), thenupdate_quotation_lineto change one,add_quotation_linesto add several,create_quotation_linefor a single one,delete_quotation_lineto remove one (nothing cascades: deleting the pricing block leaves its lines behind). - Reordering = the
weightfield (increasing = display order). Every line carries atax_id, includingtitleandsubtotalones. Several pricing blocks on the page → setblockIdon the line so it lands in the right table. - Ids are per tenant: re-read
list_taxes/list_unitieson THIS account, never reuse ids seen on another quote.
Before touching anything: a real client quote is only edited on explicit request, the Duodeal editor tab must be closed on that deal (auto-save overwrites API writes with its in-memory copy), and the changes are re-checked on the live page afterwards (Step 4).
Step 1 — Lock the design system (BEFORE any HTML)
Define 7 tokens from the sender's branding, and stick to them in ALL blocks:
| Token | Role | Rule |
|---|---|---|
ACCENT |
Accent color | ONE only — the brand's signature color |
INK |
Heading ink | Brand near-black (not pure #000) |
MUTED |
Secondary text | Readable mid grey |
PAPER |
Light card background | Often cream/ivory, not pure white |
LINE |
Soft borders | Very light grey |
DARK |
Brand dark background | For "problem" cards and CTA |
FONT |
Font + fallback | 'Police',-apple-system,'Segoe UI',Roboto,sans-serif (replace "Police" with the brand's font name) |
Validate mentally: sufficient contrast, accent used sparingly (eyebrows, key figures, ONE word of the H1 — never whole paragraphs). Typographic details and choices: references/design-system.md.
Step 2 — Narrative structure (canonical block order)
- native
header— sender logo + cover, filled in, never hidden nor recoded in HTML contacts— native- html INTRO — 2-logo lockup (sender · separator · prospect) → eyebrow "PROSPECT × SENDER" → H1 (ink + ONE accent word) → personalized hook (contact name, their context) → dark "what the current situation costs" card (3-4 bullets, the last one quantified) → 1-click video if available
- html SOLUTION — 3-4 value cards (the prospect's vocabulary) + a strip of 4 quantified KPIs
- (optional) html GALLERY / DEMO — real photos (physical business) or interface mockup (SaaS)
pricing— the priced quote (title "Your quote"); attach every line to this block (line tools carry the block'sblockId) and give every product line an image — not settable by the connector: line and product tools take no media argument, so host the file first (create_media) then bind it via REST if a key is configured, otherwise in the Duodeal interface, and tell the user- html ORDER RECAP — one-off vs recurring, spelled out. Recurring lives HERE (the native table has only one total per quote). An html block can be bound to the price table and display its real amounts live — offer it, see the rule below
- html SOCIAL PROOF — real testimonials, otherwise a logo wall (real logo files, never names typed as text). If neither exists, there is a third rung before dropping the block: quote the sender itself. Reproduce sentences they publish on their own site, WORD FOR WORD and attributed, plus figures drawn only from those sentences — never a testimonial you wrote. If two of their own pages disagree (160 vs 150 years), the contradiction does not give you a fact, it forbids two: omit it if it is decorative, quote ONE attributed sentence if it carries the argument, never arbitrate and never restate the number in the document's own voice. ⚠️ If the recipient is already a client of the sender, three things flip: the tension is EXTEND, not buy (never re-pitch the platform they use daily); scrub them from the logo wall (showing a client their own logo is an instant tell); and describe the added perimeter only — you do not know their current contract, and "this gives you a dedicated CSM" may promise as new what their tier already includes.
- html FAQ — 5-8 real objections + an "Another question?" card pointing to the
native comment button. ⚠️ Never the native
faqblock (it renders HTML entities literally) - html NEXT STEPS + CTA — 3-4 step stepper → dark CTA card pointing to the native "Accept & sign" button (French deals: « Accepter et signer ») → large sender card (real round 96 px photo, name, contact — humanize)
accept+signstamp, side by side — the signature block and its stamp always ship as a pair: once signed,acceptdisappears and onlysignstampshows the signature (see the pairing rule in duodeal-mcp-best-practices)legalnotice— designed legal pack (T&Cs as a grid of cards)
Adapt (remove/add sections depending on the offer), but keep the logic: hook → problem → solution → proof → price → reassurance → action. HTML skeletons ready to adapt: references/block-skeletons.md.
Step 3 — Golden rules for the HTML (non-negotiable)
INLINE-FIRST: no
<style>at all in a delivered block — there is no@font-faceexception any more (see the font rule inreferences/design-system.md: no custom-font route survives the editor). The Duodeal visual editor strips<style>tags as soon as the sales rep edits the block → the whole design breaks. So: everything instyle="…"on each element.Responsive without media queries: never
grid, never@media— use containersdisplay:flex;flex-wrap:wrap+ childrenflex:1 1 <base>px;min-width:<x>px.Interactivity via inline
onclick— no separate<script>for presentation. Two exceptions, both at the very END of the block, after the markup (the editor deletes whatever precedes the first element, so position matters more than count):- mandatory on EVERY html block:
<script>try{if(window.DuoDeal&&DuoDeal.autoResize){DuoDeal.autoResize()}}catch(e){}</script> - a block that genuinely reads the quote or collects data from the client (a bound
recap, a form, a file upload, a configurator) carries its logic in that same final
script — see duodeal-html-block-js. That is a micro-app, not decoration: the rule
above bans decorative scripts, not the
DuoDeallogic.
- mandatory on EVERY html block:
Spacing: spacer
<div style="height:71px" aria-hidden="true"></div>at the top AND at the bottom of each html block (legal pack: bottom ≤ 16 px, otherwise an empty PDF page).Never use an em dash "—" anywhere (titles, cards, T&Cs, product lines): use ":", ";", "·", ",". ⚠️ Floor of this rule: it targets U+2014 "—" and U+2013 "–", NEVER the ASCII hyphen of compound words. Write « ce soir-là », « ci-dessus », « c'est-à-dire », « quarante-huit », « au cœur ». A de-hyphenation pass applied too broadly has already shipped those five as spelling mistakes on a live client page: a typographic rule with no floor eats the text.
Every block has a non-empty
title(otherwise the interface shows "html"),showTitle:falsewhen the block already carries its own title in HTML.Stars/icons as inline SVG (never ★ nor emoji); every image must be hosted on Duodeal storage, never hotlinked from a third-party site (connector:
create_mediawithname+folder+filein base64 — ⚠️ reuse an existing media first (list_medias);filein base64 is the normal, supported route for the upload itself. ⚠️ What is forbidden is base64 inside the HTML: reference the url the media returns, never adata:URI (see duodeal-mcp-best-practices → images); neverfrom_url, the URL import 500s on most CDNs; noupload_media, no local path; then reference the url it returns inside the block'sdata).No orphan lines: any isolated piece of info becomes a 2-line card (title + muted text).
An amount that comes from Duodeal is read from Duodeal, never retyped. An html block can read the live quote through
window.DuoDeal(quotation,lines,deal,customFields,formatCurrency(n)) and re-render on every edit withonUpdate(cb)— see §9 of references/block-skeletons.md, and the full API in duodeal-html-block-js (which also covers collecting data FROM the client: forms, choices, uploaded documents).- Binding is good practice, not mandatory: a quote is perfectly valid with amounts written by hand. Ask the user whether they want the block bound, and say what it buys them: a bound recap follows the price table on its own, a hand-written one has to be re-checked after every line edit and silently lies if someone forgets.
- But if the figure already exists in the price table, bind it — a total, a subtotal, a VAT amount or a line price duplicated by hand will drift the day a sales rep edits a line, and nothing warns anyone.
- Hard-code only what the table does not hold: recurring amounts, options presented separately, illustrative packages, figures given by the client. Say which ones are hard-coded when you deliver.
- Anchor rule, for an interactive block tied to the price: its DEFAULT position must
display a figure that also exists in the quote (native total, recurring card, or the sum
of the
option:truelines). Correctness then becomes an equality anyone can check instead of a judgement call, and any quotation carrying option lines already contains its own anchor. A micro-app unrelated to pricing is not forced onto a quote figure — but any figure it shows that also appears elsewhere on the page must match it.
No HTML comment in a delivered block. The platform serves the block's
codeverbatim, so anything inside<!-- … -->reaches the buyer in view-source — including your own reasoning, your TODOs, and the exact wording you just removed for being wrong. A check that reads the RENDERED text is structurally blind to it (ten such comments once shipped across seven of eight blocks, one of them explaining at length why the sender's previous copy was false). Rationale belongs in the conversation, never in the payload. ⚠️ Grepping the served page also returns comments and em dashes that are not yours: the platform injects its ownDuoDealbootstrap into each block iframe. Check the strings YOU authored, not the host's chrome.
Step 4 — Delivery checklist (blocking)
Check on the LIVE quotation before delivering — one failing item = not done:
- Every block has a non-empty
title. - Native header filled in (sender logo + cover).
- Every product line has an image — out of reach of the connector: bind it via REST if a key is configured, otherwise hand the step to the user in the Duodeal interface and say it is still pending.
- No leftover
<style>at all (mental test: if you strip all<style>tags, the block still looks presentable). DuoDeal.autoResize()at the end of every html block.- Recurring in the html recap, not in the native table.
6bis. Every amount duplicated from the price table is bound to
DuoDeal, not retyped — or, if it is hard-coded on purpose, it matches the table today and the user knows it is a manual copy to re-check after any line edit. - No "—", no ★, no forgotten
{{…}}placeholder, and no<!-- … -->comment left in any block (rule 10) — check the blockcodeyou sent, not the rendered text. 7bis. The PDF exports A4 (595 × 842 pt on every page, 10 mm margins, no section cut in two, and no title left alone at the foot of a page: section heads in html blocks carrybreak-inside:avoid+break-after:avoidinline, with no empty spacer before their content): the print rule sits in the first title line'stitle, never in an html block (duodeal-mcp-best-practices → PDF export). No copy that points at the screen ("the button at the top right of this page"): it is false on paper. - Real visual verification: check the rendering, then deliver both links (client + edition).
⚠️ Opening the CLIENT link is recorded as a prospect visit: the client view posts a
visiton load and aheartbeatevery 15 s to/api/access-sessions(a plain server-side GET counts nothing). Your review then reads as a highly engaged buyer and inflates the Hot Deal Score of the deal whose re-opens drive the follow-up. There is no internal-view opt-out today, so: verify on the edit link and the PDF export for everything they can show, and ask the user before opening the client view — on a real prospect's deal, the honest default is not to open it, and if you do, say in your delivery that one visit was recorded. No tool returns a render and no tool returns the links (there is noget_links): rebuild them —get_deal(id)gives the dealuid→ client linkhttps://duodeal.app/quotations/deal/{deal.uid}; dealid+ quotationid→ edit linkhttps://duodeal.app/app/quotations/{dealId}/{quotationId}(/app/deals/…is the V1 editor, never deliver it). If you cannot open a browser, say so and ask the user to look — never claim a render you have not seen.