// Copyright (c) Meta Platforms, Inc. and affiliates.
'use client';
/**
* @file Citation.tsx
* @input Uses React, StyleX, theme tokens
* @output Exports Citation component for inline citation references
* @position Core implementation; consumed by index.ts
*
* SYNC: When modified, update these files to stay in sync:
* - /packages/core/src/Citation/index.ts (exports if types change)
* - /packages/core/src/Citation/Citation.doc.mjs (props table, features)
* - /packages/core/src/Citation/Citation.test.tsx (tests for new/changed behavior)
* - /apps/storybook/stories/Citation.stories.tsx (storybook stories)
* - /packages/cli/assets/templates/blocks/components/Citation/ (showcase blocks)
*/
import type React from 'react';
import type { ReactNode } from 'react';
import * as stylex from "../style-runtime.js";
import "../theme/tokens.stylex.js";
import { colorVars, spacingVars, radiusVars, typeScaleVars, fontWeightVars, borderVars, durationVars, easeVars } from "../theme/tokens.stylex.js";
import { mergeProps } from "../utils/index.js";
import type { BaseProps } from "../BaseProps.js";
import { themeProps } from "../utils/themeProps.js";
import { useTranslator } from "../i18n/index.js";
import { renderIconSlot } from "../Icon/index.js";
export interface CitationSource {
title?: string;
url?: string;
/**
* Image URL for a favicon or source logo, rendered as `
` inside the
* icon circle. This is the idiomatic field for image sources (mirrors
* `Avatar`/`Thumbnail` `src`). When both `src` and a non-string `icon` are
* provided, `icon` wins.
*/
src?: string;
/**
* Source icon shown before the label text (label variant only). Accepts:
* - a React node / Astryx `` / SVG element — rendered as-is, or
* - a string image URL — rendered as `
` for backward compatibility
* with callers that passed a favicon URL here.
*
* Note: unlike icon slots elsewhere in the system, a bare string is treated
* as an image URL (not a semantic icon name) to preserve the original
* favicon behavior. Pass an Astryx `` (or other node) for a real icon.
*/
icon?: ReactNode;
}
export interface CitationProps extends BaseProps {
ref?: React.Ref;
source: CitationSource;
number: number;
variant?: 'label' | 'number';
}
const styles = {
label: {
k1xSpc: "x3nfvp2",
kGNEyG: "x6s0dn4",
kOIVth: "xzye2dw",
kXLuUW: "x11njtxf",
kZKoxP: "x1grt7ep",
kGuDYH: "x141an7d",
k63SB2: "xv8on6e",
kLWn49: "x1ltkj2j",
kMwMTN: "xv1l7n4",
kaIpWk: "xh6dtrn",
kMzoRj: "x1litavf",
ksu8eU: "x1y0btm7",
kVAM5u: "x14i3s5s",
kg3NbH: "xf314gf",
keTefX: "xladpz9",
kybGjl: "x1hl2dhg",
k1ekBW: "x1eaenvl",
kIyJzY: "x2rcmwa",
kAMwcw: "xlr8y92",
ks0D6T: "x55n99s",
kVQacm: "xb3r6kr",
$$css: true as const
},
labelWithIcon: {
kZCmMZ: "x138rykx",
$$css: true as const
},
labelInteractive: {
kkrTdU: "x1ypdohk x16khyan",
$$css: true as const
},
labelHover: {
kWkggS: "xoevpu5",
kMwMTN: "xv1l7n4 xo5jvnm",
$$css: true as const
},
number: {
k1xSpc: "x3nfvp2",
kGNEyG: "x6s0dn4",
kjj79g: "xl56j7k",
kXLuUW: "x1v0h343",
kGuDYH: "x141an7d",
k63SB2: "x2mo6ok",
kLWn49: "x1ltkj2j",
kMwMTN: "xv1l7n4",
kWkggS: "xgcxg3y",
kaIpWk: "xjspbzw",
k7Eaqz: "x16asifk",
kZKoxP: "x1grt7ep",
kg3NbH: "x7a5moj",
kybGjl: "x1hl2dhg",
k1ekBW: "x15406qy",
kIyJzY: "x2rcmwa",
kAMwcw: "xlr8y92",
$$css: true as const
},
numberInteractive: {
kkrTdU: "x1ypdohk x16khyan",
$$css: true as const
},
numberHover: {
kWkggS: "xgcxg3y xoevpu5",
$$css: true as const
}
};
export function Citation({
ref,
source,
number,
variant = 'label',
xstyle,
className,
style,
'data-testid': testId,
...rest
}: CitationProps): React.ReactElement {
const t = useTranslator();
const title = source.title ?? String(number);
const href = source.url;
// Resolve the source icon. A non-string `icon` node renders as-is (an Astryx
// , SVG, avatar, etc.). Otherwise fall back to an image URL: `src`, or
// a legacy string passed to `icon` (back-compat — favicon URLs still render
// as
). The icon is decorative; the accessible name comes solely from
// the element's aria-label, so nothing is double-announced.
const iconNode = source.icon != null && typeof source.icon !== 'string' ? source.icon : null;
const imageSrc = source.src ?? (typeof source.icon === 'string' ? source.icon : undefined);
const hasIcon = iconNode != null || imageSrc != null;
const Tag = href ? 'a' : 'span';
// `doc-noteref` is a reference role — only appropriate on the interactive
// link form. On a plain (unlinked) span it is not a permitted role
// (axe: aria-allowed-role), so omit it there; the aria-label still names it.
const noteRole = href ? 'doc-noteref' as const : undefined;
const linkProps = href ? {
href,
target: '_blank' as const,
rel: 'noopener noreferrer' as const,
title
} : {
title
};
// Both and extend HTMLElement — narrow via intersection
const elementRef = ref as React.Ref & React.Ref;
if (variant === 'number') {
return
{number}
;
}
return
{hasIcon &&
{iconNode != null ? renderIconSlot(iconNode, {
size: 'sm'
}) :
}
}
{title}
;
}
Citation.displayName = 'Citation';