docs: add headless component authoring skill - #36502
docs: add headless component authoring skill#36502Dmytro Kirpa (dmytrokirpa) wants to merge 3 commits into
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 806b7b8e-562f-4b2a-b4f5-b9d06fc6131f
📊 Bundle size report✅ No changes found |
|
Pull request demo site: URL |
| 6. Record every inventory row as implemented, intentionally excluded with a | ||
| source-backed reason, or unresolved. Trace implemented rows through: | ||
| `types/exports -> hook/context -> rendered DOM/slots -> test or story`. | ||
| 7. Plan the change as two stacked PRs before editing: |
There was a problem hiding this comment.
if we need regression test for styled hook, then it should be 3 separate PRs, do you think we should have that covered by skill too?
There was a problem hiding this comment.
I think this should be a mandatory preflight
| - When adding a new v9 base hook, follow the repository-enforced signature: | ||
| `useComponentNameBase_unstable(props, ref?)`, with an explicitly typed | ||
| `props` parameter and `ref` typed as `React.Ref<...>`. | ||
| - Keep base hooks free of Tabster and other forbidden focus/keyboard runtimes. |
There was a problem hiding this comment.
should we mention that icons should not be part of base hooks & bundleIcon on module level?
There was a problem hiding this comment.
we have all of these as part of harness now, wondering if agent would recover when failing lint/verify-bundle task
There was a problem hiding this comment.
wondering if this would do the job most determinstically:
- Generate new control via `nx .....`
- Make sure `lint` targets and `verify-bundle-isolation` are passing
| State attributes are the supported CSS targeting contract. | ||
|
|
||
| - Presence state: emit `''` when true and omit when false. | ||
| - Boolean-valued state: emit `'true'` or `'false'` when both values are |
There was a problem hiding this comment.
should not it also following presence / absence approach?
There was a problem hiding this comment.
confused what is our pattern ? we agreed on absence api for boolean but I saw in Victor Genaev (@mainframev) we are doing true :D ?
| 6. Add a default story plus stories for important behavioral states. | ||
| 7. Exercise the primary interaction instead of rendering only static variants. | ||
| Interactive stories should drive the real component behavior and assert the | ||
| resulting role/ARIA/data state in a `play` function. Cover the inverse action |
There was a problem hiding this comment.
AFAIR we do not use play functions, that was probably migrated from the 1JS skill, right?
| --package @fluentui/react-<name> --type minor \ | ||
| --message "feat(react-<name>): expose headless base APIs" | ||
|
|
||
| # top branch, after editing only the headless library and stories |
There was a problem hiding this comment.
we should correct a bit these examples with beachball instructions as we following zero based semver for headless until we are stable
| `yarn beachball change` form; never create or edit them manually. | ||
| - After generation, inspect the new JSON file and verify it contains exactly the | ||
| owning package and no inherited package from another stack layer. | ||
| - New public base exports and new headless primitives are normally `minor` |
There was a problem hiding this comment.
here we should also correct it, it will be correct later when headless will become stable
|
|
||
| ### Renderer and Context | ||
|
|
||
| - Re-export or alias the upstream unstable renderer when its slot structure is |
There was a problem hiding this comment.
maybe some examples needed, I see that for CompoundButton it created it in a following format, instead of one line re-export:
import { renderCompoundButton_unstable } from '@fluentui/react-button';
/**
* Renders a CompoundButton component.
*/
export const renderCompoundButton = renderCompoundButton_unstable;| # top branch, after editing only the headless library and stories | ||
| yarn beachball change --no-commit --branch master \ | ||
| --package @fluentui/react-headless-components-preview --type minor \ | ||
| --message "feat(react-headless-components-preview): add <ComponentName>" |
There was a problem hiding this comment.
| --message "feat(react-headless-components-preview): add <ComponentName>" | |
| --message "feat: add <ComponentName>" |
I think we can omit the package scope in message as change file has packageName
| ### Component-Package PR | ||
|
|
||
| - Inspect `packages/react-components/react-<name>/library/bundle-size/`. | ||
| - Every new public runtime export must be referenced by a bundle fixture. Named |
There was a problem hiding this comment.
with the skill running for a component it created fixtures for base hooks, probably we should explicitly mention that bundle fixture should be added to components only
There was a problem hiding this comment.
Pull request overview
Adds guidance for authoring and extending headless Fluent UI v9 components.
Changes:
- Documents implementation, testing, packaging, and accessibility requirements.
- Defines stacked-PR and validation workflows.
- Registers the skill for Copilot and Claude.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
AGENTS.md |
Registers the new slash command. |
.agents/skills/headless-component/SKILL.md |
Defines the authoring workflow and requirements. |
.claude/skills/headless-component/SKILL.md |
Adds the Claude skill pointer. |
Suppressed comments (3)
.agents/skills/headless-component/SKILL.md:467
- Headless and v9 stories intentionally keep interaction assertions in tests rather than Storybook
playfunctions. Requiringplayhere would make generated stories violate that convention; keep the story interactive/demonstrable and put the role, ARIA, data-state, and inverse-action assertions in unit or Cypress coverage.
7. Exercise the primary interaction instead of rendering only static variants.
Interactive stories should drive the real component behavior and assert the
resulting role/ARIA/data state in a `play` function. Cover the inverse action
too; if the behavior requires a real browser primitive unavailable to the
Storybook test runner, cover it in Cypress and keep the story demonstrable.
.agents/skills/headless-component/SKILL.md:571
- This preview package is currently 0.2.4 and follows zero-semver: new headless components have shipped as patches (for example TagPicker and MenuButton in
library/CHANGELOG.md:12-28).minorwould incorrectly advance 0.2.x to 0.3.0 for a normal additive primitive.
yarn beachball change --no-commit --branch master \
--package @fluentui/react-headless-components-preview --type minor \
--message "feat(react-headless-components-preview): add <ComponentName>"
.agents/skills/headless-component/SKILL.md:591
- This rule incorrectly assigns
minorto new primitives in the 0.x headless preview package. Keepminorfor new exports in stable v9 component packages, but usepatchfor additive headless-preview APIs under its zero-semver policy; otherwise future skill runs will create the wrong release bump.
- New public base exports and new headless primitives are normally `minor`
changes. Use `patch` only when correcting an existing contract without adding
public API, and never use `major` without explicit approval.
| 7. Plan the change as two stacked PRs before editing: | ||
| - **bottom PR — component package:** base hook/types/renderer/context exports | ||
| and their package-local tests, API docs, bundle-size coverage, manifest | ||
| updates, and change file | ||
| - **top PR — headless package:** stable headless primitive, `data-*` |
| # bottom branch, after editing only the component package | ||
| yarn beachball change --no-commit --branch master \ | ||
| --package @fluentui/react-<name> --type minor \ | ||
| --message "feat(react-<name>): expose headless base APIs" |
| 6. Record every inventory row as implemented, intentionally excluded with a | ||
| source-backed reason, or unresolved. Trace implemented rows through: | ||
| `types/exports -> hook/context -> rendered DOM/slots -> test or story`. | ||
| 7. Plan the change as two stacked PRs before editing: |
There was a problem hiding this comment.
I think this should be a mandatory preflight
| Add `*.cy.tsx` integration coverage when behavior depends on real browser focus, | ||
| Tabster, positioning, portals, native popovers, or nested trigger composition. | ||
|
|
||
| ## Implementation |
There was a problem hiding this comment.
should we just wire a generator for this ?
| - When adding a new v9 base hook, follow the repository-enforced signature: | ||
| `useComponentNameBase_unstable(props, ref?)`, with an explicitly typed | ||
| `props` parameter and `ref` typed as `React.Ref<...>`. | ||
| - Keep base hooks free of Tabster and other forbidden focus/keyboard runtimes. |
There was a problem hiding this comment.
we have all of these as part of harness now, wondering if agent would recover when failing lint/verify-bundle task
| - When adding a new v9 base hook, follow the repository-enforced signature: | ||
| `useComponentNameBase_unstable(props, ref?)`, with an explicitly typed | ||
| `props` parameter and `ref` typed as `React.Ref<...>`. | ||
| - Keep base hooks free of Tabster and other forbidden focus/keyboard runtimes. |
There was a problem hiding this comment.
wondering if this would do the job most determinstically:
- Generate new control via `nx .....`
- Make sure `lint` targets and `verify-bundle-isolation` are passing
| State attributes are the supported CSS targeting contract. | ||
|
|
||
| - Presence state: emit `''` when true and omit when false. | ||
| - Boolean-valued state: emit `'true'` or `'false'` when both values are |
There was a problem hiding this comment.
confused what is our pattern ? we agreed on absence api for boolean but I saw in Victor Genaev (@mainframev) we are doing true :D ?
| # bottom branch, after editing only the component package | ||
| yarn beachball change --no-commit --branch master \ | ||
| --package @fluentui/react-<name> --type minor \ | ||
| --message "feat(react-<name>): expose headless base APIs" |
| | Skill | Command | Purpose | | ||
| | -------------------- | -------------------------- | ------------------------------------------------------------------------- | | ||
| | `v9-component` | `/v9-component Name` | Scaffold a new v9 component with all required files | | ||
| | `headless-component` | `/headless-component Name` | Author an unstyled v9 primitive with state attributes, tests, and stories | |
There was a problem hiding this comment.
if we have this we should consider to disable model invocation on the headless skill completely

Summary
gh stackworkflow separating component-package base APIs from the dependent headless packageValidation
gh stackand Beachball flags used by the workflowNo Beachball change file is required because this change does not modify a published package.