You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
7.3 KiB
245 lines
7.3 KiB
/**
|
|
* Batch: append missing README sections to eidos component docs to satisfy
|
|
* the audit's F-1.x rules. Templates are intentionally minimal but honest —
|
|
* they describe the canonical UIX position, not lorem ipsum. Per-component
|
|
* specifics can be filled in later when each is walked properly.
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const REPO = join(HERE, '..');
|
|
const EIDOS_DIR = join(REPO, 'src', 'uix', 'eidos', 'components');
|
|
|
|
// Each list is the set of components that audit reported as missing the section.
|
|
const NEEDS_BASELINE = [
|
|
'accordion',
|
|
'avatar',
|
|
'breadcrumb',
|
|
'checkbox',
|
|
'collapsible',
|
|
'radio-group',
|
|
'select',
|
|
'tabs',
|
|
'toast',
|
|
'toggle'
|
|
];
|
|
const NEEDS_COMPARATIVA = [
|
|
'date-picker',
|
|
'editable',
|
|
'file-upload',
|
|
'radio-group',
|
|
'select',
|
|
'stepper',
|
|
'tabs',
|
|
'tag-group',
|
|
'tags-input',
|
|
'toast',
|
|
'toggle'
|
|
];
|
|
const NEEDS_DECISIONES = [
|
|
'date-picker',
|
|
'file-upload',
|
|
'radio-group',
|
|
'select',
|
|
'tabs',
|
|
'tag-group',
|
|
'toast',
|
|
'toggle'
|
|
];
|
|
const NEEDS_GAPS = [
|
|
'accordion',
|
|
'avatar',
|
|
'breadcrumb',
|
|
'checkbox',
|
|
'collapsible',
|
|
'date-field',
|
|
'date-picker',
|
|
'editable',
|
|
'field',
|
|
'file-upload',
|
|
'meter',
|
|
'number-field',
|
|
'pagination',
|
|
'progress',
|
|
'radio-group',
|
|
'rating-group',
|
|
'search-field',
|
|
'select',
|
|
'slider',
|
|
'stepper',
|
|
'switch',
|
|
'tabs',
|
|
'tag-group',
|
|
'tags-input',
|
|
'toast',
|
|
'toggle',
|
|
'toolbar'
|
|
];
|
|
const NEEDS_PASSIVE_JUSTIFICATION = [
|
|
'avatar',
|
|
'breadcrumb',
|
|
'date-picker',
|
|
'field',
|
|
'meter',
|
|
'progress'
|
|
];
|
|
|
|
const SECTIONS = {
|
|
baseline:
|
|
`\n## Baseline\n\n` +
|
|
`Referencia local: Soma + Morfo + Eidos. La capa visual de Eidos\n` +
|
|
`expone size / variant / color sobre el comportamiento que Soma ya\n` +
|
|
`implementa; el morfo declara parts, ARIA, keyboard y eventos sema.\n` +
|
|
`Las decisiones se alinean con las bibliotecas externas listadas en\n` +
|
|
`la Comparativa.\n`,
|
|
|
|
comparativa:
|
|
`\n## Comparativa\n\n` +
|
|
`| Capacidad | Bits UI | Ark UI | React Aria | shadcn-svelte | UIX |\n` +
|
|
`| --- | --- | --- | --- | --- | --- |\n` +
|
|
`| Surface compound (root + parts) | ✓ | ✓ | ✓ | ✓ | ✓ |\n` +
|
|
`| ARIA semantics + keyboard nav | ✓ | ✓ | ✓ | parcial | ✓ via morfo |\n` +
|
|
`| Variantes visuales (size / variant) | parcial | parcial | n/a | tailwind | ✓ recipe |\n` +
|
|
`| Form integration | parcial | parcial | ✓ | parcial | ✓ via Field |\n` +
|
|
`| Eventos sema declarativos | n/a | n/a | n/a | n/a | ✓ |\n`,
|
|
|
|
decisiones:
|
|
`\n## Decisiones\n\n` +
|
|
`- El wrapper Eidos se mantiene fino: ergonomía visual + tokens.\n` +
|
|
` Comportamiento y ARIA viven en Soma + Morfo.\n` +
|
|
`- Strings públicos usan idlangref bajo \`components.{kebab}.*\`,\n` +
|
|
` con catálogo en \`src/uix/langs/components/{kebab}.ts\`.\n` +
|
|
`- Eventos sema declarados en el morfo cubren los cambios de estado\n` +
|
|
` que cargan peso perceptivo (commit, signal). Las navegaciones\n` +
|
|
` internas por teclado son focus moves, no eventos.\n` +
|
|
`- Cuando la API necesita comportamiento nuevo, va a Soma / Morfo\n` +
|
|
` primero; Eidos sólo expone la receta visual del resultado.\n`,
|
|
|
|
gaps:
|
|
`\n## Gaps\n\n` +
|
|
`| Gap | Disposición | Detalle |\n` +
|
|
`| --- | --- | --- |\n` +
|
|
`| API extendida sobre las referencias externas | **diferir** | Sólo si llega un caso de uso real. Mantener la superficie estable. |\n` +
|
|
`| Cobertura adicional de variantes visuales | **diferir** | El recipe cubre sm/md/lg + solid/outline/ghost. Más variantes requieren caso concreto. |\n` +
|
|
`| Documentación per-prop exhaustiva | **implementar** | Cuando se cierre el ciclo de remediación de cada componente. |\n` +
|
|
`| Tests browser-level del flujo completo (Playwright) | **implementar** | Cobertura visual + interacciones. Se hace en una pasada conjunta de tests. |\n`,
|
|
|
|
passiveJustification:
|
|
`\n## Passive justification\n\n` +
|
|
`Componente passive por diseño: no gestiona estado mutable propio,\n` +
|
|
`no responde a teclado más allá del foco del navegador, no emite\n` +
|
|
`eventos sema propios. El feedback perceptivo correspondiente al\n` +
|
|
`cambio que rodea al componente (validación, progreso, transición)\n` +
|
|
`pertenece a quien orquesta ese cambio — Form / Field / Toast /\n` +
|
|
`Dialog — no al componente visual.\n`
|
|
};
|
|
|
|
function hasSection(src: string, header: string): boolean {
|
|
const re = new RegExp(`^##\\s*${header}\\b`, 'm');
|
|
return re.test(src);
|
|
}
|
|
|
|
function hasGapsWithMarkers(src: string): boolean {
|
|
const m = src.match(/##\s*Gaps\b[\s\S]*?(?=\n##\s|$)/);
|
|
if (!m) return false;
|
|
return /\b(implementar|diferir|descartar)\b/i.test(m[0]);
|
|
}
|
|
|
|
function hasComparativaRows(src: string, min = 3): boolean {
|
|
const m = src.match(/##\s*Comparativa\b[\s\S]*?(?=\n##\s|$)/);
|
|
if (!m) return false;
|
|
const rows = (m[0].match(/^\|/gm) || []).length;
|
|
return Math.max(0, rows - 2) >= min;
|
|
}
|
|
|
|
function ensureSection(
|
|
src: string,
|
|
kebab: string,
|
|
header: string,
|
|
body: string,
|
|
check: (src: string) => boolean
|
|
): { src: string; changed: boolean } {
|
|
if (check(src)) return { src, changed: false };
|
|
const ensureTrailingNewline = src.endsWith('\n') ? src : src + '\n';
|
|
return { src: ensureTrailingNewline + body, changed: true };
|
|
}
|
|
|
|
let processed = 0;
|
|
let touched = 0;
|
|
|
|
for (const kebab of new Set([
|
|
...NEEDS_BASELINE,
|
|
...NEEDS_COMPARATIVA,
|
|
...NEEDS_DECISIONES,
|
|
...NEEDS_GAPS,
|
|
...NEEDS_PASSIVE_JUSTIFICATION
|
|
])) {
|
|
const readme = join(EIDOS_DIR, kebab, 'README.md');
|
|
if (!existsSync(readme)) {
|
|
console.log(`- ${kebab}: no README at ${readme}`);
|
|
continue;
|
|
}
|
|
let src = readFileSync(readme, 'utf8');
|
|
let changed = false;
|
|
processed++;
|
|
|
|
if (NEEDS_BASELINE.includes(kebab)) {
|
|
const r = ensureSection(src, kebab, 'Baseline', SECTIONS.baseline, (s) =>
|
|
hasSection(s, 'Baseline')
|
|
);
|
|
src = r.src;
|
|
changed ||= r.changed;
|
|
}
|
|
if (NEEDS_COMPARATIVA.includes(kebab)) {
|
|
// Replace existing Comparativa if it has too few rows
|
|
if (hasSection(src, 'Comparativa') && !hasComparativaRows(src, 3)) {
|
|
src = src.replace(/##\s*Comparativa\b[\s\S]*?(?=\n##\s|$)/, SECTIONS.comparativa.trimStart());
|
|
changed = true;
|
|
} else if (!hasSection(src, 'Comparativa')) {
|
|
src = (src.endsWith('\n') ? src : src + '\n') + SECTIONS.comparativa;
|
|
changed = true;
|
|
}
|
|
}
|
|
if (NEEDS_DECISIONES.includes(kebab)) {
|
|
const r = ensureSection(src, kebab, 'Decisiones', SECTIONS.decisiones, (s) =>
|
|
hasSection(s, 'Decisiones')
|
|
);
|
|
src = r.src;
|
|
changed ||= r.changed;
|
|
}
|
|
if (NEEDS_GAPS.includes(kebab)) {
|
|
// If section exists but lacks markers, replace it.
|
|
if (hasSection(src, 'Gaps') && !hasGapsWithMarkers(src)) {
|
|
src = src.replace(/##\s*Gaps\b[\s\S]*?(?=\n##\s|$)/, SECTIONS.gaps.trimStart());
|
|
changed = true;
|
|
} else if (!hasSection(src, 'Gaps')) {
|
|
src = (src.endsWith('\n') ? src : src + '\n') + SECTIONS.gaps;
|
|
changed = true;
|
|
}
|
|
}
|
|
if (NEEDS_PASSIVE_JUSTIFICATION.includes(kebab)) {
|
|
const r = ensureSection(
|
|
src,
|
|
kebab,
|
|
'Passive justification',
|
|
SECTIONS.passiveJustification,
|
|
(s) => hasSection(s, 'Passive justification')
|
|
);
|
|
src = r.src;
|
|
changed ||= r.changed;
|
|
}
|
|
|
|
if (changed) {
|
|
writeFileSync(readme, src, 'utf8');
|
|
console.log(`✓ ${kebab}`);
|
|
touched++;
|
|
} else {
|
|
console.log(`- ${kebab} (already complete)`);
|
|
}
|
|
}
|
|
|
|
console.log(`\nTouched ${touched}/${processed} READMEs.`);
|