@veluai/velu 0.1.14 → 0.1.15
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.
- package/dist/cli.js +21 -21
- package/package.json +1 -1
- package/runtime/velu-ui/components/Logo.jsx +31 -0
- package/runtime/velu-ui/components/PageFooter.jsx +6 -74
- package/runtime/velu-ui/components/PageHeader.jsx +10 -2
- package/runtime/velu-ui/components/SocialLinks.jsx +90 -0
- package/runtime/velu-ui/components/page-footer.css +5 -5
- package/runtime/velu-ui/components/page-header.css +37 -0
- package/runtime/velu-ui/components/powered-by.css +20 -0
- package/runtime/velu-ui/index.js +2 -0
- package/schema/velu.schema.json +114 -0
- package/src/runtime/App.jsx +76 -60
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
`,"utf-8");let r=await
|
|
4
|
-
`)}var
|
|
5
|
-
`),"","export const pages = {",
|
|
6
|
-
`),"};",`export const navigation = ${JSON.stringify(o)};`,""].join(`
|
|
7
|
-
`)}var
|
|
8
|
-
`))
|
|
9
|
-
`)}function
|
|
10
|
-
`)}var
|
|
2
|
+
var vt=Object.defineProperty;var k=(e,t)=>()=>(e&&(t=e(e=0)),t);var de=(e,t)=>{for(var o in t)vt(e,o,{get:t[o],enumerable:!0})};var Ne={};de(Ne,{initProject:()=>wt});import z from"node:fs/promises";import G from"node:path";import{fileURLToPath as yt}from"node:url";async function wt(e,t){try{if((await z.readdir(e)).length>0)throw new Error(`Directory "${t}" already exists and is not empty. Choose a different name or remove the folder first.`)}catch(i){if(i.code!=="ENOENT")throw i}await z.mkdir(e,{recursive:!0}),await z.cp(bt,e,{recursive:!0});let o=G.join(e,"velu.json"),n=JSON.parse(await z.readFile(o,"utf-8"));n.name=t,await z.writeFile(o,JSON.stringify(n,null,2)+`
|
|
3
|
+
`,"utf-8");let r=await Ee(e);console.log(`[velu] created ${t}/`);for(let i of r)console.log(` ${i}`);console.log(""),console.log("Next steps:"),console.log(` cd ${t}`),console.log(" velu dev")}async function Ee(e,t=e,o=[]){let n=await z.readdir(e,{withFileTypes:!0});for(let r of n){let i=G.join(e,r.name);r.isDirectory()?await Ee(i,t,o):o.push(G.relative(t,i).split(G.sep).join("/"))}return o.sort()}var xt,bt,Fe=k(()=>{xt=G.dirname(yt(import.meta.url)),bt=G.resolve(xt,"..","templates","starter")});import{visit as kt}from"unist-util-visit";import{toString as St}from"mdast-util-to-string";import $t from"github-slugger";import{valueToEstree as jt}from"estree-util-value-to-estree";function D(){return e=>{let t=new $t,o=[];kt(e,"heading",a=>{let s=St(a);o.push({depth:a.depth,id:t.slug(s),label:s})});let n={children:[]},r=[{node:n,depth:0}];for(let a of o){for(;r.length>1&&r[r.length-1].depth>=a.depth;)r.pop();let s={id:a.id,label:a.label},l=r[r.length-1].node;l.children||(l.children=[]),l.children.push(s),r.push({node:s,depth:a.depth})}let i=n.children;e.children.unshift({type:"mdxjsEsm",value:"",data:{estree:{type:"Program",sourceType:"module",body:[{type:"ExportNamedDeclaration",specifiers:[],declaration:{type:"VariableDeclaration",kind:"const",declarations:[{type:"VariableDeclarator",id:{type:"Identifier",name:"toc"},init:jt(i)}]}}]}}})}}var ge=k(()=>{});import At from"node:fs/promises";import Ie from"node:path";function Y(e){if(typeof e=="string"){let t=e.trim();return t?{light:t,dark:t,href:"/"}:null}if(e&&typeof e=="object"){let t=(e.light??e.dark)?.trim?.(),o=(e.dark??e.light)?.trim?.();return!t||!o?null:{light:t,dark:o,href:e.href?.trim?.()||"/"}}return null}function ve(e){if(!e||typeof e!="object")return null;let t=[];if(e.socials&&typeof e.socials=="object")for(let[n,r]of Object.entries(e.socials))typeof r!="string"||!r.trim()||t.push({kind:_t[n]??n,href:r.trim()});let o=Array.isArray(e.links)?e.links.filter(n=>n&&typeof n=="object").map(n=>({title:(n.header??n.title??"").trim(),items:(Array.isArray(n.items)?n.items:[]).filter(r=>r&&r.label).map(r=>({label:r.label,href:r.href??"#"}))})).filter(n=>n.title||n.items.length):[];return!t.length&&!o.length?null:{socials:t,links:o}}function he(e){return/^(https?:)?\/\//.test(e)||/^(mailto|tel):/.test(e)}function ye(e){if(!e||typeof e!="object")return null;let t=(Array.isArray(e.links)?e.links:[]).filter(r=>r&&typeof r.href=="string"&&r.href.trim()).map(r=>({label:r.label??"",href:r.href.trim(),icon:r.icon??null,external:he(r.href.trim())})),o=null,n=e.primary;if(n&&typeof n=="object"&&typeof n.href=="string"&&n.href.trim()){let r=n.href.trim();o=n.type==="github"?{kind:"github",label:n.label??"GitHub",href:r,external:he(r)}:{kind:"button",label:n.label??"",href:r,external:he(r)}}return!t.length&&!o?null:{links:t,primary:o}}function xe(e={},t){let o=e.colors?.primary??Lt;return{name:e.name??t??"docs",colors:{primary:o,light:e.colors?.light??o,dark:e.colors?.dark??o},favicon:e.favicon??null,logo:Y(e.logo),footer:ve(e.footer),navbar:ye(e.navbar),font:{family:e.font?.family??Ot},navigation:e.navigation??null}}async function Re(e){let t=Ie.join(e,"velu.json"),o;try{o=await At.readFile(t,"utf-8")}catch(r){throw r.code==="ENOENT"?new Error(`No velu.json found in ${e}. Run \`velu init <name>\` to scaffold one.`):r}let n;try{n=JSON.parse(o)}catch(r){throw new Error(`velu.json is not valid JSON: ${r.message}`)}return xe(n,Ie.basename(e))}function Me(e){let{colors:t,font:o}=e;return[":root:root {",` --accent-color: ${t.light};`,` --font-sans: "${o.family}", sans-serif;`,"}",':root:root[data-theme="dark"] {',` --accent-color: ${t.dark};`,"}"].join(`
|
|
4
|
+
`)}var Lt,Ot,ne,oe,_t,re=k(()=>{Lt="#dc143c",Ot="Google Sans Flex",ne="/@velu-logo-light",oe="/@velu-logo-dark";_t={twitter:"x","x-twitter":"x"}});import Pt from"github-slugger";import Ct from"iso-639-1";function Et(e){let t=String(e).toLowerCase();return Ct.getNativeName(t)||e}function ie(e){return new Pt().slug(String(e??""))}function Nt(e){let t=String(e).replace(/^\/+|\/+$/g,"").split("/").filter(Boolean);return t[t.length-1]==="index"&&t.pop(),"/"+t.join("/")}function V(e){let t={kind:"root",children:K(e??{})};return Ge(t.children),t}function K(e){let t=Array.isArray(e.anchors)?e.anchors.map(Ut):[];if(Array.isArray(e.products))return[...t,...e.products.map(Ft)];if(Array.isArray(e.versions))return[...t,...e.versions.map(It)];if(Array.isArray(e.languages))return[...t,...e.languages.map(Rt)];if(Array.isArray(e.tabs))return[...t,...e.tabs.map(Mt)];let o=[...t];return Array.isArray(e.groups)&&o.push(...e.groups.map(Ue)),Array.isArray(e.pages)&&o.push(...e.pages.map(ze)),o}function Ft(e){return{kind:"product",label:e.name??e.product,slug:ie(e.product),icon:e.icon,color:e.color,default:!!e.default,children:K(e)}}function It(e){return{kind:"version",label:e.version,slug:ie(e.version),default:!!e.default,children:K(e)}}function Rt(e){let t=String(e.language);return{kind:"language",code:t,label:Et(t),slug:ie(t),default:!!e.default,children:K(e)}}function Mt(e){return{kind:"tab",label:e.tab,slug:ie(e.tab),icon:e.icon,href:e.href,children:K(e)}}function Ut(e){return{kind:"anchor",label:e.anchor,icon:e.icon,href:e.href,color:e.color}}function Ue(e){let t=(e.pages??[]).map(ze);return e.root&&t.unshift({kind:"page",pagePath:e.root}),{kind:"group",label:e.group,icon:e.icon,expanded:!!e.expanded,root:e.root,children:t}}function ze(e){return typeof e=="string"?{kind:"page",pagePath:e}:Ue(e)}function Ge(e){for(let t of Tt){let o=e.filter(n=>n.kind===t);o.length&&!o.some(n=>n.default)&&(o[0].default=!0)}for(let t of e)t.children&&Ge(t.children)}function Ve(e){let t=[],o=(n,r,i)=>{if(n.kind==="page"){t.push({pagePath:n.pagePath,url:zt(n.pagePath,r),ctx:{...r},ancestors:i,node:n});return}let a=r;n.kind==="product"?a={...r,product:n.default?null:n.slug}:n.kind==="version"?a={...r,version:n.default?null:n.slug}:n.kind==="language"&&(a={...r,language:n.default?null:n.slug});let s=n.kind==="root"?i:[...i,n];for(let l of n.children??[])o(l,a,s)};return o(e,{product:null,version:null,language:null},[]),t}function zt(e,t){let o=[t.product,t.version,t.language].filter(Boolean),n=Nt(e).split("/").filter(Boolean);return"/"+[...o,...n].join("/")}var Tt,W=k(()=>{Tt=["product","version","language"]});import Gt from"node:fs";import Be from"node:path";function B(e,t){let o=[],n=[],r=new Set;for(let i of Ve(e)){if(r.has(i.url)){o.push(`duplicate URL "${i.url}" (page "${i.pagePath}") \u2014 keeping the first occurrence`);continue}r.add(i.url);let a=i.pagePath.replace(/^\/+|\/+$/g,""),s=Be.join(t,...a.split("/"))+".mdx",l=Gt.existsSync(s);l||o.push(`page "${i.pagePath}" \u2192 ${Be.relative(t,s)} not found`),n.push({pagePath:i.pagePath,url:i.url,fileAbsPath:s,exists:l})}return{pageEntries:n,warnings:o}}var se=k(()=>{W()});import Vt from"node:fs";import Z from"node:path";function He(e){return{name:"velu-site",resolveId(t){return t===qe?Je:null},load(t){return t!==Je?null:Bt(e)}}}function Bt(e){let t={};try{t=JSON.parse(Vt.readFileSync(Z.join(e,"velu.json"),"utf-8"))}catch{}let o=V(t.navigation??{}),{pageEntries:n,warnings:r}=B(o,e);for(let c of r)console.warn(`[velu] nav: ${c}`);let i=Y(t.logo),a={name:t.name??Z.basename(e),logo:i?{light:ne,dark:oe,href:i.href,mono:i.light===i.dark}:null,footer:ve(t.footer),navbar:ye(t.navbar)},s=[],l=[],u=0;for(let c of n)if(c.exists){let f=JSON.stringify(c.fileAbsPath.split(Z.sep).join("/")),A=JSON.stringify(Z.relative(e,c.fileAbsPath).split(Z.sep).join("/")),p=`__p${u++}`;s.push(`import * as ${p} from ${f};`),l.push(` ${JSON.stringify(c.url)}: { Component: ${p}.default, frontmatter: ${p}.frontmatter, toc: ${p}.toc, relPath: ${A} },`)}else l.push(` ${JSON.stringify(c.url)}: { missing: true },`);return["// AUTO-GENERATED by vite-plugin-velu-site. Do not edit.",s.join(`
|
|
5
|
+
`),"","export const pages = {",l.join(`
|
|
6
|
+
`),"};",`export const navigation = ${JSON.stringify(o)};`,`export const site = ${JSON.stringify(a)};`,""].join(`
|
|
7
|
+
`)}var qe,Je,De=k(()=>{W();se();re();qe="virtual:velu-site",Je="\0"+qe});function v(e){return{category:"render-crash",severity:"error",file:null,line:null,column:null,frame:null,title:"Something went wrong",detail:"",hint:"",suggestion:null,...e}}function Jt(e,t){if(e=String(e),t=String(t),e===t)return 0;if(!e.length)return t.length;if(!t.length)return e.length;let o=Array.from({length:t.length+1},(r,i)=>i),n=new Array(t.length+1);for(let r=1;r<=e.length;r++){n[0]=r;for(let i=1;i<=t.length;i++){let a=e[r-1]===t[i-1]?0:1;n[i]=Math.min(n[i-1]+1,o[i]+1,o[i-1]+a)}[o,n]=[n,o]}return o[t.length]}function le(e,t=[]){if(!e)return null;let o=String(e).toLowerCase(),n=null,r=1/0;for(let a of t){let s=Jt(o,String(a).toLowerCase());s<r&&(r=s,n=a)}let i=Math.min(3,Math.max(2,Math.floor(o.length/4)));return r<=i?n:null}function qt(e,t){return e==="warning"?P(t,j.yellow,"\u26A0"):e==="info"?P(t,j.cyan,"\u2139"):P(t,j.red,"\u2716")}function we(e,{color:t=!1}={}){let o=be[e.category]||e.category,n=e.line?`${e.line}${e.column?`:${e.column}`:""}`:"",r=e.file?`${e.file}${n?`:${n}`:""}`:n,i=[];if(i.push(`${qt(e.severity,t)} ${P(t,j.bold,o)}`+(r?` ${P(t,j.dim,r)}`:"")),e.title&&i.push(` ${e.title}`),e.detail&&e.detail!==e.title&&i.push(` ${P(t,j.dim,e.detail)}`),e.frame)for(let a of String(e.frame).split(`
|
|
8
|
+
`))i.push(` ${a}`);return e.suggestion&&i.push(` ${P(t,j.cyan,`Did you mean <${e.suggestion}>?`)}`),e.hint&&i.push(` ${P(t,j.dim,`\u2192 ${e.hint}`)}`),i.join(`
|
|
9
|
+
`)}function ae(e,{color:t=!1}={}){if(!e.length)return P(t,j.cyan,"\u2713 No issues found.");let o=new Map;for(let s of e){let l=s.file||"velu.json";o.has(l)||o.set(l,[]),o.get(l).push(s)}let n=[];for(let[s,l]of o){n.push(P(t,j.bold,s));for(let u of l)n.push(we({...u,file:null},{color:t}).replace(/^/gm," "));n.push("")}let r=e.filter(s=>s.severity!=="warning"&&s.severity!=="info").length,i=e.length-r,a=`${r} error${r===1?"":"s"}`+(i?`, ${i} warning${i===1?"":"s"}`:"");return n.push(P(t,r?j.red:j.yellow,a)),n.join(`
|
|
10
|
+
`)}var be,j,P,F=k(()=>{be={"mdx-syntax":"MDX syntax",frontmatter:"Frontmatter","unknown-component":"Unknown component","invalid-props":"Invalid options","render-crash":"Render error","config-json":"velu.json","config-schema":"velu.json","config-nav":"Navigation","config-asset":"Asset"};j={reset:"\x1B[0m",red:"\x1B[31m",yellow:"\x1B[33m",cyan:"\x1B[36m",dim:"\x1B[2m",bold:"\x1B[1m"},P=(e,t,o)=>e?`${t}${o}${j.reset}`:o});function Ht(e){let t=e,o=0;for(;t&&t.cause&&o<3&&!(t.loc||t.line||/`[^`]+` to be defined/.test(t.message||""));)t=t.cause,o+=1;return t||e}function ke(e,t){if(!e)return null;let o=Ye(e).replace(/[?#].*$/,""),n=t?Ye(t).replace(/\/+$/,""):"";return n&&o.toLowerCase().startsWith(n.toLowerCase())?o.slice(n.length).replace(/^\/+/,""):!/^([a-zA-Z]:\/|\/)/.test(o)&&!/node_modules/.test(o)?o:null}function Dt(e){return e?String(e).replace(/([a-zA-Z]:\\|\/)[^\s'"()]+/g,t=>/node_modules|[\\/]vite[\\/]|@veluai|velu-cli|velu-ui/.test(t)?"\u2026":t).trim():""}function Yt(e){let t=/\((\d+):(\d+)(?:-\d+:\d+)?\)\s*$/.exec(String(e||""));return t?{line:Number(t[1]),column:Number(t[2])}:null}function Kt(e){return String(e||"").replace(/\s*\(\d+:\d+(?:-\d+:\d+)?\)\s*$/,"").trim()}function Zt(e,t){if(!e||!t)return null;let o=String(e);return/node_modules|[\\/]vite[\\/]|@veluai[\\/]/.test(o)?null:o}function X(e,t={}){let{projectDir:o="",knownComponents:n=[],file:r=null}=t,i=null;for(let C=e,S=0;C&&S<6;C=C.cause,S+=1)if(C.veluIssue){i=C.veluIssue;break}if(i)return v({...i,file:i.file||r});let a=Ht(e)||{},s=a.message||String(e||""),l=/Expected component `([^`]+)` to be defined/.exec(s);if(l){let C=l[1],S=/referenced in your code at `(\d+):(\d+)/.exec(s),q=/\bin `([^`]+)`/.exec(s),y=q?ke(q[1],o):null;return v({category:"unknown-component",file:y||r,line:S?Number(S[1]):null,column:S?Number(S[2]):null,title:`<${C}> is not a built-in component`,detail:"",hint:"Components are built in \u2014 check the spelling and capitalization (you don\u2019t import them).",suggestion:le(C,n)})}let u=a.loc||(a.line!=null?{line:a.line,column:a.column,file:a.file}:null)||Yt(a.reason||s),c=a.id||u&&u.file||a.file||null,f=ke(c,o)||r,A=!!f&&ke(c,o)!=null,p=Zt(a.frame,A||!!r),L=Dt(Kt(a.reason||s)),d=Wt.test(s),N=a.source==="remark-frontmatter"||/\byaml\b|frontmatter/i.test(s)||u&&u.line===1&&/unexpected|expected/i.test(s)&&/---/.test(s);return u||d||p&&A?v({category:N?"frontmatter":"mdx-syntax",file:f,line:u?u.line:null,column:u?u.column:null,frame:p,title:L||(N?"Invalid frontmatter":"MDX syntax error"),detail:"",hint:N?"Check the YAML between the --- markers \u2014 indentation, quotes, and colons.":"Make sure every <Component> has a matching closing tag and that { } braces are balanced."}):/\.map is not a function|is not iterable|reading '|undefined \(reading/i.test(s)?v({category:"invalid-props",file:f,title:"A component received an invalid value",detail:L,hint:"Check the props/options you passed to a component on this page."}):v({category:"render-crash",file:f,title:"This page failed to render",detail:L,hint:"Set VELU_DEBUG=1 for the full stack trace."})}var Ye,Wt,Se=k(()=>{F();Ye=e=>String(e).replace(/\\/g,"/");Wt=/closing tag|unexpected closing|could not parse (?:expression|import|export)|unexpected character|unexpected end of|expected a closing|expected the closing|unexpected `|in expression|misnested|before name/i});function Xt(e){let t=be[e.category]||e.category,o=e.file?`${e.file}${e.line?`:${e.line}${e.column?`:${e.column}`:""}`:""}`:"",n=[];return n.push('<div class="velu-err-card">'),n.push('<div class="velu-err-head">'),n.push(`<span class="velu-err-pill">${T(t)}</span>`),n.push("</div>"),e.title&&n.push(`<div class="velu-err-title">${T(e.title)}</div>`),o&&n.push(`<a class="velu-err-loc" title="${T(o)}">${T(o)}</a>`),e.detail&&n.push(`<div class="velu-err-detail">${T(e.detail)}</div>`),e.frame&&n.push(`<pre class="velu-err-frame">${T(e.frame)}</pre>`),e.suggestion&&n.push(`<div class="velu-err-suggest">Did you mean <code><${T(e.suggestion)}></code>?</div>`),e.hint&&n.push(`<div class="velu-err-hint">${T(e.hint)}</div>`),n.push("</div>"),n.join("")}function $e(e,t={}){let o=Array.isArray(e)?e:[e],n=t.title||(o.length>1?`${o.length} problems to fix`:"There\u2019s a problem to fix"),r=o.map(Xt).join(`
|
|
11
11
|
`);return`<!doctype html>
|
|
12
12
|
<html lang="en">
|
|
13
13
|
<head>
|
|
@@ -74,20 +74,20 @@ var ut=Object.defineProperty;var k=(e,t)=>()=>(e&&(t=e(e=0)),t);var ce=(e,t)=>{f
|
|
|
74
74
|
<body>
|
|
75
75
|
<div class="velu-err-wrap">
|
|
76
76
|
<div class="velu-err-brand">velu</div>
|
|
77
|
-
<h1 class="velu-err-h">${
|
|
77
|
+
<h1 class="velu-err-h">${T(n)}</h1>
|
|
78
78
|
${r}
|
|
79
79
|
<div class="velu-err-foot">Fix the file and save \u2014 this page reloads automatically.</div>
|
|
80
80
|
</div>
|
|
81
81
|
</body>
|
|
82
|
-
</html>`}var
|
|
83
|
-
`).length}function
|
|
84
|
-
`).length}function
|
|
85
|
-
`).length}function
|
|
82
|
+
</html>`}var T,Ke=k(()=>{F();T=e=>String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")});var I,je=k(()=>{I=["Callout","Card","CardGroup","Accordion","AccordionGroup","Columns","Field","Prompt","Steps","Step","Tree","Folder","File","Image","CodeBlock","CodeGroup","MethodBadge","ApiPath","TryItBar","ApiField","ApiClient","ApiSidebar"]});import ce from"node:fs";import ee from"node:path";import{fileURLToPath as Qt}from"node:url";import en from"iso-639-1";function Q(e,t){if(!t)return null;let o=e.indexOf(`"${t}"`);return o===-1?null:e.slice(0,o).split(`
|
|
83
|
+
`).length}function on(e,t){return t==null||t<0?null:e.slice(0,t).split(`
|
|
84
|
+
`).length}function Ae(e,t){if(!t)return null;let o=e.indexOf(`"${t}"`);return o===-1?null:e.slice(0,o).split(`
|
|
85
|
+
`).length}function rn(e,t,o){let n=[];return s(e,t,"",null),n;function r(l){if(l&&l.$ref){let u=l.$ref.replace(/^#\//,"").split("/"),c=o;for(let f of u)c=c?.[f];return c||{}}return l}function i(l){return Array.isArray(l)?"array":l===null?"null":typeof l}function a(l,u){let c=n.length;s(l,u,"",null,!0);let f=n.length===c;return f||(n.length=c),f}function s(l,u,c,f,A){let p=r(u);if(p){if(p.oneOf){p.oneOf.some(d=>a(l,d))||n.push({path:c,key:f,message:"value does not match any allowed shape"});return}if(p.type&&i(l)!==p.type){n.push({path:c,key:f,message:`expected ${p.type}, got ${i(l)}`});return}if(p.type==="string"&&p.minLength!=null&&l.length<p.minLength&&n.push({path:c,key:f,message:"must not be empty"}),p.type==="object"&&i(l)==="object"){for(let d of p.required||[])d in l||n.push({path:c?`${c}.${d}`:d,key:d,message:`missing required "${d}"`});let L=p.properties||{};if(p.additionalProperties===!1)for(let d of Object.keys(l))d in L||n.push({path:c?`${c}.${d}`:d,key:d,message:`unknown field "${d}"`});for(let[d,N]of Object.entries(L))d in l&&s(l[d],N,c?`${c}.${d}`:d,d,A)}p.type==="array"&&Array.isArray(l)&&p.items&&l.forEach((L,d)=>s(L,p.items,`${c}[${d}]`,f,A))}}}function sn(e){if(typeof e!="string")return!1;let t=e.trim();return/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(t)||/^(rgb|rgba|hsl|hsla|color|oklch|lab|lch)\(/i.test(t)||/^[a-z][a-z0-9-]*$/i.test(t)}function ue(e){let t=[],o=ee.join(e,"velu.json"),n;try{n=ce.readFileSync(o,"utf-8")}catch(s){return s.code==="ENOENT"?t.push(v({category:"config-json",file:"velu.json",title:"No velu.json found",hint:"Run `velu init <name>` to scaffold a project."})):t.push(v({category:"config-json",file:"velu.json",title:"Could not read velu.json"})),t}let r;try{r=JSON.parse(n)}catch(s){let l=/position (\d+)/.exec(s.message||""),u=l?on(n,Number(l[1])):null;return t.push(v({category:"config-json",file:"velu.json",line:u,title:"velu.json is not valid JSON",detail:String(s.message||"").replace(/\s+in JSON.*$/,""),hint:"Check for a trailing comma, a missing quote, or an unclosed brace."})),t}let i=null;try{i=JSON.parse(ce.readFileSync(nn,"utf-8"))}catch{}if(i)for(let s of rn(r,i,i)){let l=/^unknown field/.test(s.message);t.push(v({category:"config-schema",severity:l?"warning":"error",file:"velu.json",line:Q(n,s.key),title:s.path?`${s.path}: ${s.message}`:s.message,hint:l?"Remove it, or check the spelling against the velu.json schema.":""}))}for(let s of["primary","light","dark"]){let l=r.colors?.[s];l!=null&&!sn(l)&&t.push(v({category:"config-schema",severity:"warning",file:"velu.json",line:Q(n,s),title:`colors.${s}: "${l}" doesn\u2019t look like a CSS color`,hint:"Use a hex value like #dc143c, an rgb()/hsl() function, or a CSS color name."}))}if(typeof r.favicon=="string"&&r.favicon.trim()){let s=ee.resolve(e,r.favicon.replace(/^\/+/,""));ce.existsSync(s)||t.push(v({category:"config-asset",file:"velu.json",line:Q(n,"favicon"),title:`favicon not found: ${r.favicon}`,hint:"The path is relative to the project root."}))}let a=Y(r.logo);if(a){let s=new Map;for(let l of["light","dark"]){let u=a[l];u&&s.set(u,[...s.get(u)||[],l])}for(let[l,u]of s)if(!ce.existsSync(ee.resolve(e,l.replace(/^\/+/,"")))){let c=u.length===2?"logo":`logo (${u[0]})`;t.push(v({category:"config-asset",file:"velu.json",line:Ae(n,l),title:`${c} not found: ${l}`,hint:"The path is relative to the project root."}))}}if(r.navigation&&typeof r.navigation=="object"){let s;try{s=V(r.navigation)}catch(l){t.push(v({category:"config-nav",file:"velu.json",line:Q(n,"navigation"),title:"navigation could not be processed",detail:String(l.message||"")})),s=null}if(s){let{pageEntries:l,warnings:u}=B(s,e);for(let c of u){let f=/not found$/.test(c),A=/page "([^"]+)"/.exec(c),p=A?Ae(n,A[1]):null;t.push(v({category:"config-nav",severity:f?"error":"warning",file:"velu.json",line:p,title:c,hint:f?"Create the .mdx file, or remove the page from navigation.":""}))}ln(s,n,t)}}return t}function ln(e,t,o){let n=new Set,r=i=>{if(!(!i||typeof i!="object")){if(i.language&&!n.has(i.language)){n.add(i.language);let a=String(i.language).toLowerCase();en.validate(a)||o.push(v({category:"config-nav",severity:"warning",file:"velu.json",line:Ae(t,i.language)||Q(t,"language"),title:`"${i.language}" is not a valid ISO 639-1 language code`,hint:'Use a two-letter code like "en", "fr", or "ja".'}))}for(let a of Object.values(i))Array.isArray(a)?a.forEach(r):a&&typeof a=="object"&&r(a)}};r(e)}var tn,nn,Le=k(()=>{F();W();se();re();tn=ee.dirname(Qt(import.meta.url)),nn=ee.resolve(tn,"..","..","schema","velu.schema.json")});import an from"node:fs/promises";import cn from"node:fs";import R from"node:path";import{fileURLToPath as un,pathToFileURL as fn}from"node:url";import{compile as pn}from"@mdx-js/mdx";import mn from"remark-frontmatter";import dn from"remark-mdx-frontmatter";import gn from"remark-gfm";import hn from"rehype-slug";import{visit as vn}from"unist-util-visit";function Oe(){if(fe)return fe;let e=R.dirname(un(import.meta.url)),t=[R.resolve(e,"..","runtime","velu-ui"),R.resolve(e,"..","..","runtime","velu-ui"),R.resolve(e,"..","..","..","velu-ui","src")],o=null;for(let n of t){let r=R.join(n,"lib","component-schemas.js");if(cn.existsSync(r)){o=r;break}}return fe=o?import(fn(o).href).then(n=>n.validateProps).catch(()=>null):Promise.resolve(null),fe}function yn(e){let t={};for(let o of e.attributes||[])o.type==="mdxJsxAttribute"&&(o.value==null?t[o.name]=!0:typeof o.value=="string"&&(t[o.name]=o.value));return t}function xn(e,t){return()=>o=>{vn(o,n=>{if((n.type==="mdxJsxFlowElement"||n.type==="mdxJsxTextElement")&&typeof n.name=="string"&&/^[A-Z]/.test(n.name)&&!n.name.includes(".")&&e.push({name:n.name,props:yn(n),line:n.position?.start?.line??null,column:n.position?.start?.column??null}),n.type==="mdxjsEsm"&&typeof n.value=="string")for(let r of n.value.matchAll(/\b([A-Z][A-Za-z0-9_]*)\b/g))t.add(r[1])})}}async function bn(e,t){let o=[],n;try{n=await an.readFile(t.fileAbsPath,"utf-8")}catch{return o}let r=R.relative(e,t.fileAbsPath).split(R.sep).join("/"),i=[],a=new Set;try{await pn({value:n,path:t.fileAbsPath},{remarkPlugins:[mn,dn,gn,D,xn(i,a)],rehypePlugins:[hn],providerImportSource:"@mdx-js/react"})}catch(c){return o.push(X(c,{projectDir:e,knownComponents:I,file:r})),o}let s=new Set(I),l=await Oe(),u=new Set;for(let c of i)if(!a.has(c.name)){if(!s.has(c.name)){if(u.has(c.name))continue;u.add(c.name),o.push(v({category:"unknown-component",file:r,line:c.line,column:c.column,title:`<${c.name}> is not a built-in component`,hint:"Components are built in \u2014 check the spelling and capitalization (you don\u2019t import them).",suggestion:le(c.name,I)}));continue}if(l){let f=l(c.name,c.props,{checkRequired:!1});f&&o.push(v({...f,file:r,line:c.line,column:c.column}))}}return o}async function We(e,t){let o=t.filter(s=>s.exists),n=[],r=8,i=0;async function a(){for(;i<o.length;){let s=o[i++],l=await bn(e,s);for(let u of l)n.push(u)}}return await Promise.all(Array.from({length:Math.min(r,o.length)},a)),n}var fe,_e=k(()=>{ge();F();Se();je()});import{visit as wn}from"unist-util-visit";function kn(e,t){let o=Ze(e).replace(/[?#].*$/,""),n=Ze(t).replace(/\/+$/,"");return n&&o.toLowerCase().startsWith(n.toLowerCase())?o.slice(n.length).replace(/^\/+/,""):null}function Xe(e,t){return()=>(o,n)=>{typeof e=="function"&&wn(o,r=>{if(r.type!=="mdxJsxFlowElement"&&r.type!=="mdxJsxTextElement"||typeof r.name!="string"||!/^[A-Z]/.test(r.name)||r.name.includes("."))return;let i={};for(let s of r.attributes||[])s.type==="mdxJsxAttribute"&&(s.value==null?i[s.name]=!0:typeof s.value=="string"&&(i[s.name]=s.value));let a=e(r.name,i,{checkRequired:!1});if(a){let s=new Error(a.title);throw s.veluIssue={...a,file:kn(n?.path,t),line:r.position?.start?.line??null,column:r.position?.start?.column??null},s.loc={line:s.veluIssue.line,column:s.veluIssue.column},s}})}}var Ze,Qe=k(()=>{Ze=e=>String(e||"").replace(/\\/g,"/")});var st={};de(st,{startDevServer:()=>Yn});import et from"node:fs/promises";import M from"node:fs";import Sn from"node:http";import{spawn as $n}from"node:child_process";import w from"node:path";import{fileURLToPath as jn}from"node:url";import{createRequire as An}from"node:module";import Ln from"chokidar";import On from"express";import _n from"@tailwindcss/vite";import Pn from"@vitejs/plugin-react";import Cn from"@mdx-js/rollup";import Tn from"remark-frontmatter";import En from"remark-mdx-frontmatter";import Nn from"remark-gfm";import Fn from"rehype-slug";import{createServer as In,searchForWorkspaceRoot as Rn}from"vite";function Vn(e){return`<link rel="stylesheet" href="${`https://fonts.googleapis.com/css2?family=${encodeURIComponent(e).replace(/%20/g,"+")}:wght@300;400;500;600;700&display=swap`}" />`}function nt(e,t){let o=we(e,{color:!!process.stderr.isTTY});console.error(`
|
|
86
86
|
`+o+`
|
|
87
|
-
`),process.env.VELU_DEBUG&&t&&console.error(t)}function
|
|
88
|
-
`)}function
|
|
89
|
-
`+
|
|
90
|
-
`),
|
|
91
|
-
${
|
|
92
|
-
</style>`,
|
|
93
|
-
`),
|
|
87
|
+
`),process.env.VELU_DEBUG&&t&&console.error(t)}function Bn(){try{return JSON.parse(M.readFileSync(w.join(J,"package.json"),"utf8")).version||"unknown"}catch{return"unknown"}}function Jn(e,t){let o=w.resolve(e,".velu"),n=w.resolve(o,".velu-version"),r=null;try{r=M.readFileSync(n,"utf8").trim()}catch{}if(r!==t){if(r!==null)try{M.rmSync(w.join(o,"vite-cache"),{recursive:!0,force:!0}),console.log("[velu] new version detected \u2014 refreshing the preview cache\u2026")}catch{}try{M.mkdirSync(o,{recursive:!0}),M.writeFileSync(n,t)}catch{}}}async function ot(e){try{return await Re(e)}catch{return xe({},w.basename(e))}}async function rt(e,t){let o=await e.moduleGraph.getModuleByUrl(t,!0),n=new Map,r=new Set;async function i(a){if(!(!a||r.has(a.url))){if(r.add(a.url),a.id&&Un.test(a.id)){let s=a.url.includes("?")?"&":"?",l=await e.transformRequest(`${a.url}${s}direct`);l?.code&&n.set(a.url,l.code)}for(let s of a.importedModules)await i(s)}}return await i(o),[...n.values()].join(`
|
|
88
|
+
`)}function Dn(e,t){let o=()=>{process.stdout.isTTY&&!process.env.VELU_NO_BELL&&process.stdout.write("\x07")},n=process.stdout.isTTY,r=f=>n&&process.stdout.write(`\x1B]${f}\x1B\\`),i=()=>{r(`0;\u23F3 ${t}`),r("9;4;3;0")},a=()=>{r(`0;${t}`),r("9;4;0;0")};if(n&&process.once("exit",()=>r("9;4;0;0")),i(),!e)return console.log("Starting Velu\u2026"),{ready:f=>{console.log(`Velu ready \u2192 ${f}`),a(),o()}};let s="\x1B[38;2;220;20;60m",l="\x1B[0m";console.log("");for(let f of qn)console.log(`${s}${f}${l}`);console.log("");let u=null;try{u=$n(process.execPath,["-e",Hn],{stdio:["ignore","inherit","ignore"]}),u.on("error",()=>{})}catch{}let c=()=>{if(u){try{u.kill()}catch{}u=null}};return process.once("exit",c),{ready:f=>{c(),process.stdout.write("\x1B[2K\r"),console.log(` \x1B[32m\u2713${l} Velu ready \u2192 \x1B[36m${f}${l}`),a(),o()}}}async function Yn(e){let t=!!process.env.VELU_PROFILE,o=performance.now(),n=(x,m)=>t&&console.log(`[velu-profile] ${x}: ${(performance.now()-m).toFixed(0)}ms`),r=Dn(!!process.stdout.isTTY&&!t,w.basename(e)),i=await ot(e),a=s();function s(){let m=ue(e).filter(h=>h.severity==="error"&&(h.category==="config-json"||h.category==="config-schema"));return m.length&&console.error(`
|
|
89
|
+
`+ae(m,{color:!!process.stderr.isTTY})+`
|
|
90
|
+
`),m}let l=e;try{l=M.realpathSync(e)}catch{}let u=0,c=x=>{let m=performance.now();if(m-u<500)return;u=m;let h=X(x,{projectDir:l,knownComponents:I});nt(h,x)};process.on("unhandledRejection",c),process.on("uncaughtException",c);let A=An(import.meta.url).resolve("@mdx-js/react"),p=w.resolve(J,"runtime","velu-ui"),L=M.existsSync(p)?p:w.resolve(J,"..","velu-ui","src"),d=[{find:"@mdx-js/react",replacement:A},{find:/^velu-ui$/,replacement:w.join(L,"index.js")},{find:/^velu-ui\//,replacement:L+"/"}],N=Number(process.env.PORT)||8358,C=await Oe();Jn(e,Bn());let S=On(),q=Sn.createServer(S),y=await In({root:J,appType:"custom",logLevel:t?"info":"warn",plugins:[He(e),Cn({remarkPlugins:[Tn,En,Nn,D,Xe(C,e)],rehypePlugins:[Fn],providerImportSource:"@mdx-js/react"}),Pn(),_n()],cacheDir:w.resolve(e,".velu","vite-cache"),optimizeDeps:{include:["react","react-dom","react-dom/client","react-router-dom","@mdx-js/react","lucide-react","prism-react-renderer","iso-639-1","github-slugger"],exclude:["tailwindcss","@tailwindcss/oxide","playwright","playwright-core"],esbuildOptions:{loader:{".mdx":"jsx"}}},resolve:{alias:d,dedupe:["react","react-dom","@mdx-js/react"]},server:{middlewareMode:!0,fs:{allow:[Rn(J),e,l]},hmr:{server:q}}});n("vite server created",o),S.use(y.middlewares),S.get(tt,async(x,m,h)=>{if(!i.favicon)return m.status(404).end();let g=w.resolve(e,i.favicon.replace(/^\//,""));m.sendFile(g,O=>{O&&m.status(404).end()})});let Ce=x=>(m,h)=>{let g=i.logo?.[x];if(!g)return h.status(404).end();h.sendFile(w.resolve(e,g.replace(/^\//,"")),O=>{O&&h.status(404).end()})};S.get(ne,Ce("light")),S.get(oe,Ce("dark")),S.use("*",async(x,m,h)=>{let g=x.originalUrl,O=performance.now();if(a.length)return m.status(200).set({"Content-Type":"text/html"}).end($e(a,{title:"Fix velu.json to continue"}));try{let b=performance.now(),_=await et.readFile(it,"utf-8");_=await y.transformIndexHtml(g,_),n(`transformIndexHtml ${g}`,b);let U="/src/runtime/server-entry.jsx";b=performance.now();let{render:te}=await y.ssrLoadModule(U);n(`ssrLoadModule ${g} (1st req includes dep pre-bundle)`,b),b=performance.now();let $=await te(g);n(`render ${g}`,b),b=performance.now();let H=await rt(y,U);n(`collectSsrCss ${g}`,b);let me=H?`<style data-velu-ssr>${H}</style>`:"",pt=`<style data-velu-config>
|
|
91
|
+
${Me(i)}
|
|
92
|
+
</style>`,mt=zn.has(i.font.family)?"":Vn(i.font.family),dt=i.favicon?`<link rel="icon" href="${tt}" />`:"",gt=[me,pt,mt,dt].filter(Boolean).join(`
|
|
93
|
+
`),ht=_.replace("<!--app-title-->",Gn(i.name)).replace("<!--app-head-->",gt).replace("<!--app-html-->",$);m.status(200).set({"Content-Type":"text/html"}).end(ht),n(`TOTAL ${g}`,O)}catch(b){y.ssrFixStacktrace(b);let _=X(b,{projectDir:l,knownComponents:I});nt(_,b),m.status(200).set({"Content-Type":"text/html"}).end($e([_]))}}),Ln.watch(e,{ignoreInitial:!0,ignored:/(^|[\\/])(node_modules|\.git|\.velu)([\\/]|$)/,awaitWriteFinish:{stabilityThreshold:100,pollInterval:20}}).on("all",async(x,m)=>{console.log(`[velu-cli] content ${x}: ${w.relative(e,m)}`);let h=w.basename(m)==="velu.json",g=m.endsWith(".mdx"),O=h||g&&(x==="add"||x==="unlink");if(h&&(i=await ot(e),a=s()),O)y.moduleGraph.invalidateAll();else if(g){let b=m.split(w.sep).join("/"),_=y.moduleGraph.getModulesByFile(b);if(_)for(let U of _)y.moduleGraph.invalidateModule(U)}y.ws.send({type:"full-reload"})});async function Te(x){let m=new Set,h=12,g=0,O=[],b=()=>g<h?(g+=1,Promise.resolve()):new Promise($=>O.push($)).then(()=>{g+=1}),_=()=>{g-=1,O.shift()?.()},U=$=>/[\\/]\.velu[\\/]vite-cache[\\/]|[\\/]deps[\\/]/.test($)||/[\\/]node_modules[\\/]/.test($)&&!/velu-ui/.test($);async function te($){if(!(!$||m.has($)||U($))){m.add($);try{await b();try{await y.transformRequest($,{ssr:!1})}finally{_()}let H=await y.moduleGraph.getModuleByUrl($,!1);H?.importedModules?.size&&await Promise.all([...H.importedModules].map(me=>te(me.url)))}catch{}}}return await te(x),m.size}q.listen(N,async()=>{let x=performance.now(),m=0;try{let{render:h}=await y.ssrLoadModule("/src/runtime/server-entry.jsx");await Te("/src/runtime/client-entry.jsx"),typeof y.waitForRequestsIdle=="function"&&await y.waitForRequestsIdle();let[g]=await Promise.allSettled([Te("/src/runtime/client-entry.jsx"),h("/"),y.transformIndexHtml("/",await et.readFile(it,"utf-8")),rt(y,"/src/runtime/server-entry.jsx")]).then(O=>O.map(b=>b.status==="fulfilled"?b.value:0));m=g}catch(h){t&&console.log("[velu-profile] warmup error:",h?.message)}n(`cold-start warm-up (${m} client modules)`,x),r.ready(`http://localhost:${N}`)})}var Mn,Un,zn,tt,Gn,J,it,qn,Hn,lt=k(()=>{ge();re();De();Se();Ke();F();je();Le();_e();Qe();Mn=w.dirname(jn(import.meta.url)),Un=/\.(css|less|sass|scss|styl|stylus|pcss|postcss)(\?|$)/,zn=new Set(["Google Sans Flex","Google Sans Code","Outfit"]),tt="/@velu-favicon",Gn=e=>String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");J=w.resolve(Mn,".."),it=w.resolve(J,"src","template.html"),qn=[" _ __ __","| | / /__ / /_ __","| | / / _ \\/ / / / /","| |/ / __/ / /_/ /","|___/\\___/_/\\__,_/"],Hn="const F=['\u280B','\u2819','\u2839','\u2838','\u283C','\u2834','\u2826','\u2827','\u2807','\u280F'],M=['Starting Velu','Setting up your preview','Preparing your docs','Warming things up','Almost ready'];let f=0,m=0;const t=Date.now();const C='\\x1b[38;2;220;20;60m',R='\\x1b[0m',D='\\x1b[2m';const d=()=>{const s=(Date.now()-t)/1000|0;process.stdout.write('\\x1b[2K\\r '+C+F[f]+R+' '+M[m]+'\\u2026 '+D+s+'s'+R);f=(f+1)%F.length;};d();setInterval(d,90);setInterval(()=>{if(m<M.length-1)m++;},4000);"});var ct={};de(ct,{default:()=>Xn,runValidate:()=>at});import Kn from"node:fs";import Wn from"node:path";function Zn(e){try{let t=JSON.parse(Kn.readFileSync(Wn.join(e,"velu.json"),"utf-8"));if(!t.navigation||typeof t.navigation!="object")return[];let o=V(t.navigation);return B(o,e).pageEntries}catch{return[]}}async function at(e){let t=ue(e),o=Zn(e),n=await We(e,o),r=[...t,...n],i=!!process.stdout.isTTY;return console.log(ae(r,{color:i})),r.some(s=>s.severity!=="warning"&&s.severity!=="info")?1:0}var Xn,ut=k(()=>{W();se();Le();_e();F();Xn=at});import Pe from"node:path";import E from"node:process";import{readFileSync as Qn}from"node:fs";var[,,ft,pe]=E.argv;function eo(){try{return JSON.parse(Qn(new URL("../package.json",import.meta.url),"utf8")).version}catch{return"unknown"}}async function to(){switch(ft){case"--version":case"-v":case"version":{console.log(eo());break}case"init":{pe||(console.error("Usage: velu init <project-name>"),E.exit(1));let e=pe,t=Pe.resolve(E.cwd(),e),{initProject:o}=await Promise.resolve().then(()=>(Fe(),Ne));await o(t,e);break}case"dev":{let e=Pe.resolve(E.cwd(),pe??"."),{startDevServer:t}=await Promise.resolve().then(()=>(lt(),st));await t(e);break}case"validate":{let e=Pe.resolve(E.cwd(),pe??"."),{runValidate:t}=await Promise.resolve().then(()=>(ut(),ct)),o=await t(e);E.exit(o);break}default:console.log("Usage:"),console.log(" velu init <project-name> scaffold a new docs project"),console.log(" velu dev [dir] start the dev preview server"),console.log(" velu validate [dir] check velu.json + content for problems"),E.exit(ft?1:0)}}to().catch(e=>{console.error(E.env.VELU_DEBUG?e:`Error: ${e.message}`),E.exit(1)});
|
package/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Logo — the configured site logo image(s), shown in the header (and mobile
|
|
5
|
+
* drawer) in place of the name wordmark.
|
|
6
|
+
*
|
|
7
|
+
* `logo` is `{ light, dark, mono }` where light/dark are URLs the dev server
|
|
8
|
+
* serves from the project's logo files. When `mono`, a single image covers
|
|
9
|
+
* both themes; otherwise both render and CSS shows the right one per
|
|
10
|
+
* `[data-theme]` (see page-header.css `.velu-logo--light/--dark`).
|
|
11
|
+
*
|
|
12
|
+
* SSR-safe: identical markup on server + client (the theme picks the visible
|
|
13
|
+
* image via CSS, not JS), so there's no hydration mismatch or flash.
|
|
14
|
+
*/
|
|
15
|
+
export default function Logo({ logo, alt = '' }) {
|
|
16
|
+
if (!logo) return null;
|
|
17
|
+
if (logo.mono) {
|
|
18
|
+
return <img className="velu-logo" src={logo.light} alt={alt} />;
|
|
19
|
+
}
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<img className="velu-logo velu-logo--light" src={logo.light} alt={alt} />
|
|
23
|
+
<img
|
|
24
|
+
className="velu-logo velu-logo--dark"
|
|
25
|
+
src={logo.dark}
|
|
26
|
+
alt={alt}
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
/>
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import Stack from '../primitives/Stack.jsx';
|
|
3
3
|
import Cluster from '../primitives/Cluster.jsx';
|
|
4
|
+
import SocialLinks from './SocialLinks.jsx';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* PageFooter — the docs site's footer.
|
|
@@ -65,54 +66,6 @@ function VeluMark() {
|
|
|
65
66
|
);
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
/* ── Social glyphs — inline SVGs with fill: currentColor so they
|
|
69
|
-
inherit color (themed) and flip with light/dark. ─────────────────── */
|
|
70
|
-
function GithubIcon() {
|
|
71
|
-
return (
|
|
72
|
-
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
73
|
-
<path
|
|
74
|
-
fillRule="evenodd"
|
|
75
|
-
clipRule="evenodd"
|
|
76
|
-
d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"
|
|
77
|
-
/>
|
|
78
|
-
</svg>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
function XIcon() {
|
|
82
|
-
return (
|
|
83
|
-
<svg viewBox="0 0 1200 1227" fill="currentColor" aria-hidden="true">
|
|
84
|
-
<path d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z" />
|
|
85
|
-
</svg>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
function YoutubeIcon() {
|
|
89
|
-
return (
|
|
90
|
-
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
91
|
-
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
|
92
|
-
</svg>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
function LinkedinIcon() {
|
|
96
|
-
return (
|
|
97
|
-
<svg viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
|
|
98
|
-
<path d="M218.123 218.127h-37.931v-59.403c0-14.165-.253-32.4-19.728-32.4-19.756 0-22.779 15.434-22.779 31.369v60.43h-37.93V95.967h36.413v16.694h.51a39.907 39.907 0 0 1 35.928-19.733c38.445 0 45.533 25.288 45.533 58.186l-.016 67.013ZM56.955 79.27c-12.157.002-22.014-9.852-22.016-22.009-.002-12.157 9.851-22.014 22.008-22.016 12.157-.003 22.014 9.851 22.016 22.008A22.013 22.013 0 0 1 56.955 79.27m18.966 138.858H37.95V95.967h37.97v122.16ZM237.033.018H18.89C8.58-.098.125 8.161-.001 18.471v219.053c.122 10.315 8.576 18.582 18.89 18.474h218.144c10.336.128 18.823-8.139 18.966-18.474V18.454c-.147-10.33-8.635-18.588-18.966-18.453" />
|
|
99
|
-
</svg>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const SOCIAL_ICONS = {
|
|
104
|
-
github: GithubIcon,
|
|
105
|
-
x: XIcon,
|
|
106
|
-
youtube: YoutubeIcon,
|
|
107
|
-
linkedin: LinkedinIcon,
|
|
108
|
-
};
|
|
109
|
-
const SOCIAL_LABELS = {
|
|
110
|
-
github: 'GitHub',
|
|
111
|
-
x: 'X',
|
|
112
|
-
youtube: 'YouTube',
|
|
113
|
-
linkedin: 'LinkedIn',
|
|
114
|
-
};
|
|
115
|
-
|
|
116
69
|
export default function PageFooter({
|
|
117
70
|
brand,
|
|
118
71
|
columns = [],
|
|
@@ -182,32 +135,11 @@ export default function PageFooter({
|
|
|
182
135
|
</Stack>
|
|
183
136
|
|
|
184
137
|
{/* Socials sit OUTSIDE the container, at the bottom of the footer. */}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
className="velu-footer__socials"
|
|
191
|
-
>
|
|
192
|
-
{socials.map((s, i) => {
|
|
193
|
-
const Icon = SOCIAL_ICONS[s.kind];
|
|
194
|
-
if (!Icon) return null;
|
|
195
|
-
return (
|
|
196
|
-
<a
|
|
197
|
-
key={i}
|
|
198
|
-
href={s.href}
|
|
199
|
-
target="_blank"
|
|
200
|
-
rel="noreferrer"
|
|
201
|
-
aria-label={SOCIAL_LABELS[s.kind] ?? s.kind}
|
|
202
|
-
className="velu-footer__social"
|
|
203
|
-
data-kind={s.kind}
|
|
204
|
-
>
|
|
205
|
-
<Icon />
|
|
206
|
-
</a>
|
|
207
|
-
);
|
|
208
|
-
})}
|
|
209
|
-
</Cluster>
|
|
210
|
-
)}
|
|
138
|
+
<SocialLinks
|
|
139
|
+
socials={socials}
|
|
140
|
+
justify="center"
|
|
141
|
+
className="velu-footer__socials"
|
|
142
|
+
/>
|
|
211
143
|
</Stack>
|
|
212
144
|
);
|
|
213
145
|
}
|
|
@@ -3,6 +3,7 @@ import { MoreVertical, Menu, ChevronRight } from 'lucide-react';
|
|
|
3
3
|
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
4
4
|
import Stack from '../primitives/Stack.jsx';
|
|
5
5
|
import Cluster from '../primitives/Cluster.jsx';
|
|
6
|
+
import Logo from './Logo.jsx';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* PageHeader — top of the docs site.
|
|
@@ -258,10 +259,17 @@ export default function PageHeader({
|
|
|
258
259
|
return () => window.removeEventListener('scroll', onScroll);
|
|
259
260
|
}, []);
|
|
260
261
|
|
|
262
|
+
// A configured logo replaces the mark + wordmark entirely (Mintlify-style).
|
|
261
263
|
const brandBlock = (
|
|
262
264
|
<Cluster space="var(--s-3)" align="center" className="velu-header__brand">
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
{brand?.logo ? (
|
|
266
|
+
<Logo logo={brand.logo} alt={brandLabel} />
|
|
267
|
+
) : (
|
|
268
|
+
<>
|
|
269
|
+
<VeluMark />
|
|
270
|
+
<span className="velu-header__wordmark">{brandLabel}</span>
|
|
271
|
+
</>
|
|
272
|
+
)}
|
|
265
273
|
</Cluster>
|
|
266
274
|
);
|
|
267
275
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Cluster from '../primitives/Cluster.jsx';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SocialLinks — a row of social-platform icon links. Shared by the full
|
|
6
|
+
* PageFooter (centered at its base) and the compact content-foot row (next to
|
|
7
|
+
* "Powered by Velu" when there are no footer link columns).
|
|
8
|
+
*
|
|
9
|
+
* `socials` is `[{ kind, href }]`. Unknown kinds (no icon) are skipped.
|
|
10
|
+
*
|
|
11
|
+
* @param {{ socials?: {kind: string, href: string}[],
|
|
12
|
+
* justify?: string, className?: string }} props
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/* Inline SVGs, fill: currentColor so they inherit the themed text color. */
|
|
16
|
+
function GithubIcon() {
|
|
17
|
+
return (
|
|
18
|
+
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
19
|
+
<path
|
|
20
|
+
fillRule="evenodd"
|
|
21
|
+
clipRule="evenodd"
|
|
22
|
+
d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"
|
|
23
|
+
/>
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
function XIcon() {
|
|
28
|
+
return (
|
|
29
|
+
<svg viewBox="0 0 1200 1227" fill="currentColor" aria-hidden="true">
|
|
30
|
+
<path d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z" />
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function YoutubeIcon() {
|
|
35
|
+
return (
|
|
36
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
37
|
+
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function LinkedinIcon() {
|
|
42
|
+
return (
|
|
43
|
+
<svg viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
|
|
44
|
+
<path d="M218.123 218.127h-37.931v-59.403c0-14.165-.253-32.4-19.728-32.4-19.756 0-22.779 15.434-22.779 31.369v60.43h-37.93V95.967h36.413v16.694h.51a39.907 39.907 0 0 1 35.928-19.733c38.445 0 45.533 25.288 45.533 58.186l-.016 67.013ZM56.955 79.27c-12.157.002-22.014-9.852-22.016-22.009-.002-12.157 9.851-22.014 22.008-22.016 12.157-.003 22.014 9.851 22.016 22.008A22.013 22.013 0 0 1 56.955 79.27m18.966 138.858H37.95V95.967h37.97v122.16ZM237.033.018H18.89C8.58-.098.125 8.161-.001 18.471v219.053c.122 10.315 8.576 18.582 18.89 18.474h218.144c10.336.128 18.823-8.139 18.966-18.474V18.454c-.147-10.33-8.635-18.588-18.966-18.453" />
|
|
45
|
+
</svg>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const SOCIAL_ICONS = {
|
|
50
|
+
github: GithubIcon,
|
|
51
|
+
x: XIcon,
|
|
52
|
+
youtube: YoutubeIcon,
|
|
53
|
+
linkedin: LinkedinIcon,
|
|
54
|
+
};
|
|
55
|
+
const SOCIAL_LABELS = {
|
|
56
|
+
github: 'GitHub',
|
|
57
|
+
x: 'X',
|
|
58
|
+
youtube: 'YouTube',
|
|
59
|
+
linkedin: 'LinkedIn',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default function SocialLinks({ socials = [], justify, className = '' }) {
|
|
63
|
+
const known = socials.filter((s) => s && SOCIAL_ICONS[s.kind]);
|
|
64
|
+
if (!known.length) return null;
|
|
65
|
+
return (
|
|
66
|
+
<Cluster
|
|
67
|
+
space="var(--s-2)"
|
|
68
|
+
align="center"
|
|
69
|
+
justify={justify}
|
|
70
|
+
className={`velu-socials ${className}`.trim()}
|
|
71
|
+
>
|
|
72
|
+
{known.map((s, i) => {
|
|
73
|
+
const Icon = SOCIAL_ICONS[s.kind];
|
|
74
|
+
return (
|
|
75
|
+
<a
|
|
76
|
+
key={i}
|
|
77
|
+
href={s.href}
|
|
78
|
+
target="_blank"
|
|
79
|
+
rel="noreferrer"
|
|
80
|
+
aria-label={SOCIAL_LABELS[s.kind] ?? s.kind}
|
|
81
|
+
className="velu-social"
|
|
82
|
+
data-kind={s.kind}
|
|
83
|
+
>
|
|
84
|
+
<Icon />
|
|
85
|
+
</a>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</Cluster>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
little breathing room remains around it. Each kind gets its OWN
|
|
101
101
|
brand color (set via the data-kind attribute below). Monochrome
|
|
102
102
|
brands (github, x) inherit --text-color so they theme correctly. */
|
|
103
|
-
.velu-
|
|
103
|
+
.velu-social {
|
|
104
104
|
display: inline-flex;
|
|
105
105
|
align-items: center;
|
|
106
106
|
justify-content: center;
|
|
@@ -112,19 +112,19 @@
|
|
|
112
112
|
text-decoration: none;
|
|
113
113
|
transition: opacity 0.12s ease;
|
|
114
114
|
}
|
|
115
|
-
.velu-
|
|
115
|
+
.velu-social:hover {
|
|
116
116
|
opacity: 0.78;
|
|
117
117
|
}
|
|
118
|
-
.velu-
|
|
118
|
+
.velu-social svg {
|
|
119
119
|
inline-size: var(--icon-size-lg);
|
|
120
120
|
block-size: var(--icon-size-lg);
|
|
121
121
|
}
|
|
122
122
|
/* Brand colors — YouTube red, LinkedIn blue. GitHub + X stay
|
|
123
123
|
--text-color (their canonical brand is monochrome and follows the
|
|
124
124
|
surrounding theme). */
|
|
125
|
-
.velu-
|
|
125
|
+
.velu-social[data-kind='youtube'] {
|
|
126
126
|
color: #ff0000;
|
|
127
127
|
}
|
|
128
|
-
.velu-
|
|
128
|
+
.velu-social[data-kind='linkedin'] {
|
|
129
129
|
color: #0a66c2;
|
|
130
130
|
}
|
|
@@ -100,6 +100,28 @@
|
|
|
100
100
|
color: var(--text-color);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/* Configured site logo (replaces the mark + wordmark). Height-constrained to
|
|
104
|
+
the brand row; width scales by aspect ratio up to a sane cap. */
|
|
105
|
+
.velu-logo {
|
|
106
|
+
display: block;
|
|
107
|
+
block-size: 2rem;
|
|
108
|
+
inline-size: auto;
|
|
109
|
+
max-inline-size: 12rem;
|
|
110
|
+
object-fit: contain;
|
|
111
|
+
}
|
|
112
|
+
/* Per-theme swap: light image by default, dark image under [data-theme=dark].
|
|
113
|
+
`display` (not opacity) so the inactive one leaves the a11y tree too. The
|
|
114
|
+
`mono` single-image case has neither modifier and always shows. */
|
|
115
|
+
.velu-logo--dark {
|
|
116
|
+
display: none;
|
|
117
|
+
}
|
|
118
|
+
[data-theme='dark'] .velu-logo--light {
|
|
119
|
+
display: none;
|
|
120
|
+
}
|
|
121
|
+
[data-theme='dark'] .velu-logo--dark {
|
|
122
|
+
display: block;
|
|
123
|
+
}
|
|
124
|
+
|
|
103
125
|
/* ── Action buttons ─────────────────────────────────────────────────── */
|
|
104
126
|
.velu-header__action {
|
|
105
127
|
display: inline-flex;
|
|
@@ -136,6 +158,21 @@
|
|
|
136
158
|
.velu-header__action--primary:hover {
|
|
137
159
|
background: color-mix(in srgb, var(--accent-color) 88%, #000);
|
|
138
160
|
}
|
|
161
|
+
/* Plain text link (navbar.links) — no fill or border, just text that goes
|
|
162
|
+
accent on hover. Transparent border keeps it the same height as the
|
|
163
|
+
buttons beside it; tighter inline padding reads as a link, not a button. */
|
|
164
|
+
.velu-header__action--link {
|
|
165
|
+
background: transparent;
|
|
166
|
+
border: var(--border-width) solid transparent;
|
|
167
|
+
color: var(--text-color);
|
|
168
|
+
padding-inline: var(--s-5);
|
|
169
|
+
}
|
|
170
|
+
.velu-header__action--link:hover {
|
|
171
|
+
color: var(--accent-color);
|
|
172
|
+
}
|
|
173
|
+
.velu-header__action--link:hover .velu-header__action-icon {
|
|
174
|
+
color: var(--accent-color);
|
|
175
|
+
}
|
|
139
176
|
.velu-header__action-icon {
|
|
140
177
|
display: inline-flex;
|
|
141
178
|
flex: none;
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
/* PoweredBy — "Powered by Velu" badge, right-aligned. Sits above the
|
|
2
2
|
site footer. All values are tokens; light/dark via [data-theme]. */
|
|
3
3
|
|
|
4
|
+
/* Content foot — social icons (left) + the "Powered by" badge (right) in one
|
|
5
|
+
row. Shown at the base of the article column when the footer has no link
|
|
6
|
+
columns (so there's no full footer section). The row owns the vertical
|
|
7
|
+
rhythm; the nested badge drops its own standalone margins. */
|
|
8
|
+
.velu-content-foot {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: space-between;
|
|
12
|
+
gap: var(--s2);
|
|
13
|
+
margin-block: 2rem;
|
|
14
|
+
padding-inline: var(--s3);
|
|
15
|
+
}
|
|
16
|
+
.velu-content-foot .velu-powered-by {
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 0;
|
|
19
|
+
}
|
|
20
|
+
.velu-content-foot .velu-social {
|
|
21
|
+
color: var(--muted-color);
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
.velu-powered-by {
|
|
5
25
|
display: flex;
|
|
6
26
|
justify-content: flex-end;
|
package/runtime/velu-ui/index.js
CHANGED
|
@@ -8,6 +8,8 @@ export { default as Toc } from './components/Toc.jsx';
|
|
|
8
8
|
export { default as TocBar } from './components/TocBar.jsx';
|
|
9
9
|
export { default as Callout } from './components/Callout.jsx';
|
|
10
10
|
export { default as ErrorCard } from './components/ErrorCard.jsx';
|
|
11
|
+
export { default as Logo } from './components/Logo.jsx';
|
|
12
|
+
export { default as SocialLinks } from './components/SocialLinks.jsx';
|
|
11
13
|
export {
|
|
12
14
|
default as Accordion,
|
|
13
15
|
AccordionGroup,
|
package/schema/velu.schema.json
CHANGED
|
@@ -40,6 +40,120 @@
|
|
|
40
40
|
"type": "string",
|
|
41
41
|
"description": "Path to the favicon, relative to the project root."
|
|
42
42
|
},
|
|
43
|
+
"logo": {
|
|
44
|
+
"description": "Site logo shown in the header, replacing the name wordmark. A single path used in both themes, or per-theme light/dark images with an optional click-through href. Paths are relative to the project root.",
|
|
45
|
+
"oneOf": [
|
|
46
|
+
{
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1,
|
|
49
|
+
"description": "Logo path used in both light and dark mode."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": false,
|
|
54
|
+
"required": ["light", "dark"],
|
|
55
|
+
"properties": {
|
|
56
|
+
"light": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"minLength": 1,
|
|
59
|
+
"description": "Logo shown in light mode."
|
|
60
|
+
},
|
|
61
|
+
"dark": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"minLength": 1,
|
|
64
|
+
"description": "Logo shown in dark mode."
|
|
65
|
+
},
|
|
66
|
+
"href": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Where clicking the logo links. Defaults to the site root."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"navbar": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"additionalProperties": false,
|
|
77
|
+
"description": "The header's top-right area: secondary links plus a primary CTA button.",
|
|
78
|
+
"properties": {
|
|
79
|
+
"links": {
|
|
80
|
+
"type": "array",
|
|
81
|
+
"description": "Secondary header links (to the left of the primary CTA).",
|
|
82
|
+
"items": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"additionalProperties": false,
|
|
85
|
+
"required": ["href"],
|
|
86
|
+
"properties": {
|
|
87
|
+
"label": { "type": "string", "description": "Link text." },
|
|
88
|
+
"href": { "type": "string", "description": "Link URL." },
|
|
89
|
+
"icon": { "type": "string", "description": "lucide icon id." }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"primary": {
|
|
94
|
+
"description": "The prominent CTA button at the far right.",
|
|
95
|
+
"oneOf": [
|
|
96
|
+
{
|
|
97
|
+
"type": "object",
|
|
98
|
+
"additionalProperties": false,
|
|
99
|
+
"required": ["type", "label", "href"],
|
|
100
|
+
"properties": {
|
|
101
|
+
"type": { "const": "button" },
|
|
102
|
+
"label": { "type": "string", "description": "Button text." },
|
|
103
|
+
"href": { "type": "string", "description": "Button URL." }
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"type": "object",
|
|
108
|
+
"additionalProperties": false,
|
|
109
|
+
"required": ["type", "href"],
|
|
110
|
+
"properties": {
|
|
111
|
+
"type": { "const": "github" },
|
|
112
|
+
"href": { "type": "string", "description": "GitHub repository URL." },
|
|
113
|
+
"label": { "type": "string", "description": "Button text (default \"GitHub\")." }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"footer": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"additionalProperties": false,
|
|
123
|
+
"description": "Site footer. With `links`, a full footer section renders; with only `socials`, the icons sit next to the \"Powered by\" line instead.",
|
|
124
|
+
"properties": {
|
|
125
|
+
"socials": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"description": "Social links as platform → URL. Recognized icons: github, x (or twitter), youtube, linkedin.",
|
|
128
|
+
"additionalProperties": { "type": "string" }
|
|
129
|
+
},
|
|
130
|
+
"links": {
|
|
131
|
+
"type": "array",
|
|
132
|
+
"description": "Footer link columns (max 4).",
|
|
133
|
+
"maxItems": 4,
|
|
134
|
+
"items": {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"additionalProperties": false,
|
|
137
|
+
"required": ["header", "items"],
|
|
138
|
+
"properties": {
|
|
139
|
+
"header": { "type": "string", "description": "Column heading." },
|
|
140
|
+
"items": {
|
|
141
|
+
"type": "array",
|
|
142
|
+
"items": {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"additionalProperties": false,
|
|
145
|
+
"required": ["label", "href"],
|
|
146
|
+
"properties": {
|
|
147
|
+
"label": { "type": "string", "description": "Link text." },
|
|
148
|
+
"href": { "type": "string", "description": "Link URL." }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
43
157
|
"font": {
|
|
44
158
|
"type": "object",
|
|
45
159
|
"description": "Typography. Maps onto the --font-sans token in base.css.",
|
package/src/runtime/App.jsx
CHANGED
|
@@ -4,7 +4,7 @@ import { MDXProvider } from '@mdx-js/react';
|
|
|
4
4
|
// The project's pages + navigation, generated from velu.json by
|
|
5
5
|
// vite-plugin-velu-site (see src/vite-plugin-velu-site.js). `pages` is
|
|
6
6
|
// url → { Component, frontmatter, toc } (or { missing:true }).
|
|
7
|
-
import { pages, navigation } from 'virtual:velu-site';
|
|
7
|
+
import { pages, navigation, site } from 'virtual:velu-site';
|
|
8
8
|
import { resolve, normalizeUrl } from '../navigation.js';
|
|
9
9
|
import {
|
|
10
10
|
Stack,
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
defaultMdxComponents,
|
|
39
39
|
resolveIcon,
|
|
40
40
|
Search,
|
|
41
|
+
Logo,
|
|
42
|
+
SocialLinks,
|
|
41
43
|
Tree,
|
|
42
44
|
Folder,
|
|
43
45
|
File,
|
|
@@ -944,6 +946,35 @@ function DocsPage() {
|
|
|
944
946
|
return () => scroller.removeEventListener('scroll', update);
|
|
945
947
|
}, [pathname, chatOpen]);
|
|
946
948
|
|
|
949
|
+
// Footer config (from velu.json via virtual:velu-site). Link columns →
|
|
950
|
+
// full footer section; socials with no columns → inline next to PoweredBy.
|
|
951
|
+
const footerSocials = site.footer?.socials ?? [];
|
|
952
|
+
const footerHasLinks = (site.footer?.links?.length ?? 0) > 0;
|
|
953
|
+
|
|
954
|
+
// Header top-right actions, built from velu.json's `navbar`: secondary
|
|
955
|
+
// `links` (plain text links) then the `primary` CTA (a filled button, or an
|
|
956
|
+
// outlined GitHub button). Empty when unconfigured — no placeholder buttons.
|
|
957
|
+
const navActions = [];
|
|
958
|
+
for (const l of site.navbar?.links ?? []) {
|
|
959
|
+
navActions.push({
|
|
960
|
+
label: l.label,
|
|
961
|
+
href: l.href,
|
|
962
|
+
icon: l.icon || undefined,
|
|
963
|
+
kind: 'link',
|
|
964
|
+
external: l.external,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
if (site.navbar?.primary) {
|
|
968
|
+
const p = site.navbar.primary;
|
|
969
|
+
navActions.push({
|
|
970
|
+
label: p.label,
|
|
971
|
+
href: p.href,
|
|
972
|
+
kind: p.kind === 'github' ? 'outlined' : 'primary',
|
|
973
|
+
icon: p.kind === 'github' ? 'github' : undefined,
|
|
974
|
+
external: p.external,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
947
978
|
const footerRef = React.useRef(null);
|
|
948
979
|
const [footerOverlap, setFooterOverlap] = React.useState(0);
|
|
949
980
|
React.useEffect(() => {
|
|
@@ -1043,7 +1074,11 @@ function DocsPage() {
|
|
|
1043
1074
|
tabs row. Configurable: pass any number of actions / tabs. */}
|
|
1044
1075
|
<PageHeader
|
|
1045
1076
|
linkComponent={RouterLink}
|
|
1046
|
-
brand={{
|
|
1077
|
+
brand={{
|
|
1078
|
+
label: site.name,
|
|
1079
|
+
href: site.logo?.href || '/',
|
|
1080
|
+
logo: site.logo,
|
|
1081
|
+
}}
|
|
1047
1082
|
brandTrailing={
|
|
1048
1083
|
versionSwitcher && (
|
|
1049
1084
|
<span className="velu-hide-on-mobile">{versionSwitcher}</span>
|
|
@@ -1069,13 +1104,7 @@ function DocsPage() {
|
|
|
1069
1104
|
)}
|
|
1070
1105
|
</Cluster>
|
|
1071
1106
|
}
|
|
1072
|
-
actions={
|
|
1073
|
-
{
|
|
1074
|
-
label: 'Book Demo',
|
|
1075
|
-
kind: 'primary',
|
|
1076
|
-
href: '#',
|
|
1077
|
-
},
|
|
1078
|
-
]}
|
|
1107
|
+
actions={navActions}
|
|
1079
1108
|
trailing={<ThemeToggle />}
|
|
1080
1109
|
onMenuClick={() => setDrawerOpen((v) => !v)}
|
|
1081
1110
|
breadcrumb={nav?.breadcrumb ?? []}
|
|
@@ -1111,8 +1140,14 @@ function DocsPage() {
|
|
|
1111
1140
|
href="/"
|
|
1112
1141
|
className="velu-docs-layout__drawer-brand"
|
|
1113
1142
|
>
|
|
1114
|
-
|
|
1115
|
-
|
|
1143
|
+
{site.logo ? (
|
|
1144
|
+
<Logo logo={site.logo} alt={site.name} />
|
|
1145
|
+
) : (
|
|
1146
|
+
<>
|
|
1147
|
+
<VeluMark />
|
|
1148
|
+
<span className="velu-header__wordmark">{site.name}</span>
|
|
1149
|
+
</>
|
|
1150
|
+
)}
|
|
1116
1151
|
</RouterLink>
|
|
1117
1152
|
<ThemeToggle />
|
|
1118
1153
|
<button
|
|
@@ -1417,59 +1452,40 @@ function DocsPage() {
|
|
|
1417
1452
|
}}
|
|
1418
1453
|
/>
|
|
1419
1454
|
)}
|
|
1420
|
-
{/* "Powered by Velu"
|
|
1421
|
-
article column
|
|
1422
|
-
|
|
1423
|
-
|
|
1455
|
+
{/* Content foot — "Powered by Velu" at the bottom-right of the
|
|
1456
|
+
article column. When the footer has no link columns, the
|
|
1457
|
+
configured social icons sit on the LEFT of this same row (no
|
|
1458
|
+
full footer section); with link columns they live in the full
|
|
1459
|
+
footer below instead. */}
|
|
1460
|
+
{footerHasLinks ? (
|
|
1461
|
+
<PoweredBy />
|
|
1462
|
+
) : (
|
|
1463
|
+
<div className="velu-content-foot">
|
|
1464
|
+
<SocialLinks socials={footerSocials} />
|
|
1465
|
+
<PoweredBy />
|
|
1466
|
+
</div>
|
|
1467
|
+
)}
|
|
1424
1468
|
</div>
|
|
1425
1469
|
</main>
|
|
1426
1470
|
</div>
|
|
1427
1471
|
|
|
1428
|
-
{/*
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
{ label: 'Enterprise', href: '#' },
|
|
1446
|
-
{ label: 'Status', href: '#' },
|
|
1447
|
-
],
|
|
1448
|
-
},
|
|
1449
|
-
{
|
|
1450
|
-
title: 'Company',
|
|
1451
|
-
items: [
|
|
1452
|
-
{ label: 'Careers', href: '#' },
|
|
1453
|
-
{ label: 'Blog', href: '#' },
|
|
1454
|
-
{ label: 'Community', href: '#' },
|
|
1455
|
-
],
|
|
1456
|
-
},
|
|
1457
|
-
{
|
|
1458
|
-
title: 'Policies',
|
|
1459
|
-
items: [
|
|
1460
|
-
{ label: 'Subprocessors', href: '#' },
|
|
1461
|
-
{ label: 'Terms of Service', href: '#' },
|
|
1462
|
-
],
|
|
1463
|
-
},
|
|
1464
|
-
]}
|
|
1465
|
-
socials={[
|
|
1466
|
-
{ kind: 'github', href: 'https://github.com/aravindc26/velu-cli' },
|
|
1467
|
-
{ kind: 'x', href: 'https://x.com/' },
|
|
1468
|
-
{ kind: 'youtube', href: 'https://youtube.com/' },
|
|
1469
|
-
{ kind: 'linkedin', href: 'https://linkedin.com/' },
|
|
1470
|
-
]}
|
|
1471
|
-
/>
|
|
1472
|
-
</div>
|
|
1472
|
+
{/* Full site footer — only when the config provides link columns. Spans
|
|
1473
|
+
full width below the article; its raised z-index eclipses the bottoms
|
|
1474
|
+
of the fixed sidebar and TOC as the page scrolls into it, and the ref
|
|
1475
|
+
is watched so the asides can pad their bottom by the overlap. */}
|
|
1476
|
+
{footerHasLinks && (
|
|
1477
|
+
<div
|
|
1478
|
+
ref={footerRef}
|
|
1479
|
+
data-velu-footer
|
|
1480
|
+
style={{ position: 'relative', zIndex: 20 }}
|
|
1481
|
+
>
|
|
1482
|
+
<PageFooter
|
|
1483
|
+
brand={{ href: site.logo?.href || '/' }}
|
|
1484
|
+
columns={site.footer.links}
|
|
1485
|
+
socials={footerSocials}
|
|
1486
|
+
/>
|
|
1487
|
+
</div>
|
|
1488
|
+
)}
|
|
1473
1489
|
|
|
1474
1490
|
{/* Ask-AI chatbot — slides in from the inline-end edge. */}
|
|
1475
1491
|
<Chatbot
|