@explorer02/cfm-survey-sdk 0.2.8 → 0.3.0

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.
Files changed (28) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/cli/index.mjs +1 -1
  3. package/package.json +1 -1
  4. package/templates/docs/00-integration/ui-customization-wizard.md +31 -0
  5. package/templates/docs/00-integration/wizard-chrome-contract.md +1 -0
  6. package/templates/docs/00-integration/wizard-config-handoff.md +10 -0
  7. package/templates/docs/00-integration/wizard-question-type-styling.md +4 -3
  8. package/templates/docs/templates/CsatMatrixScale.tsx +29 -20
  9. package/templates/docs/templates/CustomSliderTrack.tsx +2 -2
  10. package/templates/docs/templates/FileUploadScale.tsx +4 -4
  11. package/templates/docs/templates/Header.tsx +1 -1
  12. package/templates/docs/templates/HeatmapScale.tsx +4 -4
  13. package/templates/docs/templates/LikertMatrixScale.tsx +7 -4
  14. package/templates/docs/templates/Question.tsx +2 -1
  15. package/templates/docs/templates/RankOrderScale.tsx +10 -3
  16. package/templates/preview-harness/preview-bridge.inline.js +17 -4
  17. package/templates/preview-harness/previewPages.js +23 -15
  18. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +22 -9
  19. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +5 -2
  20. package/templates/previewBridge.ts +13 -0
  21. package/templates/wizard-dist/assets/{PreviewMock-tTMvwuEy.js → PreviewMock-Bax7oRAL.js} +1 -1
  22. package/templates/wizard-dist/assets/TypePanel-D2t4FPSd.js +1 -0
  23. package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
  24. package/templates/wizard-dist/assets/index-c5lka74l.js +34 -0
  25. package/templates/wizard-dist/index.html +2 -2
  26. package/templates/wizard-dist/assets/TypePanel-B9Ls7BSq.js +0 -1
  27. package/templates/wizard-dist/assets/index-CjkIlOUI.css +0 -1
  28. package/templates/wizard-dist/assets/index-CoCrnnxt.js +0 -34
