components/elements/CircleChain/CircleChain.jsxjavascript
import "./CircleChain.scss";
import { useElementSize } from "../../../hooks/elementSize";
// brand gradient endpoints (orange -> teal), matching the accent colors
// already used across the app (--uimc-color-orange, the teal story-icon)
// const FROM = [0xec, 0x66, 0x02];
// const TO = [0x00, 0x99, 0x99];
// const mix = (ratio) => {
// const [r, g, b] = FROM.map((from, i) => Math.round(from + (TO[i] - from) * ratio));
// return `rgb(${r} ${g} ${b})`;
// };
/**
* N labeled circles overlapping in a horizontal row ("circleChain" element
* type), brand-gradient colored left to right.
*
* @param {Object} props
* @param {Array<{text: string}>} props.items - Circles, left to right.
* @param {"landscape"|"portrait"} [props.orientation="landscape"] - Sizes each circle as a fraction of the measured ring diameter, per the Figma spec's portrait/landscape ratios.
*/
export const CircleChain = ({ items, orientation = "landscape" }) => {
// Button positions are measured off the buttons-ring's own rendered size
// rather than a hardcoded em constant - SelectWidget.scss's `--ring-size:
// min(<em>, <N>cqmin)` can shrink that ring below its em value when space
// is tight (e.g. the iPad Air's letterboxed portrait reference), and a
// fixed em radius here would then place buttons outside the now-smaller
// ring instead of on it. Measuring the actual box is the single source of
// truth instead of duplicating that min() formula in JS.
const [buttonsRingRef, { width: ringWidth, height: ringHeight }] = useElementSize();
const ringDiameter = Math.min(ringWidth, ringHeight);
// Figma spec's item diameter as a fraction of its buttons-ring diameter
// (34.375/76.25 em portrait, 28/67 em landscape) rather than its own fixed
// em value - same reasoning as ringDiameter above: an item sized in em
// wouldn't shrink when the ring's `min(<em>, <N>cqmin)` cap shrinks it, so
// buttons would end up too big for their own ring on a small enough
// container (e.g. the iPad).
const ITEM_DIAMETER_RATIO = orientation === "portrait" ? 34.375 / 76.25 : (28 / 67) * 1.5;
const itemDiameter = ringDiameter * ITEM_DIAMETER_RATIO;
return (
<div className="circle-chain">
<div className="circle-chain__ring circle-chain__ring--outer" />
<div className="circle-chain__ring circle-chain__ring--dashed" />
<div className="circle-chain__ring circle-chain__ring--buttons" ref={buttonsRingRef} />
{items.map((item, index) => {
// spread items evenly around the button ring, starting at the top
// (-90deg) and going clockwise
const angle = -90 + index * (360 / items.length);
const radians = (angle * Math.PI) / 180;
const x = (ringDiameter / 2) * Math.cos(radians);
const y = (ringDiameter / 2) * Math.sin(radians);
return (
<button
key={index}
type="button"
className="circle-chain__circle "
style={{
"--item-diameter": `${itemDiameter}px`,
"--x": `${x}px`,
"--y": `${y}px`,
}}>
<div className="circle-chain__text">{item.text}</div>
</button>
);
})}
</div>
);
};