pandorga 1.14.0 → 2.0.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/DESIGN.md +15 -30
  4. data/_includes/content-runtime/43-blocks.html +0 -3
  5. data/_includes/content-runtime/60-fragments.html +83 -149
  6. data/_includes/content-runtime/90-bootstrap.html +3 -0
  7. data/_includes/cv/cv-box.html +10 -4
  8. data/_includes/home/band-index.html +22 -7
  9. data/_includes/home/band-listing.html +7 -3
  10. data/_includes/page/timeline-section.html +8 -3
  11. data/_includes/theme/theme-vars-block.liquid +3 -0
  12. data/_layouts/articles.html +0 -2
  13. data/_layouts/bibliography.html +0 -2
  14. data/_layouts/blog.html +0 -5
  15. data/_layouts/cv.html +27 -8
  16. data/_layouts/home.html +2 -13
  17. data/_layouts/projects.html +17 -9
  18. data/_layouts/resources.html +0 -9
  19. data/_layouts/shell.html +1 -7
  20. data/assets/css/article.css +10 -2
  21. data/assets/css/base.css +68 -44
  22. data/assets/css/ledger/bibliography.css +0 -31
  23. data/assets/css/ledger/chrome.css +51 -2
  24. data/assets/css/ledger/cv.css +0 -40
  25. data/assets/css/ledger/detail.css +20 -6
  26. data/assets/css/ledger/home.css +261 -402
  27. data/assets/css/ledger/projects.css +17 -19
  28. data/assets/css/ledger/rascunhos.css +0 -27
  29. data/assets/css/ledger/sources.css +0 -57
  30. data/assets/css/ledger/writing.css +0 -62
  31. data/lib/pandorga/commands/new.rb +2 -2
  32. data/lib/pandorga/commands/thumbnails.rb +3 -2
  33. data/lib/pandorga/registry/schema.yml +0 -3
  34. data/lib/pandorga/thumbnail_variants.rb +29 -3
  35. data/lib/pandorga/version.rb +1 -1
  36. data/scripts/check_home_lists.mjs +33 -5
  37. data/scripts/content/hero_portraits.rb +59 -11
  38. data/scripts/test/test-css-structure.rb +0 -1
  39. data/scripts/test/test-hero-16x9.rb +379 -0
  40. data/scripts/test/test-hero-portraits.mjs +10 -20
  41. data/scripts/test/test-hero-portraits.rb +31 -25
  42. data/scripts/test/test-highlighter-pills.rb +12 -2
  43. data/scripts/test/test-home-lists.rb +5 -0
  44. data/scripts/test/test-new.rb +6 -6
  45. data/scripts/test/test-shell-a11y.rb +2 -1
  46. data/scripts/test/test-studio-section-texts.rb +625 -0
  47. data/scripts/test/test-theme-contrast.rb +13 -2
  48. data/scripts/test/test-v3-layout.rb +559 -0
  49. data/scripts/validate.sh +3 -0
  50. data/studio/assets/{index-BJyN-kB3.js → index-B9h1WUYY.js} +1 -1
  51. data/studio/index.html +1 -1
  52. data/studio/schema.yml +126 -2
  53. data/studio-app/src/lib/yaml-doc.js +6 -0
  54. data/templates/network/studio.yml +10 -0
  55. metadata +5 -2
@@ -265,7 +265,7 @@ Please report this to https://github.com/markedjs/marked.`,t){const r="<p>An err
265
265
  `)}function hv(t){const e=String(t.body||"").trim();if(!e)return"";const n=String(t.handle||"").trim().replace(/^@/,""),r=String(t.icon||"chat").trim()||"chat",i=String(t.url||"").trim(),o=String(t.date||"").trim(),s=String(t.note||"").trim(),l=String(t.name||"").trim(),a=i.replace(/^https?:\/\//,""),c=['<article class="sov-tweet article-tweet">','<div class="sov-tweet__head article-tweet__head">',`<span class="sov-tweet__avatar article-tweet__avatar" aria-hidden="true"><span class="material-symbols-outlined">${Se(r)}</span></span>`,'<span class="sov-tweet__id article-tweet__id">'];if(l&&c.push(`<span class="sov-tweet__name article-tweet__name">${Se(l)}</span>`),n&&c.push(`<span class="sov-tweet__handle article-tweet__handle">@${Se(n)}</span>`),c.push("</span></div>"),c.push(`<p class="sov-tweet__body article-tweet__body">${e}</p>`),i||o){let u="";i&&(u+=`<a href="${Ie(i)}">${Se(a)}</a>`),i&&o&&(u+=" | "),o&&(u+=Se(o)),c.push(`<p class="sov-tweet__cite article-tweet__cite">${u}</p>`)}return s&&c.push(`<p class="sov-tweet__note article-tweet__note">${s}</p>`),c.push("</article>"),c.join("")}function Jd(t){const e=String(t||"").trim();if(!e)return'<div class="article-note"></div>';let n="";try{n=oe.parse(e,{gfm:!0,breaks:!1})}catch{n=`<p>${Se(e)}</p>`}return`<div class="article-note">${n}</div>`}function mv(t,e){return Gm(String(t||"").trim(),{variant:"latex",display:!!e})}function Gd(t){return String(t||"").replace(/\\\s/g,"\\\\ ")}function gv(t){const e=[];let n=String(t||"");const r=(i,o)=>{const s=e.length;return e.push({display:i,math:o}),`MATH${s}`};return n=n.replace(/\$\$([\s\S]+?)\$\$/g,(i,o)=>r(!0,Gd(o))),n=n.replace(/\\\[([\s\S]+?)\\\]/g,(i,o)=>r(!0,Gd(o))),n=n.replace(/\\\(([\s\S]+?)\\\)/g,(i,o)=>r(!1,o)),n=n.replace(/(^|[^\\$])\$(?!\$)([^$\n]+?)\$(?!\$)/g,(i,o,s)=>o+r(!1,s)),{text:n,store:e}}function yv(t,e){return String(t||"").replace(/\uE000MATH(\d+)\uE001/g,(n,r)=>{const i=e[Number(r)];return i?mv(i.math,i.display):""})}const bv={"figure.html":rv,"cut-in.html":iv,"youtube.html":ov,"youtube-short.html":sv,"video.html":lv,"plotly.html":av,"xcite.html":cv,"cite.html":uv,"dict-card.html":fv,"tweet.html":hv};function kv(t){let e=String(t||"");return e=e.replace(/\{%-?\s*include\s+article-note(?:\.html)?\b(?:(?!%\})[\s\S])*?-?%\}\s*([\s\S]*?)\s*\{%-?\s*endinclude\s*-?%\}/gi,(n,r)=>Jd(r)),e=e.replace(/<div\s+([^>]*\barticle-note\b[^>]*)>([\s\S]*?)<\/div>/gi,(n,r,i)=>Jd(i)),e=e.replace(/\{%-?\s*cite\s+([^\s%]+)[\s\S]*?-?%\}/g,(n,r)=>Sc("cite",r)),e=e.replace(/\{%-?\s*include\s+([\w.-]+)\s*((?:(?!%\})[\s\S])*?)\s*-?%\}/g,(n,r,i)=>{const o=bv[r];return o?o(nv(i)):`<div class="studio-unknown-include" data-include="${Ie(r)}"></div>`}),e}function xv(t){const e=gv(String(t||"")),n=kv(e.text);let r;try{r=oe.parse(n,{gfm:!0,breaks:!1})}catch{r=`<p>${Se(n)}</p>`}return pv(yv(r,e.store))}function xn(t,e,n=1){return String(t||"").localeCompare(String(e||""),void 0,{sensitivity:"base",numeric:!0})*n}function Xd(t,e,n=1){const r=String(t||""),i=String(e||"");return!r&&!i?0:r?i?r.localeCompare(i)*n:-1:1}const es=[{id:"date-desc",label:"Date | newest first",compare:(t,e)=>Xd(t.date,e.date,-1)||xn(t.title||t.name,e.title||e.name)},{id:"date-asc",label:"Date | oldest first",compare:(t,e)=>Xd(t.date,e.date,1)||xn(t.title||t.name,e.title||e.name)},{id:"title-asc",label:"Title | A–Z",compare:(t,e)=>xn(t.title||t.name,e.title||e.name,1)},{id:"title-desc",label:"Title | Z–A",compare:(t,e)=>xn(t.title||t.name,e.title||e.name,-1)},{id:"path-asc",label:"Path | A–Z",compare:(t,e)=>xn(t.path||t.name,e.path||e.name,1)},{id:"path-desc",label:"Path | Z–A",compare:(t,e)=>xn(t.path||t.name,e.path||e.name,-1)},{id:"key-asc",label:"Key | A–Z",compare:(t,e)=>xn(t.key||t.name,e.key||e.name,1)}],ts="date-desc",ns=[{id:"10",label:"10",size:10},{id:"20",label:"20",size:20},{id:"30",label:"30",size:30},{id:"all",label:"ALL",size:null}],rs="10",Xm="studio-list-sort:",Zm="studio-list-page-size:";function Sv(t){return es.find(e=>e.id===t)||es[0]}function wv(t){return ns.find(e=>e.id===t)||ns[0]}function Qm(t){const e=t==null?void 0:t.tags;return Array.isArray(e)?e.map(n=>String(n??"").trim()).filter(Boolean):typeof e=="string"&&e.trim()?[e.trim()]:[]}function Cv(t,e){const n=String(e||"").trim().toLowerCase();return n?t.filter(r=>[r.title,r.name,r.key,r.path,r.date,Qm(r).join(" ")].filter(Boolean).join(`