package/dist/cli/index.js CHANGED
@@ -126,7 +126,7 @@ ${n}`,u.writeFileSync(e,t)}function it(e){return e.replace(/_/g," ")}async funct
126
126
  </body>
127
127
  </html>`),u.writeFileSync(p.join(a,"src","entries",`${f}.tsx`),ot(f));u.writeFileSync(p.join(a,"tsconfig.json"),JSON.stringify({compilerOptions:{target:"ES2020",lib:["ES2020","DOM","DOM.Iterable"],module:"ESNext",moduleResolution:"bundler",jsx:"react-jsx",strict:!0,skipLibCheck:!0,paths:{"@/*":["./agent/src/*"]}},include:["src","agent/src","stubs"]},null,2)),tt(a,o,i,n);let m=Zo(i,o),C=Oe(o).join(p.delimiter);(0,ho.execSync)(`node "${m}" build --config "${p.join(a,"vite.config.mjs")}"`,{cwd:a,stdio:"inherit",env:{...process.env,NODE_PATH:C}});let S=p.join(t,"previews");u.mkdirSync(S,{recursive:!0});let v=p.join(a,"dist"),R=u.readFileSync(p.join(i,"templates","preview-harness","preview-bridge.inline.js"),"utf8");for(let f of n){let T=p.join(v,f),$=p.join(S,f);if(!u.existsSync(p.join(T,"index.html")))throw new Error(`Vite build did not produce ${f}/index.html`);u.existsSync($)&&u.rmSync($,{recursive:!0,force:!0}),K(T,$),nt(p.join($,"index.html"),R)}let x=p.join(v,"assets");u.existsSync(x)&&K(x,p.join(S,"assets"));let d=p.join(a,"agent","public");if(u.existsSync(d))for(let f of u.readdirSync(d)){let T=p.join(d,f);u.statSync(T).isFile()&&u.copyFileSync(T,p.join(S,f))}u.rmSync(a,{recursive:!0,force:!0})}var Le=k(require("fs")),Me=k(require("path"));function Pe(e){if(e)return e;let o=process.cwd();if(Le.existsSync(Me.join(o,"src","components","Question.tsx")))return o;let t=process.env.INIT_CWD;return t&&Le.existsSync(Me.join(t,"src","components","Question.tsx"))?t:o}var O=c;function rt(){return I.join(__dirname,"..","..","templates")}function st(e){let o=j.readFileSync(e,"utf8"),t=new Set,n=["MCQ","TEXTFIELD","NPS_SCALE","CFM_MATRIX","CSAT_MATRIX","RATING_MATRIX","SLIDER_MATRIX","FILE_UPLOAD","TEXT_AND_MEDIA","HEATMAP","RANK_ORDER"];for(let i of n)o.includes(i)&&t.add(i);return t.size?[...t]:n}function ct(e,o){let t=["header","footer","chrome","survey_page","intro","end"],n={MCQ:["MCQ_single","MCQ_multiple"],TEXTFIELD:["TEXTFIELD_short","TEXTFIELD_long"],NPS_SCALE:["NPS_SCALE"],CFM_MATRIX:["CFM_likert","CFM_bipolar"],CSAT_MATRIX:["CSAT_emoji","CSAT_star","CSAT_numbered","CSAT_graphics","CSAT_carousel","CSAT_dropdown"],RATING_MATRIX:["RATING_star","RATING_numbered","RATING_radio","RATING_emoji","RATING_graphics","RATING_slider"],SLIDER_MATRIX:["SLIDER_matrix"],FILE_UPLOAD:["FILE_UPLOAD"],HEATMAP:["HEATMAP"],RANK_ORDER:["RANK_drag","RANK_dropdown"]},i=new Set(t);for(let s of o)for(let r of n[s]??[])i.add(r);return e.filter(s=>i.has(s))}function Ue(e,o,t){if(j.existsSync(e))for(let n of j.readdirSync(e)){let i=I.join(e,n),s=I.posix.join(o,n);if(j.statSync(i).isDirectory())Ue(i,s,t);else{let r=I.extname(n).toLowerCase(),a=r===".html"?"text/html":r===".css"?"text/css":r===".js"?"application/javascript":r===".json"?"application/json":r===".svg"?"image/svg+xml":r===".jpg"||r===".jpeg"?"image/jpeg":r===".png"?"image/png":"application/octet-stream";t.push({path:s,contentType:a,sizeBytes:j.statSync(i).size})}}}async function ce(e={}){let o=Pe(e.projectRoot),t=e.outputDir??I.join(o,"out"),n=rt(),i=I.join(n,"preview-harness");e.useTemplatesOnly&&console.log(`${O.dim} Using SDK template preview harness only (--use-templates-only)${O.reset}`);let s=I.join(i,"previewPages.js"),{PREVIEW_PAGE_DEFS:r,buildPreviewHtml:a,PREVIEW_KEYS:l}=require(s),y=(()=>{let d=ie(o);return d?(console.log(`${O.dim} Using project theme: ${I.relative(o,d)}${O.reset}`),j.readFileSync(d,"utf8")):j.readFileSync(I.join(n,"survey-theme.css"),"utf8")})(),m=j.readFileSync(I.join(i,"preview-bridge.inline.js"),"utf8"),C=e.surveyTypes;if(!C){let d=I.join(o,"implementation_plan.md");C=j.existsSync(d)?st(d):["MCQ","TEXTFIELD","NPS_SCALE","CFM_MATRIX","CSAT_MATRIX","RATING_MATRIX","SLIDER_MATRIX","FILE_UPLOAD","TEXT_AND_MEDIA","HEATMAP","RANK_ORDER"]}let S=ct(l,C),v=I.join(t,"previews");if(j.existsSync(v)&&(j.rmSync(v,{recursive:!0,force:!0}),console.log(`${O.dim} Cleared previous preview output${O.reset}`)),j.mkdirSync(v,{recursive:!0}),!e.useTemplatesOnly)try{console.log(`${O.dim} Building previews from agent components (Vite)\u2026${O.reset}`),await yo({projectRoot:o,outputDir:t,keys:S});let d={};for(let T of S)d[T]=`previews/${T}/index.html`;j.writeFileSync(I.join(t,"previews","manifest.json"),JSON.stringify(d,null,2));let f=[];return Ue(I.join(t,"previews"),"previews",f),console.log(`${O.green}\u2705 Exported ${S.length} agent component previews \u2192 ${t}${O.reset}`),{outputDir:t,manifestPath:I.join(t,"previews","manifest.json"),files:f,surveyTypes:C}}catch(d){console.warn(`${O.yellow}\u26A0 Agent Vite preview build failed; falling back to template harness.${O.reset}`),console.warn(`${O.dim} ${L(d)}${O.reset}`)}for(let d of r){if(!S.includes(d.key))continue;let f=I.join(t,"previews",d.key);j.mkdirSync(f,{recursive:!0}),j.writeFileSync(I.join(f,"index.html"),a(d,y,m))}let R={};for(let d of S)R[d]=`previews/${d}/index.html`;j.writeFileSync(I.join(t,"previews","manifest.json"),JSON.stringify(R,null,2));let x=[];return Ue(I.join(t,"previews"),"previews",x),console.log(`${O.green}\u2705 Exported ${S.length} preview pages \u2192 ${t}${O.reset}`),{outputDir:t,manifestPath:I.join(t,"previews","manifest.json"),files:x,surveyTypes:C}}var F=c,ye=["MCQ","TEXTFIELD","NPS_SCALE","CFM_MATRIX","CSAT_MATRIX","RATING_MATRIX","SLIDER_MATRIX","FILE_UPLOAD","TEXT_AND_MEDIA","HEATMAP","RANK_ORDER"];function bo(e){return new Promise(o=>{let t=we.createServer();t.listen(e,()=>{t.close(()=>o(e))}),t.on("error",()=>o(bo(e+1)))})}function wo(e){return new Promise((o,t)=>{let n="";e.on("data",i=>n+=i),e.on("end",()=>o(n)),e.on("error",t)})}function at(e){let{exec:o}=require("child_process"),t=process.platform==="darwin"?`open "${e}"`:process.platform==="win32"?`start "" "${e}"`:`xdg-open "${e}"`;o(t)}function So(e){let o=N.extname(e).toLowerCase();return{".html":"text/html",".js":"application/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".jpg":"image/jpeg",".woff2":"font/woff2",".ico":"image/x-icon"}[o]??"application/octet-stream"}function lt(e){let o=new Set,t,n=e.match(/\*\*instanceId:\*\*\s*(.+)/i);if(n){let s=n[1].replace(/[()[\]]/g,"").trim();s&&!s.startsWith("(")&&s!=="JWT"&&(t=s)}for(let s of ye)e.includes(s)&&o.add(s);let i=e.matchAll(/\|\s*`([A-Z_]+)`\s*\|/g);for(let s of i)ye.includes(s[1])&&o.add(s[1]);return{surveyTypes:o.size>0?[...o]:[...ye],instanceId:t}}async function ft(e){return new Promise(o=>{try{let t=we.get(e,n=>{o((n.statusCode??500)<500),n.resume()});t.on("error",()=>o(!1)),t.setTimeout(2e3,()=>{t.destroy(),o(!1)})}catch{o(!1)}})}function ze(e){let o=N.join(e,"out","previews");P.existsSync(o)&&(P.rmSync(o,{recursive:!0,force:!0}),console.log(`${F.dim} Removed local preview files: out/previews/${F.reset}`))}async function Co(){let e=process.env.INIT_CWD||process.cwd(),o=N.join(e,"public"),t=N.join(e,"survey-ui-config.json"),n=N.join(e,"implementation_plan.md"),i=N.join(__dirname,"..","..","templates"),s=N.join(i,"wizard-dist"),r=process.env.CFM_PREVIEW_URL??"http://localhost:3000",a={},l=N.join(e,"out","previews"),y=N.join(l,"manifest.json");P.existsSync(y)||(console.log(`${F.dim} Exporting component previews for local wizard\u2026${F.reset}`),await ce({projectRoot:e,surveyTypes:[...ye]})),P.existsSync(y)&&(a=JSON.parse(P.readFileSync(y,"utf8")));let m=await bo(4200),C=`http://localhost:${m}`;return new Promise((S,v)=>{let R=we.createServer(async(d,f)=>{try{let T=d.url??"/";if(d.method==="GET"&&T==="/api/inventory"){let $;if(P.existsSync(n)){let w=P.readFileSync(n,"utf8");$=lt(w).instanceId}if(P.existsSync(t))try{let w=JSON.parse(P.readFileSync(t,"utf8"));w.instanceId&&($=w.instanceId)}catch{}let h=[...ye],{config:E}=se({projectRoot:e,surveyTypes:h,instanceId:$});f.writeHead(200,{"Content-Type":"application/json"}),f.end(JSON.stringify({surveyTypes:h,instanceId:$,previewUrl:r,initialConfig:E,previewManifest:Object.fromEntries(Object.keys(a).map(w=>[w,`${C}/previews/${w}/index.html`]))}));return}if(d.method==="GET"&&T==="/api/preview-url"){let $=await ft(r);f.writeHead(200,{"Content-Type":"application/json"}),f.end(JSON.stringify({url:r,connected:$}));return}if(d.method==="POST"&&T.split("?")[0]==="/api/wizard-quit"){ze(e),f.writeHead(200,{"Content-Type":"application/json"}),f.end(JSON.stringify({ok:!0,deleted:"out/previews"})),setTimeout(()=>{R.close(()=>{v(new Error("Client quit the wizard without saving. Local preview garbage collection complete (out/previews removed)."))})},300);return}if(d.method==="POST"&&T==="/api/upload-logo"){let $=await wo(d),h=JSON.parse($);P.existsSync(o)||P.mkdirSync(o,{recursive:!0});let E=h.base64.replace(/^data:image\/\w+;base64,/,""),w=Buffer.from(E,"base64"),_=N.extname(h.filename),U=N.basename(h.filename,_).toLowerCase().replace(/[^a-z0-9_-]/g,""),oe=`logo_${Date.now()}_${U}${_||".png"}`;P.writeFileSync(N.join(o,oe),w),f.writeHead(200,{"Content-Type":"application/json"}),f.end(JSON.stringify({fileName:oe}));return}if(d.method==="POST"&&T==="/api/save-config"){let $=await wo(d),h=JSON.parse($);h.configuredAt=new Date().toISOString();let E=h.global?.logo;E?.url?.startsWith("data:")&&(E.url=null),P.writeFileSync(t,JSON.stringify(h,null,2),"utf8"),console.log(`
128
128
  ${F.green}\u2705 UI configuration saved to ${t}${F.reset}`),ze(e),console.log(`${F.dim} Local preview garbage collection complete (out/previews removed)${F.reset}`),f.writeHead(200,{"Content-Type":"application/json"}),f.end(JSON.stringify({ok:!0})),setTimeout(()=>{R.close(()=>S())},500);return}if(d.method==="GET"){let $=T.split("?")[0];if($==="/"&&($="/index.html"),$.startsWith("/previews/")&&P.existsSync(l)){let w=$.replace(/^\/previews\//,""),_=N.join(l,w),U=N.normalize(_);if(U.startsWith(l)&&P.existsSync(U)&&P.statSync(U).isFile()){let oe=P.readFileSync(U);f.writeHead(200,{"Content-Type":So(U)}),f.end(oe);return}}let h=P.existsSync(s),E=h?N.join(s,$):N.join(i,"wizard.html");if(h&&P.existsSync(E)&&P.statSync(E).isFile()){let w=P.readFileSync(E);f.writeHead(200,{"Content-Type":So(E)}),f.end(w);return}if(!h&&$==="/index.html"){let w=P.readFileSync(N.join(i,"wizard.html"),"utf8");f.writeHead(200,{"Content-Type":"text/html"}),f.end(w);return}}f.writeHead(404,{"Content-Type":"text/plain"}),f.end("Not found")}catch(T){f.writeHead(500,{"Content-Type":"text/plain"}),f.end(`Server error: ${L(T)}`)}});R.listen(m,()=>{let d=C;console.log(`
129
- ${F.bold}${F.cyan}\u{1F3A8} Survey UI Customization Wizard${F.reset}`),console.log(`${F.dim} Running at ${F.reset}${F.green}${d}${F.reset}`),console.log(`${F.dim} Live previews: ${F.reset}${Object.keys(a).length} components`),console.log(`${F.dim} Preview target: ${F.reset}${r}`),console.log(`${F.dim} Default wizard \u2014 saves directly to survey-ui-config.json when client clicks Review & Build${F.reset}
129
+ ${F.bold}${F.cyan}\u{1F3A8} Survey UI Customization Wizard${F.reset}`),console.log(`${F.bold}\u{1F517} Client wizard link \u2014 share this URL only:${F.reset}`),console.log(` ${F.green}${d}${F.reset}`),console.log(`${F.dim} Port ${m} (auto-selected if 4200 was in use)${F.reset}`),console.log(`${F.dim} Live previews: ${F.reset}${Object.keys(a).length} components`),console.log(`${F.dim} Preview target: ${F.reset}${r}`),console.log(`${F.dim} Default wizard \u2014 saves directly to survey-ui-config.json when client clicks Review & Build${F.reset}
130
130
  `),at(d)}),R.on("error",v);let x=()=>{ze(e),R.close(()=>process.exit(130))};process.once("SIGINT",x),process.once("SIGTERM",x)})}var de=k(require("fs")),vo=k(require("path")),Ro=k(require("http")),Io=k(require("https"));z();$e();he();Re();je();var g=c;function ut(e){let{exec:o}=require("child_process"),t=process.platform==="darwin"?`open "${e}"`:process.platform==="win32"?`start "" "${e}"`:`xdg-open "${e}"`;o(t)}function pt(e){return new Promise(o=>setTimeout(o,e))}async function Je(e,o={}){return new Promise((t,n)=>{let i=new URL(e),r=(i.protocol==="https:"?Io:Ro).request(i,{method:o.method??"GET",headers:o.headers??{}},a=>{let l="";a.on("data",y=>l+=y),a.on("end",()=>{if((a.statusCode??500)>=400){n(new Error(`HTTP ${a.statusCode}: ${l}`));return}try{t(JSON.parse(l))}catch{n(new Error(`Invalid JSON: ${l.slice(0,200)}`))}})});r.on("error",n),o.body&&r.write(o.body),r.end()})}async function mt(e,o,t,n){return new Promise((i,s)=>{let r=new URL(e),a=r.protocol==="https:"?Io:Ro,l={"Content-Type":t,"Content-Length":o.length};n&&(l["X-Wizard-Session"]=n.sessionSecret,l["X-Wizard-Session-Id"]=n.sessionId);let y=a.request(r,{method:"PUT",headers:l},m=>{if((m.statusCode??500)>=400){let C="";m.on("data",S=>C+=S),m.on("end",()=>s(new Error(`PUT failed ${m.statusCode}: ${C}`)));return}m.resume(),i()});y.on("error",s),y.write(o),y.end()})}function ht(e){try{let o=e.split(".")[1];return JSON.parse(Buffer.from(o,"base64url").toString("utf8")).surveyId??null}catch{return null}}async function jo(e={}){let o=e.wait!==!1,t=e.pollIntervalMs??Number(process.env.CFM_WIZARD_POLL_MS??3e3),n=e.timeoutMs??Number(process.env.CFM_WIZARD_TIMEOUT_MS??72e5),i=process.env.CFM_INSTANCE_ID,s=(process.env.CFM_DEPLOY_API_BASE??"http://localhost:3000").replace(/\/$/,"");if(!i)throw new Error("CFM_INSTANCE_ID (JWT) is required");let r=ht(i);if(!r)throw new Error("Could not parse surveyId from CFM_INSTANCE_ID JWT");let a=Pe(),l=Y(a),y=l.activeConfigPath,m=e.apply?!1:e.review!==!1;console.log(`
131
131
  ${g.bold}${g.cyan}\u{1F3A8} CFM UI Customization (AWS)${g.reset}
132
132
  `),m&&(console.log(`${g.dim} Review mode: seed kept in survey-ui-config.json; final + diff written on complete.${g.reset}`),console.log(`${g.dim} Apply later with: npx cfm-sdk confirm-ui-config${g.reset}
@@ -126,7 +126,7 @@ ${n}`,u.writeFileSync(e,t)}function et(e){return e.replace(/_/g," ")}async funct
126
126
  </body>
127
127
  </html>`),u.writeFileSync(p.join(a,"src","entries",`${f}.tsx`),Ko(f));u.writeFileSync(p.join(a,"tsconfig.json"),JSON.stringify({compilerOptions:{target:"ES2020",lib:["ES2020","DOM","DOM.Iterable"],module:"ESNext",moduleResolution:"bundler",jsx:"react-jsx",strict:!0,skipLibCheck:!0,paths:{"@/*":["./agent/src/*"]}},include:["src","agent/src","stubs"]},null,2)),Yo(a,o,i,n);let m=Qo(i,o),C=Ae(o).join(p.delimiter);Xo(`node "${m}" build --config "${p.join(a,"vite.config.mjs")}"`,{cwd:a,stdio:"inherit",env:{...process.env,NODE_PATH:C}});let S=p.join(t,"previews");u.mkdirSync(S,{recursive:!0});let E=p.join(a,"dist"),v=u.readFileSync(p.join(i,"templates","preview-harness","preview-bridge.inline.js"),"utf8");for(let f of n){let T=p.join(E,f),$=p.join(S,f);if(!u.existsSync(p.join(T,"index.html")))throw new Error(`Vite build did not produce ${f}/index.html`);u.existsSync($)&&u.rmSync($,{recursive:!0,force:!0}),G(T,$),Zo(p.join($,"index.html"),v)}let x=p.join(E,"assets");u.existsSync(x)&&G(x,p.join(S,"assets"));let d=p.join(a,"agent","public");if(u.existsSync(d))for(let f of u.readdirSync(d)){let T=p.join(d,f);u.statSync(T).isFile()&&u.copyFileSync(T,p.join(S,f))}u.rmSync(a,{recursive:!0,force:!0})}import*as _e from"fs";import*as De from"path";function Te(e){if(e)return e;let o=process.cwd();if(_e.existsSync(De.join(o,"src","components","Question.tsx")))return o;let t=process.env.INIT_CWD;return t&&_e.existsSync(De.join(t,"src","components","Question.tsx"))?t:o}var D=c;function ot(){return R.join(__dirname,"..","..","templates")}function tt(e){let o=I.readFileSync(e,"utf8"),t=new Set,n=["MCQ","TEXTFIELD","NPS_SCALE","CFM_MATRIX","CSAT_MATRIX","RATING_MATRIX","SLIDER_MATRIX","FILE_UPLOAD","TEXT_AND_MEDIA","HEATMAP","RANK_ORDER"];for(let i of n)o.includes(i)&&t.add(i);return t.size?[...t]:n}function nt(e,o){let t=["header","footer","chrome","survey_page","intro","end"],n={MCQ:["MCQ_single","MCQ_multiple"],TEXTFIELD:["TEXTFIELD_short","TEXTFIELD_long"],NPS_SCALE:["NPS_SCALE"],CFM_MATRIX:["CFM_likert","CFM_bipolar"],CSAT_MATRIX:["CSAT_emoji","CSAT_star","CSAT_numbered","CSAT_graphics","CSAT_carousel","CSAT_dropdown"],RATING_MATRIX:["RATING_star","RATING_numbered","RATING_radio","RATING_emoji","RATING_graphics","RATING_slider"],SLIDER_MATRIX:["SLIDER_matrix"],FILE_UPLOAD:["FILE_UPLOAD"],HEATMAP:["HEATMAP"],RANK_ORDER:["RANK_drag","RANK_dropdown"]},i=new Set(t);for(let s of o)for(let r of n[s]??[])i.add(r);return e.filter(s=>i.has(s))}function Oe(e,o,t){if(I.existsSync(e))for(let n of I.readdirSync(e)){let i=R.join(e,n),s=R.posix.join(o,n);if(I.statSync(i).isDirectory())Oe(i,s,t);else{let r=R.extname(n).toLowerCase(),a=r===".html"?"text/html":r===".css"?"text/css":r===".js"?"application/javascript":r===".json"?"application/json":r===".svg"?"image/svg+xml":r===".jpg"||r===".jpeg"?"image/jpeg":r===".png"?"image/png":"application/octet-stream";t.push({path:s,contentType:a,sizeBytes:I.statSync(i).size})}}}async function se(e={}){let o=Te(e.projectRoot),t=e.outputDir??R.join(o,"out"),n=ot(),i=R.join(n,"preview-harness");e.useTemplatesOnly&&console.log(`${D.dim} Using SDK template preview harness only (--use-templates-only)${D.reset}`);let s=R.join(i,"previewPages.js"),{PREVIEW_PAGE_DEFS:r,buildPreviewHtml:a,PREVIEW_KEYS:l}=we(s),y=(()=>{let d=ne(o);return d?(console.log(`${D.dim} Using project theme: ${R.relative(o,d)}${D.reset}`),I.readFileSync(d,"utf8")):I.readFileSync(R.join(n,"survey-theme.css"),"utf8")})(),m=I.readFileSync(R.join(i,"preview-bridge.inline.js"),"utf8"),C=e.surveyTypes;if(!C){let d=R.join(o,"implementation_plan.md");C=I.existsSync(d)?tt(d):["MCQ","TEXTFIELD","NPS_SCALE","CFM_MATRIX","CSAT_MATRIX","RATING_MATRIX","SLIDER_MATRIX","FILE_UPLOAD","TEXT_AND_MEDIA","HEATMAP","RANK_ORDER"]}let S=nt(l,C),E=R.join(t,"previews");if(I.existsSync(E)&&(I.rmSync(E,{recursive:!0,force:!0}),console.log(`${D.dim} Cleared previous preview output${D.reset}`)),I.mkdirSync(E,{recursive:!0}),!e.useTemplatesOnly)try{console.log(`${D.dim} Building previews from agent components (Vite)\u2026${D.reset}`),await go({projectRoot:o,outputDir:t,keys:S});let d={};for(let T of S)d[T]=`previews/${T}/index.html`;I.writeFileSync(R.join(t,"previews","manifest.json"),JSON.stringify(d,null,2));let f=[];return Oe(R.join(t,"previews"),"previews",f),console.log(`${D.green}\u2705 Exported ${S.length} agent component previews \u2192 ${t}${D.reset}`),{outputDir:t,manifestPath:R.join(t,"previews","manifest.json"),files:f,surveyTypes:C}}catch(d){console.warn(`${D.yellow}\u26A0 Agent Vite preview build failed; falling back to template harness.${D.reset}`),console.warn(`${D.dim} ${O(d)}${D.reset}`)}for(let d of r){if(!S.includes(d.key))continue;let f=R.join(t,"previews",d.key);I.mkdirSync(f,{recursive:!0}),I.writeFileSync(R.join(f,"index.html"),a(d,y,m))}let v={};for(let d of S)v[d]=`previews/${d}/index.html`;I.writeFileSync(R.join(t,"previews","manifest.json"),JSON.stringify(v,null,2));let x=[];return Oe(R.join(t,"previews"),"previews",x),console.log(`${D.green}\u2705 Exported ${S.length} preview pages \u2192 ${t}${D.reset}`),{outputDir:t,manifestPath:R.join(t,"previews","manifest.json"),files:x,surveyTypes:C}}var j=c,me=["MCQ","TEXTFIELD","NPS_SCALE","CFM_MATRIX","CSAT_MATRIX","RATING_MATRIX","SLIDER_MATRIX","FILE_UPLOAD","TEXT_AND_MEDIA","HEATMAP","RANK_ORDER"];function mo(e){return new Promise(o=>{let t=he.createServer();t.listen(e,()=>{t.close(()=>o(e))}),t.on("error",()=>o(mo(e+1)))})}function uo(e){return new Promise((o,t)=>{let n="";e.on("data",i=>n+=i),e.on("end",()=>o(n)),e.on("error",t)})}function it(e){let{exec:o}=we("child_process"),t=process.platform==="darwin"?`open "${e}"`:process.platform==="win32"?`start "" "${e}"`:`xdg-open "${e}"`;o(t)}function po(e){let o=F.extname(e).toLowerCase();return{".html":"text/html",".js":"application/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".jpg":"image/jpeg",".woff2":"font/woff2",".ico":"image/x-icon"}[o]??"application/octet-stream"}function rt(e){let o=new Set,t,n=e.match(/\*\*instanceId:\*\*\s*(.+)/i);if(n){let s=n[1].replace(/[()[\]]/g,"").trim();s&&!s.startsWith("(")&&s!=="JWT"&&(t=s)}for(let s of me)e.includes(s)&&o.add(s);let i=e.matchAll(/\|\s*`([A-Z_]+)`\s*\|/g);for(let s of i)me.includes(s[1])&&o.add(s[1]);return{surveyTypes:o.size>0?[...o]:[...me],instanceId:t}}async function st(e){return new Promise(o=>{try{let t=he.get(e,n=>{o((n.statusCode??500)<500),n.resume()});t.on("error",()=>o(!1)),t.setTimeout(2e3,()=>{t.destroy(),o(!1)})}catch{o(!1)}})}function Le(e){let o=F.join(e,"out","previews");k.existsSync(o)&&(k.rmSync(o,{recursive:!0,force:!0}),console.log(`${j.dim} Removed local preview files: out/previews/${j.reset}`))}async function ho(){let e=process.env.INIT_CWD||process.cwd(),o=F.join(e,"public"),t=F.join(e,"survey-ui-config.json"),n=F.join(e,"implementation_plan.md"),i=F.join(__dirname,"..","..","templates"),s=F.join(i,"wizard-dist"),r=process.env.CFM_PREVIEW_URL??"http://localhost:3000",a={},l=F.join(e,"out","previews"),y=F.join(l,"manifest.json");k.existsSync(y)||(console.log(`${j.dim} Exporting component previews for local wizard\u2026${j.reset}`),await se({projectRoot:e,surveyTypes:[...me]})),k.existsSync(y)&&(a=JSON.parse(k.readFileSync(y,"utf8")));let m=await mo(4200),C=`http://localhost:${m}`;return new Promise((S,E)=>{let v=he.createServer(async(d,f)=>{try{let T=d.url??"/";if(d.method==="GET"&&T==="/api/inventory"){let $;if(k.existsSync(n)){let w=k.readFileSync(n,"utf8");$=rt(w).instanceId}if(k.existsSync(t))try{let w=JSON.parse(k.readFileSync(t,"utf8"));w.instanceId&&($=w.instanceId)}catch{}let h=[...me],{config:P}=re({projectRoot:e,surveyTypes:h,instanceId:$});f.writeHead(200,{"Content-Type":"application/json"}),f.end(JSON.stringify({surveyTypes:h,instanceId:$,previewUrl:r,initialConfig:P,previewManifest:Object.fromEntries(Object.keys(a).map(w=>[w,`${C}/previews/${w}/index.html`]))}));return}if(d.method==="GET"&&T==="/api/preview-url"){let $=await st(r);f.writeHead(200,{"Content-Type":"application/json"}),f.end(JSON.stringify({url:r,connected:$}));return}if(d.method==="POST"&&T.split("?")[0]==="/api/wizard-quit"){Le(e),f.writeHead(200,{"Content-Type":"application/json"}),f.end(JSON.stringify({ok:!0,deleted:"out/previews"})),setTimeout(()=>{v.close(()=>{E(new Error("Client quit the wizard without saving. Local preview garbage collection complete (out/previews removed)."))})},300);return}if(d.method==="POST"&&T==="/api/upload-logo"){let $=await uo(d),h=JSON.parse($);k.existsSync(o)||k.mkdirSync(o,{recursive:!0});let P=h.base64.replace(/^data:image\/\w+;base64,/,""),w=Buffer.from(P,"base64"),A=F.extname(h.filename),M=F.basename(h.filename,A).toLowerCase().replace(/[^a-z0-9_-]/g,""),ee=`logo_${Date.now()}_${M}${A||".png"}`;k.writeFileSync(F.join(o,ee),w),f.writeHead(200,{"Content-Type":"application/json"}),f.end(JSON.stringify({fileName:ee}));return}if(d.method==="POST"&&T==="/api/save-config"){let $=await uo(d),h=JSON.parse($);h.configuredAt=new Date().toISOString();let P=h.global?.logo;P?.url?.startsWith("data:")&&(P.url=null),k.writeFileSync(t,JSON.stringify(h,null,2),"utf8"),console.log(`
128
128
  ${j.green}\u2705 UI configuration saved to ${t}${j.reset}`),Le(e),console.log(`${j.dim} Local preview garbage collection complete (out/previews removed)${j.reset}`),f.writeHead(200,{"Content-Type":"application/json"}),f.end(JSON.stringify({ok:!0})),setTimeout(()=>{v.close(()=>S())},500);return}if(d.method==="GET"){let $=T.split("?")[0];if($==="/"&&($="/index.html"),$.startsWith("/previews/")&&k.existsSync(l)){let w=$.replace(/^\/previews\//,""),A=F.join(l,w),M=F.normalize(A);if(M.startsWith(l)&&k.existsSync(M)&&k.statSync(M).isFile()){let ee=k.readFileSync(M);f.writeHead(200,{"Content-Type":po(M)}),f.end(ee);return}}let h=k.existsSync(s),P=h?F.join(s,$):F.join(i,"wizard.html");if(h&&k.existsSync(P)&&k.statSync(P).isFile()){let w=k.readFileSync(P);f.writeHead(200,{"Content-Type":po(P)}),f.end(w);return}if(!h&&$==="/index.html"){let w=k.readFileSync(F.join(i,"wizard.html"),"utf8");f.writeHead(200,{"Content-Type":"text/html"}),f.end(w);return}}f.writeHead(404,{"Content-Type":"text/plain"}),f.end("Not found")}catch(T){f.writeHead(500,{"Content-Type":"text/plain"}),f.end(`Server error: ${O(T)}`)}});v.listen(m,()=>{let d=C;console.log(`
129
- ${j.bold}${j.cyan}\u{1F3A8} Survey UI Customization Wizard${j.reset}`),console.log(`${j.dim} Running at ${j.reset}${j.green}${d}${j.reset}`),console.log(`${j.dim} Live previews: ${j.reset}${Object.keys(a).length} components`),console.log(`${j.dim} Preview target: ${j.reset}${r}`),console.log(`${j.dim} Default wizard \u2014 saves directly to survey-ui-config.json when client clicks Review & Build${j.reset}
129
+ ${j.bold}${j.cyan}\u{1F3A8} Survey UI Customization Wizard${j.reset}`),console.log(`${j.bold}\u{1F517} Client wizard link \u2014 share this URL only:${j.reset}`),console.log(` ${j.green}${d}${j.reset}`),console.log(`${j.dim} Port ${m} (auto-selected if 4200 was in use)${j.reset}`),console.log(`${j.dim} Live previews: ${j.reset}${Object.keys(a).length} components`),console.log(`${j.dim} Preview target: ${j.reset}${r}`),console.log(`${j.dim} Default wizard \u2014 saves directly to survey-ui-config.json when client clicks Review & Build${j.reset}
130
130
  `),it(d)}),v.on("error",E);let x=()=>{Le(e),v.close(()=>process.exit(130))};process.once("SIGINT",x),process.once("SIGTERM",x)})}U();import*as fe from"fs";import*as xo from"path";import*as To from"http";import*as ko from"https";be();pe();Ee();Re();var g=c;function lt(e){let{exec:o}=we("child_process"),t=process.platform==="darwin"?`open "${e}"`:process.platform==="win32"?`start "" "${e}"`:`xdg-open "${e}"`;o(t)}function ft(e){return new Promise(o=>setTimeout(o,e))}async function ze(e,o={}){return new Promise((t,n)=>{let i=new URL(e),r=(i.protocol==="https:"?ko:To).request(i,{method:o.method??"GET",headers:o.headers??{}},a=>{let l="";a.on("data",y=>l+=y),a.on("end",()=>{if((a.statusCode??500)>=400){n(new Error(`HTTP ${a.statusCode}: ${l}`));return}try{t(JSON.parse(l))}catch{n(new Error(`Invalid JSON: ${l.slice(0,200)}`))}})});r.on("error",n),o.body&&r.write(o.body),r.end()})}async function dt(e,o,t,n){return new Promise((i,s)=>{let r=new URL(e),a=r.protocol==="https:"?ko:To,l={"Content-Type":t,"Content-Length":o.length};n&&(l["X-Wizard-Session"]=n.sessionSecret,l["X-Wizard-Session-Id"]=n.sessionId);let y=a.request(r,{method:"PUT",headers:l},m=>{if((m.statusCode??500)>=400){let C="";m.on("data",S=>C+=S),m.on("end",()=>s(new Error(`PUT failed ${m.statusCode}: ${C}`)));return}m.resume(),i()});y.on("error",s),y.write(o),y.end()})}function gt(e){try{let o=e.split(".")[1];return JSON.parse(Buffer.from(o,"base64url").toString("utf8")).surveyId??null}catch{return null}}async function Po(e={}){let o=e.wait!==!1,t=e.pollIntervalMs??Number(process.env.CFM_WIZARD_POLL_MS??3e3),n=e.timeoutMs??Number(process.env.CFM_WIZARD_TIMEOUT_MS??72e5),i=process.env.CFM_INSTANCE_ID,s=(process.env.CFM_DEPLOY_API_BASE??"http://localhost:3000").replace(/\/$/,"");if(!i)throw new Error("CFM_INSTANCE_ID (JWT) is required");let r=gt(i);if(!r)throw new Error("Could not parse surveyId from CFM_INSTANCE_ID JWT");let a=Te(),l=K(a),y=l.activeConfigPath,m=e.apply?!1:e.review!==!1;console.log(`
131
131
  ${g.bold}${g.cyan}\u{1F3A8} CFM UI Customization (AWS)${g.reset}
132
132
  `),m&&(console.log(`${g.dim} Review mode: seed kept in survey-ui-config.json; final + diff written on complete.${g.reset}`),console.log(`${g.dim} Apply later with: npx cfm-sdk confirm-ui-config${g.reset}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@explorer02/cfm-survey-sdk",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -2,6 +2,37 @@
2
2
 
3
3
  > Optional **run** after Phase 6 verify. **Wizard-ready build** is mandatory in Phase 5 — see [`agent-execution-flow.md`](agent-execution-flow.md).
4
4
 
5
+ ### Port selection (strict)
6
+
7
+ The CLI calls `findPort(4200)` — tries **4200**, then **4201**, **4202**, … until a port is free.
8
+
9
+ **Agent rules:**
10
+
11
+ 1. **Do not** hardcode `localhost:4200` if the CLI printed a different port.
12
+ 2. **Share only** the URL the CLI prints under `🔗 Client wizard link — share this URL only:`.
13
+ 3. **Do not** give the client a second link, a file path, or `out/previews/` paths.
14
+
15
+ Example CLI output:
16
+
17
+ ```
18
+ 🔗 Client wizard link — share this URL only:
19
+ http://localhost:4201
20
+ Port 4201 (auto-selected if 4200 was in use)
21
+ ```
22
+
23
+ ### Client quit without save
24
+
25
+ If the client clicks **Quit** (no Review & Build):
26
+
27
+ - CLI runs preview GC (`out/previews/` deleted)
28
+ - CLI exits without `survey-ui-config.json` update
29
+ - **Agent must NOT** re-run `npx cfm-sdk customize` or send another wizard link
30
+ - **Agent must** ask deploy confirmation (INPUT 4) using the **existing** build from Phase 6
31
+
32
+ See [`wizard-config-handoff.md`](wizard-config-handoff.md) § Quit without save.
33
+
34
+ ---
35
+
5
36
  ## Default: local wizard (`customize`)
6
37
 
7
38
  When the client says **yes** to customization in agent chat, **always run:**
@@ -12,6 +12,7 @@ Pipeline: [`wizard-preview-build-guide.md`](wizard-preview-build-guide.md) · [`
12
12
  | Wizard control / symptom | Required fix |
13
13
  |--------------------------|--------------|
14
14
  | Logo not updating in Branding / Header preview | `data-cfm-logo` img + `data-cfm-logo-text` fallback in `Header.tsx` |
15
+ | Logo shows colored pad behind image | Logo `<img>` must use `background: transparent` — **never** `var(--cfm-primary)` on logo box |
15
16
  | Company name not updating | `data-cfm-company` span in `Header.tsx` |
16
17
  | Show header works but progress bar toggle does nothing | Progress bar needs `data-cfm-progress` + class `cfm-progress`; header needs class `cfm-header` only — **not** on sticky wrapper |
17
18
  | Show footer toggle does nothing | `footer` + class `cfm-footer` |
@@ -58,6 +58,16 @@ The logo is **not** fetched from wizard memory on CloudFront — only from the d
58
58
 
59
59
  **After save:** `POST /api/save-config` also deletes `out/previews/` before CLI exits (preview garbage collection).
60
60
 
61
+ **Quit without save (no second wizard run):**
62
+
63
+ | Event | Agent action |
64
+ |-------|----------------|
65
+ | Client clicks **Quit** in wizard | Wait for CLI exit; previews GC'd automatically |
66
+ | CLI exits without `✅ UI configuration saved` | **Do not** run `customize` again or share another wizard URL |
67
+ | Build still valid from Phase 6 | Ask deploy confirmation (INPUT 4) — use existing `out/` from prior `npm run build` |
68
+
69
+ The agent does **not** re-open the wizard after quit. Garbage collection removes preview artifacts only; the production build in `out/` from Phase 6 remains unless the client requests a rebuild.
70
+
61
71
  ---
62
72
 
63
73
  ## Remote client flow — `customize-ui` (fallback only)
@@ -102,11 +102,12 @@ Single-select radio: outer border = focus ring, annulus = cell selected, inner d
102
102
  | Wizard section | Config path | CSS vars | Apply in |
103
103
  |----------------|-------------|----------|----------|
104
104
  | Button style | `questionTypes.NPS_SCALE.buttonStyle` | `data-cfm-nps-button-style` | `RatingScale.tsx` |
105
- | Track | `selectedFill`, `trackBar`, `trackHighlight` | `--cfm-nps-selected-fill`, `--cfm-nps-track-bar`, `--cfm-nps-track-highlight` | Track bar background |
106
- | Hint labels | `hintMinText`, `hintMaxText` + hint colors | `--cfm-hint-label-*` | Min/max anchor row |
107
- | Number labels | `numberLabelMode`, per-index colors | `--cfm-number-label-mode`, `--cfm-number-mono-color`, `--cfm-number-color-{n}` | Badge row above radios |
105
+ | Hint labels | `hintMinText`, `hintMaxText` + hint colors | `--cfm-hint-label-*` | `hintLabelStyle()` on anchor row in `Question.tsx` |
106
+ | Number labels | `numberLabelMode`, per-index colors | `--cfm-number-*` | `resolveNumberLabelColor()` / badge row in `RatingScale.tsx` |
108
107
  | Selection | Theme → Question card | `--cfm-matrix-selected`, `--cfm-input-focus-ring` | `scaleRadioRingStyle` + `scaleRadioDotStyle` |
109
108
 
109
+ **Removed from wizard UI:** selected fill, track bar, track highlight — NPS selection uses Theme → Question card tokens only.
110
+
110
111
  ### CFM_MATRIX (`CFM_matrix`, `CFM_dropdown`, `CFM_transpose`, `CFM_likert`, `CFM_bipolar`)
111
112
 
112
113
  | Wizard section | CSS vars | Apply in |
@@ -4,6 +4,18 @@ import { getEmojiForIndex, CsatStarIcons } from './surveyUiIcons';
4
4
  import { columnSubmitValue, nonNaScaleColumns } from './surveyUiScaleUtils';
5
5
  import { CustomSliderTrack } from './CustomSliderTrack';
6
6
  import MatrixDropdown from './MatrixDropdown';
7
+ import {
8
+ matrixRadioRingStyle,
9
+ matrixRadioDotStyle,
10
+ scaleCellSelectedStyle,
11
+ unselectedOpacityStyle,
12
+ emojiSizeStyle,
13
+ cellSelectedVar,
14
+ focusRingVar,
15
+ } from '@/lib/surveyUi/selectionStyles';
16
+ import { csatColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
17
+
18
+ const MATRIX_ROW_LABEL_WIDTH = 'var(--cfm-matrix-row-width, 180px)';
7
19
 
8
20
  type CsatMatrixQuestion = CsatQuestion | RatingMatrixQuestion;
9
21
 
@@ -190,7 +202,7 @@ function CsatMatrixGrid({
190
202
  {/* Labels row */}
191
203
  {showLabels && m > 0 && (
192
204
  <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '12px' }}>
193
- <div style={{ width: '180px', flexShrink: 0 }} />
205
+ <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
194
206
  <div style={{ flex: 1, display: 'flex' }}>
195
207
  <div style={{ flex: 1, position: 'relative', height: '24px', ...(isGraphics ? { margin: '0 40px' } : {}) }}>
196
208
  {labels!.map((label, i) => {
@@ -219,7 +231,7 @@ function CsatMatrixGrid({
219
231
  {/* Column tick labels for graphics sliders (shown with or without anchor labels) */}
220
232
  {isGraphics && m > 0 && (
221
233
  <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: showLabels ? '8px' : '16px', minHeight: '24px' }}>
222
- <div style={{ width: '180px', flexShrink: 0 }} />
234
+ <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
223
235
  <div style={{ flex: 1, display: 'flex' }}>
224
236
  <GraphicsColumnLabelsRow scaleColumns={scaleColumns} />
225
237
  {hasNotApplicable && (
@@ -232,13 +244,13 @@ function CsatMatrixGrid({
232
244
  {/* Column Headers for non-graphics */}
233
245
  {!isGraphics && !showLabels && m > 0 && (
234
246
  <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '16px', minHeight: '24px' }}>
235
- <div style={{ width: '180px', flexShrink: 0 }} />
247
+ <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
236
248
  <div style={{ flex: 1, display: 'flex' }}>
237
249
  <div style={{ flex: 1, display: 'grid', gridTemplateColumns: `repeat(${scaleColumns.length}, 1fr)` }}>
238
250
  {scaleColumns.map(col => (
239
251
  <div key={col.id} style={{ display: 'flex', justifyContent: 'center', padding: '0 4px' }}>
240
252
  <span
241
- style={{ fontSize: '13px', fontWeight: 500, color: '#4b5563', textAlign: 'center', lineHeight: 1.2 }}
253
+ style={{ fontSize: '13px', fontWeight: 500, textAlign: 'center', lineHeight: 1.2, ...csatColumnLabelStyle() }}
242
254
  dangerouslySetInnerHTML={{ __html: col.label }}
243
255
  />
244
256
  </div>
@@ -260,7 +272,7 @@ function CsatMatrixGrid({
260
272
  display: 'flex', alignItems: 'center', borderRadius: '8px',
261
273
  padding: '12px 0', backgroundColor: rowIdx % 2 === 0 ? 'rgba(249, 250, 251, 0.8)' : '#fff',
262
274
  }}>
263
- <div style={{ width: '180px', flexShrink: 0, padding: '0 16px' }}>
275
+ <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0, padding: '0 16px' }}>
264
276
  <span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4 }}
265
277
  dangerouslySetInnerHTML={{ __html: row.statementText }} />
266
278
  </div>
@@ -298,13 +310,8 @@ function CsatMatrixGrid({
298
310
  const StarNode = isStar ? (isSelected ? (CsatStarIcons.filled as any) : (CsatStarIcons.empty as any)) : null;
299
311
 
300
312
  const RadioNode = (
301
- <div style={{
302
- display: 'flex', width: '18px', height: '18px',
303
- alignItems: 'center', justifyContent: 'center', borderRadius: '50%',
304
- margin: '0 auto', border: isSelected ? '2px solid #e20074' : '2px solid #d1d5db',
305
- backgroundColor: '#fff',
306
- }}>
307
- {isSelected && <div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#e20074' }} />}
313
+ <div style={matrixRadioRingStyle(isSelected)}>
314
+ <div style={matrixRadioDotStyle(isSelected)} />
308
315
  </div>
309
316
  );
310
317
 
@@ -324,22 +331,24 @@ function CsatMatrixGrid({
324
331
  cursor: 'pointer', borderRadius: '6px',
325
332
  ...(isEmoji ? {
326
333
  padding: '6px',
334
+ ...emojiSizeStyle(),
327
335
  transform: isSelected ? 'scale(1.1)' : isHovered ? 'scale(1.25)' : 'scale(1)',
328
336
  ...(isSelected ? {
329
- backgroundColor: 'rgba(252, 231, 243, 0.6)',
330
- boxShadow: '0 0 0 2px rgba(226, 0, 116, 0.3)',
337
+ outline: `2px solid ${focusRingVar}`,
338
+ backgroundColor: cellSelectedVar,
331
339
  borderRadius: '50%',
332
- } : {}),
340
+ } : unselectedOpacityStyle()),
333
341
  } : isStar ? {
334
342
  padding: '4px',
335
343
  transform: isHovered ? 'scale(1.1)' : 'scale(1)',
336
- opacity: isSelected ? 1 : isHovered ? 1 : 0.5,
344
+ color: isSelected ? cellSelectedVar : focusRingVar,
345
+ ...(isSelected ? {} : unselectedOpacityStyle()),
337
346
  } : isNumbered ? {
338
347
  width: '40px', height: '40px', borderRadius: '8px',
339
348
  fontSize: '14px', fontWeight: 500,
340
- border: `1px solid ${isSelected ? '#e20074' : '#d1d5db'}`,
341
- backgroundColor: isSelected ? '#e20074' : '#fff',
342
- color: isSelected ? '#fff' : '#111827',
349
+ ...(isSelected
350
+ ? scaleCellSelectedStyle(true)
351
+ : { border: '1px solid #d1d5db', backgroundColor: '#fff', color: '#111827' }),
343
352
  transition: 'all 0.15s',
344
353
  } : { padding: '8px' }),
345
354
  }}
@@ -609,7 +618,7 @@ export function CsatMatrixScale({ question, selectedValue = {}, onSelect }: Csat
609
618
  );
610
619
  }
611
620
 
612
- if (isDropdown || isSelectBox || isStandardRadio) {
621
+ if (isDropdown || isSelectBox || (isStandardRadio && question.type !== 'RATING_MATRIX')) {
613
622
  return (
614
623
  <CsatMatrixVerticalList
615
624
  question={question}
@@ -50,7 +50,7 @@ export function CustomSliderTrack({
50
50
  ? tooltipLabel.replace(/<[^>]*>/g, '')
51
51
  : String(Math.round(value));
52
52
 
53
- const themeColor = disabled ? '#9ca3af' : '#e20074';
53
+ const themeColor = disabled ? '#9ca3af' : 'var(--cfm-input-focus-ring, var(--cfm-primary))';
54
54
 
55
55
  return (
56
56
  <div
@@ -60,7 +60,7 @@ export function CustomSliderTrack({
60
60
  >
61
61
  <div style={{
62
62
  position: 'absolute', top: '50%', left: 0, right: 0, transform: 'translateY(-50%)',
63
- height: '4px', borderRadius: '2px', backgroundColor: '#e5e7eb',
63
+ height: '4px', borderRadius: '2px', backgroundColor: 'var(--cfm-slider-track, #e5e7eb)',
64
64
  }} />
65
65
 
66
66
  <div style={{
@@ -98,10 +98,10 @@ function FileUploadDropzone({
98
98
  flexDirection: 'column',
99
99
  alignItems: 'center',
100
100
  justifyContent: 'center',
101
- border: `2px dashed ${isDragActive ? '#e20074' : '#d1d5db'}`,
102
- borderRadius: '12px',
103
- backgroundColor: isDragActive ? '#fdf2f8' : '#f9fafb',
104
- padding: '40px 24px',
101
+ border: `2px ${isDragActive ? 'solid' : 'var(--cfm-upload-border-style, dashed)'} var(--cfm-upload-border-color, #d1d5db)`,
102
+ borderRadius: 'var(--cfm-border-radius, 12px)',
103
+ backgroundColor: isDragActive ? 'var(--cfm-mcq-selected-bg, #fdf2f8)' : 'var(--cfm-upload-fill, #f9fafb)',
104
+ padding: 'var(--cfm-upload-padding, 40px 24px)',
105
105
  cursor: disabled ? 'not-allowed' : 'pointer',
106
106
  opacity: disabled ? 0.6 : 1,
107
107
  textAlign: 'center',
@@ -57,7 +57,7 @@ export default function Header({ embedded = false }: HeaderProps) {
57
57
  width: 'var(--cfm-header-logo-width, 120px)',
58
58
  height: 'var(--cfm-header-logo-height, 120px)',
59
59
  padding: 'var(--cfm-header-logo-padding, 10px)',
60
- background: 'var(--cfm-primary)',
60
+ background: 'transparent',
61
61
  }}
62
62
  />
63
63
  ) : (
@@ -26,11 +26,11 @@ function HeatmapSpot({ spot }: { spot: { id: string; x: number; y: number } }) {
26
26
  style={{
27
27
  left: `${spot.x * 100}%`,
28
28
  top: `${spot.y * 100}%`,
29
- width: 16,
30
- height: 16,
29
+ width: 'var(--cfm-heatmap-pin-size, 16px)',
30
+ height: 'var(--cfm-heatmap-pin-size, 16px)',
31
31
  transform: 'translate(-50%, -50%)',
32
- backgroundColor: '#F97316',
33
- border: '2px solid white',
32
+ backgroundColor: 'var(--cfm-heatmap-pin, #F97316)',
33
+ border: '2px solid var(--cfm-heatmap-pin-border, #fff)',
34
34
  boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
35
35
  }}
36
36
  />
@@ -2,6 +2,9 @@ import React, { useState } from 'react';
2
2
  import type { CfmMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-survey-sdk';
3
3
  import { columnSubmitValue } from './surveyUiScaleUtils';
4
4
  import MatrixDropdown from './MatrixDropdown';
5
+ import { matrixColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
6
+
7
+ const MATRIX_ROW_LABEL_WIDTH = 'var(--cfm-matrix-row-width, 180px)';
5
8
 
6
9
  type LikertMatrixScaleProps = {
7
10
  question: CfmMatrixQuestion;
@@ -229,12 +232,12 @@ function LikertMatrixGridLayout({
229
232
  )}
230
233
  {showColumnHeaders && (
231
234
  <div style={{ display: 'flex', alignItems: 'flex-end', minHeight: '24px' }}>
232
- <div style={{ flex: isBipolar ? '0 0 25%' : '0 0 180px', paddingRight: '16px', boxSizing: 'border-box' }} />
235
+ <div style={{ flex: isBipolar ? '0 0 25%' : `0 0 ${MATRIX_ROW_LABEL_WIDTH}`, paddingRight: '16px', boxSizing: 'border-box' }} />
233
236
  <div style={{ flex: 1, display: 'flex' }}>
234
237
  <div style={{ flex: 1, display: 'grid', gridTemplateColumns: `repeat(${gridCols.length}, 1fr)` }}>
235
238
  {gridCols.map((col) => (
236
239
  <div key={col.id} style={{ display: 'flex', justifyContent: 'center', padding: '0 4px' }}>
237
- <span style={{ fontSize: '13px', fontWeight: 500, color: '#4b5563', textAlign: 'center', lineHeight: 1.2 }}
240
+ <span style={{ fontSize: '13px', fontWeight: 500, textAlign: 'center', lineHeight: 1.2, ...matrixColumnLabelStyle() }}
238
241
  dangerouslySetInnerHTML={{ __html: transposeTable ? ('statementText' in col ? col.statementText : col.label) : ('label' in col ? col.label : (col as { statementText: string }).statementText) }} />
239
242
  </div>
240
243
  ))}
@@ -261,7 +264,7 @@ function LikertMatrixGridLayout({
261
264
  padding: '12px 0', backgroundColor: !repeatColumnHeaders && rowIdx % 2 === 0 ? 'rgba(249, 250, 251, 0.8)' : '#fff',
262
265
  boxSizing: 'border-box'
263
266
  }}>
264
- <div style={{ flex: isBipolar ? '0 0 25%' : '0 0 180px', padding: '0 16px', boxSizing: 'border-box' }}>
267
+ <div style={{ flex: isBipolar ? '0 0 25%' : `0 0 ${MATRIX_ROW_LABEL_WIDTH}`, padding: '0 16px', boxSizing: 'border-box' }}>
265
268
  <span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4, wordWrap: 'break-word', display: 'block' }}
266
269
  dangerouslySetInnerHTML={{ __html: transposeTable ? ('label' in rowItem ? rowItem.label : '') : ('statementText' in rowItem ? rowItem.statementText : '') }} />
267
270
  </div>
@@ -299,7 +302,7 @@ function LikertMatrixGridLayout({
299
302
  }
300
303
  }}
301
304
  style={{
302
- width: '18px', height: '18px', accentColor: '#e20074',
305
+ width: '18px', height: '18px', accentColor: 'var(--cfm-input-focus-ring)',
303
306
  cursor: isDisabled ? 'not-allowed' : 'pointer',
304
307
  ...(isMultiple ? { borderRadius: '4px' } : {})
305
308
  }}
@@ -19,6 +19,7 @@ import { FileUploadScale } from './FileUploadScale';
19
19
  import { HeatmapScale } from './HeatmapScale';
20
20
  import { RankOrderScale } from './RankOrderScale';
21
21
  import RatingScale from './RatingScale';
22
+ import { hintLabelStyle } from '@/lib/surveyUi/labelStyles';
22
23
 
23
24
  type QuestionProps = {
24
25
  question: SurveyQuestion;
@@ -141,7 +142,7 @@ export default function Question({
141
142
  {question.type === QUESTION_TYPE.NPS_SCALE && (
142
143
  <div className="space-y-3">
143
144
  {(question.minLabel || question.midLabel || question.maxLabel) && (
144
- <div className="relative w-full h-6 text-xs font-semibold text-gray-500">
145
+ <div className="relative w-full h-6 text-xs font-semibold" style={hintLabelStyle()}>
145
146
  <span
146
147
  className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
147
148
  dangerouslySetInnerHTML={{ __html: question.minLabel ?? '' }}
@@ -119,11 +119,17 @@ function RankOrderOptionRow({
119
119
  rankBadge,
120
120
  }: RankOrderOptionRowProps) {
121
121
  return (
122
- <div className="flex items-center gap-3 rounded-lg border border-[#e5e5e5] bg-white px-4 py-3 transition-colors hover:bg-gray-50/50">
122
+ <div
123
+ className="flex items-center gap-3 rounded-lg border border-[#e5e5e5] px-4 py-3 transition-colors"
124
+ style={{ backgroundColor: 'var(--cfm-rank-item-bg, #fff)', padding: 'var(--cfm-rank-item-padding, 12px 16px)', marginBottom: 'var(--cfm-rank-item-gap, 12px)' }}
125
+ >
123
126
  {rankSelect}
124
127
  {dragHandle}
125
128
  {rankBadge !== undefined && (
126
- <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#fdf2f8] text-sm font-semibold text-[#e20074]">
129
+ <span
130
+ className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-sm font-semibold text-white"
131
+ style={{ backgroundColor: 'var(--cfm-rank-badge, var(--cfm-primary))' }}
132
+ >
127
133
  {rankBadge}
128
134
  </span>
129
135
  )}
@@ -161,7 +167,8 @@ function DropdownRankLayout({
161
167
  aria-label={`Rank for ${option.optionLabel || option.id}`}
162
168
  value={currentRank ?? ''}
163
169
  onChange={event => handleRankChange(option.id, event.target.value)}
164
- className="h-10 w-16 shrink-0 rounded border border-gray-300 bg-white px-2 text-sm outline-none focus:border-[#e20074] focus:ring-1 focus:ring-[#e20074]"
170
+ className="h-10 w-16 shrink-0 rounded border bg-white px-2 text-sm outline-none"
171
+ style={{ borderColor: 'var(--cfm-rank-select-border, #d1d5db)' }}
165
172
  >
166
173
  <option value="">-</option>
167
174
  {rankChoices.map(rank => (
@@ -49,6 +49,7 @@
49
49
  if (patch.logoUrl) {
50
50
  img.src = patch.logoUrl;
51
51
  img.style.display = '';
52
+ img.style.background = 'transparent';
52
53
  } else if (el.hasAttribute('data-cfm-logo')) {
53
54
  img.style.display = 'none';
54
55
  img.removeAttribute('src');
@@ -171,10 +172,22 @@
171
172
  }
172
173
  }
173
174
  }
174
- if (patch.previewState && patch.previewState.textfieldPlaceholder !== undefined) {
175
- document.querySelectorAll('[data-cfm-textfield]').forEach(function (el) {
176
- el.setAttribute('placeholder', patch.previewState.textfieldPlaceholder || '');
177
- });
175
+ if (patch.previewState) {
176
+ if (patch.previewState.textfieldPlaceholder !== undefined) {
177
+ document.querySelectorAll('[data-cfm-textfield]').forEach(function (el) {
178
+ el.setAttribute('placeholder', patch.previewState.textfieldPlaceholder || '');
179
+ });
180
+ }
181
+ if (patch.previewState.hintMinText !== undefined) {
182
+ document.querySelectorAll('[data-cfm-hint-min], .cfm-hint-min').forEach(function (el) {
183
+ el.textContent = patch.previewState.hintMinText || '';
184
+ });
185
+ }
186
+ if (patch.previewState.hintMaxText !== undefined) {
187
+ document.querySelectorAll('[data-cfm-hint-max], .cfm-hint-max').forEach(function (el) {
188
+ el.textContent = patch.previewState.hintMaxText || '';
189
+ });
190
+ }
178
191
  }
179
192
  }
180
193
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const sharedStyles = `
4
4
  .cfm-header { height:var(--cfm-header-height); background:var(--cfm-header-bg); border-bottom:1px solid var(--cfm-header-border); display:flex; align-items:center; padding:0 var(--cfm-header-padding-x); box-shadow:0 2px 8px rgba(0,0,0,0.06); }
5
- .cfm-header-logo { width:var(--cfm-header-logo-width); height:var(--cfm-header-logo-height); object-fit:contain; padding:var(--cfm-header-logo-padding); background:var(--cfm-primary); }
5
+ .cfm-header-logo { width:var(--cfm-header-logo-width); height:var(--cfm-header-logo-height); object-fit:contain; padding:var(--cfm-header-logo-padding); background:transparent; }
6
6
  .cfm-header-company { font-size:var(--cfm-header-company-size); font-weight:var(--cfm-header-company-weight); color:var(--cfm-header-company-color); margin-left:16px; }
7
7
  .cfm-footer { background:var(--cfm-footer-bg); color:var(--cfm-footer-text); padding:var(--cfm-footer-padding-y) var(--cfm-footer-padding-x); display:flex; flex-wrap:wrap; gap:16px; justify-content:space-between; align-items:flex-end; }
8
8
  .cfm-footer-links { display:flex; gap:24px; flex-wrap:wrap; }
@@ -12,27 +12,31 @@ const sharedStyles = `
12
12
  .cfm-mcq-option { border:2px solid #e5e7eb; border-radius:var(--cfm-mcq-border-radius); padding:var(--cfm-mcq-card-padding); margin-bottom:var(--cfm-mcq-option-gap); display:flex; align-items:center; gap:8px; }
13
13
  .cfm-mcq-option.selected { border-color:var(--cfm-mcq-selected-accent); background:var(--cfm-mcq-selected-bg); }
14
14
  .cfm-nps-row { display:flex; gap:var(--cfm-nps-cell-gap); flex-wrap:wrap; }
15
- .cfm-nps-cell { width:var(--cfm-nps-cell-size); height:var(--cfm-nps-cell-size); border-radius:var(--cfm-border-radius); background:var(--cfm-nps-track-bar); display:flex; align-items:center; justify-content:center; font-size:14px; }
16
- .cfm-nps-cell.selected { background:var(--cfm-nps-selected-fill); border:2px solid var(--cfm-accent); }
15
+ .cfm-nps-cell { width:var(--cfm-nps-cell-size); height:var(--cfm-nps-cell-size); border-radius:var(--cfm-border-radius); background:var(--cfm-nps-track-bar,#f2f2f2); display:flex; align-items:center; justify-content:center; font-size:14px; box-sizing:border-box; }
16
+ .cfm-nps-cell.selected { border:2px solid var(--cfm-input-focus-ring); background:var(--cfm-matrix-selected); color:#fff; }
17
+ .cfm-hint-row { display:flex; justify-content:space-between; font-size:12px; font-weight:600; margin-bottom:8px; }
18
+ .cfm-hint-min, .cfm-hint-max { color:var(--cfm-hint-label-color,#6b7280); background:var(--cfm-hint-label-bg,transparent); padding:2px 4px; border-radius:4px; }
19
+ .cfm-matrix-col-header { font-size:13px; font-weight:500; text-align:center; color:var(--cfm-csat-column-label-color,var(--cfm-matrix-column-label-color,#4b5563)); background:var(--cfm-csat-column-label-bg,var(--cfm-matrix-column-label-bg,transparent)); padding:2px 6px; border-radius:4px; }
17
20
  .cfm-matrix-row { display:flex; align-items:center; padding:8px 0; background:var(--cfm-row-band); margin-bottom:4px; }
18
21
  .cfm-matrix-label { width:var(--cfm-matrix-row-width); flex-shrink:0; padding:0 16px; font-size:14px; font-weight:500; }
19
22
  .cfm-matrix-cells { display:flex; gap:8px; flex:1; justify-content:center; }
20
- .cfm-matrix-cell { width:var(--cfm-csat-cell-size); height:var(--cfm-csat-cell-size); border-radius:50%; border:2px solid #d1d5db; }
21
- .cfm-matrix-cell.selected { border-color:var(--cfm-csat-ring-color); background:var(--cfm-selected-bg); }
23
+ .cfm-matrix-cell { width:var(--cfm-csat-cell-size,18px); height:var(--cfm-csat-cell-size,18px); border-radius:50%; border:2px solid #d1d5db; box-sizing:border-box; }
24
+ .cfm-matrix-cell.selected { border-color:var(--cfm-input-focus-ring); background:var(--cfm-matrix-selected); }
22
25
  .cfm-emoji { font-size:var(--cfm-csat-emoji-size); opacity:var(--cfm-csat-unselected-opacity); }
23
- .cfm-emoji.selected { opacity:1; outline:2px solid var(--cfm-csat-ring-color); border-radius:8px; }
24
- .cfm-star { color:var(--cfm-star-color); font-size:24px; opacity:var(--cfm-star-unselected-opacity); }
25
- .cfm-star.selected { opacity:1; }
26
- .cfm-badge { width:40px; height:40px; border-radius:8px; background:var(--cfm-badge-color); color:white; display:flex; align-items:center; justify-content:center; font-weight:600; }
26
+ .cfm-emoji.selected { opacity:1; outline:2px solid var(--cfm-input-focus-ring); border-radius:8px; background:var(--cfm-matrix-selected); }
27
+ .cfm-star { color:var(--cfm-input-focus-ring); font-size:var(--cfm-csat-emoji-size,24px); opacity:var(--cfm-csat-unselected-opacity); }
28
+ .cfm-star.selected { opacity:1; color:var(--cfm-matrix-selected); }
29
+ .cfm-badge { width:40px; height:40px; border-radius:8px; background:var(--cfm-number-color-0,#9ca3af); color:white; display:flex; align-items:center; justify-content:center; font-weight:600; border:1px solid #d1d5db; box-sizing:border-box; }
30
+ .cfm-badge.selected { border:2px solid var(--cfm-input-focus-ring); background:var(--cfm-matrix-selected); color:#fff; }
27
31
  .cfm-slider-track { height:8px; background:var(--cfm-slider-track); border-radius:4px; position:relative; margin:24px 0; }
28
32
  .cfm-slider-thumb { width:20px; height:20px; background:var(--cfm-slider-thumb); border-radius:50%; position:absolute; top:-6px; left:60%; }
29
33
  .cfm-tick { width:var(--cfm-slider-tick-size); height:var(--cfm-slider-tick-size); background:var(--cfm-slider-tick); border-radius:4px; font-size:11px; display:flex; align-items:center; justify-content:center; color:white; }
30
- .cfm-dropzone { border:2px dashed var(--cfm-upload-accent); border-radius:var(--cfm-border-radius); padding:var(--cfm-upload-padding); text-align:center; color:var(--cfm-text); }
34
+ .cfm-dropzone { border-width:2px; border-style:var(--cfm-upload-border-style,dashed); border-color:var(--cfm-upload-border-color,#d1d5db); border-radius:var(--cfm-border-radius); padding:var(--cfm-upload-padding,24px); background:var(--cfm-upload-fill,#f9fafb); text-align:center; color:var(--cfm-text); }
31
35
  .cfm-input { width:100%; height:var(--cfm-input-height); border:1px solid var(--cfm-input-border); border-radius:var(--cfm-input-radius); padding:0 var(--cfm-input-padding); font-family:inherit; box-sizing:border-box; }
32
36
  .cfm-rank-item { background:var(--cfm-rank-item-bg); padding:var(--cfm-rank-item-padding); margin-bottom:var(--cfm-rank-item-gap); border-radius:var(--cfm-border-radius); display:flex; align-items:center; gap:12px; }
33
37
  .cfm-rank-badge { background:var(--cfm-rank-badge); color:white; width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:12px; }
34
38
  .cfm-handle { color:var(--cfm-rank-handle); cursor:grab; }
35
- .cfm-pin { width:var(--cfm-heatmap-pin-size); height:var(--cfm-heatmap-pin-size); background:var(--cfm-heatmap-pin); border:2px solid white; border-radius:50%; position:absolute; }
39
+ .cfm-pin { width:var(--cfm-heatmap-pin-size); height:var(--cfm-heatmap-pin-size); background:var(--cfm-heatmap-pin); border:2px solid var(--cfm-heatmap-pin-border,#fff); border-radius:50%; position:absolute; }
36
40
  .cfm-media { max-width:var(--cfm-media-max-width); border-radius:var(--cfm-media-radius); background:#e5e7eb; height:200px; display:flex; align-items:center; justify-content:center; }
37
41
  .cfm-btn-primary { background:var(--cfm-primary); color:var(--cfm-button-text); border:none; border-radius:var(--cfm-border-radius); padding:10px 20px; }
38
42
  .cfm-btn-back { background:transparent; color:var(--cfm-primary); border:1px solid var(--cfm-primary); border-radius:var(--cfm-border-radius); padding:10px 20px; }
@@ -44,8 +48,12 @@ function cells(n) {
44
48
  ).join('');
45
49
  }
46
50
 
51
+ function matrixColHeaders(n) {
52
+ return `<div class="cfm-matrix-row" style="background:transparent;margin-bottom:8px"><div class="cfm-matrix-label"></div><div class="cfm-matrix-cells">${Array.from({ length: n }, (_, i) => `<span class="cfm-matrix-col-header">${i + 1}</span>`).join('')}</div></div>`;
53
+ }
54
+
47
55
  function matrixPreview(label) {
48
- return `<div class="cfm-matrix-row"><div class="cfm-matrix-label">${label}</div><div class="cfm-matrix-cells">${cells(5)}</div></div>`;
56
+ return `${matrixColHeaders(5)}<div class="cfm-matrix-row"><div class="cfm-matrix-label">${label}</div><div class="cfm-matrix-cells">${cells(5)}</div></div>`;
49
57
  }
50
58
 
51
59
  const npsCells = Array.from({ length: 11 }, (_, i) =>
@@ -63,7 +71,7 @@ const PREVIEW_PAGE_DEFS = [
63
71
  { key: 'MCQ_multiple', title: 'MCQ Multiple', body: `<p style="font-weight:600;margin-bottom:16px">Select all that apply</p><div class="cfm-mcq-option selected"><span>☑</span> Option A</div><div class="cfm-mcq-option"><span>☐</span> Option B</div>` },
64
72
  { key: 'TEXTFIELD_short', title: 'Text Short', body: `<p style="font-weight:600;margin-bottom:8px">Your name</p><input class="cfm-input" placeholder="Type here..." />` },
65
73
  { key: 'TEXTFIELD_long', title: 'Text Long', body: `<p style="font-weight:600;margin-bottom:8px">Comments</p><textarea class="cfm-input" style="height:120px;padding:12px" placeholder="Type your response..."></textarea>` },
66
- { key: 'NPS_SCALE', title: 'NPS', body: `<p style="font-weight:600;margin-bottom:8px">How likely are you to recommend us?</p><div class="cfm-nps-row">${npsCells}</div>` },
74
+ { key: 'NPS_SCALE', title: 'NPS', body: `<p style="font-weight:600;margin-bottom:8px">How likely are you to recommend us?</p><div class="cfm-hint-row"><span class="cfm-hint-min" data-cfm-hint-min>Not likely</span><span class="cfm-hint-max" data-cfm-hint-max>Very likely</span></div><div class="cfm-nps-row">${npsCells}</div>` },
67
75
  { key: 'CFM_likert', title: 'CFM Likert', body: matrixPreview('Statement item 1') },
68
76
  { key: 'CFM_bipolar', title: 'CFM Bipolar', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width)">Left</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width);text-align:right">Right</div></div>` },
69
77
  { key: 'CFM_transpose', title: 'CFM Transpose', body: matrixPreview('Scale item (transposed)') },
@@ -71,7 +79,7 @@ const PREVIEW_PAGE_DEFS = [
71
79
  { key: 'CFM_dropdown', title: 'CFM Dropdown', body: `<div class="cfm-matrix-label">Statement 1</div><select class="cfm-input" style="margin-top:8px"><option>Select...</option></select>` },
72
80
  { key: 'CSAT_emoji', title: 'CSAT Emoji', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">How satisfied?</div><div class="cfm-matrix-cells"><span class="cfm-emoji selected">😞</span><span class="cfm-emoji">🙂</span><span class="cfm-emoji">😊</span></div></div>` },
73
81
  { key: 'CSAT_star', title: 'CSAT Star', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Rate us</div><div class="cfm-matrix-cells"><span class="cfm-star selected">★</span><span class="cfm-star selected">★</span><span class="cfm-star">★</span></div></div>` },
74
- { key: 'CSAT_numbered', title: 'CSAT Numbered', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Score</div><div class="cfm-matrix-cells"><div class="cfm-badge">1</div><div class="cfm-badge">2</div><div class="cfm-badge">3</div></div></div>` },
82
+ { key: 'CSAT_numbered', title: 'CSAT Numbered', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Score</div><div class="cfm-matrix-cells"><div class="cfm-badge">1</div><div class="cfm-badge selected">2</div><div class="cfm-badge">3</div></div></div>` },
75
83
  { key: 'CSAT_graphics', title: 'CSAT Graphics', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Slider row</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div></div></div>` },
76
84
  { key: 'CSAT_carousel', title: 'CSAT Carousel', body: `${matrixPreview('Carousel row')}<div style="text-align:center"><span style="color:var(--cfm-csat-accent)">●</span> ○</div>` },
77
85
  { key: 'CSAT_dropdown', title: 'CSAT Dropdown', body: `<div class="cfm-matrix-label">Statement</div><select class="cfm-input" style="margin-top:8px"><option>Choose rating...</option></select>` },
@@ -81,7 +89,7 @@ const PREVIEW_PAGE_DEFS = [
81
89
  { key: 'RATING_emoji', title: 'Rating Emoji', body: `<div class="cfm-matrix-cells" style="padding:16px"><span class="cfm-emoji">🙂</span><span class="cfm-emoji selected">😊</span><span class="cfm-emoji">🤩</span></div>` },
82
90
  { key: 'RATING_graphics', title: 'Rating Graphics', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Experience</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div></div></div>` },
83
91
  { key: 'RATING_slider', title: 'Rating Slider', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Rate on slider</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div></div></div>` },
84
- { key: 'SLIDER_matrix', title: 'Slider Matrix', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Statement 1</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div></div></div>` },
92
+ { key: 'SLIDER_matrix', title: 'Slider Matrix', body: `<div class="cfm-matrix-row" style="background:transparent;margin-bottom:4px"><div class="cfm-matrix-label"></div><div style="flex:1;padding:0 16px;display:flex;justify-content:space-between"><span class="cfm-matrix-col-header">Low</span><span class="cfm-matrix-col-header">High</span></div></div><div class="cfm-matrix-row"><div class="cfm-matrix-label">Statement 1</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div><div style="display:flex;justify-content:space-between;margin-top:8px"><span class="cfm-tick">0</span><span class="cfm-tick">5</span><span class="cfm-tick">10</span></div></div></div>` },
85
93
  { key: 'FILE_UPLOAD', title: 'File Upload', body: `<div data-cfm-dropzone-style="dashed" class="cfm-dropzone">Drop files here or click to upload</div>` },
86
94
  { key: 'TEXT_AND_MEDIA', title: 'Text & Media', body: `<div class="cfm-media">Media preview</div>` },
87
95
  { key: 'HEATMAP', title: 'Heatmap', body: `<div style="position:relative;background:#e5e7eb;height:240px;border-radius:var(--cfm-border-radius)"><div class="cfm-pin" style="top:40%;left:55%"></div></div>` },