Skip to content

Components: Settle the FURN-only primitives boundary #4250

Description

@JasonVMo

Summary

Decide whether the five FURN-only primitives are public API, package-private, or a
separate package, then apply the resulting export, spec-policy, and test-guard changes
without altering primitive behavior.

Observed. packages/agentic/components exports five primitives -- Icon,
CheckboxIndicator, CompoundItemLayout, FocusVisual, and LayoutStableText -- from
its only public entry point. None of them has a SPEC.md, a recorded upstream catalog
entry, or any upstream contract. The package's own
authoring guidance
treats primitives as "unstyled building blocks" extracted for repeated behavioral or
structural contracts. Every consumer of every primitive is inside the same package; no
external package or application imports them directly today.

Inferred. A public export without a contract, a spec, or an explicit scope decision
is a forward compatibility hazard. Until the decision is made, primitives cannot be
assigned a readiness level, and consumers cannot know whether they are adopting stable
API or an implementation detail that may change.

Goal

Record the owner-approved scope decision -- public API, package-private, or
separate package -- and apply the resulting mechanical changes to exports, the export
guard test, the readiness model, and the component test strategy without changing
primitive behavior.

Stage

Stages 0 and 1 (scope decision through beta foundations). Stage 0 records the
owner-approved public, private, or separate-package disposition. Stage 1 applies
the corresponding export, contract, test, and readiness changes. The readiness
model cannot assign a level to primitives until their scope is settled; every
new component generated while the decision is open inherits the same ambiguity.

Why it matters

Observed. The provisional readiness assessment in
readiness-model.md places all five primitives at
experimental with the ceiling note: "no SPEC.md and no upstream catalog entry, so
the spec-fidelity axis cannot be evaluated at all." That ceiling is structural, not
coverage-driven. It does not lift until the scope is decided.

Observed. The component test strategy notes that
primitives have no SPEC.md and leaves open whether the coverage contract derives from
their types file or from authored specs. The contract cannot be written until the scope
decision resolves what a primitive is obligated to document.

Observed. Four out of five primitives are in
src/index.test.ts's
"exports shared primitives" block. Icon is exported in
src/index.ts
but is not asserted in that block, creating a silent gap in the export guard for the
most broadly consumed primitive.

Inferred. The _unstable suffix on the component pipeline exports signals "the
composition API may change," not "the component is immature." Primitives carry no
equivalent signal. A consumer who imports Icon or FocusVisual today has no
indication whether those names are stable API or subject to silent removal.

Observed current state

The five primitives

Observed. All five reside under
src/primitives
and are exported explicitly from
src/index.ts.

Primitive Exports from src/index.ts Purpose
icon Icon, FontIconSource, IconElementProps, IconProps, SvgIconSource Multi-source icon renderer: font glyph, SVG component, or image. Discriminated union on source type.
checkbox-indicator CheckboxIndicator, CheckboxIndicatorProps, CheckboxIndicatorStatus Renders checked/unchecked/indeterminate state with configurable icon sources and colors.
compound-item-layout CompoundItemLayout, CompoundItemLayoutProps Flex layout with leading, primary, secondary, and trailing slots. Secondary placement (right or under) is controllable.
focus-visual FocusVisual, createFocusVisualProps, FocusVisualOptions, FocusVisualProps, FocusVisualRingProps, FocusVisualStyles Single or dual-ring focus visual that mounts eagerly and changes only opacity. createFocusVisualProps converts token values into ring ViewStyle arrays.
layout-stable-text LayoutStableText, LayoutStableTextProps Reserve-and-visible text pair: a hidden element reserves metrics for the widest state; the visible element renders over it.

Coverage

Observed. Each primitive has a colocated runtime test, a type test, and a
Storybook story. None has a __snapshots__ directory and none has a SPEC.md. The
Storybook story titles use the Primitives/ prefix (e.g., 'Primitives/Icon'),
placing them in the same sidebar tier as 'Primitives/Callout' from
packages/native/Callout.

Observed. primitives-icon--default is one of only three story IDs in
apps/storybook/scripts/smoke-stories.json,
alongside components-button--default and primitives-callout--default. Icon is
therefore the only one of the five primitives with any on-device render coverage.

