Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/public-primitives-subpath.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fluentui-react-native/components": minor
---

Move public primitive exports from the package root to the `./primitives`
subpath and document their contracts.
3 changes: 2 additions & 1 deletion packages/agentic/components/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ invariants; detailed authoring recipes live in the
- Use `src/components/button` as the canonical higher-order implementation and `src/primitives/icon` as the canonical
primitive.
- Keep public props and slots small, typed, and spec-driven.
- Export components and public types explicitly from `src/index.ts`; never use wildcard exports.
- Export higher-order components and public types explicitly from `src/index.ts`; export primitives and their public types
explicitly from `src/primitives/index.ts`. Never use wildcard exports.
- Colocate runtime tests, type tests, and Storybook stories with the implementation.
- Use package scripts for format, lint, build, tests, and snapshots.
- Do not copy web-only APIs, CSS behavior, or DOM assumptions into React Native.
6 changes: 6 additions & 0 deletions packages/agentic/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
"react-native": "./src/index.ts",
"import": "./lib/index.js",
"default": "./src/index.ts"
},
"./primitives": {
"types": "./lib/primitives/index.d.ts",
"react-native": "./src/primitives/index.ts",
"import": "./lib/primitives/index.js",
"default": "./src/primitives/index.ts"
}
},
"scripts": {
Expand Down
7 changes: 5 additions & 2 deletions packages/agentic/components/src/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ the change crosses component boundaries.
- Generalizable non-styling hooks belong in `framework-base/src/hooks`.
- Styling helpers belong in `agentic/design/src/styling`.
- Component-library-specific non-public types, constants, and helpers belong in `src/common`.
- Primitives must remain unstyled and should only be extracted for repeated behavioral or structural contracts.
- Primitives are public from `@fluentui-react-native/components/primitives`, must remain unstyled, and require a colocated
`CONTRACT.md`. Extract one only for a repeated behavioral or structural contract that is useful to consumers; keep
package-private helpers in `src/common`.

## Optimization principles

- Audit dependency direction before introducing shared code.
- Look for repeated types, constants, routines, and subtrees across components.
- Validate extraction payoff before creating another layer of indirection.
- Preserve public component-qualified APIs and explicit exports.
- Preserve public component-qualified APIs and explicit exports. Higher-order components belong to the root entry point;
primitives belong to the `./primitives` entry point.
- Keep local component fixes local; do not widen a tiny edit into a whole-package audit unless repetition or extraction
is part of the work.
- Do not extract one-off logic, styling choices, or thin wrappers that are clearer in place.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';

import { attachSlotProps } from '@fluentui-react-native/framework-base';
import { createFocusVisualProps } from '../../primitives/focus-visual/focus-visual';
import { createFocusVisualProps_unstable } from '../../primitives/focus-visual/focus-visual';

import {
accordionStyles,
Expand Down Expand Up @@ -51,7 +51,7 @@ export function useAccordionStyles_unstable(state: AccordionState) {
];
const iconSize = getAccordionIconSize();

state.focusVisualProps = createFocusVisualProps({
state.focusVisualProps = createFocusVisualProps_unstable({
borderRadius: headerLayoutStyle.borderRadius,
innerColor: state.tokens.color.strokeFocusInner,
innerWidth: state.tokens.strokeWidth.thin,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';

import { attachSlotProps } from '@fluentui-react-native/framework-base';
import { createFocusVisualProps } from '../../primitives/focus-visual/focus-visual';
import { createFocusVisualProps_unstable } from '../../primitives/focus-visual/focus-visual';

import { buttonStyles, getButtonColorStyles, getButtonContentStyle, getButtonIconSize, getButtonRootStyle } from './button.styles';
import type { ButtonState } from './button.types';
Expand All @@ -18,7 +18,7 @@ export function useButtonStyles_unstable(state: ButtonState) {
const contentStyle: StyleProp<TextStyle> = [buttonStyles.content, getButtonContentStyle(state), colors.foreground];
const iconSize = getButtonIconSize(size);

state.focusVisualProps = createFocusVisualProps({
state.focusVisualProps = createFocusVisualProps_unstable({
borderRadius: rootLayoutStyle.borderRadius,
innerColor: state.tokens.color.strokeFocusInner,
innerWidth: state.tokens.strokeWidth.thin,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StyleProp, ViewStyle } from 'react-native';

import { attachSlotProps } from '@fluentui-react-native/framework-base';
import { createFocusVisualProps } from '../../primitives/focus-visual/focus-visual';
import { createFocusVisualProps_unstable } from '../../primitives/focus-visual/focus-visual';

import { cardStyles, getCardNestedBlockStyle, getCardOverlayStyle, getCardRootStyle, getCardSurfaceColors } from './card.styles';
import type { CardState } from './card.types';
Expand All @@ -14,7 +14,7 @@ export function useCardStyles_unstable(state: CardState) {
const rootStyle: StyleProp<ViewStyle> = [cardStyles.root, getCardRootStyle(state), colors, state.userStyle];
const overlayStyle = getCardOverlayStyle(state);

state.focusVisualProps = createFocusVisualProps({
state.focusVisualProps = createFocusVisualProps_unstable({
borderRadius: overlayStyle.borderRadius,
innerColor: state.tokens.color.strokeFocusInner,
innerWidth: state.tokens.strokeWidth.thin,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';

import { attachSlotProps } from '@fluentui-react-native/framework-base';
import { createFocusVisualProps } from '../../primitives/focus-visual/focus-visual';
import { createFocusVisualProps_unstable } from '../../primitives/focus-visual/focus-visual';

import {
checkboxStyles,
Expand Down Expand Up @@ -30,7 +30,7 @@ export function useCheckboxStyles_unstable(state: CheckboxState) {
const labelStyle: StyleProp<TextStyle> = [checkboxStyles.labelText, textThemeStyles.label, labelColors];
const secondaryTextStyle: StyleProp<TextStyle> = [checkboxStyles.secondaryText, textThemeStyles.secondaryText, secondaryTextColors];

state.focusVisualProps = createFocusVisualProps({
state.focusVisualProps = createFocusVisualProps_unstable({
borderRadius: state.tokens.borderRadius.base300,
innerColor: state.tokens.color.strokeFocusInner,
innerWidth: state.tokens.strokeWidth.thin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
import { attachSlotProps } from '@fluentui-react-native/framework-base';

import { hiddenFromAccessibilityProps } from '../../common/accessibility';
import { createFocusVisualProps } from '../../primitives/focus-visual/focus-visual';
import { createFocusVisualProps_unstable } from '../../primitives/focus-visual/focus-visual';
import {
getListItemBackgroundStyle,
getListItemContentStyle,
Expand Down Expand Up @@ -55,7 +55,7 @@ export function useListItemStyles_unstable(state: ListItemState) {
},
];

state.focusVisualProps = createFocusVisualProps({
state.focusVisualProps = createFocusVisualProps_unstable({
borderRadius: rootSizeStyle.borderRadius,
innerColor: state.tokens.color.strokeFocusInner,
innerWidth: state.tokens.strokeWidth.thin,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';

import { attachSlotProps } from '@fluentui-react-native/framework-base';
import { createFocusVisualProps } from '../../primitives/focus-visual/focus-visual';
import { createFocusVisualProps_unstable } from '../../primitives/focus-visual/focus-visual';

import {
getListboxItemAvatarSize,
Expand All @@ -20,7 +20,7 @@ import type { ListboxItemState } from './listbox-item.types';
export function useListboxItemStyles_unstable(state: ListboxItemState) {
const resolvedRootStyle = getListboxItemRootStyle(state);
const rootStyle: StyleProp<ViewStyle> = [listboxItemStyles.root, resolvedRootStyle, state.userStyle];
state.focusVisualProps = createFocusVisualProps({
state.focusVisualProps = createFocusVisualProps_unstable({
borderRadius: resolvedRootStyle.borderRadius,
innerColor: state.tokens.color.strokeFocusInner,
innerWidth: state.tokens.strokeWidth.thin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { StyleProp, ViewStyle } from 'react-native';
import { attachSlotProps } from '@fluentui-react-native/framework-base';

import { hiddenFromAccessibilityProps } from '../../common/accessibility';
import { createFocusVisualProps } from '../../primitives/focus-visual/focus-visual';
import { createFocusVisualProps_unstable } from '../../primitives/focus-visual/focus-visual';
import {
getMenuItemCheckboxStyle,
getMenuItemLeadingStyle,
Expand All @@ -21,7 +21,7 @@ export function useMenuItemStyles_unstable(state: MenuItemState) {
const rootLayoutStyle = getMenuItemRootLayoutStyle(state);
const rootStyle: StyleProp<ViewStyle> = [menuItemStyles.root, rootLayoutStyle, getMenuItemRootStyle(state), state.userStyle];

state.focusVisualProps = createFocusVisualProps({
state.focusVisualProps = createFocusVisualProps_unstable({
borderRadius: rootLayoutStyle.borderRadius,
innerColor: state.tokens.color.strokeFocusInner,
innerWidth: state.tokens.strokeWidth.thin,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { attachSlotProps } from '@fluentui-react-native/framework-base';
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
import { createFocusVisualProps } from '../../primitives/focus-visual/focus-visual';
import { createFocusVisualProps_unstable } from '../../primitives/focus-visual/focus-visual';

import {
radioStyles,
Expand Down Expand Up @@ -38,7 +38,7 @@ export function useRadioStyles_unstable(state: RadioState) {
getRadioSecondaryTextColorStyle(state),
];

state.focusVisualProps = createFocusVisualProps({
state.focusVisualProps = createFocusVisualProps_unstable({
borderRadius: rootLayoutStyle.borderRadius,
innerColor: state.tokens.color.strokeFocusInner,
innerWidth: state.tokens.strokeWidth.thin,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';

import { attachSlotProps } from '@fluentui-react-native/framework-base';
import { createFocusVisualProps } from '../../primitives/focus-visual/focus-visual';
import { createFocusVisualProps_unstable } from '../../primitives/focus-visual/focus-visual';

import {
getSwitchLabelStyle,
Expand Down Expand Up @@ -63,7 +63,7 @@ export function useSwitchStyles_unstable(state: SwitchState) {
];
const labelStyle: StyleProp<TextStyle> = [switchStyles.label, getSwitchLabelStyle(state)];

state.focusVisualProps = createFocusVisualProps({
state.focusVisualProps = createFocusVisualProps_unstable({
borderRadius: rootBaseStyle.borderRadius,
innerColor: state.tokens.color.strokeFocusInner,
innerWidth: state.tokens.strokeWidth.thin,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';

import { attachSlotProps } from '@fluentui-react-native/framework-base';
import { createFocusVisualProps } from '../../primitives/focus-visual/focus-visual';
import { createFocusVisualProps_unstable } from '../../primitives/focus-visual/focus-visual';

import { tabStyles, getTabColorStyles, getTabContentStyle, getTabIconSize, getTabRootStyle } from './tab.styles';
import type { TabState } from './tab.types';
Expand All @@ -17,7 +17,7 @@ export function useTabStyles_unstable(state: TabState) {
const hiddenContentStyle: StyleProp<TextStyle> = [tabStyles.content, getTabContentStyle(state, true), colors.foreground];
const iconSize = getTabIconSize();

state.focusVisualProps = createFocusVisualProps({
state.focusVisualProps = createFocusVisualProps_unstable({
borderRadius: rootLayoutStyle.borderRadius,
innerColor: state.tokens.color.strokeFocusInner,
innerWidth: state.tokens.strokeWidth.thin,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
import { attachSlotProps } from '@fluentui-react-native/framework-base';
import { createFocusVisualProps } from '../../primitives/focus-visual/focus-visual';
import { createFocusVisualProps_unstable } from '../../primitives/focus-visual/focus-visual';

import { tagStyles, getTagBackgroundStyle, getTagContentStyle, getTagForegroundStyle, getTagIconSize, getTagRootStyle } from './tag.styles';
import type { TagState } from './tag.types';
Expand All @@ -14,7 +14,7 @@ export function useTagStyles_unstable(state: TagState) {
const contentStyle: StyleProp<TextStyle> = [tagStyles.content, getTagContentStyle(state), foreground];
const iconSizes = getTagIconSize(size);

state.focusVisualProps = createFocusVisualProps({
state.focusVisualProps = createFocusVisualProps_unstable({
borderRadius: rootLayoutStyle.borderRadius,
innerColor: state.tokens.color.strokeFocusInner,
innerWidth: state.tokens.strokeWidth.thin,
Expand Down
149 changes: 78 additions & 71 deletions packages/agentic/components/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,82 @@
import * as components from './index';

const compositionHelpers = [
components.useAccordion_unstable,
components.useAccordionStyles_unstable,
components.renderAccordion_unstable,
components.useAvatar_unstable,
components.useAvatarStyles_unstable,
components.renderAvatar_unstable,
components.useBadge_unstable,
components.useBadgeStyles_unstable,
components.renderBadge_unstable,
components.useButton_unstable,
components.useButtonStyles_unstable,
components.renderButton_unstable,
components.useCard_unstable,
components.useCardStyles_unstable,
components.renderCard_unstable,
components.useCheckbox_unstable,
components.useCheckboxStyles_unstable,
components.renderCheckbox_unstable,
components.useDivider_unstable,
components.useDividerStyles_unstable,
components.renderDivider_unstable,
components.useInput_unstable,
components.useInputStyles_unstable,
components.renderInput_unstable,
components.useListItem_unstable,
components.useListItemStyles_unstable,
components.renderListItem_unstable,
components.useListboxItem_unstable,
components.useListboxItemStyles_unstable,
components.renderListboxItem_unstable,
components.useMenuItem_unstable,
components.useMenuItemStyles_unstable,
components.renderMenuItem_unstable,
components.useProgressBar_unstable,
components.useProgressBarStyles_unstable,
components.renderProgressBar_unstable,
components.useRadio_unstable,
components.useRadioStyles_unstable,
components.renderRadio_unstable,
components.useSkeleton_unstable,
components.useSkeletonStyles_unstable,
components.renderSkeleton_unstable,
components.useSpinner_unstable,
components.useSpinnerStyles_unstable,
components.renderSpinner_unstable,
components.useSwitch_unstable,
components.useSwitchStyles_unstable,
components.renderSwitch_unstable,
components.useTab_unstable,
components.useTabStyles_unstable,
components.renderTab_unstable,
components.useTag_unstable,
components.useTagStyles_unstable,
components.renderTag_unstable,
] as const;

describe('component composition exports', () => {
it('exports each state, style, and render helper', () => {
expect(compositionHelpers).toHaveLength(54);
compositionHelpers.forEach((helper) => {
expect(helper).toEqual(expect.any(Function));
});
});

it('exports shared primitives', () => {
expect(components.CheckboxIndicator).toEqual(expect.any(Function));
expect(components.CompoundItemLayout).toEqual(expect.any(Function));
expect(components.FocusVisual).toEqual(expect.any(Function));
expect(components.createFocusVisualProps).toEqual(expect.any(Function));
expect(components.LayoutStableText).toEqual(expect.any(Function));
describe('component exports', () => {
it('exports exactly the higher-order component runtime API', () => {
expect(Object.keys(components).sort()).toEqual(
[
'Accordion',
'Avatar',
'Badge',
'Button',
'Card',
'Checkbox',
'Divider',
'Input',
'ListItem',
'ListboxItem',
'MenuItem',
'ProgressBar',
'Radio',
'Skeleton',
'Spinner',
'Switch',
'Tab',
'Tag',
'renderAccordion_unstable',
'renderAvatar_unstable',
'renderBadge_unstable',
'renderButton_unstable',
'renderCard_unstable',
'renderCheckbox_unstable',
'renderDivider_unstable',
'renderInput_unstable',
'renderListItem_unstable',
'renderListboxItem_unstable',
'renderMenuItem_unstable',
'renderProgressBar_unstable',
'renderRadio_unstable',
'renderSkeleton_unstable',
'renderSpinner_unstable',
'renderSwitch_unstable',
'renderTab_unstable',
'renderTag_unstable',
'useAccordionStyles_unstable',
'useAccordion_unstable',
'useAvatarStyles_unstable',
'useAvatar_unstable',
'useBadgeStyles_unstable',
'useBadge_unstable',
'useButtonStyles_unstable',
'useButton_unstable',
'useCardStyles_unstable',
'useCard_unstable',
'useCheckboxStyles_unstable',
'useCheckbox_unstable',
'useDividerStyles_unstable',
'useDivider_unstable',
'useInputStyles_unstable',
'useInput_unstable',
'useListItemStyles_unstable',
'useListItem_unstable',
'useListboxItemStyles_unstable',
'useListboxItem_unstable',
'useMenuItemStyles_unstable',
'useMenuItem_unstable',
'useProgressBarStyles_unstable',
'useProgressBar_unstable',
'useRadioStyles_unstable',
'useRadio_unstable',
'useSkeletonStyles_unstable',
'useSkeleton_unstable',
'useSpinnerStyles_unstable',
'useSpinner_unstable',
'useSwitchStyles_unstable',
'useSwitch_unstable',
'useTabStyles_unstable',
'useTab_unstable',
'useTagStyles_unstable',
'useTag_unstable',
].sort(),
);
});
});
Loading
Loading