src/field-hints.js
Rama main · réplica de lectura del repositorio original.
import { FIELD_TYPE_VOCABULARY } from './vocabulary.js';
const NUMBER_PATHS = {
format: { when: '"currency" for money, "percent" for a 0..1 share, "compact" for 1.2M', opens: { currency: ['currency', 'accounting'], '*': ['decimals', 'separator'] } },
unit: { when: 'a word after the figure: "days"' },
display: { when: '"bar", "ring" or "heat" graphic', opens: { '*': ['scale', 'color'] } },
};
const DATE_PATHS = {
grain: { when: 'parts stored, a list: ["year","month"] for a month', opens: { '*': ['format'] } },
time: { when: 'true adds a time of day', opens: { '*': ['clock', 'zone'] } },
};
export const FIELD_PATHS = {
number: NUMBER_PATHS,
formula: { ...NUMBER_PATHS, display: { when: '"bar", "ring" or "heat" graphic; "sparkline" for a list', opens: { sparkline: ['style', 'color'], '*': ['scale', 'color'] } } },
date: DATE_PATHS,
daterange: { ...DATE_PATHS, time: { when: 'true adds a time of day', opens: { '*': ['clock', 'zone', 'elapsed'] } } },
rating: { max: { when: 'icons, 1-100 (default 5)' }, icon: { when: 'lucide:<name> (default star)' }, color: { when: '"ink", "icon" or "accent"' } },
toggle: { on: { when: 'label when true (default On)' }, off: { when: 'label when false (default Off)' } },
select: { options: { when: '[{name, hue, icon}], a hue only where it means something' } },
multiselect: { options: { when: '[{name, hue, icon}], a hue only where it means something' } },
workflow: { states: { when: '[{name, category, default, icon, hue}]' } },
};
const WHEN = {
currency: 'ISO code: "USD"',
accounting: 'true puts negatives in parentheses',
decimals: 'places shown',
separator: 'true groups thousands',
scale: '"column" (default) or the number that is 100%',
color: '"ink", "icon" or "accent"',
style: '"line", "column" or "winloss"',
format: 'iso, us, eu, long, short, month, quarter, ordinal or relative',
clock: '"24h" or "12h"',
zone: '"floating" (default), "fixed" (with zoneName) or "instant"',
elapsed: 'true shows the span',
};
const unset = (v) => v == null || v === '' || v === 'number' || v === 'text' || v === false;
export function nextFor(type, config = {}) {
const tree = FIELD_PATHS[type];
if (!tree) return [];
const out = [];
for (const [key, node] of Object.entries(tree)) {
const v = config[key];
if (unset(v)) { out.push(`${key}: ${node.when}`); continue; }
const opened = [...(node.opens?.[v] ?? []), ...(node.opens?.['*'] ?? [])];
for (const child of new Set(opened)) if (unset(config[child])) out.push(`${child}: ${WHEN[child]}`);
}
return out;
}
const singular = (s) => String(s ?? '').trim().toLowerCase().replace(/ies$/, 'y').replace(/s$/, '');
const slug = (s) => String(s ?? '').toLowerCase().replace(/[^a-z0-9]/g, '');
const MONEY = /\b(amount|price|cost|balance|total|budget|spend|spent|income|expense|salary|fee|revenue|payment|paid|owed)\b/i;
const FIGURE = /\b(count|qty|quantity|hours|minutes|days|percent|rate)\b/i;
const STATUS = /\b(status|state|stage|priority|phase)\b/i;
const EVERY = ['width', 'description', 'default'];
export function fieldHints(field, { sent = {}, tables = [] } = {}) {
const { type, name, config = {} } = field;
const hints = [];
const takes = new Set([...(FIELD_TYPE_VOCABULARY.find((v) => v.type === type)?.config ?? []), ...EVERY]);
const dropped = Object.keys(sent ?? {}).filter((k) => !(k in config) && !takes.has(k));
if (dropped.length) hints.push(`Ignored ${dropped.join(', ')}: ${type} takes ${[...takes].join(', ')}.`);
if (type === 'text') {
const table = tables.find((t) => singular(t) === singular(name));
if (table) hints.push(`'${name}' names the ${table} table, so it is a relation: weave_update_field {type:"relation", config:{targetDb:"${table}"}} links each value to the ${table} record of that name.`);
else if (MONEY.test(name) || FIGURE.test(name)) hints.push(`'${name}' holds figures: weave_update_field {type:"number"${MONEY.test(name) ? ', config:{format:"currency", currency:"USD"}' : ''}} so it sums and aligns.`);
}
if (type === 'number' && MONEY.test(name) && config.format !== 'currency') {
hints.push(`'${name}' is money? format:"currency", currency:"USD".`);
}
const options = type === 'select' || type === 'multiselect' ? config.options ?? [] : [];
if (options.length > 1 && options.every((o) => !o.hue || o.hue === 'slate')) {
hints.push('All options slate: give a hue where it means something (green income, red overdue).');
}
const marks = type === 'workflow' ? config.states ?? [] : STATUS.test(name) ? options : [];
if (marks.length && marks.every((o) => !o.icon)) {
hints.push(`Give each ${type === 'workflow' ? 'state' : 'option'} an icon: "lucide:<name>".`);
}
return hints;
}
export function containerHints(kind, made, { workspace = '' } = {}) {
const hints = [];
if (workspace && slug(made.name) === slug(workspace)) {
hints.push(`'${made.name}' repeats the workspace name: name the workspace for the domain (personal-finance) and make the request a space in it.`);
}
if (!made.icon) hints.push('No icon: icon:"lucide:<name>".');
return hints;
}
export function guided(weave, kind, result, sent = {}) {
const add = ({ next = [], hints = [] }) => ({ ...result, ...(next.length ? { next } : {}), ...(hints.length ? { hints } : {}) });
if (kind === 'field') return add({ next: nextFor(result.type, result.config), hints: fieldHints(result, { sent, tables: weave.userTables().map((t) => t.name) }) });
return add({ hints: containerHints(kind, result, { workspace: weave.getWorkspace().name }) });
}