Observed. The export guard in
src/index.test.ts
asserts CheckboxIndicator, CompoundItemLayout, FocusVisual,
createFocusVisualProps, and LayoutStableText in an "exports shared primitives"
block. Icon is exported from src/index.ts but is not asserted in that block.

Internal consumer inventory

Observed. All consumers are inside packages/agentic/components/src/components.
No external package and no application imports a primitive directly from
@fluentui-react-native/components.

Primitive Consuming components (internal only)
Icon accordion, avatar, badge, button, divider, input, list-item, listbox-item, menu-item, progress-bar, tab, tag (12 of 18)
FocusVisual accordion, button, card, checkbox, list-item, listbox-item, menu-item, radio, switch, tab, tag (11 of 18)
LayoutStableText button, list-item, listbox-item, menu-item, tab (5 of 18)
CompoundItemLayout list-item, listbox-item, menu-item (3 of 18)
CheckboxIndicator checkbox, listbox-item, menu-item (3 of 18)

Observed. FocusVisual is the most cross-cutting primitive (11 consumers, 5
exported types plus a helper function, and named explicitly in
src/components/AGENTS.md
as the mandated focus-feedback path). Icon is the most broadly consumed (12
consumers) and the only primitive with on-device smoke coverage. CheckboxIndicator
and CompoundItemLayout are narrower (3 consumers each) and more directly
substitutable.

Relationship to src/common

Observed.
src/common
contains item.types.ts, which exports ItemSecondaryContentPosition -- a type used
by CompoundItemLayoutProps. src/common is not exported from src/index.ts and is
documented in
src/AGENTS.md
as "component-library-specific non-public types, constants, and helpers." A functioning
package-private tier therefore already exists.

Upstream catalog position

Observed. The canonical catalog is x3-design/fluent-design at
d334acf,
indexed by
component-map.yaml.
Its 50 entries have no "primitives" tier; components are atom or molecule. None of
the five FURN primitives maps to a catalog entry. Icon is a concept that appears in
many upstream component slot definitions but is not itself a catalog component. The
other four have no upstream analogue at any tier.

Inferred. The primitives are a FURN implementation artifact, extracted because
multiple higher-order components share the same behavioral or structural contract.
That origin is consistent with package-private status but does not preclude making them
public; it means the contract obligation would be entirely self-imposed.

Scope

1. Owner decision (resolved)

The five primitives are public API, with these approved boundaries:

  • Export primitive components and their public types only from
    @fluentui-react-native/components/primitives; do not retain compatibility exports
    at the package root.
  • Keep primitive component names stable, matching the naming convention for normal
    components (CheckboxIndicator, CompoundItemLayout, FocusVisual, Icon, and
    LayoutStableText).
  • Apply the normal _unstable convention to composition and helper APIs. Export
    createFocusVisualProps_unstable from the primitive subpath.
  • Give each public primitive a lightweight local CONTRACT.md covering behavior,
    accepted sources and props, accessibility, platform branches, and stability rather
    than inventing an upstream SPEC.md.
  • Apply the normal component readiness ladder to primitives, substituting fidelity to
    the local contract, public types, and renderer branches for upstream-spec fidelity.

Implementation: #4288.

2. Export guard repair

Guard the package root and primitive subpath as separate exact runtime surfaces. The
root guard must contain only higher-order APIs; the primitive guard must contain all
five primitives, including Icon, plus createFocusVisualProps_unstable.

3. Contract and spec policy for public primitives

Each public primitive requires a lightweight CONTRACT.md stating behavioral
invariants, accepted source types and props, accessibility behavior, renderer or
platform branches, and stability expectations, without a design-system spec tie.

The coverage contract in
component-test-strategy.md
derives primitive coverage from that contract, the public types, and renderer branches.

4. Readiness model implications

Apply the same readiness ladder used for normal components. For primitives, local
contract fidelity replaces upstream-spec fidelity, so the absence of an upstream catalog
entry does not impose a readiness ceiling.

5. Authoring guidance update

Update
src/primitives/AGENTS.md
and
src/AGENTS.md
to state the approved scope, the contract obligation for any public primitive, and the
process for deciding when a new primitive warrants extraction.

Out of scope

  • Changing the behavior of any primitive.
  • Adding or removing primitives; this task settles the boundary for the existing five.
  • Deciding the _unstable suffix policy for higher-order component pipeline exports;
    that is readiness-model.md.
  • Extracting src/common into its own package; the package-private tier already
    works and is not at issue.
  • Replacing FocusVisual with a new focus-feedback mechanism; that is
    focus-zone.md.

