This commit is contained in:
83
src/lib/ui/skillNodes.js
Normal file
83
src/lib/ui/skillNodes.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* A skill's declared section, as a node in the page tree.
|
||||
*
|
||||
* This is the join between the two halves of the UI system. A Board skill's
|
||||
* `ui:` block is still parsed by `uiConfig.normalizeSection`, still validated
|
||||
* against the closed vocabulary, and still drawn by the same nine components —
|
||||
* nothing about definitions changes. What this adds is *identity*: the section
|
||||
* becomes an addressable node, so it can be hidden, moved and reordered by the
|
||||
* same operations that move a built-in.
|
||||
*
|
||||
* **Nothing here writes back to Markdown.** A node carries `origin: 'skill'`,
|
||||
* and a change to it is stored as an operation in the person's own layout
|
||||
* patch. The definition on disk, and the definition in the account's custom
|
||||
* skills, are read-only from here — which is what keeps a layout preference
|
||||
* from silently editing something another user also sees.
|
||||
*/
|
||||
|
||||
import { dataSourceFor, sourceSupportsOption } from '@/lib/skills/surfaces';
|
||||
import { makeNode } from './node';
|
||||
|
||||
/** Ids are `skill-<skill>-<section>`, so provenance is legible in the DOM. */
|
||||
export const skillNodeId = (skillId, sectionId) => [
|
||||
'skill', slug(skillId), slug(sectionId),
|
||||
].filter(Boolean).join('-');
|
||||
|
||||
const slug = (value) => String(value || '')
|
||||
.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
|
||||
/**
|
||||
* One normalized section, adapted.
|
||||
*
|
||||
* Only options the source actually declares are carried across. The skill
|
||||
* format checks that a period is a real period; this checks that the reading
|
||||
* takes periods at all — a stricter rule, and one a definition written against
|
||||
* the looser one could fail. Dropping the option is right where refusing the
|
||||
* node would not be: a Board card must never disappear because of a parameter
|
||||
* that was doing nothing anyway.
|
||||
*/
|
||||
export function skillSectionNode(skill, section) {
|
||||
const params = {};
|
||||
if (section.periods?.length && sourceSupportsOption(section.source, 'periods')) {
|
||||
params.periods = [...section.periods];
|
||||
}
|
||||
if (section.limit && sourceSupportsOption(section.source, 'limit')) {
|
||||
params.limit = section.limit;
|
||||
}
|
||||
|
||||
return makeNode({
|
||||
id: skillNodeId(skill.id, section.id),
|
||||
type: section.type,
|
||||
origin: 'skill',
|
||||
data: { source: section.source, params },
|
||||
props: {
|
||||
/* The same fallback the surface uses, so a section with no title of its
|
||||
own is still named by the skill that contributed it. */
|
||||
title: section.title || skill.name,
|
||||
...(section.description ? { description: section.description } : {}),
|
||||
/* Attribution is what lets a reader tell an extension from a built-in
|
||||
panel, and which skill to switch off. */
|
||||
attribution: skill.name,
|
||||
...(section.editable ? { editable: true } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Every section a page's definitions contribute, grouped by placement.
|
||||
*
|
||||
* Grouped because that is how the composition consumes them: each
|
||||
* `skill-surface` node names one placement and takes the sections that declared
|
||||
* it. A placement no slot offers simply has nowhere to render, which is the
|
||||
* same outcome as today.
|
||||
*/
|
||||
export function skillNodesByPlacement(sections = []) {
|
||||
const out = {};
|
||||
for (const { skill, section } of sections) {
|
||||
if (!dataSourceFor(section.source)) continue;
|
||||
const key = section.placement || '';
|
||||
if (!out[key]) out[key] = [];
|
||||
out[key].push(skillSectionNode(skill, section));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user