266
266
  `).toLowerCase().includes(n)):t.slice()}function vv(t,e){const n=String(e||"").trim().toLowerCase();return n?t.filter(r=>Qm(r).some(i=>i.toLowerCase()===n)):t.slice()}function Mv(t,{query:e="",sortId:n=ts,tag:r=""}={}){const i=vv(Cv(t,e),r),o=Sv(n);return i.sort((s,l)=>o.compare(s,l))}function Tv(t,{pageSizeId:e=rs,page:n=1}={}){const r=wv(e),i=t.length,o=r.size;if(o==null||o<=0)return{items:t.slice(),page:1,pageCount:1,pageSize:null,total:i,from:i?1:0,to:i};const s=Math.max(1,Math.ceil(i/o)),l=Math.min(Math.max(1,Number(n)||1),s),a=(l-1)*o,c=t.slice(a,a+o);return{items:c,page:l,pageCount:s,pageSize:o,total:i,from:i?a+1:0,to:a+c.length}}function Av(t,e=ts){try{const n=localStorage.getItem(Xm+t);if(n&&es.some(r=>r.id===n))return n}catch{}return e}function Ev(t,e){try{localStorage.setItem(Xm+t,e)}catch{}}function Nv(t,e=rs){try{const n=localStorage.getItem(Zm+t);if(n&&ns.some(r=>r.id===n))return n}catch{}return e}function Ov(t,e){try{localStorage.setItem(Zm+t,e)}catch{}}const Zd={posts:["date"],articles:["date"],resources:["category","type","language"],bibliography:["key"],products:["company","start_date","end_date"],projects:["status","start_date","end_date"],jobs:["company","engagement","start","end"]},Lv=new Set(["name","filename"]);function Iv(t){const e=String(t||"").split("/").filter(Boolean);return e.length?e[e.length-1]:""}function Rv(t,e){if(t==="path")return String(e.path||"").split("/").filter(Boolean).join(" | ")||"";const n=e==null?void 0:e[t];return n==null?"":Array.isArray(n)?n.map(r=>String(r??"").trim()).filter(Boolean).join(", "):String(n).trim()}function Dv(t){if(Array.isArray(t==null?void 0:t.listMetaFields)&&t.listMetaFields.length)return t.listMetaFields.map(String);const e=t==null?void 0:t.name;return e&&Zd[e]?Zd[e].slice():[]}function Pv(t,e){const n=Array.isArray(e)?e.map(String):Dv(e),r=[];for(const o of n){if(Lv.has(o))continue;const s=Rv(o,t);s&&r.push(s)}const i=String((t==null?void 0:t.name)||"").trim()||Iv(t==null?void 0:t.path);return i&&r[r.length-1]!==i&&r.push(i),r.join(" | ")}const $v={lineWidth:100,noRefs:!0,sortKeys:!1};function gr(t){return!t||t.format!=="yaml"?null:t.list?"list":t.root_key||(t.fields||[]).length?"map":"source"}function St(t){const e=dn.dump(t??null,$v);return e.endsWith(`
267
267
  `)?e:e+`