Deliverables

  1. An owner decision record stating the approved scope for each primitive (public,
    package-private, or separate package), with the rationale.
  2. src/index.ts updated to match the decision: exports removed, renamed, or
    unchanged.
  3. src/index.test.ts updated so every symbol exported from src/index.ts is
    asserted, including Icon if it remains public.
  4. For any primitive that becomes package-private: public slot types in higher-order
    components updated to remove constructor references, using structural types or
    internal aliases instead.
  5. For any primitive that remains public: a contract document at the agreed depth
    stating behavioral invariants and stability expectations.
  6. src/primitives/AGENTS.md and src/AGENTS.md updated to state the approved
    scope and the process for adding a new primitive.
  7. readiness-model.md updated to reflect the decision in the
    primitives row and the "ceiling" note.
  8. component-test-strategy.md updated to close the
    open decision on whether primitives derive their coverage contract from authored
    specs or from types files.

Acceptance criteria

  • An owner decision record exists and is linked from this task.
  • src/index.ts contains exactly the exports approved by the decision.
  • Every symbol exported from src/index.ts is asserted in src/index.test.ts.
  • No higher-order component's public slot type leaks an unexported primitive
    constructor; typeof CheckboxIndicator, typeof CompoundItemLayout, etc. in
    slot types are either from a public export or replaced by a structural type.
  • Any primitive approved as public has a contract document at the agreed depth.
  • src/primitives/AGENTS.md states the approved scope and the addition process.
  • The readiness table in readiness-model.md reflects
    the decision.
  • The open spec-policy decision in
    component-test-strategy.md is closed.
  • yarn workspace @fluentui-react-native/components format, lint, build, and
    test pass after the changes.
  • The root TypeScript build passes after any export or slot-type change.

Dependencies and ordering

  • Soft-blocks readiness-model.md: primitives cannot be
    assigned a meaningful readiness level until their scope is decided; the readiness
    model can proceed for the 18 higher-order components independently.
  • Soft-blocks component-test-strategy.md: the
    coverage contract for primitives cannot be written until this task resolves what a
    primitive is obligated to document.
  • Independent of focus-zone.md: the FocusZone work does not
    depend on whether FocusVisual is public; it depends only on the primitive's
    behavioral contract, which does not change here.
  • Independent of spec-ingestion-and-generation.md:
    spec ingestion is a higher-order component concern; primitives have no upstream entry
    to ingest regardless of scope.
  • Independent of the missing-component wave tasks: wave 1 and wave 2 components
    will use the existing primitives as internal building blocks; the scope decision does
    not change that usage.

Risks and open decisions

  1. Slot type leakage under Option B. Resolved by retaining
    CheckboxIndicator and the other primitive constructors as public exports from the
    primitive subpath, so higher-order public slot declarations continue to reference
    public constructors.

  2. Icon source types as implicit public API. Resolved by exporting
    FontIconSource, IconElementProps, IconProps, and SvgIconSource with Icon
    from the primitive subpath.

  3. Separate package overhead. Resolved by using a package subpath rather than
    creating a separate published package.

  4. createFocusVisualProps is a function, not a component. It converts
    FocusVisualOptions into a pair of ring ViewStyle arrays. It is the most
    "internal-feeling" export among the five -- no component slot references it and it
    exists purely to share ring-geometry computation. Resolved by retaining it as the
    public primitive composition helper createFocusVisualProps_unstable.

Evidence and references

Source Reference
Primitive source packages/agentic/components/src/primitives
Package index packages/agentic/components/src/index.ts
Export guard test packages/agentic/components/src/index.test.ts
Primitive authoring rules packages/agentic/components/src/primitives/AGENTS.md
Source routing and boundaries packages/agentic/components/src/AGENTS.md
Component authoring invariants packages/agentic/components/src/components/AGENTS.md
Readiness model readiness-model.md
Coverage contract component-test-strategy.md
Upstream catalog (no primitives tier) x3-design/fluent-design component-map.yaml
Smoke-stories render set apps/storybook/scripts/smoke-stories.json
Package identity and exports field packages/agentic/components/package.json

Sources retrieved 2026-08-21.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions