Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
699 changes: 136 additions & 563 deletions README.md

Large diffs are not rendered by default.

Empty file added docs/.nojekyll
Empty file.
95 changes: 95 additions & 0 deletions docs/ar/index.html

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions docs/assets/build-locales.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fs from 'node:fs';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
const root=fileURLToPath(new URL('../',import.meta.url));
const template=fs.readFileSync(path.join(root,'assets/home.template'),'utf8');
const base='https://arpanjot-coding.github.io/Datly/';
const specs=[['en','en','English'],['ru','ru','Русский'],['zh','zh-Hans','简体中文'],['ja','ja','日本語'],['vi','vi','Tiếng Việt'],['th','th','ไทย'],['hi','hi','हिन्दी'],['ar','ar','العربية'],['fr','fr','Français'],['de','de','Deutsch'],['es','es','Español'],['it','it','Italiano']];
const english=JSON.parse(fs.readFileSync(path.join(root,'locales/en.json'),'utf8'));
const esc=s=>s.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('"','&quot;');
const urlFor=key=>base+(key==='en'?'':key+'/');
let built=0;
for(const [key,lang,native]of specs){
const file=path.join(root,'locales',key+'.json');if(!fs.existsSync(file)){console.log('Waiting for '+key);continue;}
const copy=JSON.parse(fs.readFileSync(file,'utf8'));
const missing=Object.keys(english.translations).filter(k=>typeof copy.translations[k]!=='string'||!copy.translations[k].trim());
if(missing.length)throw new Error(key+' missing '+missing.join('; '));
const t=s=>copy.translations[s]??s;
const prefix=key==='en'?'':'../',canonical=urlFor(key);
let skip=0;
let html=template.match(/<[^>]+>|[^<]+/g).map(token=>{
if(/^<(script|pre)\b/.test(token)){skip++;return token;}
if(/^<\/(script|pre)>/.test(token)){skip--;return token;}
if(skip)return token;
if(token.startsWith('<'))return token.replace(/(aria-label|alt|content)="([^"]+)"/g,(m,attr,text)=>attr+'="'+esc(t(text))+'"');
const clean=token.trim();return clean&&copy.translations[clean]?token.replace(clean,esc(t(clean))):token;
}).join('');
html=html.replace('<html lang="en">','<html lang="'+lang+'" dir="'+copy.direction+'">');
html=html.replace(/(src|href)="(images\/|favicon\.svg|styles\.css|site\.js|site\.webmanifest)/g,(_,attr,asset)=>attr+'="'+prefix+asset);
html=html.replace('<link rel="canonical" href="'+base+'">','<link rel="canonical" href="'+canonical+'">\n'+specs.map(([route,code])=>'<link rel="alternate" hreflang="'+code+'" href="'+urlFor(route)+'">').join('\n')+'\n<link rel="alternate" hreflang="x-default" href="'+base+'">');
html=html.replace('<meta property="og:url" content="'+base+'">','<meta property="og:url" content="'+canonical+'">');
html=html.replace(/<script type="application\/ld\+json">[\s\S]*?<\/script>/,()=>{
const data={'@context':'https://schema.org','@type':'SoftwareSourceCode',name:'Datly',description:t('An open-source analytics workspace. Explore tables with AI, refine charts, inspect underlying rows, and turn your findings into reports.'),codeRepository:'https://github.com/arpanjot-coding/Datly',license:'https://github.com/arpanjot-coding/Datly/blob/main/LICENSE',programmingLanguage:['TypeScript','Python'],runtimePlatform:'Python 3.11 or later',inLanguage:lang,url:canonical};
return '<script type="application/ld+json">'+JSON.stringify(data).replaceAll('<','\\u003c')+'</script>';
});
const menu='<details id="languages" class="language-menu"><summary aria-label="'+esc(t('Choose a language'))+'"><span aria-hidden="true">◎</span> '+key.toUpperCase()+' <span aria-hidden="true">⌄</span></summary><div class="language-options"><p>'+esc(t('Language'))+'</p>'+specs.map(([route,code,label])=>'<a href="'+prefix+(route==='en'?'./':route+'/')+'" lang="'+code+'" hreflang="'+code+'"'+(route===key?' aria-current="page"':'')+'>'+label+(route===key?' <span aria-hidden="true">✓</span>':'')+'</a>').join('')+'</div></details>';
html=html.replace('<!-- LANGUAGE_MENU -->',menu);
const runtime={language:lang,translations:key==='en'?{}:copy.translations};
html=html.replace('<!-- LOCALE_COPY -->','<script id="locale-copy" type="application/json">'+JSON.stringify(runtime).replaceAll('<','\\u003c')+'</script>');
const dir=key==='en'?root:path.join(root,key);fs.mkdirSync(dir,{recursive:true});fs.writeFileSync(path.join(dir,'index.html'),html.trimEnd()+'\n');
built++;
}
const sitemap='<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'+specs.map(([key])=>' <url><loc>'+urlFor(key)+'</loc><lastmod>2026-11-02</lastmod></url>').join('\n')+'\n</urlset>\n';
fs.writeFileSync(path.join(root,'sitemap.xml'),sitemap);
console.log('Built '+built+' static language pages; '+specs.length+' language URLs in sitemap.');
30 changes: 30 additions & 0 deletions docs/assets/build-product-images.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = fileURLToPath(new URL('../images/', import.meta.url));
const image = name => `data:image/${name.endsWith('.webp') ? 'webp' : 'png'};base64,${fs.readFileSync(path.join(root,name)).toString('base64')}`;
const esc = text => text.replaceAll('&','&amp;');
const logo = `<rect width="40" height="40" rx="10" fill="#206b4e"/><path d="M10 29h21M11 25l6-8 5 4 7-11" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>`;
function card({name,title,subtitle,file,crop,number}) {
const [x,y,w,h] = crop;
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1100" viewBox="0 0 1600 1100"><defs><pattern id="grid" width="32" height="32" patternUnits="userSpaceOnUse"><circle cx="1" cy="1" r="1" fill="#cbd9ca"/></pattern><filter id="shadow" x="-10%" y="-10%" width="120%" height="140%"><feDropShadow dx="0" dy="15" stdDeviation="20" flood-color="#193d2a" flood-opacity=".1"/></filter><clipPath id="clip"><rect x="64" y="236" width="1472" height="768" rx="12"/></clipPath></defs><rect width="1600" height="1100" fill="#e9eee4"/><rect width="1600" height="1100" fill="url(#grid)"/><g transform="translate(64 54)">${logo}</g><text x="120" y="82" fill="#19392d" font-family="Arial,sans-serif" font-weight="700" font-size="30">Datly</text><text x="1536" y="80" text-anchor="end" fill="#527363" font-family="Arial,sans-serif" font-size="17" letter-spacing="2">PRODUCT TOUR / ${number}</text><text x="64" y="153" fill="#19392d" font-family="Georgia,serif" font-size="51">${esc(title)}</text><text x="66" y="193" fill="#617367" font-family="Arial,sans-serif" font-size="22">${esc(subtitle)}</text><rect x="64" y="236" width="1472" height="768" rx="12" fill="#f4f5f0" filter="url(#shadow)"/><g clip-path="url(#clip)"><svg x="64" y="236" width="1472" height="768" viewBox="${x} ${y} ${w} ${h}" preserveAspectRatio="xMidYMin slice"><image href="${image(file)}" width="3840" height="${file.includes('movies-report') ? 2160 : 2064}"/></svg></g><rect x="64" y="236" width="1472" height="768" rx="12" fill="none" stroke="#bdcdbc"/><text x="64" y="1057" fill="#527363" font-family="Arial,sans-serif" font-size="17">ACTUAL APPLICATION CAPTURE · ${file.includes('movies-report')?'ARCHIVED REPORT EXAMPLE':'EXAMPLE DATA'}</text><text x="1536" y="1057" text-anchor="end" fill="#527363" font-family="Arial,sans-serif" font-size="17">datly / ${name}</text></svg>`;
let composed = svg;
if(name === 'product-explore') {
const start = composed.indexOf('<g clip-path="url(#clip)">');
const end = composed.indexOf('</g>', start) + 4;
const source = image(file);
composed = composed.slice(0,start) + `<g clip-path="url(#clip)"><rect x="64" y="236" width="1472" height="768" fill="#f3f5f0"/><text x="104" y="284" fill="#527363" font-family="Arial,sans-serif" font-size="18" letter-spacing="2">THE CHART</text><svg x="84" y="306" width="790" height="650" viewBox="2560 340 1100 1000" preserveAspectRatio="xMidYMid meet"><image href="${source}" width="3840" height="2064"/></svg><path d="M896 280v680" stroke="#cfdbcb"/><text x="936" y="372" fill="#527363" font-family="Arial,sans-serif" font-size="18" letter-spacing="2">THE COMPUTED ROWS</text><svg x="926" y="402" width="575" height="385" viewBox="2530 1400 1100 720" preserveAspectRatio="xMidYMin meet"><image href="${source}" width="3840" height="2064"/></svg><text x="936" y="848" fill="#617367" font-family="Arial,sans-serif" font-size="21">A closer look at the data</text><text x="936" y="882" fill="#617367" font-family="Arial,sans-serif" font-size="21">behind the visualisation.</text></g>` + composed.slice(end);
}
if(name === 'product-reports') {
const start = composed.indexOf('<g clip-path="url(#clip)">');
const end = composed.indexOf('</g>', start) + 4;
composed = composed.slice(0,start) + `<g clip-path="url(#clip)"><rect x="64" y="236" width="1472" height="768" fill="#f3f5f0"/><text x="118" y="342" fill="#527363" font-family="Arial,sans-serif" font-size="17" letter-spacing="2">FROM CHART TO STORY</text><text x="114" y="425" fill="#19392d" font-family="Georgia,serif" font-size="47">Give the numbers</text><text x="114" y="483" fill="#19392d" font-family="Georgia,serif" font-size="47">a narrative.</text><path d="M118 537h490" stroke="#cfdbcb"/><text x="118" y="592" fill="#617367" font-family="Arial,sans-serif" font-size="23">Write the finding.</text><text x="118" y="640" fill="#617367" font-family="Arial,sans-serif" font-size="23">Bring a chart into the report.</text><text x="118" y="688" fill="#617367" font-family="Arial,sans-serif" font-size="23">Explain the context.</text><text x="118" y="876" fill="#527363" font-family="Arial,sans-serif" font-size="17">ARCHIVED DATA FORMULATOR EXAMPLE</text><text x="118" y="909" fill="#617367" font-family="Arial,sans-serif" font-size="18">Current report editing uses TipTap.</text><svg x="724" y="260" width="780" height="720" viewBox="2030 300 1260 1450" preserveAspectRatio="xMidYMid meet"><image href="${image(file)}" width="3840" height="2160"/></svg></g>` + composed.slice(end);
}
fs.writeFileSync(path.join(root,`${name}.svg`),composed);
}
card({name:'product-workspace',title:'One workspace. The whole question.',subtitle:'Explore your data, follow the analysis, and inspect the result.',file:'workspace.png',crop:[0,0,3840,2004],number:'01'});
card({name:'product-explore',title:'Look closer. The numbers are right here.',subtitle:'A focused view of the Explore canvas and its underlying data table.',file:'workspace.png',crop:[2445,235,1360,1840],number:'02'});
card({name:'product-threads',title:'Keep the path to your answer.',subtitle:'Questions, derived tables, and charts, organised into data threads.',file:'about-explore.png',crop:[720,325,1190,621],number:'03'});
card({name:'product-data',title:'Get to know the table behind the chart.',subtitle:'Inspect data quality signals, browse rows, and export a CSV.',file:'about-surfaces.png',crop:[2040,240,1690,882],number:'04'});
card({name:'product-reports',title:'Give your analysis a little more context.',subtitle:'An archived report example: written findings alongside chart images.',file:'screenshot-movies-report.png',crop:[2050,540,1240,647],number:'05'});
console.log('Created five source-backed SVG product compositions.');
21 changes: 21 additions & 0 deletions docs/assets/export-product-images.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Usage: node docs/assets/export-product-images.mjs /absolute/path/to/node_modules/sharp/dist/index.mjs
// Keep authoring tools outside the application dependency tree.
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
if (!process.argv[2]) throw new Error('Pass the absolute path to the Sharp module.');
const sharp=(await import(pathToFileURL(path.resolve(process.argv[2])).href)).default;
const root=fileURLToPath(new URL('../images/',import.meta.url));
for(const key of ['workspace','explore','threads','data','reports']){
const name='product-'+key, input=path.join(root,name+'.svg');
await Promise.all([
sharp(input).jpeg({quality:94,chromaSubsampling:'4:4:4'}).toFile(path.join(root,name+'.jpg')),
sharp(input).webp({quality:91,effort:6}).toFile(path.join(root,name+'.webp')),
sharp(input,{density:144}).png().toFile(path.join(root,name+'.png'))
]);
}
const original=await fs.readFile(path.join(root,'product-workspace.svg'),'utf8');
const social=original.replace('height="1100" viewBox="0 0 1600 1100"','height="900" viewBox="0 0 1600 900"').replace('One workspace. The whole question.','Good questions. Clearer answers.');
await fs.writeFile(path.join(root,'social-card.svg'),social);
await sharp(Buffer.from(social)).jpeg({quality:94,chromaSubsampling:'4:4:4'}).toFile(path.join(root,'social-card.jpg'));
console.log('Exported JPG, WebP, 2x PNG, and social card assets.');
Loading
Loading