268
- `}function Qr(t){try{return dn.load(t)}catch(e){const n=new Error((e==null?void 0:e.message)||"Invalid YAML");throw n.cause=e,n}}function wc(t){const e={};for(const n of t||[])!(n!=null&&n.name)||n.hidden||(Object.prototype.hasOwnProperty.call(n,"default")?e[n.name]=Bv(n.default):n.list?e[n.name]=[]:n.type==="boolean"?e[n.name]=!1:e[n.name]="");return e}function Bv(t){if(t==null||typeof t!="object")return t;try{return structuredClone(t)}catch{return JSON.parse(JSON.stringify(t))}}function zn(t,e){if(!t||typeof t!="object"||Array.isArray(t))return t;const n=new Set((e||[]).map(i=>i.name).filter(Boolean)),r={...t};for(const i of Object.keys(r)){if(!n.has(i))continue;const o=r[i];(o===""||o==null||Array.isArray(o)&&o.length===0)&&delete r[i]}return r}function eg(t){return Array.isArray(t)?t.map(e=>e&&typeof e=="object"&&!Array.isArray(e)?{...e}:{}):[]}function ir(t){return t&&typeof t=="object"&&!Array.isArray(t)?{...t}:{}}const xt="content/media",Qd=25*1024*1024,_v=/\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)$/i,zv=/\.(mp4|webm|mov|m4v)$/i,Fv=/\.html?$/i,tg=/^[A-Za-z0-9][A-Za-z0-9._-]*$/;function Hv(t){var n;const e=String(((n=t==null?void 0:t.media)==null?void 0:n.input)||xt).replace(/\/+$/,"");return e===xt||e.startsWith(`${xt}/`),xt}function Ms(t){const e=String(t||"");return e.startsWith("content/media")?e.replace(/^content/,"")||"/media":""}function ng(t){return _v.test(String(t||""))}function rg(t){return zv.test(String(t||""))}function ig(t){return Fv.test(String(t||""))}function og(t){return ng(t)?"image":rg(t)?"video":ig(t)?"html":null}function sg(t){const e=Number(t)||0;return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function jv(t){const e=String(t||"").split(/[/\\]/).pop().trim();if(!e)return null;const n=e.replace(/\s+/g,"-").replace(/[^A-Za-z0-9._-]/g,"");return tg.test(n)?n:null}function Vv(t){const e=String(t||"").trim().replace(/\s+/g,"-").replace(/[^A-Za-z0-9._-]/g,"");return!tg.test(e)||e===".gitkeep"?null:e}function qv(t){return new Promise((e,n)=>{const r=new FileReader;r.onload=()=>{const i=String(r.result||""),o=i.indexOf(",");e(o>=0?i.slice(o+1):i)},r.onerror=()=>n(r.error||new Error("read_failed")),r.readAsDataURL(t)})}function Uv(t,e=xt){const r=String(t).split("/").filter(Boolean),i=e.split("/").filter(Boolean),o=[];let s="";for(let l=0;l<r.length;l++){s=s?`${s}/${r[l]}`:r[l];const a=l===i.length-1&&s===e;!(l>=i.length-1)&&!a||o.push({label:a||s===e?"media":r[l],path:s})}return o.length||o.push({label:"media",path:e}),o}function Wv(t){const{api:e,state:n,el:r,materialIcon:i,resolveMediaUrl:o,render:s,captureError:l,clearError:a,goHome:c}=t,u=n.mediaPath||xt,d=n.mediaItems||[],p=Uv(u),m=r("nav",{className:"media-breadcrumb","aria-label":"Media path"});p.forEach((w,T)=>{T>0&&m.append(r("span",{className:"media-breadcrumb-sep",text:"/"})),T===p.length-1?m.append(r("span",{className:"media-breadcrumb-current",text:w.label})):m.append(r("button",{className:"media-breadcrumb-link",type:"button",text:w.label,onClick:()=>void yr(w.path,t)}))});const g=r("input",{type:"file",className:"media-file-input",multiple:"multiple",accept:"image/*,video/*,.html,.svg,.pdf",hidden:!0,onChange:async w=>{const T=Array.from(w.target.files||[]);w.target.value="",T.length&&await Qv(T,t)}}),b=r("div",{className:"media-toolbar"},[r("button",{className:"btn btn-nav",type:"button",title:"Back to Studio map",onClick:()=>c()},[i("chevron_left","btn-nav-icon"),r("span",{text:"Studio map"})]),r("span",{className:"spacer"}),r("button",{className:"btn",type:"button",text:"New folder",disabled:!!n.mediaBusy,onClick:()=>void Zv(t)}),r("button",{className:"btn btn-primary",type:"button",text:n.mediaBusy?"Working…":"Upload",disabled:!!n.mediaBusy,onClick:()=>g.click()}),g]);let x;if(n.mediaLoading)x=r("p",{className:"loading-msg",text:"Loading media…"});else if(!d.length)x=r("p",{className:"empty-msg",text:"This folder is empty. Upload a file or create a subfolder."});else{const w=d.map(T=>Kv(T,t));x=r("div",{className:"media-grid"},w)}return r("div",{className:"pane media-pane"},[r("header",{className:"media-header"},[r("h1",{className:"media-title",text:"Media"}),m]),b,x])}function Kv(t,e){const{el:n,materialIcon:r,resolveMediaUrl:i,state:o}=e,s=t.type==="dir",l=s?null:og(t.name),a=t.public_path||(s?null:Ms(t.path)),c=!s&&ng(t.name)&&a?i(a):"";let u;if(s)u=n("span",{className:"media-thumb media-thumb--folder"},[r("folder","media-thumb-icon")]);else if(c){const b=n("img",{className:"media-thumb-img",src:c,alt:"",loading:"lazy",referrerpolicy:"no-referrer"});b.addEventListener("error",()=>{b.replaceWith(n("span",{className:"media-thumb media-thumb--file"},[r("image","media-thumb-icon")]))}),u=n("span",{className:"media-thumb media-thumb--image"},[b])}else rg(t.name)?u=n("span",{className:"media-thumb media-thumb--file"},[r("movie","media-thumb-icon")]):ig(t.name)?u=n("span",{className:"media-thumb media-thumb--file"},[r("html","media-thumb-icon")]):u=n("span",{className:"media-thumb media-thumb--file"},[r("draft","media-thumb-icon")]);const d=[];!s&&t.size&&d.push(sg(t.size)),a&&d.push(a);const p=s?()=>void yr(t.path,e):l?b=>Gv(t,b.currentTarget,e):()=>void cg(a,e),m=s?`Open ${t.name}`:l?`Preview ${t.name}`:`Copy ${a}`,g=!s&&a?Yv(t,a,e):null;return n("div",{className:"media-row"+(s?" media-row--dir":"")+(l?" media-row--preview":""),role:"button",tabindex:o.mediaBusy?"-1":"0",title:m,"aria-disabled":o.mediaBusy?"true":"false",onClick:b=>{o.mediaBusy||b.target.closest(".media-ops")||p(b)},onKeydown:b=>{o.mediaBusy||(b.key==="Enter"||b.key===" ")&&(b.preventDefault(),p(b))}},[u,n("span",{className:"media-row-body"},[n("span",{className:"media-row-name",text:t.name}),n("span",{className:"media-row-meta",text:d.join(" · ")||(s?"Folder":"")})]),g].filter(Boolean))}function Yv(t,e,n){const{el:r,materialIcon:i,state:o}=n,s=!!o.mediaBusy,l=r("button",{className:"btn btn-icon media-op-btn media-copy-btn",type:"button",title:`Copy ${e}`,"aria-label":`Copy path ${e}`,disabled:s,onClick:c=>{c.stopPropagation(),cg(e,n)}},[i("content_copy")]),a=r("button",{className:"btn btn-icon media-op-btn media-delete-btn",type:"button",title:`Delete ${t.name}`,"aria-label":`Delete ${t.name}`,disabled:s,onClick:c=>{c.stopPropagation(),Xv(t,n)}},[i("delete")]);return r("div",{className:"media-ops",role:"group","aria-label":"Operations"},[l,a])}let kt=null,Ma=null;function Jv(t,e){if(kt)return kt;const n=t("img",{className:"figure-lightbox-img",alt:""}),r=t("video",{className:"figure-lightbox-video",controls:!0,playsinline:!0}),i=t("iframe",{className:"figure-lightbox-frame",title:"Media preview"}),o=t("p",{className:"figure-lightbox-caption"});n.hidden=!0,r.hidden=!0,i.hidden=!0,o.hidden=!0,r.controls=!0;const s=t("button",{type:"button",className:"figure-lightbox-close","data-lightbox-close":"true","aria-label":"Close",onClick:a=>{a.preventDefault(),Ll()}},[e("close")]),l=t("div",{id:"figure-lightbox",className:"figure-lightbox",role:"dialog","aria-modal":"true","aria-label":"Media preview","aria-hidden":"true"},[t("div",{className:"figure-lightbox-backdrop","data-lightbox-close":"true",tabindex:"-1",onClick:()=>Ll()}),t("div",{className:"figure-lightbox-panel"},[s,n,r,i,o])]);return l.hidden=!0,document.body.append(l),document.addEventListener("keydown",a=>{!kt||kt.root.hidden||a.key==="Escape"&&(a.preventDefault(),Ll())}),kt={root:l,img:n,video:r,frame:i,caption:o,closeBtn:s},kt}function lg(t){t.img.removeAttribute("src"),t.img.alt="",t.img.hidden=!0,t.video.pause(),t.video.removeAttribute("src"),t.video.load(),t.video.hidden=!0,t.frame.removeAttribute("src"),t.frame.hidden=!0,t.caption.textContent="",t.caption.hidden=!0}function ag(t,e){t.root.hidden=!e,t.root.setAttribute("aria-hidden",String(!e)),document.documentElement.classList.toggle("figure-lightbox-open",e),document.body.classList.toggle("figure-lightbox-open",e)}function Ll(){if(!kt||kt.root.hidden)return;lg(kt),ag(kt,!1);const t=Ma;if(Ma=null,t&&typeof t.focus=="function")try{t.focus()}catch{}}function Gv(t,e,n){const{el:r,materialIcon:i,resolveMediaUrl:o}=n,s=og(t.name),l=t.public_path||Ms(t.path),a=l?o(l):"";if(!s||!a)return;const c=Jv(r,i);Ma=e||null,lg(c),s==="image"?(c.img.hidden=!1,c.img.referrerPolicy="no-referrer",c.img.alt=t.name||"",c.img.src=a):s==="video"?(c.video.hidden=!1,c.video.referrerPolicy="no-referrer",c.video.src=a):(c.frame.hidden=!1,c.frame.title=t.name||"HTML preview",c.frame.src=a),c.caption.textContent=t.name||"",c.caption.hidden=!t.name,ag(c,!0),c.closeBtn.focus()}async function cg(t,e){const{state:n,render:r,captureError:i,clearError:o}=e;if(t){o();try{await navigator.clipboard.writeText(t),n.status=`Copied ${t}`}catch(s){i(s),n.status=t}r()}}async function Xv(t,e){const{api:n,state:r,render:i,captureError:o,clearError:s}=e,l=t==null?void 0:t.path;if(!l||t.type==="dir")return;const a=t.name||String(l).split("/").pop()||l;if(window.confirm(`Delete ${a}?
268
+ `}function Qr(t){try{return dn.load(t)}catch(e){const n=new Error((e==null?void 0:e.message)||"Invalid YAML");throw n.cause=e,n}}function wc(t){const e={};for(const n of t||[])!(n!=null&&n.name)||n.hidden||(Object.prototype.hasOwnProperty.call(n,"default")?e[n.name]=Bv(n.default):n.list?e[n.name]=[]:n.type==="boolean"?e[n.name]=!1:e[n.name]="");return e}function Bv(t){if(t==null||typeof t!="object")return t;try{return structuredClone(t)}catch{return JSON.parse(JSON.stringify(t))}}function zn(t,e){if(!t||typeof t!="object"||Array.isArray(t))return t;const n=new Set((e||[]).map(o=>o.name).filter(Boolean)),r=new Set((e||[]).filter(o=>o&&o.keep_empty===!0).map(o=>o.name)),i={...t};for(const o of Object.keys(i)){if(!n.has(o))continue;const s=i[o];r.has(o)&&typeof s=="string"||(s===""||s==null||Array.isArray(s)&&s.length===0)&&delete i[o]}return i}function eg(t){return Array.isArray(t)?t.map(e=>e&&typeof e=="object"&&!Array.isArray(e)?{...e}:{}):[]}function ir(t){return t&&typeof t=="object"&&!Array.isArray(t)?{...t}:{}}const xt="content/media",Qd=25*1024*1024,_v=/\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)$/i,zv=/\.(mp4|webm|mov|m4v)$/i,Fv=/\.html?$/i,tg=/^[A-Za-z0-9][A-Za-z0-9._-]*$/;function Hv(t){var n;const e=String(((n=t==null?void 0:t.media)==null?void 0:n.input)||xt).replace(/\/+$/,"");return e===xt||e.startsWith(`${xt}/`),xt}function Ms(t){const e=String(t||"");return e.startsWith("content/media")?e.replace(/^content/,"")||"/media":""}function ng(t){return _v.test(String(t||""))}function rg(t){return zv.test(String(t||""))}function ig(t){return Fv.test(String(t||""))}function og(t){return ng(t)?"image":rg(t)?"video":ig(t)?"html":null}function sg(t){const e=Number(t)||0;return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function jv(t){const e=String(t||"").split(/[/\\]/).pop().trim();if(!e)return null;const n=e.replace(/\s+/g,"-").replace(/[^A-Za-z0-9._-]/g,"");return tg.test(n)?n:null}function Vv(t){const e=String(t||"").trim().replace(/\s+/g,"-").replace(/[^A-Za-z0-9._-]/g,"");return!tg.test(e)||e===".gitkeep"?null:e}function qv(t){return new Promise((e,n)=>{const r=new FileReader;r.onload=()=>{const i=String(r.result||""),o=i.indexOf(",");e(o>=0?i.slice(o+1):i)},r.onerror=()=>n(r.error||new Error("read_failed")),r.readAsDataURL(t)})}function Uv(t,e=xt){const r=String(t).split("/").filter(Boolean),i=e.split("/").filter(Boolean),o=[];let s="";for(let l=0;l<r.length;l++){s=s?`${s}/${r[l]}`:r[l];const a=l===i.length-1&&s===e;!(l>=i.length-1)&&!a||o.push({label:a||s===e?"media":r[l],path:s})}return o.length||o.push({label:"media",path:e}),o}function Wv(t){const{api:e,state:n,el:r,materialIcon:i,resolveMediaUrl:o,render:s,captureError:l,clearError:a,goHome:c}=t,u=n.mediaPath||xt,d=n.mediaItems||[],p=Uv(u),m=r("nav",{className:"media-breadcrumb","aria-label":"Media path"});p.forEach((w,T)=>{T>0&&m.append(r("span",{className:"media-breadcrumb-sep",text:"/"})),T===p.length-1?m.append(r("span",{className:"media-breadcrumb-current",text:w.label})):m.append(r("button",{className:"media-breadcrumb-link",type:"button",text:w.label,onClick:()=>void yr(w.path,t)}))});const g=r("input",{type:"file",className:"media-file-input",multiple:"multiple",accept:"image/*,video/*,.html,.svg,.pdf",hidden:!0,onChange:async w=>{const T=Array.from(w.target.files||[]);w.target.value="",T.length&&await Qv(T,t)}}),b=r("div",{className:"media-toolbar"},[r("button",{className:"btn btn-nav",type:"button",title:"Back to Studio map",onClick:()=>c()},[i("chevron_left","btn-nav-icon"),r("span",{text:"Studio map"})]),r("span",{className:"spacer"}),r("button",{className:"btn",type:"button",text:"New folder",disabled:!!n.mediaBusy,onClick:()=>void Zv(t)}),r("button",{className:"btn btn-primary",type:"button",text:n.mediaBusy?"Working…":"Upload",disabled:!!n.mediaBusy,onClick:()=>g.click()}),g]);let x;if(n.mediaLoading)x=r("p",{className:"loading-msg",text:"Loading media…"});else if(!d.length)x=r("p",{className:"empty-msg",text:"This folder is empty. Upload a file or create a subfolder."});else{const w=d.map(T=>Kv(T,t));x=r("div",{className:"media-grid"},w)}return r("div",{className:"pane media-pane"},[r("header",{className:"media-header"},[r("h1",{className:"media-title",text:"Media"}),m]),b,x])}function Kv(t,e){const{el:n,materialIcon:r,resolveMediaUrl:i,state:o}=e,s=t.type==="dir",l=s?null:og(t.name),a=t.public_path||(s?null:Ms(t.path)),c=!s&&ng(t.name)&&a?i(a):"";let u;if(s)u=n("span",{className:"media-thumb media-thumb--folder"},[r("folder","media-thumb-icon")]);else if(c){const b=n("img",{className:"media-thumb-img",src:c,alt:"",loading:"lazy",referrerpolicy:"no-referrer"});b.addEventListener("error",()=>{b.replaceWith(n("span",{className:"media-thumb media-thumb--file"},[r("image","media-thumb-icon")]))}),u=n("span",{className:"media-thumb media-thumb--image"},[b])}else rg(t.name)?u=n("span",{className:"media-thumb media-thumb--file"},[r("movie","media-thumb-icon")]):ig(t.name)?u=n("span",{className:"media-thumb media-thumb--file"},[r("html","media-thumb-icon")]):u=n("span",{className:"media-thumb media-thumb--file"},[r("draft","media-thumb-icon")]);const d=[];!s&&t.size&&d.push(sg(t.size)),a&&d.push(a);const p=s?()=>void yr(t.path,e):l?b=>Gv(t,b.currentTarget,e):()=>void cg(a,e),m=s?`Open ${t.name}`:l?`Preview ${t.name}`:`Copy ${a}`,g=!s&&a?Yv(t,a,e):null;return n("div",{className:"media-row"+(s?" media-row--dir":"")+(l?" media-row--preview":""),role:"button",tabindex:o.mediaBusy?"-1":"0",title:m,"aria-disabled":o.mediaBusy?"true":"false",onClick:b=>{o.mediaBusy||b.target.closest(".media-ops")||p(b)},onKeydown:b=>{o.mediaBusy||(b.key==="Enter"||b.key===" ")&&(b.preventDefault(),p(b))}},[u,n("span",{className:"media-row-body"},[n("span",{className:"media-row-name",text:t.name}),n("span",{className:"media-row-meta",text:d.join(" · ")||(s?"Folder":"")})]),g].filter(Boolean))}function Yv(t,e,n){const{el:r,materialIcon:i,state:o}=n,s=!!o.mediaBusy,l=r("button",{className:"btn btn-icon media-op-btn media-copy-btn",type:"button",title:`Copy ${e}`,"aria-label":`Copy path ${e}`,disabled:s,onClick:c=>{c.stopPropagation(),cg(e,n)}},[i("content_copy")]),a=r("button",{className:"btn btn-icon media-op-btn media-delete-btn",type:"button",title:`Delete ${t.name}`,"aria-label":`Delete ${t.name}`,disabled:s,onClick:c=>{c.stopPropagation(),Xv(t,n)}},[i("delete")]);return r("div",{className:"media-ops",role:"group","aria-label":"Operations"},[l,a])}let kt=null,Ma=null;function Jv(t,e){if(kt)return kt;const n=t("img",{className:"figure-lightbox-img",alt:""}),r=t("video",{className:"figure-lightbox-video",controls:!0,playsinline:!0}),i=t("iframe",{className:"figure-lightbox-frame",title:"Media preview"}),o=t("p",{className:"figure-lightbox-caption"});n.hidden=!0,r.hidden=!0,i.hidden=!0,o.hidden=!0,r.controls=!0;const s=t("button",{type:"button",className:"figure-lightbox-close","data-lightbox-close":"true","aria-label":"Close",onClick:a=>{a.preventDefault(),Ll()}},[e("close")]),l=t("div",{id:"figure-lightbox",className:"figure-lightbox",role:"dialog","aria-modal":"true","aria-label":"Media preview","aria-hidden":"true"},[t("div",{className:"figure-lightbox-backdrop","data-lightbox-close":"true",tabindex:"-1",onClick:()=>Ll()}),t("div",{className:"figure-lightbox-panel"},[s,n,r,i,o])]);return l.hidden=!0,document.body.append(l),document.addEventListener("keydown",a=>{!kt||kt.root.hidden||a.key==="Escape"&&(a.preventDefault(),Ll())}),kt={root:l,img:n,video:r,frame:i,caption:o,closeBtn:s},kt}function lg(t){t.img.removeAttribute("src"),t.img.alt="",t.img.hidden=!0,t.video.pause(),t.video.removeAttribute("src"),t.video.load(),t.video.hidden=!0,t.frame.removeAttribute("src"),t.frame.hidden=!0,t.caption.textContent="",t.caption.hidden=!0}function ag(t,e){t.root.hidden=!e,t.root.setAttribute("aria-hidden",String(!e)),document.documentElement.classList.toggle("figure-lightbox-open",e),document.body.classList.toggle("figure-lightbox-open",e)}function Ll(){if(!kt||kt.root.hidden)return;lg(kt),ag(kt,!1);const t=Ma;if(Ma=null,t&&typeof t.focus=="function")try{t.focus()}catch{}}function Gv(t,e,n){const{el:r,materialIcon:i,resolveMediaUrl:o}=n,s=og(t.name),l=t.public_path||Ms(t.path),a=l?o(l):"";if(!s||!a)return;const c=Jv(r,i);Ma=e||null,lg(c),s==="image"?(c.img.hidden=!1,c.img.referrerPolicy="no-referrer",c.img.alt=t.name||"",c.img.src=a):s==="video"?(c.video.hidden=!1,c.video.referrerPolicy="no-referrer",c.video.src=a):(c.frame.hidden=!1,c.frame.title=t.name||"HTML preview",c.frame.src=a),c.caption.textContent=t.name||"",c.caption.hidden=!t.name,ag(c,!0),c.closeBtn.focus()}async function cg(t,e){const{state:n,render:r,captureError:i,clearError:o}=e;if(t){o();try{await navigator.clipboard.writeText(t),n.status=`Copied ${t}`}catch(s){i(s),n.status=t}r()}}async function Xv(t,e){const{api:n,state:r,render:i,captureError:o,clearError:s}=e,l=t==null?void 0:t.path;if(!l||t.type==="dir")return;const a=t.name||String(l).split("/").pop()||l;if(window.confirm(`Delete ${a}?
269
269
 
