// 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 && } {title} ; } Citation.displayName = 'Citation';