Penta
${typeof d.memberCount === 'number' ? html`${d.memberCount} members` : nothing}
${this.renderChart(d)}
${renderHdFacts([
{ label: 'Members', value: d.memberCount?.toString() },
{
label: 'Defined channels',
value:
typeof s?.definedChannels === 'number' && channels.length > 0
? `${s.definedChannels} of ${channels.length}`
: undefined,
},
{
label: 'Filled gates',
value:
typeof s?.filledGates === 'number' && gates.length > 0
? `${s.filledGates} of ${gates.length}`
: undefined,
},
{
label: 'Core',
value:
typeof s?.coreDefined === 'boolean'
? s.coreDefined
? 'Defined'
: 'Open'
: undefined,
},
{
label: 'Gap gates',
value: gaps.length > 0 ? gaps.join(', ') : undefined,
},
])}
${
// The tiles above count the members, the defined channels, the filled
// gates and the gaps; this paragraph is what those counts mean.
this.hideReadings
? nothing
: html`
A penta is the field three to five people form when they work as a group.
It is read from the twelve penta gates the members bring between them: a
channel with both of its gates held somewhere in the group is a defined
Strength, and a gate no member holds is a gap the group has to compensate
for.
`
}
${this.renderGroup(
'Upper (direction)',
channels.filter((c) => c.position === 'upper'),
'Upper channels run from the G Center to the Throat. They carry the leadership of the group and how it presents itself.',
)}
${this.renderGroup(
'Lower (execution)',
channels.filter((c) => c.position === 'lower'),
'Lower channels run from the G Center to the Sacral. They carry the managed, generative, resource work.',
)}
${
// A position the API adds later must not silently drop the channel from
// the card: anything that is neither triangle still renders.
this.renderGroup(
'Channels',
channels.filter(
(c) => c.position !== 'upper' && c.position !== 'lower',
),
)
}
${
// The Core badge on the row is the fact; this explains the tradition
// behind it.
hasCore && !this.hideReadings
? html``
: nothing
}
${this.renderGates(gates)}
${
// Not exposition but the legend: every attribution above is a letter, and
// without this line there is nothing to read them against.
channels.length > 0 || gates.length > 0
? html``
: nothing
}
`;
}
/**
* One triangle of the penta. The note is what the triangle does, so it sits with the heading rather than being repeated on each of its three channels.
*/
private renderGroup(label: string, channels: PentaChannel[], note?: string) {
if (channels.length === 0) return nothing;
return html`
${label}
${note && !this.hideReadings ? html`
${note}
` : nothing}
${channels.map(
(c) => html`
${c.gateA}-${c.gateB}
${c.name ?? ''}
${c.circuit ? html`${c.circuit} circuit` : nothing}
${c.defined ? 'Defined' : 'Open'}
${c.isCore ? html`Core` : nothing}
Gate ${c.gateA} held by ${members(c.gateAHeldBy)}. Gate ${c.gateB} held
by ${members(c.gateBHeldBy)}.
`,
)}
`;
}
/**
* The twelve penta gates, each with the role it brings and the members who carry it. A gate nobody holds is the gap the summary counts, so it is flagged rather than left as an empty attribution.
*/
private renderGates(gates: PentaGate[]) {
if (gates.length === 0) return nothing;
const filled = gates.filter((g) => g.filled).length;
return html`
Gates (${filled} of ${gates.length} filled)
${
this.hideReadings
? nothing
: html`
The role each gate brings to the group, and who carries it. A gap is a
role no member holds, so the group compensates for it.
`
}
${gates.map(
(g) => html`
${g.gate}
${g.gateName ?? ''}
${
g.filled
? html`Held by ${members(g.heldBy)}`
: html`Gap`
}
`,
)}
`;
}
}
/**
* Members are lettered, not numbered. The response reports them as zero-based indices into the member list that was sent, and printing a raw "0" on a card reads as a bug; "Member 1" for index 0 would contradict the API the developer is reading beside it. A letter is unambiguous either way, and it matches how `roxy-hd-connection` names person A and person B. Five is the ceiling: the endpoint takes 3 to 5 members.
*/
const MEMBER_LETTERS = 'ABCDE';
/** How a member index is named anywhere on the card, so a badge on the ladder and an attribution line under a channel cannot letter the same member differently. */
const memberLetter = (i: number): string => MEMBER_LETTERS[i] ?? String(i + 1);
function members(indices: number[] | undefined): string {
const held = (indices ?? []).map(memberLetter);
return held.length > 0 ? held.join(', ') : 'nobody';
}
declare global {
interface HTMLElementTagNameMap {
'roxy-hd-penta': RoxyHdPenta;
}
}