270
270
  This commits a deletion to main and cannot be undone from Studio.`)){s(),r.mediaBusy=!0,r.status=`Deleting ${l}…`,i();try{let c=t.sha||null;c||(c=(await n.getFile(l)).sha);const u=await n.deleteFile({path:l,sha:c,message:`studio: delete ${l}`});r.mediaBusy=!1,r.status=`Deleted ${l}`,typeof e.onCommitted=="function"&&e.onCommitted(u==null?void 0:u.commit),await yr(r.mediaPath,e)}catch(c){r.mediaBusy=!1,o(c),i()}}}async function yr(t,e){const{api:n,state:r,render:i,captureError:o,clearError:s}=e;s(),r.mediaPath=t||xt,r.mediaLoading=!0,r.mediaBusy=!1,r.status=`Loading ${r.mediaPath}…`,i();try{const l=await n.listMedia(r.mediaPath);r.mediaItems=l.items||[],r.mediaPath=l.path||r.mediaPath,r.mediaLoading=!1;const a=r.mediaItems.filter(u=>u.type==="dir").length,c=r.mediaItems.filter(u=>u.type==="file").length;r.status=`${a} folders · ${c} files`}catch(l){r.mediaItems=[],r.mediaLoading=!1,o(l)}i()}async function Zv(t){const{api:e,state:n,render:r,captureError:i,clearError:o}=t,s=window.prompt("New folder name (letters, numbers, .-_)");if(s==null)return;const l=Vv(s);if(!l){n.error="Invalid folder name",n.status="",r();return}const a=`${n.mediaPath||xt}/${l}`;o(),n.mediaBusy=!0,n.status=`Creating ${l}…`,r();try{const c=await e.createMediaFolder({path:a,message:`studio: mkdir ${a}`});n.mediaBusy=!1,n.status=`Created ${Ms(a)||a}`,typeof t.onCommitted=="function"&&t.onCommitted(c==null?void 0:c.commit),await yr(n.mediaPath,t)}catch(c){n.mediaBusy=!1,i(c),r()}}async function Qv(t,e){const{api:n,state:r,render:i,captureError:o,clearError:s}=e;s();const l=r.mediaPath||xt;let a=0,c=null;r.mediaBusy=!0;for(const u of t){const d=jv(u.name);if(!d){r.error=`Skipped invalid name: ${u.name}`;continue}if(u.size>Qd){r.error=`${d} exceeds ${sg(Qd)} (GitHub Contents API soft limit in Studio)`;continue}r.status=`Uploading ${d}…`,i();try{const p=await qv(u),m=`${l}/${d}`,g=await n.uploadMedia({path:m,content_base64:p,message:`studio: upload ${m}`});a+=1,r.status=`Uploaded ${g.public_path||Ms(m)}`,c=(g==null?void 0:g.commit)||c}catch(p){o(p),r.mediaBusy=!1,i();return}}r.mediaBusy=!1,a&&(r.status=a===1?r.status:`Uploaded ${a} files`),a&&c&&typeof e.onCommitted=="function"&&e.onCommitted(c),await yr(l,e)}const eM="pk_test_c291bmQtc3F1aXJyZWwtNDgzMC5jbGVyay5hY2NvdW50cy5kZXYk",Qt="/studio/",ef="",ug={forceRedirectUrl:Qt,fallbackRedirectUrl:Qt,signInForceRedirectUrl:Qt,signInFallbackRedirectUrl:Qt,signUpForceRedirectUrl:Qt,signUpFallbackRedirectUrl:Qt};function tM(t){const e=String(t).replace(/^pk_(test|live)_/,"");if(!e)return"";try{const n="=".repeat((4-e.length%4)%4),r=(e+n).replace(/-/g,"+").replace(/_/g,"/");return atob(r).replace(/\$$/,"").trim()}catch{return""}}function tf(t,e={}){return new Promise((n,r)=>{const i=document.querySelector(`script[src="${t}"]`);if(i){if(i.dataset.loaded==="1")return n();i.addEventListener("load",()=>n(),{once:!0}),i.addEventListener("error",()=>r(new Error(`Failed to load ${t}`)),{once:!0});return}const o=document.createElement("script");o.src=t,o.async=!0,o.defer=!0,o.crossOrigin="anonymous";for(const[s,l]of Object.entries(e))if(s==="dataset"&&l&&typeof l=="object")for(const[a,c]of Object.entries(l))o.dataset[a]=c;else l!=null&&o.setAttribute(s,l);o.onload=()=>{o.dataset.loaded="1",n()},o.onerror=()=>r(new Error(`Failed to load ${t}`)),document.head.appendChild(o)})}async function nM(t){if(window.Clerk&&typeof window.Clerk.load=="function"&&window.__internal_ClerkUICtor)return window.Clerk;const e="".replace(/^https?:\/\//,"").replace(/\/$/,"")||tM(t);if(!e)throw new Error("Cannot resolve Clerk Frontend API host from publishable key");const n=`https://${e}/npm`;await tf(`${n}/@clerk/ui@1/dist/ui.browser.js`),await tf(`${n}/@clerk/clerk-js@6/dist/clerk.browser.js`,{dataset:{clerkPublishableKey:t}});for(let r=0;r<80;r++){if(window.Clerk&&typeof window.Clerk.load=="function"&&window.__internal_ClerkUICtor)return window.Clerk;await new Promise(o=>setTimeout(o,25))}throw new Error("Clerk / ClerkUI global missing after script load")}async function rM(){const t=await nM(eM);return await t.load({clerkUICtor:window.__internal_ClerkUICtor,...ug}),Gg(async()=>t.session?t.session.getToken():null),t}const at=document.getElementById("app"),h={schema:null,session:null,collection:null,items:[],file:null,sha:null,fields:{},body:"",yamlDoc:null,yamlList:null,yamlUiMode:"structured",bodyMode:"visual",lastEditorMode:"visual",baseline:null,dirty:!1,saving:!1,status:"",error:"",errorCode:"",errorDetail:"",listLoading:!1,fileLoading:!1,previewHtml:"",bodyEditor:null,insertMenuOpen:!1,aiProofHint:"",aiProofBusy:!1,aiProofSelection:null,navOpenGroups:new Set,navMenuOpen:!1,themeMenuOpen:!1,pipeline:{state:"idle",sha:"",runUrl:"",message:"",pollGen:0},reexporting:!1,listQueryByCollection:Object.create(null),listSortByCollection:Object.create(null),listPageSizeByCollection:Object.create(null),listPageByCollection:Object.create(null),navCountByCollection:Object.create(null),treeCacheByCollection:Object.create(null),treePrefetchGen:0,frontMatterOpen:null,mediaOpen:!1,mediaPath:"content/media",mediaItems:[],mediaLoading:!1,mediaBusy:!1},iM={figure:"image",cutIn:"transition_push",youtube:"smart_display",youtubeShort:"mobile_screen_share",video:"videocam",plotly:"analytics",xcite:"format_quote",cite:"bookmark",articleNote:"sticky_note_2",poem:"history_edu"},Ta={Writing:"edit_note",References:"menu_book",Portfolio:"work",Media:"photo_library",Pages:"description","CV Summary":"badge",Presentation:"campaign",Profile:"person"},oM=[{label:"Default",value:""},{label:"Primary",value:"var(--primary)"},{label:"Secondary",value:"var(--secondary)"},{label:"Tertiary",value:"var(--tertiary)"},{label:"Muted",value:"var(--on-surface-variant)"},{label:"Error",value:"var(--error)"}],sM={Writing:"Articles and Portuguese rascunhos",References:"Source cards and bibliography entries",Portfolio:"Products, projects, and CV jobs",Media:"Upload and browse content/media — cite /media/… in includes",Pages:"Listing page headers and furniture copy","CV Summary":"Public and PDF CV summary variants",Presentation:"Home catch phrase and CV side notes",Profile:"Contacts, education, languages, and skills"};function Ai(t){return String(t||"").split("/").filter(Boolean).join(" | ")}function dg(t){return sM[t]||`Browse ${t} collections`}function Cc(t){return"nav-group-"+String(t).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function fg(t){var i;const e=!h.navOpenGroups.has(t);e?h.navOpenGroups.add(t):h.navOpenGroups.delete(t);const n=document.getElementById(Cc(t));if(!n)return;n.hidden=!e;const r=(i=n.closest(".studio-nav-group"))==null?void 0:i.querySelector(".studio-nav-group-toggle");if(r){r.setAttribute("aria-expanded",e?"true":"false");const o=r.querySelector(".material-symbols-outlined");o&&(o.textContent=e?"expand_more":"chevron_right")}}function pg(t){const e=hg(t.description);if(e)return e;const n=Ai(t.path)||t.name;return t.type==="file"?`Open ${t.label} | ${n}`:`List entries in ${t.label} | ${n}`}function hg(t){let e=String(t||"").trim();if(!e)return"";const n=[/\s*The filename uses Key as YYYY-MM-DD-key[^.]*\./gi,/\s*Also the filename stem:[^.]*\./gi,/\s*Also the filename:[^.]*\./gi,/\s*Must match the YYYY-MM-DD in the filename[^.]*\./gi,/\s*;?\s*must match the YYYY-MM-DD in the filename\./gi,/\s*The Updated date is read from Git, never set here\./gi,/\s*ASCII kebab-case\./gi,/\s*\(44 characters total for the key segment is a good ceiling\)\.?/gi,/Metadata-first forms[^.]*\./gi,/TipTap with Liquid blocks[^.]*\./gi,/preview via content-runtime contract\.?/gi,/Paste Liquid \{%\s*include\s*%\} markers[^.]*\./gi,/Do not use rich-text:[^.]*\./gi,/its Editor mode rewrites URLs inside includes\.?/gi,/Markdown source \(code editor\)\.?/gi];for(const r of n)e=e.replace(r,"");return e=e.replace(/\s{2,}/g," ").replace(/\s+\./g,".").trim(),/filename|YYYY-MM-DD-key|content-runtime|autolink|TipTap/i.test(e)?"":e}function nf(t){return hg(t==null?void 0:t.description)}function Ct(t){var i,o;const e=String((t==null?void 0:t.message)||t||"Request failed"),n=[];t!=null&&t.status&&n.push(`HTTP ${t.status}`);const r=(i=t==null?void 0:t.data)==null?void 0:i.detail;if(r)n.push(String(r));else if(t!=null&&t.data&&typeof t.data=="object")try{n.push(JSON.stringify(t.data,null,2))}catch{}else t!=null&&t.stack&&!String(t.stack).includes(e)&&n.push(String(t.stack).split(`
271
271
  `).slice(0,6).join(`
data/studio/index.html CHANGED
@@ -21,7 +21,7 @@
21
21
  rel="stylesheet"
22
22
  href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css"
23
23
  />
24
- <script type="module" crossorigin src="/studio/assets/index-BJyN-kB3.js"></script>
24
+ <script type="module" crossorigin src="/studio/assets/index-B9h1WUYY.js"></script>
25
25
  <link rel="stylesheet" crossorigin href="/studio/assets/index-Cd1mlY_H.css">
26
26
  </head>
27
27
  <body>
data/studio/schema.yml CHANGED
@@ -541,12 +541,12 @@ content:
541
541
  type: group
542
542
  items:
543
543
  - name: page_home
544
- label: Home
544
+ label: Home references band
545
545
  type: file
546
546
  path: content/pages/headers.yml
547
547
  format: yaml
548
548
  root_key: index
549
- description: "Home page detail, including how Sources, Network, and Bibliography are presented on the index band."
549
+ description: "Title, kicker and description of the home band that groups Sources, Network and Bibliography."
550
550
  fields:
551
551
  - name: title
552
552
  label: Title
@@ -568,6 +568,93 @@ content:
568
568
  - name: title
569
569
  label: Title
570
570
  type: string
571
+ - name: summary_title
572
+ label: Summary title
573
+ type: string
574
+ keep_empty: true
575
+ description: "Heading of the summary section of the CV page. Default: Summary. Clear the field to hide it; delete the line in the YAML source to bring the default back."
576
+ - name: experience_title
577
+ label: Experience title
578
+ type: string
579
+ keep_empty: true
580
+ description: "Heading of the experience section of the CV page. Default: Experience. Clear the field to hide it; delete the line in the YAML source to bring the default back."
581
+ - name: products_title
582
+ label: Products title
583
+ type: string
584
+ keep_empty: true
585
+ description: "Heading of the products section of the CV page. Default: Products. Clear the field to hide it; delete the line in the YAML source to bring the default back."
586
+ - name: education_title
587
+ label: Education title
588
+ type: string
589
+ keep_empty: true
590
+ description: "Heading of the education section of the CV page. Default: Education. Clear the field to hide it; delete the line in the YAML source to bring the default back."
591
+ - name: languages_title
592
+ label: Languages title
593
+ type: string
594
+ keep_empty: true
595
+ description: "Heading of the languages section of the CV page. Default: Languages. Clear the field to hide it; delete the line in the YAML source to bring the default back."
596
+ - name: timeline_title
597
+ label: Timeline title
598
+ type: string
599
+ keep_empty: true
600
+ description: "Heading of the timeline section of the CV page. Default: Timeline. Clear the field to hide it; delete the line in the YAML source to bring the default back."
601
+ - name: contact_label
602
+ label: Contact label
603
+ type: string
604
+ keep_empty: true
605
+ description: "Heading of the contact section of the CV page. Default: Contact. Clear the field to hide it; delete the line in the YAML source to bring the default back."
606
+ - name: accent
607
+ label: Accent
608
+ type: select
609
+ options:
610
+ values: [green, ochre, blue, slate]
611
+ - name: tags
612
+ label: Tags
613
+ type: string
614
+ list: true
615
+ - name: page_projects
616
+ label: Projects
617
+ type: file
618
+ path: content/pages/headers.yml
619
+ format: yaml
620
+ root_key: projects
621
+ description: "Projects listing page chrome (title, kicker, note). Project entries live under Portfolio."
622
+ fields:
623
+ - name: title
624
+ label: Title
625
+ type: string
626
+ - name: kicker
627
+ label: Kicker
628
+ type: string
629
+ - name: note
630
+ label: Description
631
+ type: string
632
+ description: "Shown under the projects band title on the home page. Clear the field to hide it."
633
+ - name: cta
634
+ label: Home link label
635
+ type: string
636
+ keep_empty: true
637
+ description: "Label of the home band link to the projects page. Default: View all. Clear the field to hide it; delete the line in the YAML source to bring the default back."
638
+ - name: others_kicker
639
+ label: Project grid kicker
640
+ type: string
641
+ keep_empty: true
642
+ description: "Kicker of the section banner above the project grid on the projects page. Default: Index. Clear the field to hide it; delete the line in the YAML source to bring the default back."
643
+ - name: others_title
644
+ label: Project grid title
645
+ type: string
646
+ keep_empty: true
647
+ description: "Title of the section banner above the project grid on the projects page. Default: Other projects. Clear the field to hide it; delete the line in the YAML source to bring the default back."
648
+ - name: repositories_kicker
649
+ label: Repositories kicker
650
+ type: string
651
+ keep_empty: true
652
+ description: "Kicker of the section banner above the repository list on the projects page. Default: Open source. Clear the field to hide it; delete the line in the YAML source to bring the default back."
653
+ - name: repositories_title
654
+ label: Repositories title
655
+ type: string
656
+ keep_empty: true
657
+ description: "Title of the section banner above the repository list on the projects page. Default: Repositories. Clear the field to hide it; delete the line in the YAML source to bring the default back."
571
658
  - name: accent
572
659
  label: Accent
573
660
  type: select
@@ -595,6 +682,11 @@ content:
595
682
  - name: note
596
683
  label: Description
597
684
  type: string
685
+ - name: cta
686
+ label: Home link label
687
+ type: string
688
+ keep_empty: true
689
+ description: "Label of the home band link to this page. Default: View all. Clear the field to hide it; delete the line in the YAML source to bring the default back."
598
690
  - name: accent
599
691
  label: Accent
600
692
  type: select
@@ -621,6 +713,11 @@ content:
621
713
  - name: note
622
714
  label: Description
623
715
  type: string
716
+ - name: cta
717
+ label: Home link label
718
+ type: string
719
+ keep_empty: true
720
+ description: "Label of the home band link to this page. Default: View all. Clear the field to hide it; delete the line in the YAML source to bring the default back."
624
721
  - name: accent
625
722
  label: Accent
626
723
  type: select
@@ -641,6 +738,16 @@ content:
641
738
  - name: title
642
739
  label: Title
643
740
  type: string
741
+ - name: note
742
+ label: Description
743
+ type: string
744
+ keep_empty: true
745
+ description: "Shown under this page's panel title in the home references band. Clear the field to hide it."
746
+ - name: cta
747
+ label: Home link label
748
+ type: string
749
+ keep_empty: true
750
+ description: "Label of the panel link on the home page. Default: Open. Clear the field to hide it; delete the line in the YAML source to bring the default back."
644
751
  - name: accent
645
752
  label: Accent
646
753
  type: select
@@ -661,6 +768,16 @@ content:
661
768
  - name: title
662
769
  label: Title
663
770
  type: string
771
+ - name: note
772
+ label: Description
773
+ type: string
774
+ keep_empty: true
775
+ description: "Shown under this page's panel title in the home references band. Clear the field to hide it."
776
+ - name: cta
777
+ label: Home link label
778
+ type: string
779
+ keep_empty: true
780
+ description: "Label of the panel link on the home page. Default: Open. Clear the field to hide it; delete the line in the YAML source to bring the default back."
664
781
  - name: accent
665
782
  label: Accent
666
783
  type: select
@@ -684,6 +801,13 @@ content:
684
801
  - name: note
685
802
  label: Description
686
803
  type: string
804
+ keep_empty: true
805
+ description: "Shown under the Bibliography panel title in the home references band. Clear the field to hide it."
806
+ - name: cta
807
+ label: Home link label
808
+ type: string
809
+ keep_empty: true
810
+ description: "Label of the panel link on the home page. Default: Open. Clear the field to hide it; delete the line in the YAML source to bring the default back."
687
811
  - name: accent
688
812
  label: Accent
689
813
  type: select
@@ -68,14 +68,20 @@ function structuredCloneSafe(value) {
68
68
  /**
69
69
  * Drop empty optional scalars/arrays from a map before dump, keeping unknown keys.
70
70
  * Empty string / null / undefined / [] are omitted for known schema fields.
71
+ * Fields with `keep_empty: true` keep any string, "" included, because empty
72
+ * (hide the text) and absent (theme default) mean different things for them.
71
73
  */
72
74
  export function pruneEmptyFields(doc, fields) {
73
75
  if (!doc || typeof doc !== "object" || Array.isArray(doc)) return doc;
74
76
  const known = new Set((fields || []).map((f) => f.name).filter(Boolean));
77
+ const keepEmpty = new Set(
78
+ (fields || []).filter((f) => f && f.keep_empty === true).map((f) => f.name)
79
+ );
75
80
  const out = { ...doc };
76
81
  for (const key of Object.keys(out)) {
77
82
  if (!known.has(key)) continue;
78
83
  const val = out[key];
84
+ if (keepEmpty.has(key) && typeof val === "string") continue;
79
85
  if (val === "" || val == null) {
80
86
  delete out[key];
81
87
  } else if (Array.isArray(val) && val.length === 0) {
@@ -17,6 +17,16 @@ files:
17
17
  - name: title
18
18
  label: Title
19
19
  type: string
20
+ - name: note
21
+ label: Description
22
+ type: string
23
+ keep_empty: true
24
+ description: "Shown under this page's panel title in the home references band. Clear the field to hide it."
25
+ - name: cta
26
+ label: Home link label
27
+ type: string
28
+ keep_empty: true
29
+ description: "Label of the panel link on the home page. Default: Open. Clear the field to hide it; delete the line in the YAML source to bring the default back."
20
30
  - name: accent
21
31
  label: Accent
22
32
  type: select
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pandorga
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.14.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors
@@ -433,6 +433,7 @@ files:
433
433
  - scripts/test/test-fonts.rb
434
434
  - scripts/test/test-form-control-theming.rb
435
435
  - scripts/test/test-git-chronology.rb
436
+ - scripts/test/test-hero-16x9.rb
436
437
  - scripts/test/test-hero-name.rb
437
438
  - scripts/test/test-hero-portraits.mjs
438
439
  - scripts/test/test-hero-portraits.rb
@@ -472,6 +473,7 @@ files:
472
473
  - scripts/test/test-studio-rewrite-api-contract.rb
473
474
  - scripts/test/test-studio-rewrite-sanitize.mjs
474
475
  - scripts/test/test-studio-schema-composition.rb
476
+ - scripts/test/test-studio-section-texts.rb
475
477
  - scripts/test/test-studio-slug-config.rb
476
478
  - scripts/test/test-studio-taxonomy.rb
477
479
  - scripts/test/test-template-packages.rb
@@ -482,6 +484,7 @@ files:
482
484
  - scripts/test/test-timeline-pan.rb
483
485
  - scripts/test/test-unwrap-media-src.mjs
484
486
  - scripts/test/test-unwrap-media-src.rb
487
+ - scripts/test/test-v3-layout.rb
485
488
  - scripts/test/test-validate-allowlist.rb
486
489
  - scripts/test/test-withdrawn-content-export.rb
487
490
  - scripts/test/test-writing-index-export-slugs.rb
@@ -511,7 +514,7 @@ files:
511
514
  - studio-app/src/preview/render.js
512
515
  - studio-app/src/styles/studio.css
513
516
  - studio-app/vite.config.js
514
- - studio/assets/index-BJyN-kB3.js
517
+ - studio/assets/index-B9h1WUYY.js
515
518
  - studio/assets/index-Cd1mlY_H.css
516
519
  - studio/index.html
517
520
  - studio/schema.yml