From 00f44627afa68acc921a7691ee2afa65664c23a5 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Thu, 11 Jun 2026 15:07:41 -0700 Subject: [PATCH 1/6] add initial plans to branch --- packages/agentic/PLAN.md | 46 ++++++++ packages/agentic/STAGING.md | 92 +++++++++++++++ packages/agentic/agentic-analyzer/PLAN.md | 77 +++++++++++++ packages/agentic/agentic-authoring/PLAN.md | 84 ++++++++++++++ packages/agentic/agentic-components/PLAN.md | 94 +++++++++++++++ packages/agentic/agentic-concepts/PLAN.md | 81 +++++++++++++ packages/agentic/agentic-tokens/PLAN.md | 121 ++++++++++++++++++++ 7 files changed, 595 insertions(+) create mode 100644 packages/agentic/PLAN.md create mode 100644 packages/agentic/STAGING.md create mode 100644 packages/agentic/agentic-analyzer/PLAN.md create mode 100644 packages/agentic/agentic-authoring/PLAN.md create mode 100644 packages/agentic/agentic-components/PLAN.md create mode 100644 packages/agentic/agentic-concepts/PLAN.md create mode 100644 packages/agentic/agentic-tokens/PLAN.md diff --git a/packages/agentic/PLAN.md b/packages/agentic/PLAN.md new file mode 100644 index 0000000000..d0f1ab9458 --- /dev/null +++ b/packages/agentic/PLAN.md @@ -0,0 +1,46 @@ +# Overview + +We are working on a plan to modernize furn and enable authoring fluent themed react-native components with agents. + +This work will result in the creation of new packages underneath this path. + +Do the following: + +1. Investigate the following topics with one dedicated sub-agent for each. +2. When they complete, look at the plans and see how they intersect. +3. Create a PLAN.md or README.md for each package in the new folder for that package then a STAGING.md file with an execution plan for this. + +## Gather v1 concepts + +- Create a concepts package that is used for common concepts, types, and instructions for understanding current components and building new components. +- Analyze the v1 components in the repo (those that use the packages in packages/framework), gather information about states, appearance, interactions, accessibility, and common token references. +- Create a plan to create pinning tests for the existing components to ensure that we don't break things as we refactor + +## Agentic authoring + +- Create a distributable agent and potentially MCP server to enable authoring new components using fluent themes +- Investigate the code in https://github.com/microsoft/fluentui referenced by AGENTS.md and the skills to mimic the new structure they are using to create a similar pattern for react-native. +- Read https://microsoft-my.sharepoint-df.com/:w:/p/popatudor/cQqZQwg00VNJRby4i2YVjIxJEgUCkrXkbKQophGWqlFA9ANc2Q for modern authoring concepts to apply those + +## New common styles and tokens + +- Instead of authoring the new controls directly against the theme, create a common tokens object with semantic slots. +- Look at https://github.com/x3-design/fluent-design/blob/main/plugins/tokens/skills/core/SKILL.md for latest thinking on how they are defined for web +- Pull together what components actually reference to map the old ideas to the new ones + +## Analysis package + +Create an analyzer package that can: + +- Create a custom theme with unique values per entry so that resolved styles or tokens can be mapped back to their semantic names. +- Create tests that use the newer @testing-library/react-native and can do things like extract accessibility trees, styles, snapshots, etc. +- Pin token values and style values in v1 components such that we can safely refactor. +- Have a strategy for multiplexing jest tests for each platform when needed. + +## New components + +Create a single component package that creates equivalent components to the v1 components, but authored in the new style + +- Don't use compose or customize +- Create a structure that more closely mirrors modern fluent ideas +- Have a flat structure where components don't depend on other components but instead reference common hooks, tokens, styles, helpers etc. (similar to fluent) diff --git a/packages/agentic/STAGING.md b/packages/agentic/STAGING.md new file mode 100644 index 0000000000..bea4bd0fbe --- /dev/null +++ b/packages/agentic/STAGING.md @@ -0,0 +1,92 @@ +# Agentic Modernization — Staging & Execution Plan + +This is the unified execution plan for the five new packages under `packages/agentic/`, derived from the per-package plans. Goal (from [`PLAN.md`](./PLAN.md)): modernize furn and enable authoring fluent-themed React Native components with agents. + +## The five packages + +| Package | Path | Role | +|---|---|---| +| [`agentic-concepts`](./agentic-concepts/PLAN.md) | `packages/agentic/agentic-concepts/` | **Leaf vocabulary** — concept types, component catalog, agent skill docs, the pinning **spec** | +| [`agentic-tokens`](./agentic-tokens/PLAN.md) | `packages/agentic/agentic-tokens/` | **Fluent Modern token layer** — x3-design primitives→generics; `createTokens(theme)`, `useTokens()`, derived interaction | +| [`agentic-analyzer`](./agentic-analyzer/PLAN.md) | `packages/agentic/agentic-analyzer/` | **Refactor safety net** — sentinel theme + reverse-map, RNTL harness, pinning, per-platform jest | +| [`agentic-components`](./agentic-components/PLAN.md) | `packages/agentic/agentic-components/` | **Fluent Headless + Modern components** — three forms (`X`/`useX`/`renderX`), no compose/customize | +| [`agentic-authoring`](./agentic-authoring/PLAN.md) | `packages/agentic/agentic-authoring/` | **Distributable agent + skills (+ optional MCP)** — sits on top of all four | + +✅ **Both previously-locked sources received & incorporated:** `Fluent Headless and Fluent Modern.docx` (repo root) defines the Headless→Modern strategy and the three-forms component model; the **x3-design token skills** (`~/dev/fluent-design/plugins/tokens/skills/{core,interaction,textstyle}`) define the primitives→generics token model now adopted by `agentic-tokens`. + +## How they intersect (the seams) + +- **State vocabulary** is defined once in `agentic-concepts`. In `agentic-tokens` interaction states (hover/pressed) are **derived from rest generics** (OKLCH deltas, precomputed for RN), *not* enumerated as suffixed slots — components declare `interaction.applies-to` per category. +- **Pinning** splits: `agentic-concepts` = the **spec** (per-component scenario matrix + assertion contract); `agentic-analyzer` = the **mechanism** (sentinel theme, RNTL, per-platform jest); `agentic-components` = runs the same matrix to prove **v1→new parity**. Because interaction is derived, analyzer pins catch both rest-token and delta-formula regressions. +- **Token migration** is a chain: `agentic-concepts` catalogs *current* usage → `agentic-tokens` maps it to the x3-design `--gnrc-*` generics → `agentic-analyzer`'s reverse-map *proves* resolved values are preserved → `agentic-components` consumes via `useTokens()`. +- **Authoring** ships `agentic-concepts`' skill docs (in the x3-design `SKILL.md` format), generates the three-forms Headless component + Modern token styling, uses `agentic-tokens`' generic vocabulary, and drives `agentic-analyzer` for pin-tests. + +## Dependency graph + +``` +agentic-concepts ──────────────┬───────────────┬──────────────┐ + (leaf: types/catalog/skills) │ │ │ + ▼ ▼ ▼ +agentic-tokens ──► agentic-analyzer ──► agentic-components ──► agentic-authoring + (semantic slots) (pin v1 + verify) (new flat comps) (skills/MCP; last) + └───────────────► (sentinel theme injection point) ◄──┘ +``` + +Key principle: **pin v1 behavior BEFORE refactoring anything.** The analyzer + concepts pinning spec must exist and baseline the current components first, so every later change is guarded. + +## Cross-cutting setup (applies to every package) + +- Add each new package's `tsconfig.json` to the **root `tsconfig.json` `references`** so it joins the unified `tsgo -b` build (per `AGENTS.md`). +- Dev deps (tsgo/jest/eslint/prettier) come automatically via `scripts/dynamic.extensions.mts`. +- **`@testing-library/react-native` is new to the repo** — introduced by `agentic-analyzer`; thread it through align-deps / dynamic extensions and add a catalog entry. +- **Platform forks:** keep `.ios/.android/.win32/.macos/.windows` splits; never co-load multiple RN forks in one program (per `AGENTS.md` Compatibility Notes). `globalTokens` is a static JSON (not theme-injectable) → analyzer sentinelizes it via jest module mocking. +- Each package needs a changeset on first publish-relevant change (`yarn change`). + +## Staged execution + +### Stage 0 — Scaffolding & contracts (all packages, parallel) +Create all five package folders with `package.json` + `tsconfig.json`, add to root `references`, confirm they join `tsgo -b` as empty packages. Lock the shared contracts: the **state vocabulary** (`agentic-concepts`), the `CommonTokens` **interface** (`agentic-tokens`, provisional), the pinning **assertion contract** (concepts ↔ analyzer seam), and the component **file-template** (`agentic-components` ↔ `agentic-authoring` golden template). + +### Stage 1 — Foundation: concepts + analyzer harness +- `agentic-concepts`: concept types, component catalog (7 families), skill docs, pinning spec. +- `agentic-analyzer`: sentinel theme + reverse-map (theme namespace), RNTL `renderWithTheme`, extraction helpers; then `globalTokens` sentinel via jest mock. +- **Milestone:** baseline-pin the existing v1 components (value + semantic snapshots) — the safety net is live *before* any refactor. + +### Stage 2 — Tokens (Fluent Modern layer) +- `agentic-tokens`: model `prmt-*` primitives + `--gnrc-*` generics (color `variant-role-modifier`, scalars, spacing, shadow, `textstyle-*` bundles) → `createTokens(theme)` producer (rest values, light/dark; HC/PlatformColor via `theme.host`) → JS port of the OKLCH **derived interaction** (deltas + lightness curve + inverse rule), precomputed for RN → `useTokens()` + the furn→generic crosswalk. +- Resolve the open decisions: primitives source-of-truth (port x3 vs project existing theme), interaction precompute mechanism, radius ramp (no x3 generic yet). +- **Milestone:** analyzer validates every generic resolves uniquely and every derived hover/pressed matches the algorithm; pin the v1 components' equivalent token references. + +### Stage 3 — New components (pattern-setters) +- `agentic-components`: build **Button** end-to-end as the three forms (`Button`/`useButton`/`renderButton` + `useButtonStyles`) + the shared `hooks/`/`tokens/`/`styles/`/`helpers/`; gate on analyzer parity with `ButtonV1`. Then **Text** + **Switch/Checkbox**. Integrate the real `agentic-tokens` (swap the stub; rest + derived interaction); re-run pinning to confirm zero value drift. +- **Milestone:** Button/Text/Switch reach pinned parity with their v1 counterparts; the three-forms template is locked. + +### Stage 4 — Long tail + authoring +- `agentic-components`: migrate Link, Checkbox, FAB, CompoundButton, ToggleButton, … each as the same file set + platform splits, gated by analyzer parity. +- `agentic-authoring`: ship `new-component`/`register-tester`/`changeset`/`lint-package` skills (in the x3-design `SKILL.md` format) + `.agents`+`.claude` transclusion stubs; then `token-lookup`/`pin-tests`/`e2e-scaffold`; then the optional MCP server; then distribution (`init`/`postinstall`, multi-runtime adapters). +- **Milestone:** an agent can scaffold a new flat component end-to-end, with pinning, from the skills/MCP. + +## Watch items (no longer blocked) + +- Per-platform pinning will surface **new** snapshots for previously-untested platforms (first-run churn). +- `agentic-components` `Text` flat-rule decision (render RN `Text` via shared `textStyles` vs a narrow exception) — recommend pure-flat. +- **RN has no runtime OKLCH** → interaction states are precomputed at theme-build time (RN = a "pre-compiled environment" per the x3-design interaction skill). + +## Open decisions for you + +Resolved: + +- ✅ **Token source-of-truth** — adopt the x3 _structure_, map _values_ from the furn themes + v1 component mappings (project-first). +- ✅ **Interaction precompute** — ship precomputed values from `agentic-tokens`; the OKLCH algorithm lives in `agentic-analyzer` (validates them; reused by the tokens build-time generator). +- ✅ **Radius** — furn-local radius ramp derived from current v1 component usage. + +Still open: + +1. **Build the analyzer + pin v1 first** (recommended — guards everything), or build new components in parallel and backfill pins? +2. **Component scope for v1:** the catalog covers 7 families first (Button, Checkbox, Switch, Radio/RadioGroup, Tab/TabList, Link, Text) — confirm that first slice and the Button/Text/Switch pattern-setter order. +3. **`agentic-tokens` object shape** — nested (`color.background.neutral.subtle`) + flat alias, or flat-only. +4. **`agentic-authoring` distribution target & runtimes** (Claude only, or also Copilot/Cursor) and whether the **MCP server** is in-scope now or later. + +## Provenance + +Authored from five parallel investigation agents (one per workstream), each grounded in the current repo (framework/composition, v1 components, theming, jest/test setup). Token + authoring specifics are now grounded in the received sources: `Fluent Headless and Fluent Modern.docx` (repo root) and the x3-design token skills (`~/dev/fluent-design/plugins/tokens/skills/`). diff --git a/packages/agentic/agentic-analyzer/PLAN.md b/packages/agentic/agentic-analyzer/PLAN.md new file mode 100644 index 0000000000..d92cb22810 --- /dev/null +++ b/packages/agentic/agentic-analyzer/PLAN.md @@ -0,0 +1,77 @@ +# @fluentui-react-native/agentic-analyzer + +> Status: **Plan** (package not yet created). Part of the `packages/agentic/` modernization — see [`../STAGING.md`](../STAGING.md). + +## Purpose + +A test/analysis toolkit that lets us **safely refactor** v1 components by: + +1. building a **sentinel theme** whose every leaf value is unique, +2. rendering components with **`@testing-library/react-native`** to capture accessibility trees, computed styles, and snapshots, and +3. **reverse-mapping** resolved style values back to the exact theme/token slot they came from — so a refactor that changes *which* token feeds a style is caught even when the final pixel value is unchanged. + +Plus a strategy for **multiplexing jest per platform**. + +## Identity + +- **npm:** `@fluentui-react-native/agentic-analyzer` +- **path:** `packages/agentic/agentic-analyzer/` +- Introduces the repo's **first** dependency on `@testing-library/react-native` (must thread through `scripts/dynamic.extensions.mts` / align-deps). Add to root `tsconfig.json` `references`. + +## Findings that shape it + +- **Theme is injectable; values pass through untransformed.** `Theme` (theme-types) is delivered via context; `themedStyleSheet`/`buildUseTokens` cache **by theme object identity** and do no value parsing — so swapping a sentinel theme cleanly busts caches and unique values survive into `StyleSheet.create`. Sentinel feasibility is HIGH for everything reachable through the theme. +- **`globalTokens` is NOT injectable.** It's a static JSON import (`theme-tokens/src/tokens-global.ts` → `design-tokens-windows/.../tokens-global.json`). Sentinelizing it requires **jest module mocking**, not theme injection — the reverse map must span both the `theme.*` and `globalTokens.*` namespaces. +- **Current tests:** `react-test-renderer` + `toMatchSnapshot` (`Checkbox.test.tsx` etc.); `test-tools` exports `validateHookValueNotChanged` + `mockTheme`. No `@testing-library/react-native` anywhere yet. +- **Platform is fixed per package, one platform per jest run.** `scripts/configs/jest/jest.config.cjs` reads `projectManifest.furn.jestPlatform` (default `ios`) and calls `@rnx-kit/jest-preset(platform, …)`; `scripts/src/tasks/jest.ts` runs jest once. **No multiplexing** — each component is currently tested on exactly one platform. + +## Proposed capabilities + +1. **Sentinel theme + reverse map** — `createSentinelTheme(base?)` clones a real `Theme`, replacing every leaf with a unique type-valid sentinel (colors → reserved unique hex; spacing → unique `'NNNpx'`; numeric sizes/shadows → reserved integers; strings → reserved-but-valid pool), emitting a `SentinelMap: value → "colors.buttonBackground" | "spacing.m" | …`. `createSentinelGlobalTokens()` + a jest mock factory for `@fluentui-react-native/theme-tokens` covers the static namespace. `resolveStyleToSemantic(style)` walks a computed RN style and substitutes sentinels with semantic names. +2. **testing-library helpers** — `renderWithTheme(el, {theme?, platform?})`, `getAccessibilityTree(result)`, `getComputedStyles(result, query)`, `snapshotSemantic(result)` (snapshot with sentinel→semantic substitution; stable across pixel changes, sensitive to slot-source changes). +3. **Pinning** — `pinComponent(Component, scenarios)` producing **dual** snapshots: a *value* snapshot (catches visual regressions) + a *semantic* snapshot (catches silent token-source swaps). Drives the v1 state matrix (hovered/pressed/disabled/checked via `.customize`/state layers). +4. **Per-platform jest multiplexing** — `makeJestConfig(platform)` that takes the platform from `FURN_JEST_PLATFORM` (falling back to `furn.jestPlatform`), and a `multiplex` runner that, for a `furn.jestPlatforms: [...]` array, spawns one jest process per platform with platform-suffixed snapshot dirs (preserving the one-platform-per-process model the preset requires — and respecting the AGENTS.md "no multiple RN forks in one program" rule). +5. **OKLCH interaction algorithm (home + validator).** A JS port of the x3-design hover/pressed derivation — `--lightness-{hover,press}`/`--alpha-{hover,press}` deltas, the lightness curve `1 + clamp(0, (0.40 - L)/0.20, 1)`, and the `loud`/`heavy`/`onloud` inverse rule (per the `tokens-interaction` skill). This is the **single home** of the algorithm. Two consumers: (a) `agentic-analyzer` uses it to **validate** that `agentic-tokens`' shipped precomputed hover/pressed values match what the algorithm produces from the rest generics — a regression guard on both rest tokens and the formula; (b) `agentic-tokens`' build-time generator imports it (dev dependency only) to *produce* those precomputed values, so the runtime token output stays static and analyzer-free. Pin tests assert solid/inverse/transparent cases against the worked examples in the skill. + +## Proposed structure + +``` +agentic-analyzer/ + package.json tsconfig.json README.md PLAN.md + src/ + index.ts + sentinel/ createSentinelTheme.ts sentinelGlobalTokens.ts reverseMap.ts allocator.ts + render/ renderWithTheme.tsx accessibility.ts styles.ts snapshot.ts + pinning/ pinComponent.ts states.ts + jest/ makeJestConfig.cjs multiplex.ts + interaction/ oklch.ts deltas.ts derive.ts validate.ts # OKLCH hover/pressed algorithm (home + validator) +``` + +> The `interaction/` module is importable on its own (no test deps) so `agentic-tokens`' build-time generator can reuse it to precompute values, while `agentic-tokens`' runtime output carries no dependency on `agentic-analyzer`. + +## Dependencies & intersections + +- **agentic-concepts:** concepts owns *what to pin* (component catalog + scenario matrix + assertion contract); analyzer owns the *machinery* those specs call. +- **agentic-tokens:** analyzer's reverse map is the bridge proving a refactor from old refs → x3-design `--gnrc-*` generics preserves resolved values. `createTokens` is the sentinel injection point. Because hover/pressed are **derived** (OKLCH deltas, precomputed for RN), the analyzer must also assert each derived interaction value matches the algorithm — so pins catch both rest-token and delta-formula regressions. +- **agentic-components:** provides the parity gate — new components must reproduce v1's pinned a11y tree / styles / snapshots. +- Internal: reuse `Theme`/`useTheme` (theme-types), `ThemeProvider` (theme); may supersede `test-tools`' `mockTheme` as the sentinel base. + +## Open questions + +- `globalTokens` mocking: mock the `theme-tokens` package or the underlying `design-tokens-windows` JSON? +- Reserved-but-valid value pools for non-arbitrary leaves (font families/weights); avoid colliding color hexes. +- First multiplex run surfaces **new** snapshots for previously-untested platforms → expect churn / platform-specific failures unrelated to refactors. +- Platform-suffixed snapshot layout to avoid cross-platform `.snap` clobbering. +- RNTL output differs from `react-test-renderer` — pin in RNTL's format from the start, don't chase legacy snapshot parity. +- Confirm `@rnx-kit/jest-preset` honors an env-driven platform and that its `transformIgnorePatterns`/`transform` overrides still compose when wrapped. + +## Phased plan + +1. **Scaffold + deps:** create package, add `@testing-library/react-native`, get a trivial `renderWithTheme` passing for one component (Button, ios) under the existing preset. +2. **Sentinel theme + reverse map** over the theme namespace; prove `resolveStyleToSemantic` round-trips on a real component. +2b. **OKLCH interaction module** (`interaction/`) — port the deltas + lightness curve + inverse rule; unit-test against the skill's worked examples. Land this early: `agentic-tokens`' Stage-2 build depends on it to precompute hover/pressed, and the analyzer uses it to validate them. +3. **globalTokens sentinel + jest mock;** extend reverse map to `globalTokens.*`. +4. **Extraction + semantic snapshots** (`getAccessibilityTree`/`getComputedStyles`/`snapshotSemantic`). +5. **Pinning API + state matrix;** hand off to `agentic-concepts` to author per-component specs. +6. **Per-platform multiplex** (`makeJestConfig.cjs` + `multiplex` + suffixed snapshot dirs + `furn.jestPlatforms`). +7. **Harden + baseline:** align-deps/depcheck/dynamic-extensions integration; run a full multiplexed sweep to establish v1 baseline pins. diff --git a/packages/agentic/agentic-authoring/PLAN.md b/packages/agentic/agentic-authoring/PLAN.md new file mode 100644 index 0000000000..9158983c57 --- /dev/null +++ b/packages/agentic/agentic-authoring/PLAN.md @@ -0,0 +1,84 @@ +# @fluentui-react-native/agentic-authoring + +> Status: **Plan** (package not yet created). Part of the `packages/agentic/` modernization — see [`../STAGING.md`](../STAGING.md). +> +> ✅ **Sources received & incorporated.** The "modern authoring concepts" come from **`Fluent Headless and Fluent Modern.docx`** (repo root) and the skill format/structure from the **x3-design token skills** (`~/dev/fluent-design/plugins/tokens/skills/{core,interaction,textstyle}/SKILL.md`), alongside the public **microsoft/fluentui `AGENTS.md` + skills** pattern. + +## Purpose + +A distributable bundle of agent **skills**, **instructions**, and an optional **MCP server** that lets a coding agent (Claude Code, Copilot, Cursor, …) author new fluent-themed React Native components in the new **Fluent Headless → Fluent Modern** style — consuming `agentic-concepts`, `agentic-tokens`, `agentic-analyzer`, and `agentic-components`. It is the "router + verb-decomposed skills" layer modeled on how microsoft/fluentui (web) and the x3-design token plugin structure their agent guidance. + +**What it authors:** the three-forms headless component (`X` + `useX` + `renderX`) plus the Modern styling layer (`useXStyles` consuming `agentic-tokens` generics) — behavior/ARIA separated from the swappable styled layer, per the Fluent Headless thesis in the docx. + +## Identity + +- **npm:** `@fluentui-react-native/agentic-authoring` +- **path:** `packages/agentic/agentic-authoring/` +- Ships `skills/` + an `AGENTS.md` router + an optional `mcp/` server (`bin`). Add to root `tsconfig.json` `references`. + +## Findings that shape it + +- **microsoft/fluentui (web) pattern (verified):** a terse **router `AGENTS.md`** (critical rules → one golden template → anti-patterns → link tables → a Skills table → package layout) plus **verb-decomposed skills** canonical at `.agents/skills//SKILL.md`, **mirrored** at `.claude/skills//SKILL.md` as a one-line transclusion (`@../../../.agents/skills//SKILL.md`). `SKILL.md` = YAML frontmatter (`name`, `description`, `disable-model-invocation?`, `argument-hint`, `allowed-tools`) + imperative `## Steps` (numbered, fenced commands) + local `## Rules`/`## Anti-patterns`. Bulky knowledge offloaded to a `references/` subdir ("fat skill"). +- **Fluent Headless / Fluent Modern doc (received):** defines the strategy the authoring agent implements — ship behavior as stable unstyled primitives in **three forms** (`X` / `useX` / `renderX`), keep the styled (Modern/token) layer a swappable concern on top. The agent's output must follow this split. +- **x3-design token skills (received):** the `SKILL.md` format to mirror — YAML frontmatter (`name`, `description`, `argument-hint`) + a `| Field | Value |` table (Type/Category/Related) + a "Files in this skill" table + reference `*.yaml`. The token plugin's three skills (`core` primitives/generics, `interaction` hover/pressed derivation, `textstyle` bundles) are the canonical vocabulary the `token-lookup` skill resolves against. +- **This repo:** has a root `AGENTS.md` + `CLAUDE.md`; `apps/component-generator` is a **Gulp string-replacement scaffolder** emitting the **old compose/tokens** shape — i.e. it generates the *legacy anti-pattern*; the authoring skill must target the new flat three-forms `agentic-components` shape, not wrap the legacy generator. No `SKILL.md`/`.agents/` exist yet. + +## Proposed structure (PROVISIONAL — pending sources) + +``` +agentic-authoring/ + package.json # name, files[], bin (mcp), exports + AGENTS.md # package-scoped router (component-authoring focused) + skills/ # CANONICAL skills (shipped in the package) + new-component/SKILL.md # scaffold a new-style fluent RN component (the flat shape) + new-component/references/ # file-layout.md, platform-matrix.md, golden-template.md + token-lookup/SKILL.md # resolve semantic slot -> agentic-tokens value + pin-tests/SKILL.md # author analyzer-based pinning/snapshot tests + register-tester/SKILL.md # add FluentTester page + testPages. + e2e-scaffold/SKILL.md # PageObject + Spec + consts + changeset/SKILL.md lint-package/SKILL.md + instructions/ concepts.md authoring-rules.md # critical rules; compose/customize = anti-pattern + mcp/ server.ts # optional: list_tokens, resolve_token, scaffold_component, + # list_v1_components, get_component_spec, run_pin_tests +``` + +Plus repo-root discovery stubs (one-line transclusions — no duplication, exactly fluentui's pattern): + +``` +.agents/skills//SKILL.md -> @../../../packages/agentic/agentic-authoring/skills//SKILL.md +.claude/skills//SKILL.md -> @../../../packages/agentic/agentic-authoring/skills//SKILL.md +``` + +**MCP server: optional, later.** Skills alone (markdown + the agent's native file/bash tools) cover most authoring. Add the MCP server once `agentic-tokens`/`agentic-analyzer` exist, where deterministic repo-aware ops beat free-form tool use (`resolve_token`, `scaffold_component`, `get_component_spec`, `run_pin_tests`). Skills should *prefer* MCP tools when present, falling back to Bash/Read. + +## How the agent authors a component (`/new-component Badge`) + +1. **Load concepts** — read `agentic-concepts` (states/appearance/interactions/a11y/token refs) + the matching v1 spec. +2. **Map tokens** — translate v1 theme/token refs to `agentic-tokens` semantic slots (`token-lookup` / MCP `resolve_token`). +3. **Scaffold (Headless three-forms + Modern styling)** — emit the `agentic-components` file set: `useX` (behavior/ARIA) → `renderX` → `useXStyles` (tokens) → `X`, **no compose/customize, no inter-component deps**; declare `interaction.applies-to` in the token map; use `references/golden-template.md` (kept in sync with `agentic-components`). +4. **Register** — `register-tester` adds the FluentTester page + `testPages.`; add the new tsconfig to root `references`. +5. **Pin & test** — `pin-tests` uses `agentic-analyzer` (sentinel theme + RNTL) to snapshot resolved styles/tokens, a11y tree, multiplexed per platform. +6. **e2e-scaffold;** then `yarn change`, `yarn lint`, `yarn build`. + +## Dependencies & intersections + +- **agentic-concepts** — knowledge source (skills payload + v1→new mapping); `get_component_spec`/`list_v1_components` read it. Hard dep. +- **agentic-tokens** — target vocabulary; `token-lookup`/`resolve_token` resolve slots. Hard dep. +- **agentic-analyzer** — verification engine behind `pin-tests`/`run_pin_tests`. +- **agentic-components** — output target; `golden-template.md` mirrors its conventions (single source of truth for the shape). +- Supersedes `apps/component-generator` for new-style output (mark the Gulp/compose template a legacy anti-pattern so the agent doesn't regenerate the old style). + +## Open questions + +- **Sequencing:** thin without the other four — build with stubs/contracts early, fill as they land (it's the *last* useful workstream). +- **MCP vs skills duplication:** keep logic in one place (MCP server or shared lib); skills call it to avoid drift. +- **"Distributable" target:** shipping `skills/` + `AGENTS.md` in npm is easy; discoverability in a *consumer* repo needs an install step — `npx … init` or `postinstall` to copy/transclude into the consumer's `.claude`/`.agents`. +- **Multi-runtime:** Claude (`.claude/skills`) covered by transclusion; Copilot (`.github/instructions`) / Cursor (`.cursor/rules`) need generated adapters if in scope. + +## Phased plan + +0. **Contracts (parallel):** `package.json`, the `SKILL.md` contract, a stub `AGENTS.md` router, `authoring-rules.md` (compose/customize = anti-pattern), `golden-template.md` from the agreed `agentic-components` shape. No runtime deps. +1. **Skills (after concepts + components):** `new-component`, `register-tester`, `changeset`, `lint-package` as markdown skills using native Read/Write/Bash; add root `.agents/`+`.claude/` transclusion stubs. +2. **Token + test skills (after tokens + analyzer):** `token-lookup`, `pin-tests`, `e2e-scaffold`. +3. **Optional MCP server** sharing logic with the skills; add `bin`. +4. **Distribution:** `init`/`postinstall`, optional Copilot/Cursor adapters, README; root `references` + changeset. diff --git a/packages/agentic/agentic-components/PLAN.md b/packages/agentic/agentic-components/PLAN.md new file mode 100644 index 0000000000..bc50f85cad --- /dev/null +++ b/packages/agentic/agentic-components/PLAN.md @@ -0,0 +1,94 @@ +# @fluentui-react-native/agentic-components + +> Status: **Plan** (package not yet created). Part of the `packages/agentic/` modernization — see [`../STAGING.md`](../STAGING.md). +> +> ✅ Informed by **Fluent Headless / Fluent Modern** (`Fluent Headless and Fluent Modern.docx`, repo root) and the **x3-design token model** (via `agentic-tokens`). + +## Purpose + +A flat component library that recreates the v1 components in the modern Fluent structure, applying the **Fluent Headless → Fluent Modern** strategy to React Native: + +- **Headless layer** — stable, public, **unstyled** primitives that own behavior, ARIA/accessibility, keyboard handling, and semantic structure. **No pixels, no design props.** Each component ships **three forms** (matching the docx): the primitive component `X`, the stable hook `useX`, and the render function `renderX`. +- **Modern layer** — the headless primitive **plus tokens** (`agentic-tokens`), native-first and lightweight. Styling is a *swappable concern* layered on top of the same headless behavior. + +It deliberately avoids `compose`/`customize`/`buildProps`/`stylingSettings`. Components depend only on shared sibling dirs (`hooks/`, `tokens/`, `styles/`, `helpers/`) and `agentic-tokens` — **never on each other**. + +## Identity + +- **npm:** `@fluentui-react-native/agentic-components` +- **path:** `packages/agentic/agentic-components/` +- Add to root `tsconfig.json` `references`. Keep `.ios/.android/.win32/.macos/.windows` splits; never co-load multiple RN forks in one program (per `AGENTS.md`). + +## The pattern (Headless three-forms + Modern styling; NO compose/customize) + +For each component `X`, mirroring the headless `Button`/`useButton`/`renderButton` triple and v9's hook order: + +1. **`X.types.ts`** — `XProps` (RN-prop-extended + variant props) and `XState` (`ComponentState` analog: defaulted variants + computed flags + resolved `slots`/`rootProps` + interaction state). +2. **`useX(props, ref): XState`** — **HEADLESS behavior.** Normalizes props/defaults, runs shared behavior hooks (`usePressable`, `useControllableState`, focus), assembles accessibility props and semantic slots. **No styling, no token reads.** (Replaces v1 `useX.ts` + the `lookup` function — lookup logic becomes plain boolean flags on state.) +3. **`renderX(state): JSX` ** — **HEADLESS render.** Pure function over resolved slots; plain RN JSX (no slot interception / no `@jsxImportSource` pragma). +4. **`useXStyles(state)`** — **MODERN styling.** Reads `useTokens()` + the component-local `tokens/xTokens.ts` map (which declares `interaction.applies-to`), resolves generic tokens (incl. **derived** hover/pressed for the active rest token), and writes RN style objects onto `state..style`. Variant/state selection is plain `state.flag ? a : b`. +5. **`X.tsx`** — the **Modern primitive**: `forwardRef` shell calling `useX → useXStyles → renderX`. Also re-export the **unstyled** form (`useX` + `renderX`) for headless consumers. User `style`/props merged **last** via `helpers/mergeStyles`. +6. **`index.ts`** — barrel exporting all three forms + types. + +Because behavior (headless) is separated from styling (modern), the styled layer is swappable and the accessibility/keyboard/semantics stay identical regardless of styling — the core Fluent Headless thesis, applied to RN. + +Explicitly **not used:** `compose`, `.customize`, `.compose`, `buildProps`, `stylingSettings`, `useSlots`, `applyTokenLayers`, the `framework-base` jsx-runtime. Customization happens through the theme/tokens + ordinary props. + +## Proposed structure (flat) + +``` +agentic-components/ + package.json tsconfig.json babel.config.js jest.config.cjs eslint.config.js + src/ + index.ts # barrel: every component's X / useX / renderX + types + hooks/ usePressable.ts useFocusState.ts useControllableState.ts useTokens.ts index.ts + tokens/ buttonTokens.ts … # per-component map: variant×state -> generic names + interaction.applies-to + styles/ borderStyles.ts layoutStyles.ts textStyles.ts focusStyles.ts index.ts + helpers/ mergeStyles.ts mergeProps.ts getNativeProps.ts index.ts + Button/ Button.tsx useButton.ts renderButton.tsx useButtonStyles.ts Button.types.ts index.ts + Text/ Switch/ Checkbox/ Link/ … +``` + +`hooks/usePressable` wraps `@fluentui-react-native/interactive-hooks` behind one seam; `styles/*` are the RN analog of the old `@fluentui-react-native/tokens` mixins; `tokens/xTokens.ts` maps variants/states → `agentic-tokens` **generic names** and declares `interaction.applies-to: [...]` so the styles hook resolves derived hover/pressed for the active rest token. + +## Web/Headless → RN bridges + +| Fluent web | RN replacement (in shared dirs) | +|---|---| +| Griffel `makeStyles`/`mergeClasses` | RN style objects + `helpers/mergeStyles` (user style wins last) | +| `--gnrc-*` CSS vars + OKLCH runtime interaction | `useTokens()` generic object + **precomputed** interaction from `agentic-tokens` (RN has no runtime OKLCH) | +| `react-aria`/`react-tabster`, focusgroup roving tabindex | RN `accessibility*` props + `interactive-hooks` (+ `FocusZone`) behind `hooks/` | +| native `popover`/``/anchor positioning | RN equivalents / existing furn Callout/Popover primitives (headless behavior only) | +| `slot.always` + jsx-runtime | render RN primitives directly; optional `helpers/getNativeProps` for prop filtering | + +## First components (pattern-setters) + +1. **Button** — full variant/icon/pressable/focus stack; locks the three-forms file template. +2. **Text** — minimal (no interaction/slots); validates `styles/textStyles` against the `textstyle-*` bundles; scales the pattern down. +3. **Switch** (or Checkbox) — controlled/uncontrolled + toggle state (multi-state interaction: hover/press derive from the **active** rest token) + `Animated.Value`; proves the hardest model. + +## Dependencies & intersections + +- **agentic-tokens (Modern layer)** — `styles/`/`tokens/` consume its generic surface via `useTokens` and declare `interaction.applies-to`. Code against an interface + local stub so the swap-in is mechanical. +- **agentic-concepts** — supplies the per-component spec (states/appearance/interactions/a11y/token refs); contribute the three-forms template back as the canonical authoring recipe. +- **agentic-analyzer** — the parity gate: each new component must reproduce its v1 counterpart's resolved styles (incl. derived interaction values), a11y tree, and snapshots. +- **agentic-authoring** — the scaffolding agent emits exactly this three-forms file set (its "golden template"). +- Allowed leaf deps: `@fluentui-react-native/icon`, `interactive-hooks` (wrapped), `theme`. + +## Open questions + +- **`Text` flat-rule:** Button should render RN `Text` styled by shared `textStyles` (pure-flat) rather than import a sibling `Text` component — requires perfect typography parity from the `textstyle-*` bundles. +- **Headless vs Modern packaging:** one package exporting both unstyled (`useX`/`renderX`) and styled (`X`) forms, or split headless into its own entry/subpath later? (Recommend one package, two entry points: `.../headless` and the default Modern.) +- **Derived interaction parity:** the styles hook must request the right hover/pressed for the *active* rest token in multi-state components; confirm the analyzer pins these. +- Platform forks: keep per-platform files (`useButtonStyles.win32.ts`, ripple, two-tone focus, Win32 keyboard quirks). +- Animation parity (Switch `Animated.Value`) → `hooks/useSwitchAnimation`; confirm analyzer can pin animated styles. +- Memoization: replace v1 `buildProps`/`getMemoCache` with `useMemo` keyed on theme + state flags in `useXStyles`. + +## Phased plan + +0. **Scaffold & contracts:** create package, root `references`, empty `hooks/tokens/styles/helpers` with the `useTokens`/generic interface **stubbed**; publish the three-forms authoring-template doc (to concepts/authoring). +1. **Button exemplar** (X + useButton + renderButton + useButtonStyles) + shared dirs; wire analyzer pinning to assert parity with v1 `ButtonV1`; lock the template once green. +2. **Text + Switch/Checkbox;** extend shared dirs only as forced; pin against v1. +3. **agentic-tokens integration:** replace the stub with the real generic surface (rest + derived interaction); re-run pinning to confirm zero value drift. +4. **Long tail** (Link, Checkbox, FAB, CompoundButton, ToggleButton, …), each the same three-forms set + platform splits, gated by analyzer parity. +5. **Surface & docs:** finalize barrel, SPEC docs, register fluent-tester pages; hand the template to `agentic-authoring`. diff --git a/packages/agentic/agentic-concepts/PLAN.md b/packages/agentic/agentic-concepts/PLAN.md new file mode 100644 index 0000000000..11275601d5 --- /dev/null +++ b/packages/agentic/agentic-concepts/PLAN.md @@ -0,0 +1,81 @@ +# @fluentui-react-native/agentic-concepts + +> Status: **Plan** (package not yet created). Part of the `packages/agentic/` modernization — see [`../STAGING.md`](../STAGING.md). + +## Purpose + +The shared **vocabulary** of furn: a small, framework-agnostic set of TypeScript concept types, a machine-readable component **catalog**, and agent-facing **skill docs** that teach a human or an agent how to read the existing v1 (composition-framework) components and author new ones with consistent states, appearance, interactions, accessibility, and token usage. + +This is the dependency-light **leaf** package the other agentic workstreams build on. It owns *concept definitions, the component inventory, and prose*. It does **not** own runtime styling, the new token object (→ `agentic-tokens`), or the test harness (→ `agentic-analyzer`). + +## Identity + +- **npm:** `@fluentui-react-native/agentic-concepts` +- **path:** `packages/agentic/agentic-concepts/` +- **kit type:** types + docs (no runtime UI). Must be added to the root `tsconfig.json` `references`. + +## Findings that shape it + +Every v1 component follows one regular shape (`compose({ ...stylingSettings, slots, useRender })`) reducible to a small concept set: + +- **States** = interaction (`hovered|pressed|focused`, from `usePressableState`) × semantic (`disabled|checked|toggled|selected|required|visited`). These become *nested token layers* selected by a `lookup` predicate and `applyTokenLayers`. +- **Appearance** = `appearance` × `size` × `shape` × `labelPosition` (per-family enums; defaults are platform-specific). +- **Interactions** = press (`useOnPressWithFocus`), keyboard (`useKeyProps` → macOS `validKeys*` vs Win32/Windows `keyDownEvents`), toggle/selection (`useAsToggleWithEvent`, `useSelectedKey`), focus (`useViewCommandFocus`, `FocusZone`). +- **Accessibility** = roles (`button`/`checkbox`/`switch`/`radio`/`tab`/`link`), `getAccessibilityState`, label-from-first-string-child, `accessibilityActions`, pos/setSize. +- **Common token references** = color families (neutral/brand/compound/ghost/default/status foreground·background·stroke·icon·focus), typography variants (legacy `bodyStandard` **and** modern `body1` coexist), `globalTokens` ramps (size/corner/stroke/font), and the `borderStyles`/`layoutStyles`/`fontStyles` mixins. +- Current tests use bare `react-test-renderer` snapshots; there is **no** sentinel theme and `@testing-library/react-native` is not yet a dependency. + +## Proposed structure + +``` +agentic-concepts/ + package.json tsconfig.json README.md PLAN.md + src/ + index.ts # re-exports all concept types + states.ts # VisualState union; InteractionState / SemanticState (mirrors IPressableState) + appearance.ts # Appearance / ControlSize / ControlShape / LabelPosition + per-family aliases + interactions.ts # InteractionConcept (press/toggle/select/keyboard/focus) + accessibility.ts # A11yConcept (role/states/labelSource/actions/setSemantics) + tokens.ts # TokenReference catalog: maps current furn token usage -> x3-design --gnrc-* generics (background/foreground/stroke/surface roles+modifiers, scalars, textstyle bundles) + component-shape.ts # ConceptualComponent descriptor (the component "anatomy" type) + pinning.ts # PINNING-TEST SPEC: per-component scenario matrix + assertion contract (typed data) + catalog/ + components.json # machine-readable inventory: per component -> concepts/states/tokens it uses + skills/ # natural-language, agent-consumable (SKILL.md format aligned with agentic-authoring) + SKILL.md understanding-v1.md states.md appearance.md interactions.md + accessibility.md tokens.md authoring-checklist.md +``` + +**Types vs prose:** `src/` is the small, stable, importable vocabulary (kept a true leaf — re-state the unions rather than importing from `composition`/`use-tokens` to avoid cycles). `skills/` is the teaching payload `agentic-authoring` ships. `catalog/components.json` is the bridge both the analyzer (iterate) and the authoring agent (query) consume. + +## Key exports + +`VisualState`, `InteractionState`, `SemanticState`; `Appearance`/`ControlSize`/`ControlShape`/`LabelPosition`; `InteractionConcept`, `A11yConcept`, `TokenReference`, `ConceptualComponent`; and the pinning **spec** types (`PinScenario`, `PinAssertionContract`). + +## Pinning-tests ownership + +`agentic-concepts` owns the **spec** (a generated, capped per-component prop matrix + the assertion contract: a11y tree, resolved style/token set per slot, structural snapshot). `agentic-analyzer` owns the **mechanism** (sentinel theme, `@testing-library/react-native` harness, per-platform jest). `agentic-components` runs the same matrix to prove v1→new parity. Interim safety net: keep/extend existing `react-test-renderer` snapshots until the analyzer harness lands. + +## Dependencies & intersections + +- **No** dependency on framework/component/testing packages (stays a leaf). +- **agentic-tokens:** `TokenReference` here = the *old→new mapping input*; tokens defines the future object, concepts catalogs current usage. Share the **state vocabulary**. +- **agentic-analyzer:** concepts = spec/data; analyzer = mechanism. +- **agentic-components:** concepts is the authoring contract; new components satisfy the same `ConceptualComponent` + matrix. +- **agentic-authoring:** ships `skills/` as the agent payload (same `SKILL.md` format). + +## Open questions + +- Type duplication vs coupling (re-stating component unions risks drift → add a catalog-vs-source drift check, likely in analyzer). +- Encode per-platform concept deltas (Win32 two-tone focus, Android ripple) as explicit catalog data? (Recommend yes.) +- Matrix-explosion cap / "conceptually significant" pruning rule. +- Typography: map the legacy/v9 variants onto the x3-design `textstyle-*` bundles (`agentic-tokens` owns the taxonomy; concepts catalogs the mapping). Note interaction states are *derived* (rest token + delta), not enumerated — the catalog records rest tokens + which categories get `interaction.applies-to`. + +## Phased plan + +0. **Scaffold** package (types/docs only), add to root `references`, verify it joins `tsgo -b`. +1. **Concept types** (`states`/`appearance`/`interactions`/`accessibility`/`tokens`/`component-shape`). +2. **Component catalog** for the 7 studied families (Button, Checkbox, Switch, Radio/RadioGroup, Tab/TabList, Link, Text). +3. **Skill docs** (file-path-anchored examples). +4. **Pinning spec** (matrix + assertion contract as typed data); coordinate seam with analyzer. +5. **Integration & cross-checks** (consumers wire in; add catalog-vs-source drift check). diff --git a/packages/agentic/agentic-tokens/PLAN.md b/packages/agentic/agentic-tokens/PLAN.md new file mode 100644 index 0000000000..b3efa719b4 --- /dev/null +++ b/packages/agentic/agentic-tokens/PLAN.md @@ -0,0 +1,121 @@ +# @fluentui-react-native/agentic-tokens + +> Status: **Plan** (package not yet created). Part of the `packages/agentic/` modernization — see [`../STAGING.md`](../STAGING.md). +> +> ✅ **Sources received & incorporated.** This plan now adopts the **x3-design Fluent token model** (`~/dev/fluent-design/plugins/tokens/skills/{core,interaction,textstyle}/SKILL.md` + the `*.yaml` token files) and the **Fluent Headless / Fluent Modern** strategy (`Fluent Headless and Fluent Modern.docx`, repo root). `agentic-tokens` is the **Fluent Modern** styled layer for React Native. + +## Purpose + +A platform-neutral **semantic token layer** (the "Fluent Modern" tokens) that new headless components author against, instead of reaching into `theme.colors.*` and the static `globalTokens` JSON. It implements the x3-design two-layer model adapted to React Native: + +- **Primitives** (`prmt-*`) — raw, themeless base values (color stops, spacing steps, font sizes). Stable referenceable names only. +- **Generics** (`--gnrc-*`) — the **semantic layer** components consume: each maps a primitive to a role and carries light + dark values. Components consume generics, **never primitives**. + +## Identity + +- **npm:** `@fluentui-react-native/agentic-tokens` +- **path:** `packages/agentic/agentic-tokens/` +- Add to root `tsconfig.json` `references`. + +## The token model (adopted from x3-design) + +### Generic color tokens — `--gnrc-color-{variant}-{role}-{modifier}` + +In RN we expose these as a typed object (working convention: camelCase of the generic name, e.g. `colorBackgroundNeutralSubtle`). + +- **variant** (usage context): `surface`, `background`, `foreground`, `stroke`, `fixed` +- **role** (palette family): `neutral`, `brand`, `danger`, `success`, `warning` +- **modifier** (qualifier within role): + - background / stroke: `heavy`, `loud`, `soft`, `subtle`, `transparent`, `disabled` (role-dependent subset) + - foreground: `primary`, `secondary`, `tertiary`, `onloud`, `disabled` + - surface (neutral): `farther`, `far`, `near`, `nearer`, `translucent` + - stroke also: `focus-inner`, `focus-outer` + - fixed: `white`, `black` (theme-invariant) + +Examples: `--gnrc-color-background-neutral-subtle`, `--gnrc-color-foreground-brand-onloud`, `--gnrc-color-stroke-focus-outer`, `--gnrc-color-surface-neutral-translucent`. + +### Scalars — `--gnrc-{type}-{modifier}` + +- `--gnrc-font-weight-{regular|medium|semibold|bold}` (variable axis 420/550/600/625) +- `--gnrc-stroke-width-{thin|thick|thicker}` + +### Spacing — `--gnrc-spacing-{component|layout}-base-{step}` + +- `component-base-{50,100,150,200,250,300,400,500,600,700}` +- `layout-base-{100,200,400,450,500,600,700,800,1000,1200}` + +### Shadow — `--gnrc-shadow-{lowest|lower|low|high|higher|highest}` + +### Typography — text styles (`textstyle-{set}-{role}[-{size}][-strong]`) + +Text styles **bundle five generics** (`font-family`, `font-weight`, `font-size`, `line-height`, `letter-spacing`) — they cannot be a single token. Sets: `functional` (sans UI ramp), `content`, `content-expressive`, `content-editorial` (serif). Roles per `textstyle.yaml` (display/pagetitle/title/subtitle/body/caption/h1–h5/paragraph/…). `optical_size` axis (36 display / 8 body) resolves to null on static platform fonts (iOS SF Pro, Android Roboto) — relevant for RN. We expose each text style as a resolved `{ fontFamily, fontWeight, fontSize, lineHeight, letterSpacing }` object. + +> **Gap:** the x3-design generics do **not** yet define corner-radius tokens. furn currently uses `globalTokens.corner.*`. **Open decision** (below): keep a furn-local radius ramp until x3-design adds radius generics, or propose radius generics upstream. + +## Interaction states are DERIVED, not enumerated (key RN adaptation) + +The x3-design model does **not** ship `…Hover`/`…Pressed` slots. Hover/pressed are computed from the **rest** token via OKLCH channel deltas (`interaction-deltas.yaml`): `--lightness-{hover|press}` (light `-0.03/-0.06`, dark `+0.03/+0.06`) and `--alpha-{hover|press}` for transparent/translucent, with a lightness curve that doubles the delta as `L → 0.20`, and an **inverse rule** (flip sign for `loud`/`onloud`/`heavy` tokens). `disabled` and `focus` are excluded from the algorithm. + +**Web does this at runtime via OKLCH relative color. React Native cannot** (no relative-color/OKLCH at runtime) — so RN is a **"pre-compiled environment"** (the same bucket as iOS/Android/Figma in the skill). **Decision: `agentic-tokens` ships PRECOMPUTED hover/pressed values**, and the OKLCH derivation **algorithm lives in `agentic-analyzer`** (deltas + lightness curve + inverse rule) where it is used to **validate** that the precomputed values are correct. Therefore: + +- `agentic-tokens` exposes concrete precomputed interaction values (no runtime OKLCH). These are generated at build/theme-creation time; the generator reuses the analyzer's OKLCH implementation as a **build-time/dev dependency** so the *runtime* token output stays static with **zero analyzer dependency**, and there is a single source of the algorithm. +- `agentic-analyzer` re-derives the expected hover/pressed from the rest generics + `interaction-deltas` and asserts the shipped precomputed values match — a regression guard on both the rest tokens and the formula. +- Components declare which categories get interaction via an **`interaction.applies-to: [background, foreground, stroke]`** block (carried in each component's token map, see `agentic-components`). For a multi-state component (Selected/Checked/Expanded), hover/pressed derive from the **active rest token** for that state-axis value. +- API sketch: `tokens.color.backgroundNeutralSubtle` (rest) + `interactionState(token, 'hover'|'pressed')` returning the precomputed value; or a per-component resolved set keyed by `{state-axis × interaction-state}`. + +## Source of truth (decision) + +**Adopt the x3-design _structure_ (`prmt-`/`--gnrc-` naming, variant-role-modifier, scalars, textstyle bundles, derived interaction) but source the _values_ from the existing `@fluentui-react-native` themes and the token mappings already encoded in the v1 components** — i.e. project the current theme into the x3 generic surface rather than porting x3's `primitives.yaml`. The OLD→NEW table below is that projection. (Migrating the value base to x3 primitives later remains possible without changing the generic surface components author against.) + +## Producer & consumption + +- `createTokens(theme): CommonTokens` — resolves the full generic surface (rest values, light/dark) by **mapping from `theme.colors.*` / `theme.typography.*` / `globalTokens.*` per the OLD→NEW table**, and attaches the precomputed interaction variants for declared categories. Memoized per-theme (reuse `buildUseTokens`'s cache-by-theme-identity). +- `useTokens(): CommonTokens` over `useFluentTheme()` (no new provider). +- Components consume **generics only** (`tokens.color.foregroundNeutralPrimary`), never primitives, `theme.colors.*`, or `globalTokens.*`. + +## OLD → NEW mapping (furn v1 → x3 generics) + +| OLD (furn) | NEW generic | +|---|---| +| `colors.neutralForeground1` / legacy `buttonText` / `bodyText` | `color-foreground-neutral-primary` | +| `colors.neutralForeground2/3` | `color-foreground-neutral-secondary` / `-tertiary` | +| `colors.neutralForegroundDisabled` | `color-foreground-neutral-disabled` | +| `colors.neutralForegroundOnBrand` / `OnColor` | `color-foreground-{neutral|brand}-onloud` | +| `colors.neutralBackground1` / legacy `buttonBackground` / `defaultBackground` | `color-background-neutral-subtle` | +| `colors.brandBackground` (rest) | `color-background-brand-heavy` (Primary button rest) | +| `colors.brandBackgroundPressed` | *(derived from `background-brand-heavy` via press delta — not a slot)* | +| `colors.subtleBackground*` / `ghost*` | `color-background-neutral-transparent` (+ derived hover/press) | +| `colors.neutralStroke1/2/3` / `buttonBorder` | `color-stroke-neutral-{soft|subtle|loud}` | +| `colors.strokeFocus1` / `strokeFocus2` | `color-stroke-focus-inner` / `color-stroke-focus-outer` | +| `colors.redForeground1` (Checkbox required) | `color-foreground-danger-primary` | +| `variant: 'body1'` → `typography.variants.*` | `textstyle-functional-body-medium` (+ `-strong`) | +| `globalTokens.font.weight.semibold` | `font-weight-semibold` | +| `globalTokens.size80/120` (padding) | `spacing-component-base-{…}` | +| `globalTokens.stroke.width10/20` | `stroke-width-thin` / `stroke-width-thick` | +| `globalTokens.corner.radius*` | furn-local radius ramp **derived from current v1 component usage** (no x3 generic exists yet) | + +(Full crosswalk finalized during Stage 2; the `loud`/`heavy`/`onloud` inverse-rule names must be tracked so derived interaction uses the correct sign.) + +## Dependencies & intersections + +- **agentic-concepts:** shares the state vocabulary; its `TokenReference` catalog cites these generic names. The interaction model (rest + derived hover/press) replaces concepts' "state-suffixed slot" assumption. +- **agentic-analyzer:** `createTokens` is the sentinel injection point; the analyzer's reverse-map maps resolved values back to `--gnrc-*` names, and **must validate the precomputed interaction values** against the algorithm. Because hover/press are derived, the analyzer's pin tests catch both rest-token and delta-formula regressions. +- **agentic-components (Fluent Modern layer):** primary consumer via `useTokens()` + per-component `interaction.applies-to`. +- **agentic-authoring:** the generic taxonomy + the `interaction`/text-style skills are the vocabulary the authoring agent generates against; keep names 1:1 with the x3-design skills so guidance transfers. + +## Decisions (resolved) & remaining questions + +- ✅ **Source of truth** — adopt the x3 _structure_, map _values_ from the furn themes + v1 component token mappings (project-first; an x3-primitive value migration can follow later without changing the generic surface components author against). +- ✅ **Interaction** — ship **precomputed** values; the OKLCH algorithm lives in `agentic-analyzer` and validates them. The build-time generator reuses that implementation as a dev dependency, so runtime token output is static with **no analyzer dependency** and the algorithm has a single home. +- ✅ **Radius** — furn-local radius ramp **derived from current v1 component usage** (`globalTokens.corner.*`); revisit if x3 adds radius generics. +- ❓ **Object shape** — flat camelCase (`colorBackgroundNeutralSubtle`) vs nested (`color.background.neutral.subtle`). Recommend nested for authoring + a flat alias for ergonomics. +- ❓ **Theme-invariant + HC** — `fixed-white/black` stay invariant; map Win32 `PlatformColor`/high-contrast through `createTokens` per `theme.host`. + +## Phased plan + +1. **Primitives + generics types** — model `prmt-*` and `--gnrc-*` (color/scalar/spacing/shadow) as typed structures; bring in the text-style bundles. +2. **Producer (rest values)** — `createTokens(theme)` resolving every generic by mapping from `theme.colors/typography` + `globalTokens` + v1 component mappings (OLD→NEW table), light/dark; `useTokens()`. +3. **Precomputed interaction** — generate hover/pressed for declared categories using the OKLCH delta+curve+inverse implementation owned by `agentic-analyzer` (reused as a build-time dev dependency); ship the precomputed values; expose `interactionState(...)`. `agentic-analyzer` validates them against the algorithm. +4. **Text styles + radius** — resolve `textstyle-*` bundles (opsz → null on native fonts); settle radius (decision #3). +5. **Validation via analyzer** — sentinel theme asserts each generic resolves uniquely and each derived interaction value matches the algorithm; pin furn v1 equivalents. +6. **Adoption** — `agentic-components` authors against `useTokens()` + `interaction.applies-to`; feed the finalized taxonomy to `agentic-authoring`. From 860fb9de2342a1dde159f8bae193c2b1bfe51a61 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Thu, 11 Jun 2026 17:26:41 -0700 Subject: [PATCH 2/6] format updates --- packages/agentic/STAGING.md | 25 ++++++++------ packages/agentic/agentic-analyzer/PLAN.md | 10 +++--- packages/agentic/agentic-authoring/PLAN.md | 8 ++--- packages/agentic/agentic-components/PLAN.md | 18 +++++----- packages/agentic/agentic-concepts/PLAN.md | 8 ++--- packages/agentic/agentic-tokens/PLAN.md | 38 ++++++++++----------- 6 files changed, 56 insertions(+), 51 deletions(-) diff --git a/packages/agentic/STAGING.md b/packages/agentic/STAGING.md index bea4bd0fbe..1456a472b8 100644 --- a/packages/agentic/STAGING.md +++ b/packages/agentic/STAGING.md @@ -4,21 +4,21 @@ This is the unified execution plan for the five new packages under `packages/age ## The five packages -| Package | Path | Role | -|---|---|---| -| [`agentic-concepts`](./agentic-concepts/PLAN.md) | `packages/agentic/agentic-concepts/` | **Leaf vocabulary** — concept types, component catalog, agent skill docs, the pinning **spec** | -| [`agentic-tokens`](./agentic-tokens/PLAN.md) | `packages/agentic/agentic-tokens/` | **Fluent Modern token layer** — x3-design primitives→generics; `createTokens(theme)`, `useTokens()`, derived interaction | -| [`agentic-analyzer`](./agentic-analyzer/PLAN.md) | `packages/agentic/agentic-analyzer/` | **Refactor safety net** — sentinel theme + reverse-map, RNTL harness, pinning, per-platform jest | -| [`agentic-components`](./agentic-components/PLAN.md) | `packages/agentic/agentic-components/` | **Fluent Headless + Modern components** — three forms (`X`/`useX`/`renderX`), no compose/customize | -| [`agentic-authoring`](./agentic-authoring/PLAN.md) | `packages/agentic/agentic-authoring/` | **Distributable agent + skills (+ optional MCP)** — sits on top of all four | +| Package | Path | Role | +| ---------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| [`agentic-concepts`](./agentic-concepts/PLAN.md) | `packages/agentic/agentic-concepts/` | **Leaf vocabulary** — concept types, component catalog, agent skill docs, the pinning **spec** | +| [`agentic-tokens`](./agentic-tokens/PLAN.md) | `packages/agentic/agentic-tokens/` | **Fluent Modern token layer** — x3-design primitives→generics; `createTokens(theme)`, `useTokens()`, derived interaction | +| [`agentic-analyzer`](./agentic-analyzer/PLAN.md) | `packages/agentic/agentic-analyzer/` | **Refactor safety net** — sentinel theme + reverse-map, RNTL harness, pinning, per-platform jest | +| [`agentic-components`](./agentic-components/PLAN.md) | `packages/agentic/agentic-components/` | **Fluent Headless + Modern components** — three forms (`X`/`useX`/`renderX`), no compose/customize | +| [`agentic-authoring`](./agentic-authoring/PLAN.md) | `packages/agentic/agentic-authoring/` | **Distributable agent + skills (+ optional MCP)** — sits on top of all four | ✅ **Both previously-locked sources received & incorporated:** `Fluent Headless and Fluent Modern.docx` (repo root) defines the Headless→Modern strategy and the three-forms component model; the **x3-design token skills** (`~/dev/fluent-design/plugins/tokens/skills/{core,interaction,textstyle}`) define the primitives→generics token model now adopted by `agentic-tokens`. ## How they intersect (the seams) -- **State vocabulary** is defined once in `agentic-concepts`. In `agentic-tokens` interaction states (hover/pressed) are **derived from rest generics** (OKLCH deltas, precomputed for RN), *not* enumerated as suffixed slots — components declare `interaction.applies-to` per category. +- **State vocabulary** is defined once in `agentic-concepts`. In `agentic-tokens` interaction states (hover/pressed) are **derived from rest generics** (OKLCH deltas, precomputed for RN), _not_ enumerated as suffixed slots — components declare `interaction.applies-to` per category. - **Pinning** splits: `agentic-concepts` = the **spec** (per-component scenario matrix + assertion contract); `agentic-analyzer` = the **mechanism** (sentinel theme, RNTL, per-platform jest); `agentic-components` = runs the same matrix to prove **v1→new parity**. Because interaction is derived, analyzer pins catch both rest-token and delta-formula regressions. -- **Token migration** is a chain: `agentic-concepts` catalogs *current* usage → `agentic-tokens` maps it to the x3-design `--gnrc-*` generics → `agentic-analyzer`'s reverse-map *proves* resolved values are preserved → `agentic-components` consumes via `useTokens()`. +- **Token migration** is a chain: `agentic-concepts` catalogs _current_ usage → `agentic-tokens` maps it to the x3-design `--gnrc-*` generics → `agentic-analyzer`'s reverse-map _proves_ resolved values are preserved → `agentic-components` consumes via `useTokens()`. - **Authoring** ships `agentic-concepts`' skill docs (in the x3-design `SKILL.md` format), generates the three-forms Headless component + Modern token styling, uses `agentic-tokens`' generic vocabulary, and drives `agentic-analyzer` for pin-tests. ## Dependency graph @@ -45,23 +45,28 @@ Key principle: **pin v1 behavior BEFORE refactoring anything.** The analyzer + c ## Staged execution ### Stage 0 — Scaffolding & contracts (all packages, parallel) + Create all five package folders with `package.json` + `tsconfig.json`, add to root `references`, confirm they join `tsgo -b` as empty packages. Lock the shared contracts: the **state vocabulary** (`agentic-concepts`), the `CommonTokens` **interface** (`agentic-tokens`, provisional), the pinning **assertion contract** (concepts ↔ analyzer seam), and the component **file-template** (`agentic-components` ↔ `agentic-authoring` golden template). ### Stage 1 — Foundation: concepts + analyzer harness + - `agentic-concepts`: concept types, component catalog (7 families), skill docs, pinning spec. - `agentic-analyzer`: sentinel theme + reverse-map (theme namespace), RNTL `renderWithTheme`, extraction helpers; then `globalTokens` sentinel via jest mock. -- **Milestone:** baseline-pin the existing v1 components (value + semantic snapshots) — the safety net is live *before* any refactor. +- **Milestone:** baseline-pin the existing v1 components (value + semantic snapshots) — the safety net is live _before_ any refactor. ### Stage 2 — Tokens (Fluent Modern layer) + - `agentic-tokens`: model `prmt-*` primitives + `--gnrc-*` generics (color `variant-role-modifier`, scalars, spacing, shadow, `textstyle-*` bundles) → `createTokens(theme)` producer (rest values, light/dark; HC/PlatformColor via `theme.host`) → JS port of the OKLCH **derived interaction** (deltas + lightness curve + inverse rule), precomputed for RN → `useTokens()` + the furn→generic crosswalk. - Resolve the open decisions: primitives source-of-truth (port x3 vs project existing theme), interaction precompute mechanism, radius ramp (no x3 generic yet). - **Milestone:** analyzer validates every generic resolves uniquely and every derived hover/pressed matches the algorithm; pin the v1 components' equivalent token references. ### Stage 3 — New components (pattern-setters) + - `agentic-components`: build **Button** end-to-end as the three forms (`Button`/`useButton`/`renderButton` + `useButtonStyles`) + the shared `hooks/`/`tokens/`/`styles/`/`helpers/`; gate on analyzer parity with `ButtonV1`. Then **Text** + **Switch/Checkbox**. Integrate the real `agentic-tokens` (swap the stub; rest + derived interaction); re-run pinning to confirm zero value drift. - **Milestone:** Button/Text/Switch reach pinned parity with their v1 counterparts; the three-forms template is locked. ### Stage 4 — Long tail + authoring + - `agentic-components`: migrate Link, Checkbox, FAB, CompoundButton, ToggleButton, … each as the same file set + platform splits, gated by analyzer parity. - `agentic-authoring`: ship `new-component`/`register-tester`/`changeset`/`lint-package` skills (in the x3-design `SKILL.md` format) + `.agents`+`.claude` transclusion stubs; then `token-lookup`/`pin-tests`/`e2e-scaffold`; then the optional MCP server; then distribution (`init`/`postinstall`, multi-runtime adapters). - **Milestone:** an agent can scaffold a new flat component end-to-end, with pinning, from the skills/MCP. diff --git a/packages/agentic/agentic-analyzer/PLAN.md b/packages/agentic/agentic-analyzer/PLAN.md index d92cb22810..71bc1a0e1d 100644 --- a/packages/agentic/agentic-analyzer/PLAN.md +++ b/packages/agentic/agentic-analyzer/PLAN.md @@ -8,7 +8,7 @@ A test/analysis toolkit that lets us **safely refactor** v1 components by: 1. building a **sentinel theme** whose every leaf value is unique, 2. rendering components with **`@testing-library/react-native`** to capture accessibility trees, computed styles, and snapshots, and -3. **reverse-mapping** resolved style values back to the exact theme/token slot they came from — so a refactor that changes *which* token feeds a style is caught even when the final pixel value is unchanged. +3. **reverse-mapping** resolved style values back to the exact theme/token slot they came from — so a refactor that changes _which_ token feeds a style is caught even when the final pixel value is unchanged. Plus a strategy for **multiplexing jest per platform**. @@ -29,9 +29,9 @@ Plus a strategy for **multiplexing jest per platform**. 1. **Sentinel theme + reverse map** — `createSentinelTheme(base?)` clones a real `Theme`, replacing every leaf with a unique type-valid sentinel (colors → reserved unique hex; spacing → unique `'NNNpx'`; numeric sizes/shadows → reserved integers; strings → reserved-but-valid pool), emitting a `SentinelMap: value → "colors.buttonBackground" | "spacing.m" | …`. `createSentinelGlobalTokens()` + a jest mock factory for `@fluentui-react-native/theme-tokens` covers the static namespace. `resolveStyleToSemantic(style)` walks a computed RN style and substitutes sentinels with semantic names. 2. **testing-library helpers** — `renderWithTheme(el, {theme?, platform?})`, `getAccessibilityTree(result)`, `getComputedStyles(result, query)`, `snapshotSemantic(result)` (snapshot with sentinel→semantic substitution; stable across pixel changes, sensitive to slot-source changes). -3. **Pinning** — `pinComponent(Component, scenarios)` producing **dual** snapshots: a *value* snapshot (catches visual regressions) + a *semantic* snapshot (catches silent token-source swaps). Drives the v1 state matrix (hovered/pressed/disabled/checked via `.customize`/state layers). +3. **Pinning** — `pinComponent(Component, scenarios)` producing **dual** snapshots: a _value_ snapshot (catches visual regressions) + a _semantic_ snapshot (catches silent token-source swaps). Drives the v1 state matrix (hovered/pressed/disabled/checked via `.customize`/state layers). 4. **Per-platform jest multiplexing** — `makeJestConfig(platform)` that takes the platform from `FURN_JEST_PLATFORM` (falling back to `furn.jestPlatform`), and a `multiplex` runner that, for a `furn.jestPlatforms: [...]` array, spawns one jest process per platform with platform-suffixed snapshot dirs (preserving the one-platform-per-process model the preset requires — and respecting the AGENTS.md "no multiple RN forks in one program" rule). -5. **OKLCH interaction algorithm (home + validator).** A JS port of the x3-design hover/pressed derivation — `--lightness-{hover,press}`/`--alpha-{hover,press}` deltas, the lightness curve `1 + clamp(0, (0.40 - L)/0.20, 1)`, and the `loud`/`heavy`/`onloud` inverse rule (per the `tokens-interaction` skill). This is the **single home** of the algorithm. Two consumers: (a) `agentic-analyzer` uses it to **validate** that `agentic-tokens`' shipped precomputed hover/pressed values match what the algorithm produces from the rest generics — a regression guard on both rest tokens and the formula; (b) `agentic-tokens`' build-time generator imports it (dev dependency only) to *produce* those precomputed values, so the runtime token output stays static and analyzer-free. Pin tests assert solid/inverse/transparent cases against the worked examples in the skill. +5. **OKLCH interaction algorithm (home + validator).** A JS port of the x3-design hover/pressed derivation — `--lightness-{hover,press}`/`--alpha-{hover,press}` deltas, the lightness curve `1 + clamp(0, (0.40 - L)/0.20, 1)`, and the `loud`/`heavy`/`onloud` inverse rule (per the `tokens-interaction` skill). This is the **single home** of the algorithm. Two consumers: (a) `agentic-analyzer` uses it to **validate** that `agentic-tokens`' shipped precomputed hover/pressed values match what the algorithm produces from the rest generics — a regression guard on both rest tokens and the formula; (b) `agentic-tokens`' build-time generator imports it (dev dependency only) to _produce_ those precomputed values, so the runtime token output stays static and analyzer-free. Pin tests assert solid/inverse/transparent cases against the worked examples in the skill. ## Proposed structure @@ -51,7 +51,7 @@ agentic-analyzer/ ## Dependencies & intersections -- **agentic-concepts:** concepts owns *what to pin* (component catalog + scenario matrix + assertion contract); analyzer owns the *machinery* those specs call. +- **agentic-concepts:** concepts owns _what to pin_ (component catalog + scenario matrix + assertion contract); analyzer owns the _machinery_ those specs call. - **agentic-tokens:** analyzer's reverse map is the bridge proving a refactor from old refs → x3-design `--gnrc-*` generics preserves resolved values. `createTokens` is the sentinel injection point. Because hover/pressed are **derived** (OKLCH deltas, precomputed for RN), the analyzer must also assert each derived interaction value matches the algorithm — so pins catch both rest-token and delta-formula regressions. - **agentic-components:** provides the parity gate — new components must reproduce v1's pinned a11y tree / styles / snapshots. - Internal: reuse `Theme`/`useTheme` (theme-types), `ThemeProvider` (theme); may supersede `test-tools`' `mockTheme` as the sentinel base. @@ -69,7 +69,7 @@ agentic-analyzer/ 1. **Scaffold + deps:** create package, add `@testing-library/react-native`, get a trivial `renderWithTheme` passing for one component (Button, ios) under the existing preset. 2. **Sentinel theme + reverse map** over the theme namespace; prove `resolveStyleToSemantic` round-trips on a real component. -2b. **OKLCH interaction module** (`interaction/`) — port the deltas + lightness curve + inverse rule; unit-test against the skill's worked examples. Land this early: `agentic-tokens`' Stage-2 build depends on it to precompute hover/pressed, and the analyzer uses it to validate them. + 2b. **OKLCH interaction module** (`interaction/`) — port the deltas + lightness curve + inverse rule; unit-test against the skill's worked examples. Land this early: `agentic-tokens`' Stage-2 build depends on it to precompute hover/pressed, and the analyzer uses it to validate them. 3. **globalTokens sentinel + jest mock;** extend reverse map to `globalTokens.*`. 4. **Extraction + semantic snapshots** (`getAccessibilityTree`/`getComputedStyles`/`snapshotSemantic`). 5. **Pinning API + state matrix;** hand off to `agentic-concepts` to author per-component specs. diff --git a/packages/agentic/agentic-authoring/PLAN.md b/packages/agentic/agentic-authoring/PLAN.md index 9158983c57..c3bb8e51e0 100644 --- a/packages/agentic/agentic-authoring/PLAN.md +++ b/packages/agentic/agentic-authoring/PLAN.md @@ -21,7 +21,7 @@ A distributable bundle of agent **skills**, **instructions**, and an optional ** - **microsoft/fluentui (web) pattern (verified):** a terse **router `AGENTS.md`** (critical rules → one golden template → anti-patterns → link tables → a Skills table → package layout) plus **verb-decomposed skills** canonical at `.agents/skills//SKILL.md`, **mirrored** at `.claude/skills//SKILL.md` as a one-line transclusion (`@../../../.agents/skills//SKILL.md`). `SKILL.md` = YAML frontmatter (`name`, `description`, `disable-model-invocation?`, `argument-hint`, `allowed-tools`) + imperative `## Steps` (numbered, fenced commands) + local `## Rules`/`## Anti-patterns`. Bulky knowledge offloaded to a `references/` subdir ("fat skill"). - **Fluent Headless / Fluent Modern doc (received):** defines the strategy the authoring agent implements — ship behavior as stable unstyled primitives in **three forms** (`X` / `useX` / `renderX`), keep the styled (Modern/token) layer a swappable concern on top. The agent's output must follow this split. - **x3-design token skills (received):** the `SKILL.md` format to mirror — YAML frontmatter (`name`, `description`, `argument-hint`) + a `| Field | Value |` table (Type/Category/Related) + a "Files in this skill" table + reference `*.yaml`. The token plugin's three skills (`core` primitives/generics, `interaction` hover/pressed derivation, `textstyle` bundles) are the canonical vocabulary the `token-lookup` skill resolves against. -- **This repo:** has a root `AGENTS.md` + `CLAUDE.md`; `apps/component-generator` is a **Gulp string-replacement scaffolder** emitting the **old compose/tokens** shape — i.e. it generates the *legacy anti-pattern*; the authoring skill must target the new flat three-forms `agentic-components` shape, not wrap the legacy generator. No `SKILL.md`/`.agents/` exist yet. +- **This repo:** has a root `AGENTS.md` + `CLAUDE.md`; `apps/component-generator` is a **Gulp string-replacement scaffolder** emitting the **old compose/tokens** shape — i.e. it generates the _legacy anti-pattern_; the authoring skill must target the new flat three-forms `agentic-components` shape, not wrap the legacy generator. No `SKILL.md`/`.agents/` exist yet. ## Proposed structure (PROVISIONAL — pending sources) @@ -49,7 +49,7 @@ Plus repo-root discovery stubs (one-line transclusions — no duplication, exact .claude/skills//SKILL.md -> @../../../packages/agentic/agentic-authoring/skills//SKILL.md ``` -**MCP server: optional, later.** Skills alone (markdown + the agent's native file/bash tools) cover most authoring. Add the MCP server once `agentic-tokens`/`agentic-analyzer` exist, where deterministic repo-aware ops beat free-form tool use (`resolve_token`, `scaffold_component`, `get_component_spec`, `run_pin_tests`). Skills should *prefer* MCP tools when present, falling back to Bash/Read. +**MCP server: optional, later.** Skills alone (markdown + the agent's native file/bash tools) cover most authoring. Add the MCP server once `agentic-tokens`/`agentic-analyzer` exist, where deterministic repo-aware ops beat free-form tool use (`resolve_token`, `scaffold_component`, `get_component_spec`, `run_pin_tests`). Skills should _prefer_ MCP tools when present, falling back to Bash/Read. ## How the agent authors a component (`/new-component Badge`) @@ -70,9 +70,9 @@ Plus repo-root discovery stubs (one-line transclusions — no duplication, exact ## Open questions -- **Sequencing:** thin without the other four — build with stubs/contracts early, fill as they land (it's the *last* useful workstream). +- **Sequencing:** thin without the other four — build with stubs/contracts early, fill as they land (it's the _last_ useful workstream). - **MCP vs skills duplication:** keep logic in one place (MCP server or shared lib); skills call it to avoid drift. -- **"Distributable" target:** shipping `skills/` + `AGENTS.md` in npm is easy; discoverability in a *consumer* repo needs an install step — `npx … init` or `postinstall` to copy/transclude into the consumer's `.claude`/`.agents`. +- **"Distributable" target:** shipping `skills/` + `AGENTS.md` in npm is easy; discoverability in a _consumer_ repo needs an install step — `npx … init` or `postinstall` to copy/transclude into the consumer's `.claude`/`.agents`. - **Multi-runtime:** Claude (`.claude/skills`) covered by transclusion; Copilot (`.github/instructions`) / Cursor (`.cursor/rules`) need generated adapters if in scope. ## Phased plan diff --git a/packages/agentic/agentic-components/PLAN.md b/packages/agentic/agentic-components/PLAN.md index bc50f85cad..79f21469b5 100644 --- a/packages/agentic/agentic-components/PLAN.md +++ b/packages/agentic/agentic-components/PLAN.md @@ -9,7 +9,7 @@ A flat component library that recreates the v1 components in the modern Fluent structure, applying the **Fluent Headless → Fluent Modern** strategy to React Native: - **Headless layer** — stable, public, **unstyled** primitives that own behavior, ARIA/accessibility, keyboard handling, and semantic structure. **No pixels, no design props.** Each component ships **three forms** (matching the docx): the primitive component `X`, the stable hook `useX`, and the render function `renderX`. -- **Modern layer** — the headless primitive **plus tokens** (`agentic-tokens`), native-first and lightweight. Styling is a *swappable concern* layered on top of the same headless behavior. +- **Modern layer** — the headless primitive **plus tokens** (`agentic-tokens`), native-first and lightweight. Styling is a _swappable concern_ layered on top of the same headless behavior. It deliberately avoids `compose`/`customize`/`buildProps`/`stylingSettings`. Components depend only on shared sibling dirs (`hooks/`, `tokens/`, `styles/`, `helpers/`) and `agentic-tokens` — **never on each other**. @@ -53,13 +53,13 @@ agentic-components/ ## Web/Headless → RN bridges -| Fluent web | RN replacement (in shared dirs) | -|---|---| -| Griffel `makeStyles`/`mergeClasses` | RN style objects + `helpers/mergeStyles` (user style wins last) | -| `--gnrc-*` CSS vars + OKLCH runtime interaction | `useTokens()` generic object + **precomputed** interaction from `agentic-tokens` (RN has no runtime OKLCH) | -| `react-aria`/`react-tabster`, focusgroup roving tabindex | RN `accessibility*` props + `interactive-hooks` (+ `FocusZone`) behind `hooks/` | -| native `popover`/``/anchor positioning | RN equivalents / existing furn Callout/Popover primitives (headless behavior only) | -| `slot.always` + jsx-runtime | render RN primitives directly; optional `helpers/getNativeProps` for prop filtering | +| Fluent web | RN replacement (in shared dirs) | +| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| Griffel `makeStyles`/`mergeClasses` | RN style objects + `helpers/mergeStyles` (user style wins last) | +| `--gnrc-*` CSS vars + OKLCH runtime interaction | `useTokens()` generic object + **precomputed** interaction from `agentic-tokens` (RN has no runtime OKLCH) | +| `react-aria`/`react-tabster`, focusgroup roving tabindex | RN `accessibility*` props + `interactive-hooks` (+ `FocusZone`) behind `hooks/` | +| native `popover`/``/anchor positioning | RN equivalents / existing furn Callout/Popover primitives (headless behavior only) | +| `slot.always` + jsx-runtime | render RN primitives directly; optional `helpers/getNativeProps` for prop filtering | ## First components (pattern-setters) @@ -79,7 +79,7 @@ agentic-components/ - **`Text` flat-rule:** Button should render RN `Text` styled by shared `textStyles` (pure-flat) rather than import a sibling `Text` component — requires perfect typography parity from the `textstyle-*` bundles. - **Headless vs Modern packaging:** one package exporting both unstyled (`useX`/`renderX`) and styled (`X`) forms, or split headless into its own entry/subpath later? (Recommend one package, two entry points: `.../headless` and the default Modern.) -- **Derived interaction parity:** the styles hook must request the right hover/pressed for the *active* rest token in multi-state components; confirm the analyzer pins these. +- **Derived interaction parity:** the styles hook must request the right hover/pressed for the _active_ rest token in multi-state components; confirm the analyzer pins these. - Platform forks: keep per-platform files (`useButtonStyles.win32.ts`, ripple, two-tone focus, Win32 keyboard quirks). - Animation parity (Switch `Animated.Value`) → `hooks/useSwitchAnimation`; confirm analyzer can pin animated styles. - Memoization: replace v1 `buildProps`/`getMemoCache` with `useMemo` keyed on theme + state flags in `useXStyles`. diff --git a/packages/agentic/agentic-concepts/PLAN.md b/packages/agentic/agentic-concepts/PLAN.md index 11275601d5..c324bb6f88 100644 --- a/packages/agentic/agentic-concepts/PLAN.md +++ b/packages/agentic/agentic-concepts/PLAN.md @@ -6,7 +6,7 @@ The shared **vocabulary** of furn: a small, framework-agnostic set of TypeScript concept types, a machine-readable component **catalog**, and agent-facing **skill docs** that teach a human or an agent how to read the existing v1 (composition-framework) components and author new ones with consistent states, appearance, interactions, accessibility, and token usage. -This is the dependency-light **leaf** package the other agentic workstreams build on. It owns *concept definitions, the component inventory, and prose*. It does **not** own runtime styling, the new token object (→ `agentic-tokens`), or the test harness (→ `agentic-analyzer`). +This is the dependency-light **leaf** package the other agentic workstreams build on. It owns _concept definitions, the component inventory, and prose_. It does **not** own runtime styling, the new token object (→ `agentic-tokens`), or the test harness (→ `agentic-analyzer`). ## Identity @@ -18,7 +18,7 @@ This is the dependency-light **leaf** package the other agentic workstreams buil Every v1 component follows one regular shape (`compose({ ...stylingSettings, slots, useRender })`) reducible to a small concept set: -- **States** = interaction (`hovered|pressed|focused`, from `usePressableState`) × semantic (`disabled|checked|toggled|selected|required|visited`). These become *nested token layers* selected by a `lookup` predicate and `applyTokenLayers`. +- **States** = interaction (`hovered|pressed|focused`, from `usePressableState`) × semantic (`disabled|checked|toggled|selected|required|visited`). These become _nested token layers_ selected by a `lookup` predicate and `applyTokenLayers`. - **Appearance** = `appearance` × `size` × `shape` × `labelPosition` (per-family enums; defaults are platform-specific). - **Interactions** = press (`useOnPressWithFocus`), keyboard (`useKeyProps` → macOS `validKeys*` vs Win32/Windows `keyDownEvents`), toggle/selection (`useAsToggleWithEvent`, `useSelectedKey`), focus (`useViewCommandFocus`, `FocusZone`). - **Accessibility** = roles (`button`/`checkbox`/`switch`/`radio`/`tab`/`link`), `getAccessibilityState`, label-from-first-string-child, `accessibilityActions`, pos/setSize. @@ -59,7 +59,7 @@ agentic-concepts/ ## Dependencies & intersections - **No** dependency on framework/component/testing packages (stays a leaf). -- **agentic-tokens:** `TokenReference` here = the *old→new mapping input*; tokens defines the future object, concepts catalogs current usage. Share the **state vocabulary**. +- **agentic-tokens:** `TokenReference` here = the _old→new mapping input_; tokens defines the future object, concepts catalogs current usage. Share the **state vocabulary**. - **agentic-analyzer:** concepts = spec/data; analyzer = mechanism. - **agentic-components:** concepts is the authoring contract; new components satisfy the same `ConceptualComponent` + matrix. - **agentic-authoring:** ships `skills/` as the agent payload (same `SKILL.md` format). @@ -69,7 +69,7 @@ agentic-concepts/ - Type duplication vs coupling (re-stating component unions risks drift → add a catalog-vs-source drift check, likely in analyzer). - Encode per-platform concept deltas (Win32 two-tone focus, Android ripple) as explicit catalog data? (Recommend yes.) - Matrix-explosion cap / "conceptually significant" pruning rule. -- Typography: map the legacy/v9 variants onto the x3-design `textstyle-*` bundles (`agentic-tokens` owns the taxonomy; concepts catalogs the mapping). Note interaction states are *derived* (rest token + delta), not enumerated — the catalog records rest tokens + which categories get `interaction.applies-to`. +- Typography: map the legacy/v9 variants onto the x3-design `textstyle-*` bundles (`agentic-tokens` owns the taxonomy; concepts catalogs the mapping). Note interaction states are _derived_ (rest token + delta), not enumerated — the catalog records rest tokens + which categories get `interaction.applies-to`. ## Phased plan diff --git a/packages/agentic/agentic-tokens/PLAN.md b/packages/agentic/agentic-tokens/PLAN.md index b3efa719b4..343028b694 100644 --- a/packages/agentic/agentic-tokens/PLAN.md +++ b/packages/agentic/agentic-tokens/PLAN.md @@ -58,7 +58,7 @@ The x3-design model does **not** ship `…Hover`/`…Pressed` slots. Hover/press **Web does this at runtime via OKLCH relative color. React Native cannot** (no relative-color/OKLCH at runtime) — so RN is a **"pre-compiled environment"** (the same bucket as iOS/Android/Figma in the skill). **Decision: `agentic-tokens` ships PRECOMPUTED hover/pressed values**, and the OKLCH derivation **algorithm lives in `agentic-analyzer`** (deltas + lightness curve + inverse rule) where it is used to **validate** that the precomputed values are correct. Therefore: -- `agentic-tokens` exposes concrete precomputed interaction values (no runtime OKLCH). These are generated at build/theme-creation time; the generator reuses the analyzer's OKLCH implementation as a **build-time/dev dependency** so the *runtime* token output stays static with **zero analyzer dependency**, and there is a single source of the algorithm. +- `agentic-tokens` exposes concrete precomputed interaction values (no runtime OKLCH). These are generated at build/theme-creation time; the generator reuses the analyzer's OKLCH implementation as a **build-time/dev dependency** so the _runtime_ token output stays static with **zero analyzer dependency**, and there is a single source of the algorithm. - `agentic-analyzer` re-derives the expected hover/pressed from the rest generics + `interaction-deltas` and asserts the shipped precomputed values match — a regression guard on both the rest tokens and the formula. - Components declare which categories get interaction via an **`interaction.applies-to: [background, foreground, stroke]`** block (carried in each component's token map, see `agentic-components`). For a multi-state component (Selected/Checked/Expanded), hover/pressed derive from the **active rest token** for that state-axis value. - API sketch: `tokens.color.backgroundNeutralSubtle` (rest) + `interactionState(token, 'hover'|'pressed')` returning the precomputed value; or a per-component resolved set keyed by `{state-axis × interaction-state}`. @@ -75,24 +75,24 @@ The x3-design model does **not** ship `…Hover`/`…Pressed` slots. Hover/press ## OLD → NEW mapping (furn v1 → x3 generics) -| OLD (furn) | NEW generic | -|---|---| -| `colors.neutralForeground1` / legacy `buttonText` / `bodyText` | `color-foreground-neutral-primary` | -| `colors.neutralForeground2/3` | `color-foreground-neutral-secondary` / `-tertiary` | -| `colors.neutralForegroundDisabled` | `color-foreground-neutral-disabled` | -| `colors.neutralForegroundOnBrand` / `OnColor` | `color-foreground-{neutral|brand}-onloud` | -| `colors.neutralBackground1` / legacy `buttonBackground` / `defaultBackground` | `color-background-neutral-subtle` | -| `colors.brandBackground` (rest) | `color-background-brand-heavy` (Primary button rest) | -| `colors.brandBackgroundPressed` | *(derived from `background-brand-heavy` via press delta — not a slot)* | -| `colors.subtleBackground*` / `ghost*` | `color-background-neutral-transparent` (+ derived hover/press) | -| `colors.neutralStroke1/2/3` / `buttonBorder` | `color-stroke-neutral-{soft|subtle|loud}` | -| `colors.strokeFocus1` / `strokeFocus2` | `color-stroke-focus-inner` / `color-stroke-focus-outer` | -| `colors.redForeground1` (Checkbox required) | `color-foreground-danger-primary` | -| `variant: 'body1'` → `typography.variants.*` | `textstyle-functional-body-medium` (+ `-strong`) | -| `globalTokens.font.weight.semibold` | `font-weight-semibold` | -| `globalTokens.size80/120` (padding) | `spacing-component-base-{…}` | -| `globalTokens.stroke.width10/20` | `stroke-width-thin` / `stroke-width-thick` | -| `globalTokens.corner.radius*` | furn-local radius ramp **derived from current v1 component usage** (no x3 generic exists yet) | +| OLD (furn) | NEW generic | +| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -------------- | ------ | +| `colors.neutralForeground1` / legacy `buttonText` / `bodyText` | `color-foreground-neutral-primary` | +| `colors.neutralForeground2/3` | `color-foreground-neutral-secondary` / `-tertiary` | +| `colors.neutralForegroundDisabled` | `color-foreground-neutral-disabled` | +| `colors.neutralForegroundOnBrand` / `OnColor` | `color-foreground-{neutral | brand}-onloud` | +| `colors.neutralBackground1` / legacy `buttonBackground` / `defaultBackground` | `color-background-neutral-subtle` | +| `colors.brandBackground` (rest) | `color-background-brand-heavy` (Primary button rest) | +| `colors.brandBackgroundPressed` | _(derived from `background-brand-heavy` via press delta — not a slot)_ | +| `colors.subtleBackground*` / `ghost*` | `color-background-neutral-transparent` (+ derived hover/press) | +| `colors.neutralStroke1/2/3` / `buttonBorder` | `color-stroke-neutral-{soft | subtle | loud}` | +| `colors.strokeFocus1` / `strokeFocus2` | `color-stroke-focus-inner` / `color-stroke-focus-outer` | +| `colors.redForeground1` (Checkbox required) | `color-foreground-danger-primary` | +| `variant: 'body1'` → `typography.variants.*` | `textstyle-functional-body-medium` (+ `-strong`) | +| `globalTokens.font.weight.semibold` | `font-weight-semibold` | +| `globalTokens.size80/120` (padding) | `spacing-component-base-{…}` | +| `globalTokens.stroke.width10/20` | `stroke-width-thin` / `stroke-width-thick` | +| `globalTokens.corner.radius*` | furn-local radius ramp **derived from current v1 component usage** (no x3 generic exists yet) | (Full crosswalk finalized during Stage 2; the `loud`/`heavy`/`onloud` inverse-rule names must be tracked so derived interaction uses the correct sign.) From 835602a9c2aea2ec001163e7301605849df908cb Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Tue, 16 Jun 2026 11:36:23 -0700 Subject: [PATCH 3/6] update repo attributes to use lf so that format doesn't generate noise on windows --- .gitattributes | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index a48ea2b55f..134eb93ff9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ -# Set the default line ending behavior for text, in case people don't have core.autocrlf set. -* text=auto +# Ensure all text files use LF line endings in the repo and working tree. +* text=auto eol=lf *.pbxproj -text \ No newline at end of file From ea3f3615e01bae3263d620192fbf42059d82af5f Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Tue, 16 Jun 2026 17:05:54 -0700 Subject: [PATCH 4/6] add ability to run tests for multiple platforms --- .../ComponentTemplate/package.json | 2 +- scripts/configs/jest/jest.config.cjs | 3 +- scripts/src/const.ts | 8 +++ scripts/src/pkgContext.ts | 5 +- scripts/src/tasks/format.ts | 2 +- scripts/src/tasks/jest.ts | 67 ++++++++++++++++++- scripts/src/utils/env.ts | 5 +- scripts/src/utils/runScript.ts | 5 +- scripts/targets/tsconfig.check.json | 2 + 9 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 scripts/src/const.ts diff --git a/apps/component-generator/component-templates/ComponentTemplate/package.json b/apps/component-generator/component-templates/ComponentTemplate/package.json index f669d06496..00f8927e62 100644 --- a/apps/component-generator/component-templates/ComponentTemplate/package.json +++ b/apps/component-generator/component-templates/ComponentTemplate/package.json @@ -14,7 +14,7 @@ "module": "src/index.ts", "types": "lib/index.d.ts", "scripts": { - "build": "fluentui-scripts build", + "build": "tsgo -b", "clean": "fluentui-scripts clean", "depcheck": "fluentui-scripts depcheck", "just": "fluentui-scripts", diff --git a/scripts/configs/jest/jest.config.cjs b/scripts/configs/jest/jest.config.cjs index 0cc0644743..9d2bac6123 100644 --- a/scripts/configs/jest/jest.config.cjs +++ b/scripts/configs/jest/jest.config.cjs @@ -1,10 +1,11 @@ const fs = require('fs'); const path = require('path'); +const { PLATFORM_ENV_VAR } = require('../../src/const.ts'); const projectManifestPath = path.resolve(process.cwd(), 'package.json'); const foundProject = fs.existsSync(projectManifestPath); const projectManifest = foundProject ? require(projectManifestPath) : null; -const platform = projectManifest?.furn?.jestPlatform ?? 'ios'; +const platform = process.env[PLATFORM_ENV_VAR] ?? projectManifest?.furn?.jestPlatform ?? 'ios'; const rnPlatforms = ['ios', 'android', 'windows', 'macos', 'win32']; diff --git a/scripts/src/const.ts b/scripts/src/const.ts new file mode 100644 index 0000000000..0e6d32fb38 --- /dev/null +++ b/scripts/src/const.ts @@ -0,0 +1,8 @@ +// env variable to signal the current platform for scripts that need to know (like jest) +export const PLATFORM_ENV_VAR = 'FURN_RN_PLATFORM' as const; +// env variable to signal that we're in fix mode for scripts that support it +export const FIX_ENV_VAR = 'FURN_FIX_MODE' as const; + +export const REACT_PLATFORM = 'react' as const; +export const NATIVE_PLATFORMS = ['ios', 'android', 'windows', 'macos', 'win32'] as const; +export const ALL_PLATFORMS = [...NATIVE_PLATFORMS, REACT_PLATFORM] as const; diff --git a/scripts/src/pkgContext.ts b/scripts/src/pkgContext.ts index d820018886..f7dcd4396b 100644 --- a/scripts/src/pkgContext.ts +++ b/scripts/src/pkgContext.ts @@ -5,13 +5,16 @@ import type { Yarn } from '@yarnpkg/types'; import path from 'node:path'; import fs from 'node:fs'; import { styleText } from 'util'; +import type { NATIVE_PLATFORMS, REACT_PLATFORM } from './const.ts'; export type PackageType = 'library' | 'component' | 'app' | 'tooling'; -export type PlatformTarget = 'react' | 'win32' | 'macos' | 'ios' | 'android' | 'windows'; +export type NativeTargets = (typeof NATIVE_PLATFORMS)[number]; +export type PlatformTarget = typeof REACT_PLATFORM | NativeTargets; export type FurnConfig = { packageType?: PackageType; jestPlatform?: PlatformTarget; + platforms?: NativeTargets[]; depcheck?: { ignoreMatches?: string[]; ignorePatterns?: string[]; diff --git a/scripts/src/tasks/format.ts b/scripts/src/tasks/format.ts index 8878eeaec2..68202d2e50 100644 --- a/scripts/src/tasks/format.ts +++ b/scripts/src/tasks/format.ts @@ -17,6 +17,6 @@ export class FormatCommand extends Command { async execute() { const args = isFixMode(!this.check) ? [] : ['--check']; - return await runScript('oxfmt', ...args); + return await runScript('oxfmt', args); } } diff --git a/scripts/src/tasks/jest.ts b/scripts/src/tasks/jest.ts index bbffcfbd25..803b65e19e 100644 --- a/scripts/src/tasks/jest.ts +++ b/scripts/src/tasks/jest.ts @@ -1,6 +1,11 @@ import { Command, Option } from 'clipanion'; import { runScript } from '../utils/runScript.ts'; import { hasJestConfig } from '../preinstall/tool-versions.ts'; +import { PackageContext, type PlatformTarget } from '../pkgContext.ts'; +import { PLATFORM_ENV_VAR, ALL_PLATFORMS } from '../const.ts'; +import path from 'node:path'; + +const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']); export class JestCommand extends Command { static override paths = [['jest']]; @@ -14,15 +19,73 @@ export class JestCommand extends Command { args = Option.Proxy(); async execute() { + const context = PackageContext.init(process.cwd()); if (!hasJestConfig(process.cwd())) { console.warn('No jest configuration found, skipping jest.'); return; } - const args = ['--passWithNoTests']; + const args = ['--passWithNoTests', '--runTestsByPath']; if (process.env.TF_BUILD) { args.push('--runInBand'); } - return await runScript('jest', 'src/', ...args, ...this.args); + const defaultPlatform = getBaseJestPlatform(context); + const targetFiles = getJestTargetFiles(context, defaultPlatform); + if (targetFiles) { + for (const key in targetFiles) { + const platform = key as PlatformTarget; + const files = targetFiles[platform]; + if (files && files.length > 0) { + console.log(`Running jest for platform ${platform} with ${files.length} test files.`); + const options = platform !== defaultPlatform ? { env: { ...process.env, [PLATFORM_ENV_VAR]: platform } } : undefined; + const result = await runScript('jest', [...args, ...this.args, ...files], options); + if (result !== 0) { + return result; + } + } + } + } else { + console.warn('No test files found for any platform, skipping jest.'); + } + return 0; + } +} + +export function getBaseJestPlatform(context: PackageContext): PlatformTarget { + return context.manifest.furn?.jestPlatform ?? 'ios'; +} + +/** + * Get the list of test files for the given package context, defaulting to the base jest platform but allowing other platforms to + * execute in parallel if they have platform-specific test files. + */ +export function getJestTargetFiles( + context: PackageContext, + defaultPlatform: PlatformTarget, +): Partial> | undefined { + let results: Partial> | undefined = undefined; + + const files = context.files; + for (const file of files) { + let platform: string | undefined = undefined; + const parts = path.parse(file); + if (!SUPPORTED_EXTENSIONS.has(parts.ext)) { + continue; + } + const suffixParts = parts.name.split('.'); + for (let i = suffixParts.length - 1; i > 0; i--) { + const suffix = suffixParts[i].toLocaleLowerCase(); + if (!platform && ALL_PLATFORMS.includes(suffix as PlatformTarget)) { + platform = suffix; + } else if (suffix === 'test' || suffix === 'spec') { + const key = (platform as PlatformTarget) ?? defaultPlatform; + results ??= {}; + const fileList = (results[key] ??= []); + fileList.push(file); + } else { + break; + } + } } + return results; } diff --git a/scripts/src/utils/env.ts b/scripts/src/utils/env.ts index 4726019637..f7dd034fc1 100644 --- a/scripts/src/utils/env.ts +++ b/scripts/src/utils/env.ts @@ -1,12 +1,11 @@ +import { FIX_ENV_VAR } from '../const.ts'; + /** * Lage doesn't support cleanly passing parameters to sub scripts, so this allows our scripts that * support a "fix" mode to be toggled via an environment variable. This allows things like lint --fix * to be run from the root level without having to have duplicate scripts entries for each package. */ -// env variable to use -export const FIX_ENV_VAR = 'FURN_FIX_MODE'; - // standard helper function to check for fix mode export function isFixMode(fromParam?: boolean): boolean { return fromParam || Boolean(process.env[FIX_ENV_VAR]); diff --git a/scripts/src/utils/runScript.ts b/scripts/src/utils/runScript.ts index 50d60fdb90..bf7a86e788 100644 --- a/scripts/src/utils/runScript.ts +++ b/scripts/src/utils/runScript.ts @@ -1,4 +1,4 @@ -import { spawn } from 'node:child_process'; +import { spawn, type SpawnOptions } from 'node:child_process'; import { getProjectRoot } from './projectRoot.ts'; import os from 'node:os'; @@ -17,7 +17,7 @@ function getBinPath(command: string): string | undefined { return wdRoot.openModule(cmdModule).getBinPath(command); } -export async function runScript(command: string, ...args: string[]): Promise { +export async function runScript(command: string, args: string[], options?: SpawnOptions): Promise { const binPath = getBinPath(command); const verb = binPath ? process.execPath : yarnVerb; const spawnArgs = binPath ? [binPath, ...args] : ['exec', command, ...args]; @@ -27,6 +27,7 @@ export async function runScript(command: string, ...args: string[]): Promise { resolve(code ?? -1); }); diff --git a/scripts/targets/tsconfig.check.json b/scripts/targets/tsconfig.check.json index 4add80b804..a5dcaf12b0 100644 --- a/scripts/targets/tsconfig.check.json +++ b/scripts/targets/tsconfig.check.json @@ -1,6 +1,8 @@ { "extends": "@rnx-kit/tsconfig/tsconfig.node.json", "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", "noEmit": true, "allowSyntheticDefaultImports": true, "target": "es2022", From 40d55a0525e18948a970a7cacdf94808d867f357 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Tue, 16 Jun 2026 19:10:12 -0700 Subject: [PATCH 5/6] fix stack tests (that were previously not running at all --- packages/components/Stack/package.json | 3 ++- ...Utils.test.win32.ts => StackUtils.test.ts} | 26 +++++++++---------- .../__snapshots__/Stack.test.tsx.snap | 6 ++--- yarn.lock | 1 + 4 files changed, 19 insertions(+), 17 deletions(-) rename packages/components/Stack/src/{StackUtils.test.win32.ts => StackUtils.test.ts} (50%) diff --git a/packages/components/Stack/package.json b/packages/components/Stack/package.json index 8b247ccf13..6d3844e04d 100644 --- a/packages/components/Stack/package.json +++ b/packages/components/Stack/package.json @@ -44,6 +44,7 @@ "@fluentui-react-native/framework-base": "workspace:*", "@fluentui-react-native/scripts": "workspace:*", "@fluentui-react-native/text": "workspace:*", + "@office-iss/react-native-win32": "^0.81.8", "@react-native-community/cli": "^20.0.0", "@react-native-community/cli-platform-android": "^20.0.0", "@react-native-community/cli-platform-ios": "^20.0.0", @@ -80,7 +81,7 @@ } }, "furn": { - "jestPlatform": "android" + "jestPlatform": "win32" }, "rnx-kit": { "kitType": "library", diff --git a/packages/components/Stack/src/StackUtils.test.win32.ts b/packages/components/Stack/src/StackUtils.test.ts similarity index 50% rename from packages/components/Stack/src/StackUtils.test.win32.ts rename to packages/components/Stack/src/StackUtils.test.ts index be13510543..ba7f68775c 100644 --- a/packages/components/Stack/src/StackUtils.test.win32.ts +++ b/packages/components/Stack/src/StackUtils.test.ts @@ -9,35 +9,35 @@ describe('StackUtils', () => { } as unknown as Theme; it('returns a default value when given undefined', () => { - expect(parseGap(undefined, theme)).toEqual({ rowGap: { value: 0, unit: 'px' }, columnGap: { value: 0, unit: 'px' } }); + expect(parseGap(undefined, theme)).toEqual({ rowGap: 0, columnGap: 0 }); }); - it('returns a value with px when given a number', () => { - expect(parseGap(10, theme)).toEqual({ rowGap: { value: 10, unit: 'px' }, columnGap: { value: 10, unit: 'px' } }); + it('returns the numeric value when given a number', () => { + expect(parseGap(10, theme)).toEqual({ rowGap: 10, columnGap: 10 }); }); it('can parse a string with px', () => { - expect(parseGap('32px', theme)).toEqual({ rowGap: { value: 32, unit: 'px' }, columnGap: { value: 32, unit: 'px' } }); + expect(parseGap('32px', theme)).toEqual({ rowGap: 32, columnGap: 32 }); }); it('can parse a string with a float', () => { - expect(parseGap('20.5px', theme)).toEqual({ rowGap: { value: 20.5, unit: 'px' }, columnGap: { value: 20.5, unit: 'px' } }); + expect(parseGap('20.5px', theme)).toEqual({ rowGap: 20.5, columnGap: 20.5 }); }); it('parses the value from the theme when given a spacing key', () => { - expect(parseGap('m', theme)).toEqual({ rowGap: { value: 16, unit: 'em' }, columnGap: { value: 16, unit: 'em' } }); + expect(parseGap('m', theme)).toEqual({ rowGap: 16, columnGap: 16 }); }); it('can parse a string with both horizontal and vertical gap', () => { - expect(parseGap('30px 10px', theme)).toEqual({ rowGap: { value: 30, unit: 'px' }, columnGap: { value: 10, unit: 'px' } }); + expect(parseGap('30px 10px', theme)).toEqual({ rowGap: 30, columnGap: 10 }); }); it('defaults to px with a string with horizontal and vertical gap with no units', () => { - expect(parseGap('50 30', theme)).toEqual({ rowGap: { value: 50, unit: 'px' }, columnGap: { value: 30, unit: 'px' } }); + expect(parseGap('50 30', theme)).toEqual({ rowGap: 50, columnGap: 30 }); }); it('can parse a string with horizontal and vertical gap with one of them getting value from the theme when given a spacing key', () => { - expect(parseGap('50px m', theme)).toEqual({ rowGap: { value: 50, unit: 'px' }, columnGap: { value: 16, unit: 'em' } }); + expect(parseGap('50px m', theme)).toEqual({ rowGap: 50, columnGap: 16 }); }); }); @@ -57,12 +57,12 @@ describe('StackUtils', () => { expect(parsePadding(0, theme)).toEqual(0); }); - it('returns its argument when given a CSS-style padding', () => { - expect(parsePadding('10px', theme)).toEqual('10px'); + it('returns the numeric value when given a CSS-style padding', () => { + expect(parsePadding('10px', theme)).toEqual(10); }); - it('converts themed spacing keys to CSS-style paddings', () => { - expect(parsePadding('s2', theme)).toEqual('5px'); + it('converts themed spacing keys to their numeric padding value', () => { + expect(parsePadding('s2', theme)).toEqual(5); }); }); }); diff --git a/packages/components/Stack/src/__tests__/__snapshots__/Stack.test.tsx.snap b/packages/components/Stack/src/__tests__/__snapshots__/Stack.test.tsx.snap index c93c71bb55..cdf90f64c3 100644 --- a/packages/components/Stack/src/__tests__/__snapshots__/Stack.test.tsx.snap +++ b/packages/components/Stack/src/__tests__/__snapshots__/Stack.test.tsx.snap @@ -15,7 +15,7 @@ exports[`Stack with tokens 1`] = ` { "color": "#323130", "fontFamily": "Segoe UI", - "fontSize": 13, + "fontSize": 12, "fontWeight": "400", "margin": 0, } @@ -28,7 +28,7 @@ exports[`Stack with tokens 1`] = ` { "color": "#323130", "fontFamily": "Segoe UI", - "fontSize": 13, + "fontSize": 12, "fontWeight": "400", "margin": 0, "marginTop": 8, @@ -42,7 +42,7 @@ exports[`Stack with tokens 1`] = ` { "color": "#323130", "fontFamily": "Segoe UI", - "fontSize": 13, + "fontSize": 12, "fontWeight": "400", "margin": 0, "marginTop": 8, diff --git a/yarn.lock b/yarn.lock index ce3ba0605c..fe7547f456 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4804,6 +4804,7 @@ __metadata: "@fluentui-react-native/scripts": "workspace:*" "@fluentui-react-native/text": "workspace:*" "@fluentui-react-native/tokens": "workspace:*" + "@office-iss/react-native-win32": "npm:^0.81.8" "@react-native-community/cli": "npm:^20.0.0" "@react-native-community/cli-platform-android": "npm:^20.0.0" "@react-native-community/cli-platform-ios": "npm:^20.0.0" From 92baabf613e848e5fe8b9d59143ae3272b067632 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Wed, 17 Jun 2026 08:29:26 -0700 Subject: [PATCH 6/6] build up analyzer package and add use in button --- packages/agentic/agentic-analyzer/README.md | 5 + .../agentic/agentic-analyzer/jest.config.cjs | 1 + .../agentic/agentic-analyzer/oxlint.config.ts | 6 + .../agentic/agentic-analyzer/package.json | 75 ++++++ .../agentic/agentic-analyzer/src/index.ts | 11 + .../src/sentinel/createSentinelTheme.ts | 59 ++++ .../src/sentinel/resolveSemantic.ts | 57 ++++ .../src/sentinel/reverseMap.ts | 40 +++ .../src/sentinel/sentinel.test.ts | 68 +++++ .../agentic-analyzer/src/tests/validate.ts | 71 +++++ .../agentic/agentic-analyzer/tsconfig.json | 24 ++ packages/components/Button/package.json | 1 + .../validate.test.android.ts.snap | 253 ++++++++++++++++++ .../__snapshots__/validate.test.ios.ts.snap | 253 ++++++++++++++++++ .../__snapshots__/validate.test.macos.ts.snap | 253 ++++++++++++++++++ .../__snapshots__/validate.test.win32.ts.snap | 253 ++++++++++++++++++ .../validate.test.windows.ts.snap | 253 ++++++++++++++++++ .../Button/src/__tests__/metadata.ts | 88 ++++++ .../src/__tests__/validate.test.android.ts | 11 + .../Button/src/__tests__/validate.test.ios.ts | 11 + .../src/__tests__/validate.test.macos.ts | 11 + .../src/__tests__/validate.test.win32.ts | 11 + .../src/__tests__/validate.test.windows.ts | 11 + packages/components/Button/tsconfig.json | 3 + tsconfig.json | 3 + yarn.lock | 154 ++++++++++- 26 files changed, 1979 insertions(+), 7 deletions(-) create mode 100644 packages/agentic/agentic-analyzer/README.md create mode 100644 packages/agentic/agentic-analyzer/jest.config.cjs create mode 100644 packages/agentic/agentic-analyzer/oxlint.config.ts create mode 100644 packages/agentic/agentic-analyzer/package.json create mode 100644 packages/agentic/agentic-analyzer/src/index.ts create mode 100644 packages/agentic/agentic-analyzer/src/sentinel/createSentinelTheme.ts create mode 100644 packages/agentic/agentic-analyzer/src/sentinel/resolveSemantic.ts create mode 100644 packages/agentic/agentic-analyzer/src/sentinel/reverseMap.ts create mode 100644 packages/agentic/agentic-analyzer/src/sentinel/sentinel.test.ts create mode 100644 packages/agentic/agentic-analyzer/src/tests/validate.ts create mode 100644 packages/agentic/agentic-analyzer/tsconfig.json create mode 100644 packages/components/Button/src/__tests__/__snapshots__/validate.test.android.ts.snap create mode 100644 packages/components/Button/src/__tests__/__snapshots__/validate.test.ios.ts.snap create mode 100644 packages/components/Button/src/__tests__/__snapshots__/validate.test.macos.ts.snap create mode 100644 packages/components/Button/src/__tests__/__snapshots__/validate.test.win32.ts.snap create mode 100644 packages/components/Button/src/__tests__/__snapshots__/validate.test.windows.ts.snap create mode 100644 packages/components/Button/src/__tests__/metadata.ts create mode 100644 packages/components/Button/src/__tests__/validate.test.android.ts create mode 100644 packages/components/Button/src/__tests__/validate.test.ios.ts create mode 100644 packages/components/Button/src/__tests__/validate.test.macos.ts create mode 100644 packages/components/Button/src/__tests__/validate.test.win32.ts create mode 100644 packages/components/Button/src/__tests__/validate.test.windows.ts diff --git a/packages/agentic/agentic-analyzer/README.md b/packages/agentic/agentic-analyzer/README.md new file mode 100644 index 0000000000..423e3844c0 --- /dev/null +++ b/packages/agentic/agentic-analyzer/README.md @@ -0,0 +1,5 @@ +# @fluentui-react-native/agentic-analyzer + +A test/analysis toolkit for safely refactoring v1 components. It builds a **sentinel theme** whose every leaf value is unique, renders components with `@testing-library/react-native` to capture accessibility trees, computed styles, and snapshots, and **reverse-maps** resolved style values back to the exact theme/token slot they came from — so a refactor that changes _which_ token feeds a style is caught even when the final pixel value is unchanged. Also provides a strategy for multiplexing jest per platform. + +See [`PLAN.md`](./PLAN.md) for the full design and phased plan. diff --git a/packages/agentic/agentic-analyzer/jest.config.cjs b/packages/agentic/agentic-analyzer/jest.config.cjs new file mode 100644 index 0000000000..b391f5b662 --- /dev/null +++ b/packages/agentic/agentic-analyzer/jest.config.cjs @@ -0,0 +1 @@ +module.exports = require('@fluentui-react-native/scripts/jest-config'); diff --git a/packages/agentic/agentic-analyzer/oxlint.config.ts b/packages/agentic/agentic-analyzer/oxlint.config.ts new file mode 100644 index 0000000000..f4a0c4823d --- /dev/null +++ b/packages/agentic/agentic-analyzer/oxlint.config.ts @@ -0,0 +1,6 @@ +import baseConfig from '@fluentui-react-native/scripts/lint-config'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [baseConfig], +}); diff --git a/packages/agentic/agentic-analyzer/package.json b/packages/agentic/agentic-analyzer/package.json new file mode 100644 index 0000000000..5e990b4c54 --- /dev/null +++ b/packages/agentic/agentic-analyzer/package.json @@ -0,0 +1,75 @@ +{ + "name": "@fluentui-react-native/agentic-analyzer", + "version": "0.1.0", + "private": true, + "description": "Test/analysis toolkit for safely refactoring v1 components: sentinel themes, reverse token mapping, and per-platform jest multiplexing", + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/fluentui-react-native.git", + "directory": "packages/agentic/agentic-analyzer" + }, + "type": "module", + "main": "lib/index.js", + "module": "lib/index.js", + "types": "lib/index.d.ts", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "react-native": "./src/index.ts", + "import": "./lib/index.js", + "default": "./src/index.ts" + } + }, + "scripts": { + "build": "tsgo -b", + "clean": "fluentui-scripts clean", + "depcheck": "fluentui-scripts depcheck", + "format": "fluentui-scripts format", + "lint": "fluentui-scripts lint", + "test": "fluentui-scripts jest", + "update-snapshots": "fluentui-scripts jest -u" + }, + "dependencies": { + "@fluentui-react-native/default-theme": "workspace:*", + "@fluentui-react-native/theme": "workspace:*", + "@fluentui-react-native/theme-types": "workspace:*", + "@testing-library/react-native": "^13.2.0" + }, + "devDependencies": { + "@babel/core": "catalog:", + "@fluentui-react-native/scripts": "workspace:*", + "@react-native/babel-preset": "^0.81.0", + "@types/react": "~19.1.4", + "@types/react-test-renderer": "^19.1.0", + "react": "19.1.4", + "react-native": "^0.81.6", + "react-test-renderer": "19.1.4" + }, + "peerDependencies": { + "@types/react": "~18.2.0 || ~19.0.0 || ~19.1.4", + "react": "18.2.0 || 19.0.0 || 19.1.4", + "react-native": "^0.73.0 || ^0.74.0 || ^0.78.0 || ^0.81.6", + "react-test-renderer": "18.2.0 || 19.0.0 || 19.1.4" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + }, + "furn": { + "jestPlatform": "ios" + }, + "rnx-kit": { + "kitType": "library", + "extends": "@fluentui-react-native/scripts/kit-config", + "alignDeps": { + "capabilities": [ + "core", + "react", + "react-test-renderer" + ] + } + } +} diff --git a/packages/agentic/agentic-analyzer/src/index.ts b/packages/agentic/agentic-analyzer/src/index.ts new file mode 100644 index 0000000000..8d5fc5feb3 --- /dev/null +++ b/packages/agentic/agentic-analyzer/src/index.ts @@ -0,0 +1,11 @@ +// @fluentui-react-native/agentic-analyzer +// Public API surface for the sentinel-theme token resolver (PLAN capability #1). +export { resolveTokensForTheme } from './tests/validate.ts'; +export type { ResolveTokensOptions, TokenFunction } from './tests/validate.ts'; + +// Sentinel-theme building blocks, exported for advanced/standalone use. +export { createSentinelTheme } from './sentinel/createSentinelTheme.ts'; +export type { SentinelTheme } from './sentinel/createSentinelTheme.ts'; +export { SentinelAllocator } from './sentinel/reverseMap.ts'; +export type { ReverseMap } from './sentinel/reverseMap.ts'; +export { deepMerge, resolveSemantic } from './sentinel/resolveSemantic.ts'; diff --git a/packages/agentic/agentic-analyzer/src/sentinel/createSentinelTheme.ts b/packages/agentic/agentic-analyzer/src/sentinel/createSentinelTheme.ts new file mode 100644 index 0000000000..70838f53fe --- /dev/null +++ b/packages/agentic/agentic-analyzer/src/sentinel/createSentinelTheme.ts @@ -0,0 +1,59 @@ +import type { Theme } from '@fluentui-react-native/theme-types'; + +import { SentinelAllocator } from './reverseMap.ts'; +import type { ReverseMap } from './reverseMap.ts'; + +/** + * The result of sentinelizing a base theme: the cloned theme (every targeted + * leaf replaced with a unique sentinel) plus the reverse map that lets a + * resolved token tree be mapped back to semantic theme paths. + */ +export interface SentinelTheme { + theme: Theme; + reverseMap: ReverseMap; +} + +/** + * Build a SENTINEL theme from a base `Theme`. + * + * Every leaf under `theme.colors` is deep-cloned and replaced with a unique, + * type-valid sentinel color hex (e.g. `#FF0001`). A reverse map is produced that + * maps each sentinel value back to its semantic path (e.g. + * `"#FF0001" -> "colors.buttonBackground"`). + * + * The `colors` namespace is the primary target because the v1 token functions + * we pin read almost exclusively from `theme.colors.*`. Other namespaces + * (`shadows`, `typography`, `spacing`) are intentionally left untouched for this + * pass — they carry structured/typed values where a flat unique-string sentinel + * would be type-invalid and the token functions don't consume them by value. + * + * NOTE: `globalTokens.*` (a static JSON import, not part of the theme) is NOT + * sentinelized here. Those values resolve to their real static numbers and pass + * through `resolveTokensForTheme` unchanged. Sentinelizing them requires jest + * module mocking. + * TODO(agentic-analyzer PLAN phase 3): add a `createSentinelGlobalTokens()` + + * jest mock factory and extend the reverse map across the `globalTokens.*` + * namespace. + */ +export function createSentinelTheme(base: Theme): SentinelTheme { + const allocator = new SentinelAllocator(); + + // Structured clone of the whole theme so we never mutate the caller's base. + // structuredClone drops functions, but a `Theme` is plain data. + const theme = structuredClone(base) as Theme; + + // Sentinelize every leaf under `colors`. ThemeColorDefinition is a flat + // map of ColorValue, so each own enumerable key is a single leaf. + const colors = theme.colors as Record; + for (const key of Object.keys(colors)) { + const value = colors[key]; + // Only replace string-valued colors. Platform/semantic color objects + // (e.g. { semantic: '...' }) are left as-is — they are rare on the default + // web theme and a flat hex sentinel would not be type-valid for them. + if (typeof value === 'string') { + colors[key] = allocator.allocateColor(`colors.${key}`); + } + } + + return { theme, reverseMap: allocator.getReverseMap() }; +} diff --git a/packages/agentic/agentic-analyzer/src/sentinel/resolveSemantic.ts b/packages/agentic/agentic-analyzer/src/sentinel/resolveSemantic.ts new file mode 100644 index 0000000000..0f251a2628 --- /dev/null +++ b/packages/agentic/agentic-analyzer/src/sentinel/resolveSemantic.ts @@ -0,0 +1,57 @@ +import type { ReverseMap } from './reverseMap.ts'; + +/** + * Deep-merge `source` into `target` (mutating `target`), merging nested plain + * objects recursively and letting `source` leaves override `target` leaves. + * + * Arrays and non-plain values are treated as leaves (replaced wholesale). + */ +export function deepMerge(target: Record, source: Record): Record { + for (const key of Object.keys(source)) { + const sourceValue = source[key]; + const targetValue = target[key]; + if (isPlainObject(sourceValue) && isPlainObject(targetValue)) { + deepMerge(targetValue, sourceValue); + } else if (isPlainObject(sourceValue)) { + // Clone the incoming object so later merges don't alias the source tree. + target[key] = deepMerge({}, sourceValue); + } else { + target[key] = sourceValue; + } + } + return target; +} + +/** + * Recursively walk `value`, replacing any string leaf that is present in + * `reverseMap` with its semantic path name. All other leaves (numbers, literal + * strings like `'100%'`, variant names like `'bodySemibold'`, globalTokens-derived + * numbers) are returned unchanged. + * + * Returns a new plain, JSON-serializable structure; the input is not mutated. + */ +export function resolveSemantic(value: unknown, reverseMap: ReverseMap): unknown { + if (typeof value === 'string') { + return reverseMap.get(value) ?? value; + } + if (Array.isArray(value)) { + return value.map((item) => resolveSemantic(item, reverseMap)); + } + if (isPlainObject(value)) { + const result: Record = {}; + for (const key of Object.keys(value)) { + result[key] = resolveSemantic(value[key], reverseMap); + } + return result; + } + // numbers, booleans, null, undefined, functions — leave as-is + return value; +} + +function isPlainObject(value: unknown): value is Record { + if (typeof value !== 'object' || value === null) { + return false; + } + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} diff --git a/packages/agentic/agentic-analyzer/src/sentinel/reverseMap.ts b/packages/agentic/agentic-analyzer/src/sentinel/reverseMap.ts new file mode 100644 index 0000000000..e31d387c82 --- /dev/null +++ b/packages/agentic/agentic-analyzer/src/sentinel/reverseMap.ts @@ -0,0 +1,40 @@ +/** + * A reverse map from a unique sentinel leaf value back to the semantic theme path + * that produced it (e.g. `"#FF0001" -> "colors.buttonBackground"`). + * + * Only string sentinels are recorded — those are the values we can reliably detect + * when they reappear inside a resolved token tree and substitute with their + * semantic name. + */ +export type ReverseMap = Map; + +/** + * Allocates unique, type-valid sentinel color hex strings. + * + * Colors are reserved in a high-red band (`#FF0001`, `#FF0002`, …) that is + * extremely unlikely to collide with any real palette value. The allocator is + * stateful so every leaf gets a distinct value within a single sentinel theme. + */ +export class SentinelAllocator { + private colorCounter = 0; + private readonly reverseMap: ReverseMap = new Map(); + + /** + * Allocate the next unique sentinel color hex for the given semantic path and + * record the reverse mapping. + */ + allocateColor(semanticPath: string): string { + this.colorCounter += 1; + // Reserve the high-red band: #FF0001, #FF0002, ... up to #FFFFFF. + const hex = `#FF${this.colorCounter.toString(16).toUpperCase().padStart(4, '0')}`; + this.reverseMap.set(hex, semanticPath); + return hex; + } + + /** + * The accumulated sentinel-value -> semantic-path reverse map. + */ + getReverseMap(): ReverseMap { + return this.reverseMap; + } +} diff --git a/packages/agentic/agentic-analyzer/src/sentinel/sentinel.test.ts b/packages/agentic/agentic-analyzer/src/sentinel/sentinel.test.ts new file mode 100644 index 0000000000..7b9d45429f --- /dev/null +++ b/packages/agentic/agentic-analyzer/src/sentinel/sentinel.test.ts @@ -0,0 +1,68 @@ +import { defaultFluentTheme } from '@fluentui-react-native/default-theme'; +import type { Theme } from '@fluentui-react-native/theme-types'; + +import { resolveTokensForTheme } from '../tests/validate.ts'; +import { createSentinelTheme } from './createSentinelTheme.ts'; + +describe('createSentinelTheme', () => { + it('replaces every color leaf with a unique sentinel value', () => { + const { theme, reverseMap } = createSentinelTheme(defaultFluentTheme); + + const colorValues = Object.values(theme.colors).filter((v) => typeof v === 'string') as string[]; + const uniqueValues = new Set(colorValues); + // Every string color leaf must be unique. + expect(uniqueValues.size).toBe(colorValues.length); + // Every sentinel must round-trip through the reverse map. + for (const value of colorValues) { + expect(reverseMap.get(value)).toBe(`colors.${findColorKey(theme, value)}`); + } + }); + + it('does not mutate the base theme', () => { + const before = defaultFluentTheme.colors.buttonBackground; + createSentinelTheme(defaultFluentTheme); + expect(defaultFluentTheme.colors.buttonBackground).toBe(before); + }); +}); + +describe('resolveTokensForTheme', () => { + it('maps a color token back to its semantic theme path', () => { + const colorTokens = (t: Theme) => ({ backgroundColor: t.colors.buttonBackground }); + const resolved = resolveTokensForTheme({}, colorTokens); + expect(resolved.backgroundColor).toBe('colors.buttonBackground'); + }); + + it('deep-merges multiple token functions with later overriding earlier', () => { + const first = (t: Theme) => ({ backgroundColor: t.colors.buttonBackground, nested: { color: t.colors.buttonText } }); + const second = (t: Theme) => ({ nested: { color: t.colors.buttonBorder } }); + const resolved = resolveTokensForTheme({}, first, second) as Record; + expect(resolved.backgroundColor).toBe('colors.buttonBackground'); + expect(resolved.nested.color).toBe('colors.buttonBorder'); + }); + + it('leaves non-theme values (numbers, literal strings) unchanged', () => { + const tokens = () => ({ padding: 12, width: '100%', variant: 'bodySemibold' }); + const resolved = resolveTokensForTheme({}, tokens); + expect(resolved).toEqual({ padding: 12, width: '100%', variant: 'bodySemibold' }); + }); + + it('accepts literal token objects as well as functions', () => { + const resolved = resolveTokensForTheme({}, { iconSize: 16 }); + expect(resolved).toEqual({ iconSize: 16 }); + }); + + it('returns a JSON-serializable plain object', () => { + const tokens = (t: Theme) => ({ backgroundColor: t.colors.buttonBackground, padding: 8 }); + const resolved = resolveTokensForTheme({}, tokens); + expect(JSON.parse(JSON.stringify(resolved))).toEqual(resolved); + }); +}); + +function findColorKey(theme: Theme, value: string): string | undefined { + for (const [key, v] of Object.entries(theme.colors)) { + if (v === value) { + return key; + } + } + return undefined; +} diff --git a/packages/agentic/agentic-analyzer/src/tests/validate.ts b/packages/agentic/agentic-analyzer/src/tests/validate.ts new file mode 100644 index 0000000000..127ee6d3bd --- /dev/null +++ b/packages/agentic/agentic-analyzer/src/tests/validate.ts @@ -0,0 +1,71 @@ +import { defaultFluentTheme } from '@fluentui-react-native/default-theme'; +import type { Theme } from '@fluentui-react-native/theme-types'; + +import { createSentinelTheme } from '../sentinel/createSentinelTheme.ts'; +import { deepMerge, resolveSemantic } from '../sentinel/resolveSemantic.ts'; + +/** + * A token-settings argument: a function that derives a (partial) token tree from + * a theme, a literal (partial) token object, or a `string` (a named token-set + * reference, which carries no resolvable values and is skipped at runtime). + * + * This mirrors the v1 `TokenSettings = string | T | ((theme) => T)` + * shape used by component token functions such as `defaultButtonColorTokens`, + * without taking a dependency on `@fluentui-react-native/use-styling`, so those + * token functions are assignable to it directly. + */ +export type TokenFunction = string | ((theme: Theme) => Partial) | Partial; + +/** + * Options for {@link resolveTokensForTheme}. + */ +export interface ResolveTokensOptions { + /** + * Base theme to sentinelize. Defaults to the repo's real default Fluent theme + * (`defaultFluentTheme` from `@fluentui-react-native/default-theme`). + */ + theme?: Theme; + /** + * Optional platform hint. Reserved for future per-platform theme selection; + * currently unused. + */ + platform?: string; +} + +/** + * Resolve token functions against a SENTINEL theme and map the resolved leaves + * back to the semantic theme slot that fed them. + * + * Pipeline: + * 1. Resolve a base `Theme` (`options.theme` or the repo default). + * 2. Build a sentinel theme: every `theme.colors.*` leaf becomes a unique hex, + * with a reverse map of `sentinel -> "colors."`. + * 3. Evaluate each token argument against the sentinel theme (functions are + * called; objects are used as-is) and deep-merge the results in argument + * order (later overrides earlier). + * 4. Walk the merged tree and replace any leaf equal to a sentinel value with + * its semantic name. All other leaves are left unchanged. + * + * The returned value is a plain, JSON-serializable object suitable for + * snapshotting — e.g. `backgroundColor: "colors.buttonBackground"` instead of a + * resolved hex, pinning which theme slot feeds each token. + * + * Values sourced from `globalTokens.*` (static numbers/literals) resolve to + * their real values and are intentionally left unchanged in this pass. + * TODO(agentic-analyzer PLAN phase 3): sentinelize `globalTokens.*` via jest + * module mocking so those leaves also map to semantic names. + */ +export function resolveTokensForTheme(options: ResolveTokensOptions, ...tokenFunctions: TokenFunction[]): Record { + const base = options.theme ?? defaultFluentTheme; + const { theme, reverseMap } = createSentinelTheme(base); + + const merged: Record = {}; + for (const tokenFn of tokenFunctions) { + const resolved = typeof tokenFn === 'function' ? tokenFn(theme) : tokenFn; + if (resolved && typeof resolved === 'object') { + deepMerge(merged, resolved as Record); + } + } + + return resolveSemantic(merged, reverseMap) as Record; +} diff --git a/packages/agentic/agentic-analyzer/tsconfig.json b/packages/agentic/agentic-analyzer/tsconfig.json new file mode 100644 index 0000000000..8a0da3c655 --- /dev/null +++ b/packages/agentic/agentic-analyzer/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "@fluentui-react-native/scripts/tsconfig", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src", + "composite": true, + "tsBuildInfoFile": ".cache/tsconfig.tsbuildinfo" + }, + "include": ["src"], + "references": [ + { + "path": "../../framework/theme/tsconfig.json" + }, + { + "path": "../../theming/default-theme/tsconfig.json" + }, + { + "path": "../../theming/theme-types/tsconfig.json" + }, + { + "path": "../../../scripts/tsconfig.json" + } + ] +} diff --git a/packages/components/Button/package.json b/packages/components/Button/package.json index 7afc5d8b29..e400456263 100644 --- a/packages/components/Button/package.json +++ b/packages/components/Button/package.json @@ -52,6 +52,7 @@ }, "devDependencies": { "@babel/core": "catalog:", + "@fluentui-react-native/agentic-analyzer": "workspace:*", "@fluentui-react-native/framework-base": "workspace:*", "@fluentui-react-native/scripts": "workspace:*", "@fluentui-react-native/test-tools": "workspace:*", diff --git a/packages/components/Button/src/__tests__/__snapshots__/validate.test.android.ts.snap b/packages/components/Button/src/__tests__/__snapshots__/validate.test.android.ts.snap new file mode 100644 index 0000000000..bb9715f2a8 --- /dev/null +++ b/packages/components/Button/src/__tests__/__snapshots__/validate.test.android.ts.snap @@ -0,0 +1,253 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Button pinning tests (android) Button color tokens map to semantic theme slots 1`] = ` +{ + "backgroundColor": "colors.buttonBackground", + "borderColor": "colors.buttonBorder", + "color": "colors.buttonText", + "disabled": { + "backgroundColor": "colors.defaultDisabledBackground", + "borderColor": "colors.defaultDisabledBorder", + "color": "colors.defaultDisabledContent", + "iconColor": "colors.defaultDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.defaultFocusedBackground", + "borderColor": "colors.defaultFocusedBorder", + "color": "colors.defaultFocusedContent", + "icon": "colors.defaultFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.defaultHoveredBackground", + "borderColor": "colors.defaultHoveredBorder", + "color": "colors.defaultHoveredContent", + "iconColor": "colors.defaultHoveredIcon", + }, + "iconColor": undefined, + "pressed": { + "backgroundColor": "colors.defaultPressedBackground", + "borderColor": "colors.defaultPressedBorder", + "color": "colors.defaultPressedContent", + "iconColor": "colors.defaultPressedIcon", + }, + "primary": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.brandStroke1", + "color": "colors.neutralForegroundOnColor", + "disabled": { + "backgroundColor": "colors.brandBackgroundDisabled", + "color": "colors.neutralForegroundDisabled1", + "iconColor": "colors.neutralForegroundDisabled1", + }, + "focused": { + "backgroundColor": "colors.brandBackground", + "borderColor": undefined, + "color": "colors.neutralForegroundOnColor", + "iconColor": "colors.neutralForegroundOnColor", + }, + "iconColor": "colors.neutralForegroundOnColor", + "pressed": { + "backgroundColor": "colors.brandBackgroundPressed", + "color": "colors.neutralForegroundOnColor", + "iconColor": "colors.neutralForegroundOnColor", + }, + }, + "subtle": { + "backgroundColor": "colors.ghostBackground", + "borderColor": "colors.ghostBorder", + "color": "colors.ghostContent", + "disabled": { + "backgroundColor": "colors.ghostDisabledBackground", + "borderColor": "colors.ghostDisabledBorder", + "color": "colors.ghostDisabledContent", + "iconColor": "colors.ghostDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.ghostFocusedBackground", + "borderColor": "colors.ghostFocusedBorder", + "color": "colors.ghostFocusedContent", + "icon": "colors.ghostFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.ghostHoveredBackground", + "borderColor": "colors.ghostHoveredBorder", + "color": "colors.ghostHoveredContent", + "iconColor": "colors.ghostHoveredIcon", + }, + "iconColor": "colors.ghostIcon", + "pressed": { + "backgroundColor": "colors.ghostPressedBackground", + "borderColor": "colors.ghostPressedBorder", + "color": "colors.ghostPressedContent", + "icon": "colors.ghostPressedIcon", + }, + }, +} +`; + +exports[`Button pinning tests (android) Button tokens match snapshot 1`] = ` +{ + "backgroundColor": "colors.buttonBackground", + "block": { + "width": "100%", + }, + "borderColor": "colors.buttonBorder", + "circular": { + "borderRadius": 9999, + }, + "color": "colors.buttonText", + "disabled": { + "backgroundColor": "colors.defaultDisabledBackground", + "borderColor": "colors.defaultDisabledBorder", + "color": "colors.defaultDisabledContent", + "iconColor": "colors.defaultDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.defaultFocusedBackground", + "borderColor": "colors.defaultFocusedBorder", + "color": "colors.defaultFocusedContent", + "icon": "colors.defaultFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.defaultHoveredBackground", + "borderColor": "colors.defaultHoveredBorder", + "color": "colors.defaultHoveredContent", + "iconColor": "colors.defaultHoveredIcon", + }, + "iconColor": undefined, + "large": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 8, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 16, + }, + "hasIconAfter": { + "spacingIconContentAfter": 6, + }, + "hasIconBefore": { + "spacingIconContentBefore": 6, + }, + "minWidth": 96, + "paddingHorizontal": 15, + }, + "iconSize": 20, + "padding": 7, + "variant": "subheaderSemibold", + }, + "medium": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 6, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 12, + }, + "hasIconAfter": { + "spacingIconContentAfter": 6, + }, + "hasIconBefore": { + "spacingIconContentBefore": 6, + }, + "minWidth": 96, + "paddingHorizontal": 11, + "variant": "bodySemibold", + }, + "iconSize": 16, + "padding": 5, + }, + "pressed": { + "backgroundColor": "colors.defaultPressedBackground", + "borderColor": "colors.defaultPressedBorder", + "color": "colors.defaultPressedContent", + "iconColor": "colors.defaultPressedIcon", + }, + "primary": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.brandStroke1", + "color": "colors.neutralForegroundOnColor", + "disabled": { + "backgroundColor": "colors.brandBackgroundDisabled", + "color": "colors.neutralForegroundDisabled1", + "iconColor": "colors.neutralForegroundDisabled1", + }, + "focused": { + "backgroundColor": "colors.brandBackground", + "borderColor": undefined, + "color": "colors.neutralForegroundOnColor", + "iconColor": "colors.neutralForegroundOnColor", + }, + "iconColor": "colors.neutralForegroundOnColor", + "pressed": { + "backgroundColor": "colors.brandBackgroundPressed", + "color": "colors.neutralForegroundOnColor", + "iconColor": "colors.neutralForegroundOnColor", + }, + }, + "rounded": { + "borderRadius": 4, + }, + "small": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 4, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 8, + }, + "hasIconAfter": { + "spacingIconContentAfter": 4, + }, + "hasIconBefore": { + "spacingIconContentBefore": 4, + }, + "minHeight": 24, + "minWidth": 64, + "paddingHorizontal": 7, + "variant": "secondaryStandard", + }, + "iconSize": 16, + "padding": 3, + }, + "square": { + "borderRadius": 0, + }, + "subtle": { + "backgroundColor": "colors.ghostBackground", + "borderColor": "colors.ghostBorder", + "color": "colors.ghostContent", + "disabled": { + "backgroundColor": "colors.ghostDisabledBackground", + "borderColor": "colors.ghostDisabledBorder", + "color": "colors.ghostDisabledContent", + "iconColor": "colors.ghostDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.ghostFocusedBackground", + "borderColor": "colors.ghostFocusedBorder", + "color": "colors.ghostFocusedContent", + "icon": "colors.ghostFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.ghostHoveredBackground", + "borderColor": "colors.ghostHoveredBorder", + "color": "colors.ghostHoveredContent", + "iconColor": "colors.ghostHoveredIcon", + }, + "iconColor": "colors.ghostIcon", + "pressed": { + "backgroundColor": "colors.ghostPressedBackground", + "borderColor": "colors.ghostPressedBorder", + "color": "colors.ghostPressedContent", + "icon": "colors.ghostPressedIcon", + }, + }, +} +`; diff --git a/packages/components/Button/src/__tests__/__snapshots__/validate.test.ios.ts.snap b/packages/components/Button/src/__tests__/__snapshots__/validate.test.ios.ts.snap new file mode 100644 index 0000000000..4b0beedc6d --- /dev/null +++ b/packages/components/Button/src/__tests__/__snapshots__/validate.test.ios.ts.snap @@ -0,0 +1,253 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Button pinning tests (ios) Button color tokens map to semantic theme slots 1`] = ` +{ + "backgroundColor": "colors.buttonBackground", + "borderColor": "colors.buttonBorder", + "color": "colors.buttonText", + "disabled": { + "backgroundColor": "colors.defaultDisabledBackground", + "borderColor": "colors.defaultDisabledBorder", + "color": "colors.defaultDisabledContent", + "iconColor": "colors.defaultDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.defaultFocusedBackground", + "borderColor": "colors.defaultFocusedBorder", + "color": "colors.defaultFocusedContent", + "icon": "colors.defaultFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.defaultHoveredBackground", + "borderColor": "colors.defaultHoveredBorder", + "color": "colors.defaultHoveredContent", + "iconColor": "colors.defaultHoveredIcon", + }, + "iconColor": undefined, + "pressed": { + "backgroundColor": "colors.defaultPressedBackground", + "borderColor": "colors.defaultPressedBorder", + "color": "colors.defaultPressedContent", + "iconColor": "colors.defaultPressedIcon", + }, + "primary": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.brandStroke1", + "color": "colors.neutralForegroundOnColor", + "disabled": { + "backgroundColor": "colors.brandBackgroundDisabled", + "color": "colors.neutralForegroundDisabled1", + "iconColor": "colors.neutralForegroundDisabled1", + }, + "focused": { + "backgroundColor": "colors.brandBackground", + "borderColor": undefined, + "color": "colors.neutralForegroundOnColor", + "iconColor": "colors.neutralForegroundOnColor", + }, + "iconColor": "colors.neutralForegroundOnColor", + "pressed": { + "backgroundColor": "colors.brandBackgroundPressed", + "color": "colors.neutralForegroundOnColor", + "iconColor": "colors.neutralForegroundOnColor", + }, + }, + "subtle": { + "backgroundColor": "colors.ghostBackground", + "borderColor": "colors.ghostBorder", + "color": "colors.ghostContent", + "disabled": { + "backgroundColor": "colors.ghostDisabledBackground", + "borderColor": "colors.ghostDisabledBorder", + "color": "colors.ghostDisabledContent", + "iconColor": "colors.ghostDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.ghostFocusedBackground", + "borderColor": "colors.ghostFocusedBorder", + "color": "colors.ghostFocusedContent", + "icon": "colors.ghostFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.ghostHoveredBackground", + "borderColor": "colors.ghostHoveredBorder", + "color": "colors.ghostHoveredContent", + "iconColor": "colors.ghostHoveredIcon", + }, + "iconColor": "colors.ghostIcon", + "pressed": { + "backgroundColor": "colors.ghostPressedBackground", + "borderColor": "colors.ghostPressedBorder", + "color": "colors.ghostPressedContent", + "icon": "colors.ghostPressedIcon", + }, + }, +} +`; + +exports[`Button pinning tests (ios) Button tokens match snapshot 1`] = ` +{ + "backgroundColor": "colors.buttonBackground", + "block": { + "width": "100%", + }, + "borderColor": "colors.buttonBorder", + "circular": { + "borderRadius": 9999, + }, + "color": "colors.buttonText", + "disabled": { + "backgroundColor": "colors.defaultDisabledBackground", + "borderColor": "colors.defaultDisabledBorder", + "color": "colors.defaultDisabledContent", + "iconColor": "colors.defaultDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.defaultFocusedBackground", + "borderColor": "colors.defaultFocusedBorder", + "color": "colors.defaultFocusedContent", + "icon": "colors.defaultFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.defaultHoveredBackground", + "borderColor": "colors.defaultHoveredBorder", + "color": "colors.defaultHoveredContent", + "iconColor": "colors.defaultHoveredIcon", + }, + "iconColor": undefined, + "large": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 8, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 16, + }, + "hasIconAfter": { + "spacingIconContentAfter": 6, + }, + "hasIconBefore": { + "spacingIconContentBefore": 6, + }, + "minWidth": 96, + "paddingHorizontal": 15, + }, + "iconSize": 20, + "padding": 7, + "variant": "subheaderSemibold", + }, + "medium": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 6, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 12, + }, + "hasIconAfter": { + "spacingIconContentAfter": 6, + }, + "hasIconBefore": { + "spacingIconContentBefore": 6, + }, + "minWidth": 96, + "paddingHorizontal": 11, + "variant": "bodySemibold", + }, + "iconSize": 16, + "padding": 5, + }, + "pressed": { + "backgroundColor": "colors.defaultPressedBackground", + "borderColor": "colors.defaultPressedBorder", + "color": "colors.defaultPressedContent", + "iconColor": "colors.defaultPressedIcon", + }, + "primary": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.brandStroke1", + "color": "colors.neutralForegroundOnColor", + "disabled": { + "backgroundColor": "colors.brandBackgroundDisabled", + "color": "colors.neutralForegroundDisabled1", + "iconColor": "colors.neutralForegroundDisabled1", + }, + "focused": { + "backgroundColor": "colors.brandBackground", + "borderColor": undefined, + "color": "colors.neutralForegroundOnColor", + "iconColor": "colors.neutralForegroundOnColor", + }, + "iconColor": "colors.neutralForegroundOnColor", + "pressed": { + "backgroundColor": "colors.brandBackgroundPressed", + "color": "colors.neutralForegroundOnColor", + "iconColor": "colors.neutralForegroundOnColor", + }, + }, + "rounded": { + "borderRadius": 4, + }, + "small": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 4, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 8, + }, + "hasIconAfter": { + "spacingIconContentAfter": 4, + }, + "hasIconBefore": { + "spacingIconContentBefore": 4, + }, + "minHeight": 24, + "minWidth": 64, + "paddingHorizontal": 7, + "variant": "secondaryStandard", + }, + "iconSize": 16, + "padding": 3, + }, + "square": { + "borderRadius": 0, + }, + "subtle": { + "backgroundColor": "colors.ghostBackground", + "borderColor": "colors.ghostBorder", + "color": "colors.ghostContent", + "disabled": { + "backgroundColor": "colors.ghostDisabledBackground", + "borderColor": "colors.ghostDisabledBorder", + "color": "colors.ghostDisabledContent", + "iconColor": "colors.ghostDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.ghostFocusedBackground", + "borderColor": "colors.ghostFocusedBorder", + "color": "colors.ghostFocusedContent", + "icon": "colors.ghostFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.ghostHoveredBackground", + "borderColor": "colors.ghostHoveredBorder", + "color": "colors.ghostHoveredContent", + "iconColor": "colors.ghostHoveredIcon", + }, + "iconColor": "colors.ghostIcon", + "pressed": { + "backgroundColor": "colors.ghostPressedBackground", + "borderColor": "colors.ghostPressedBorder", + "color": "colors.ghostPressedContent", + "icon": "colors.ghostPressedIcon", + }, + }, +} +`; diff --git a/packages/components/Button/src/__tests__/__snapshots__/validate.test.macos.ts.snap b/packages/components/Button/src/__tests__/__snapshots__/validate.test.macos.ts.snap new file mode 100644 index 0000000000..e8304e920f --- /dev/null +++ b/packages/components/Button/src/__tests__/__snapshots__/validate.test.macos.ts.snap @@ -0,0 +1,253 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Button pinning tests (macos) Button color tokens map to semantic theme slots 1`] = ` +{ + "backgroundColor": "colors.buttonBackground", + "borderColor": "colors.buttonBorder", + "color": "colors.buttonText", + "disabled": { + "backgroundColor": "colors.defaultDisabledBackground", + "borderColor": "colors.defaultDisabledBorder", + "color": "colors.defaultDisabledContent", + "iconColor": "colors.defaultDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.defaultFocusedBackground", + "borderColor": "colors.defaultFocusedBorder", + "color": "colors.defaultFocusedContent", + "icon": "colors.defaultFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.defaultHoveredBackground", + "borderColor": "colors.defaultHoveredBorder", + "color": "colors.defaultHoveredContent", + "iconColor": "colors.defaultHoveredIcon", + }, + "iconColor": undefined, + "pressed": { + "backgroundColor": "colors.defaultPressedBackground", + "borderColor": "colors.defaultPressedBorder", + "color": "colors.defaultPressedContent", + "iconColor": "colors.defaultPressedIcon", + }, + "primary": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.brandStroke1", + "color": undefined, + "disabled": { + "backgroundColor": undefined, + "color": undefined, + "iconColor": undefined, + }, + "focused": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.strokeFocus2", + "color": undefined, + "iconColor": undefined, + }, + "iconColor": undefined, + "pressed": { + "backgroundColor": "colors.brandBackgroundPressed", + "color": undefined, + "iconColor": undefined, + }, + }, + "subtle": { + "backgroundColor": "colors.ghostBackground", + "borderColor": "colors.ghostBorder", + "color": "colors.ghostContent", + "disabled": { + "backgroundColor": "colors.ghostDisabledBackground", + "borderColor": "colors.ghostDisabledBorder", + "color": "colors.ghostDisabledContent", + "iconColor": "colors.ghostDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.ghostFocusedBackground", + "borderColor": "colors.ghostFocusedBorder", + "color": "colors.ghostFocusedContent", + "icon": "colors.ghostFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.ghostHoveredBackground", + "borderColor": "colors.ghostHoveredBorder", + "color": "colors.ghostHoveredContent", + "iconColor": "colors.ghostHoveredIcon", + }, + "iconColor": "colors.ghostIcon", + "pressed": { + "backgroundColor": "colors.ghostPressedBackground", + "borderColor": "colors.ghostPressedBorder", + "color": "colors.ghostPressedContent", + "icon": "colors.ghostPressedIcon", + }, + }, +} +`; + +exports[`Button pinning tests (macos) Button tokens match snapshot 1`] = ` +{ + "backgroundColor": "colors.buttonBackground", + "block": { + "width": "100%", + }, + "borderColor": "colors.buttonBorder", + "circular": { + "borderRadius": 9999, + }, + "color": "colors.buttonText", + "disabled": { + "backgroundColor": "colors.defaultDisabledBackground", + "borderColor": "colors.defaultDisabledBorder", + "color": "colors.defaultDisabledContent", + "iconColor": "colors.defaultDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.defaultFocusedBackground", + "borderColor": "colors.defaultFocusedBorder", + "color": "colors.defaultFocusedContent", + "icon": "colors.defaultFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.defaultHoveredBackground", + "borderColor": "colors.defaultHoveredBorder", + "color": "colors.defaultHoveredContent", + "iconColor": "colors.defaultHoveredIcon", + }, + "iconColor": undefined, + "large": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 8, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 16, + }, + "hasIconAfter": { + "spacingIconContentAfter": 6, + }, + "hasIconBefore": { + "spacingIconContentBefore": 6, + }, + "minWidth": 96, + "paddingHorizontal": 15, + }, + "iconSize": 20, + "padding": 7, + "variant": "subheaderSemibold", + }, + "medium": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 6, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 12, + }, + "hasIconAfter": { + "spacingIconContentAfter": 6, + }, + "hasIconBefore": { + "spacingIconContentBefore": 6, + }, + "minWidth": 96, + "paddingHorizontal": 11, + "variant": "bodySemibold", + }, + "iconSize": 16, + "padding": 5, + }, + "pressed": { + "backgroundColor": "colors.defaultPressedBackground", + "borderColor": "colors.defaultPressedBorder", + "color": "colors.defaultPressedContent", + "iconColor": "colors.defaultPressedIcon", + }, + "primary": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.brandStroke1", + "color": undefined, + "disabled": { + "backgroundColor": undefined, + "color": undefined, + "iconColor": undefined, + }, + "focused": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.strokeFocus2", + "color": undefined, + "iconColor": undefined, + }, + "iconColor": undefined, + "pressed": { + "backgroundColor": "colors.brandBackgroundPressed", + "color": undefined, + "iconColor": undefined, + }, + }, + "rounded": { + "borderRadius": 4, + }, + "small": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 4, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 8, + }, + "hasIconAfter": { + "spacingIconContentAfter": 4, + }, + "hasIconBefore": { + "spacingIconContentBefore": 4, + }, + "minHeight": 24, + "minWidth": 64, + "paddingHorizontal": 7, + "variant": "secondaryStandard", + }, + "iconSize": 16, + "padding": 3, + }, + "square": { + "borderRadius": 0, + }, + "subtle": { + "backgroundColor": "colors.ghostBackground", + "borderColor": "colors.ghostBorder", + "color": "colors.ghostContent", + "disabled": { + "backgroundColor": "colors.ghostDisabledBackground", + "borderColor": "colors.ghostDisabledBorder", + "color": "colors.ghostDisabledContent", + "iconColor": "colors.ghostDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.ghostFocusedBackground", + "borderColor": "colors.ghostFocusedBorder", + "color": "colors.ghostFocusedContent", + "icon": "colors.ghostFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.ghostHoveredBackground", + "borderColor": "colors.ghostHoveredBorder", + "color": "colors.ghostHoveredContent", + "iconColor": "colors.ghostHoveredIcon", + }, + "iconColor": "colors.ghostIcon", + "pressed": { + "backgroundColor": "colors.ghostPressedBackground", + "borderColor": "colors.ghostPressedBorder", + "color": "colors.ghostPressedContent", + "icon": "colors.ghostPressedIcon", + }, + }, +} +`; diff --git a/packages/components/Button/src/__tests__/__snapshots__/validate.test.win32.ts.snap b/packages/components/Button/src/__tests__/__snapshots__/validate.test.win32.ts.snap new file mode 100644 index 0000000000..28fcdb88ae --- /dev/null +++ b/packages/components/Button/src/__tests__/__snapshots__/validate.test.win32.ts.snap @@ -0,0 +1,253 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Button pinning tests (win32) Button color tokens map to semantic theme slots 1`] = ` +{ + "backgroundColor": "colors.buttonBackground", + "borderColor": "colors.buttonBorder", + "color": "colors.buttonText", + "disabled": { + "backgroundColor": "colors.defaultDisabledBackground", + "borderColor": "colors.defaultDisabledBorder", + "color": "colors.defaultDisabledContent", + "iconColor": "colors.defaultDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.defaultFocusedBackground", + "borderColor": "colors.defaultFocusedBorder", + "color": "colors.defaultFocusedContent", + "icon": "colors.defaultFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.defaultHoveredBackground", + "borderColor": "colors.defaultHoveredBorder", + "color": "colors.defaultHoveredContent", + "iconColor": "colors.defaultHoveredIcon", + }, + "iconColor": undefined, + "pressed": { + "backgroundColor": "colors.defaultPressedBackground", + "borderColor": "colors.defaultPressedBorder", + "color": "colors.defaultPressedContent", + "iconColor": "colors.defaultPressedIcon", + }, + "primary": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.brandStroke1", + "color": undefined, + "disabled": { + "backgroundColor": undefined, + "color": undefined, + "iconColor": undefined, + }, + "focused": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.strokeFocus2", + "color": undefined, + "iconColor": undefined, + }, + "iconColor": undefined, + "pressed": { + "backgroundColor": "colors.brandBackgroundPressed", + "color": undefined, + "iconColor": undefined, + }, + }, + "subtle": { + "backgroundColor": "colors.ghostBackground", + "borderColor": "colors.ghostBorder", + "color": "colors.ghostContent", + "disabled": { + "backgroundColor": "colors.ghostDisabledBackground", + "borderColor": "colors.ghostDisabledBorder", + "color": "colors.ghostDisabledContent", + "iconColor": "colors.ghostDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.ghostFocusedBackground", + "borderColor": "colors.ghostFocusedBorder", + "color": "colors.ghostFocusedContent", + "icon": "colors.ghostFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.ghostHoveredBackground", + "borderColor": "colors.ghostHoveredBorder", + "color": "colors.ghostHoveredContent", + "iconColor": "colors.ghostHoveredIcon", + }, + "iconColor": "colors.ghostIcon", + "pressed": { + "backgroundColor": "colors.ghostPressedBackground", + "borderColor": "colors.ghostPressedBorder", + "color": "colors.ghostPressedContent", + "icon": "colors.ghostPressedIcon", + }, + }, +} +`; + +exports[`Button pinning tests (win32) Button tokens match snapshot 1`] = ` +{ + "backgroundColor": "colors.buttonBackground", + "block": { + "width": "100%", + }, + "borderColor": "colors.buttonBorder", + "circular": { + "borderRadius": 9999, + }, + "color": "colors.buttonText", + "disabled": { + "backgroundColor": "colors.defaultDisabledBackground", + "borderColor": "colors.defaultDisabledBorder", + "color": "colors.defaultDisabledContent", + "iconColor": "colors.defaultDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.defaultFocusedBackground", + "borderColor": "colors.defaultFocusedBorder", + "color": "colors.defaultFocusedContent", + "icon": "colors.defaultFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.defaultHoveredBackground", + "borderColor": "colors.defaultHoveredBorder", + "color": "colors.defaultHoveredContent", + "iconColor": "colors.defaultHoveredIcon", + }, + "iconColor": undefined, + "large": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 8, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 16, + }, + "hasIconAfter": { + "spacingIconContentAfter": 6, + }, + "hasIconBefore": { + "spacingIconContentBefore": 6, + }, + "minWidth": 96, + "paddingHorizontal": 15, + }, + "iconSize": 20, + "padding": 7, + "variant": "subheaderSemibold", + }, + "medium": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 6, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 12, + }, + "hasIconAfter": { + "spacingIconContentAfter": 6, + }, + "hasIconBefore": { + "spacingIconContentBefore": 6, + }, + "minWidth": 96, + "paddingHorizontal": 11, + "variant": "bodySemibold", + }, + "iconSize": 16, + "padding": 5, + }, + "pressed": { + "backgroundColor": "colors.defaultPressedBackground", + "borderColor": "colors.defaultPressedBorder", + "color": "colors.defaultPressedContent", + "iconColor": "colors.defaultPressedIcon", + }, + "primary": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.brandStroke1", + "color": undefined, + "disabled": { + "backgroundColor": undefined, + "color": undefined, + "iconColor": undefined, + }, + "focused": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.strokeFocus2", + "color": undefined, + "iconColor": undefined, + }, + "iconColor": undefined, + "pressed": { + "backgroundColor": "colors.brandBackgroundPressed", + "color": undefined, + "iconColor": undefined, + }, + }, + "rounded": { + "borderRadius": 4, + }, + "small": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 4, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 8, + }, + "hasIconAfter": { + "spacingIconContentAfter": 4, + }, + "hasIconBefore": { + "spacingIconContentBefore": 4, + }, + "minHeight": 24, + "minWidth": 64, + "paddingHorizontal": 7, + "variant": "secondaryStandard", + }, + "iconSize": 16, + "padding": 3, + }, + "square": { + "borderRadius": 0, + }, + "subtle": { + "backgroundColor": "colors.ghostBackground", + "borderColor": "colors.ghostBorder", + "color": "colors.ghostContent", + "disabled": { + "backgroundColor": "colors.ghostDisabledBackground", + "borderColor": "colors.ghostDisabledBorder", + "color": "colors.ghostDisabledContent", + "iconColor": "colors.ghostDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.ghostFocusedBackground", + "borderColor": "colors.ghostFocusedBorder", + "color": "colors.ghostFocusedContent", + "icon": "colors.ghostFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.ghostHoveredBackground", + "borderColor": "colors.ghostHoveredBorder", + "color": "colors.ghostHoveredContent", + "iconColor": "colors.ghostHoveredIcon", + }, + "iconColor": "colors.ghostIcon", + "pressed": { + "backgroundColor": "colors.ghostPressedBackground", + "borderColor": "colors.ghostPressedBorder", + "color": "colors.ghostPressedContent", + "icon": "colors.ghostPressedIcon", + }, + }, +} +`; diff --git a/packages/components/Button/src/__tests__/__snapshots__/validate.test.windows.ts.snap b/packages/components/Button/src/__tests__/__snapshots__/validate.test.windows.ts.snap new file mode 100644 index 0000000000..b0b43f1d2e --- /dev/null +++ b/packages/components/Button/src/__tests__/__snapshots__/validate.test.windows.ts.snap @@ -0,0 +1,253 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Button pinning tests (windows) Button color tokens map to semantic theme slots 1`] = ` +{ + "backgroundColor": "colors.buttonBackground", + "borderColor": "colors.buttonBorder", + "color": "colors.buttonText", + "disabled": { + "backgroundColor": "colors.defaultDisabledBackground", + "borderColor": "colors.defaultDisabledBorder", + "color": "colors.defaultDisabledContent", + "iconColor": "colors.defaultDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.defaultFocusedBackground", + "borderColor": "colors.defaultFocusedBorder", + "color": "colors.defaultFocusedContent", + "icon": "colors.defaultFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.defaultHoveredBackground", + "borderColor": "colors.defaultHoveredBorder", + "color": "colors.defaultHoveredContent", + "iconColor": "colors.defaultHoveredIcon", + }, + "iconColor": undefined, + "pressed": { + "backgroundColor": "colors.defaultPressedBackground", + "borderColor": "colors.defaultPressedBorder", + "color": "colors.defaultPressedContent", + "iconColor": "colors.defaultPressedIcon", + }, + "primary": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.brandStroke1", + "color": undefined, + "disabled": { + "backgroundColor": undefined, + "color": undefined, + "iconColor": undefined, + }, + "focused": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.strokeFocus2", + "color": undefined, + "iconColor": undefined, + }, + "iconColor": undefined, + "pressed": { + "backgroundColor": "colors.brandBackgroundPressed", + "color": undefined, + "iconColor": undefined, + }, + }, + "subtle": { + "backgroundColor": "colors.ghostBackground", + "borderColor": "colors.ghostBorder", + "color": "colors.ghostContent", + "disabled": { + "backgroundColor": "colors.ghostDisabledBackground", + "borderColor": "colors.ghostDisabledBorder", + "color": "colors.ghostDisabledContent", + "iconColor": "colors.ghostDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.ghostFocusedBackground", + "borderColor": "colors.ghostFocusedBorder", + "color": "colors.ghostFocusedContent", + "icon": "colors.ghostFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.ghostHoveredBackground", + "borderColor": "colors.ghostHoveredBorder", + "color": "colors.ghostHoveredContent", + "iconColor": "colors.ghostHoveredIcon", + }, + "iconColor": "colors.ghostIcon", + "pressed": { + "backgroundColor": "colors.ghostPressedBackground", + "borderColor": "colors.ghostPressedBorder", + "color": "colors.ghostPressedContent", + "icon": "colors.ghostPressedIcon", + }, + }, +} +`; + +exports[`Button pinning tests (windows) Button tokens match snapshot 1`] = ` +{ + "backgroundColor": "colors.buttonBackground", + "block": { + "width": "100%", + }, + "borderColor": "colors.buttonBorder", + "circular": { + "borderRadius": 9999, + }, + "color": "colors.buttonText", + "disabled": { + "backgroundColor": "colors.defaultDisabledBackground", + "borderColor": "colors.defaultDisabledBorder", + "color": "colors.defaultDisabledContent", + "iconColor": "colors.defaultDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.defaultFocusedBackground", + "borderColor": "colors.defaultFocusedBorder", + "color": "colors.defaultFocusedContent", + "icon": "colors.defaultFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.defaultHoveredBackground", + "borderColor": "colors.defaultHoveredBorder", + "color": "colors.defaultHoveredContent", + "iconColor": "colors.defaultHoveredIcon", + }, + "iconColor": undefined, + "large": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 8, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 16, + }, + "hasIconAfter": { + "spacingIconContentAfter": 6, + }, + "hasIconBefore": { + "spacingIconContentBefore": 6, + }, + "minWidth": 96, + "paddingHorizontal": 15, + }, + "iconSize": 20, + "padding": 7, + "variant": "subheaderSemibold", + }, + "medium": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 6, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 12, + }, + "hasIconAfter": { + "spacingIconContentAfter": 6, + }, + "hasIconBefore": { + "spacingIconContentBefore": 6, + }, + "minWidth": 96, + "paddingHorizontal": 11, + "variant": "bodySemibold", + }, + "iconSize": 16, + "padding": 5, + }, + "pressed": { + "backgroundColor": "colors.defaultPressedBackground", + "borderColor": "colors.defaultPressedBorder", + "color": "colors.defaultPressedContent", + "iconColor": "colors.defaultPressedIcon", + }, + "primary": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.brandStroke1", + "color": undefined, + "disabled": { + "backgroundColor": undefined, + "color": undefined, + "iconColor": undefined, + }, + "focused": { + "backgroundColor": "colors.brandBackground", + "borderColor": "colors.strokeFocus2", + "color": undefined, + "iconColor": undefined, + }, + "iconColor": undefined, + "pressed": { + "backgroundColor": "colors.brandBackgroundPressed", + "color": undefined, + "iconColor": undefined, + }, + }, + "rounded": { + "borderRadius": 4, + }, + "small": { + "borderWidth": 1, + "focused": { + "borderWidth": 0, + "padding": 4, + }, + "hasContent": { + "focused": { + "paddingHorizontal": 8, + }, + "hasIconAfter": { + "spacingIconContentAfter": 4, + }, + "hasIconBefore": { + "spacingIconContentBefore": 4, + }, + "minHeight": 24, + "minWidth": 64, + "paddingHorizontal": 7, + "variant": "secondaryStandard", + }, + "iconSize": 16, + "padding": 3, + }, + "square": { + "borderRadius": 0, + }, + "subtle": { + "backgroundColor": "colors.ghostBackground", + "borderColor": "colors.ghostBorder", + "color": "colors.ghostContent", + "disabled": { + "backgroundColor": "colors.ghostDisabledBackground", + "borderColor": "colors.ghostDisabledBorder", + "color": "colors.ghostDisabledContent", + "iconColor": "colors.ghostDisabledIcon", + }, + "focused": { + "backgroundColor": "colors.ghostFocusedBackground", + "borderColor": "colors.ghostFocusedBorder", + "color": "colors.ghostFocusedContent", + "icon": "colors.ghostFocusedIcon", + }, + "hovered": { + "backgroundColor": "colors.ghostHoveredBackground", + "borderColor": "colors.ghostHoveredBorder", + "color": "colors.ghostHoveredContent", + "iconColor": "colors.ghostHoveredIcon", + }, + "iconColor": "colors.ghostIcon", + "pressed": { + "backgroundColor": "colors.ghostPressedBackground", + "borderColor": "colors.ghostPressedBorder", + "color": "colors.ghostPressedContent", + "icon": "colors.ghostPressedIcon", + }, + }, +} +`; diff --git a/packages/components/Button/src/__tests__/metadata.ts b/packages/components/Button/src/__tests__/metadata.ts new file mode 100644 index 0000000000..8844fcaf0e --- /dev/null +++ b/packages/components/Button/src/__tests__/metadata.ts @@ -0,0 +1,88 @@ +import { resolveTokensForTheme } from '@fluentui-react-native/agentic-analyzer'; + +import { defaultButtonColorTokens } from '../ButtonColorTokens.ts'; +import { defaultButtonFontTokens } from '../ButtonFontTokens.ts'; +import { defaultButtonTokens } from '../ButtonTokens.ts'; + +/** + * The v1 components in this repository. + * + * "v1" components are those that build on the modern composition framework — + * i.e. they depend on `@fluentui-react-native/framework` (the `framework/framework` + * package) rather than (only) the legacy `@uifabricshared/*` foundation packages. + * + * This list was derived by grepping every `package.json` under + * `packages/components/*` and `packages/experimental/*` for a direct dependency + * on `@fluentui-react-native/framework`. A component may also still depend on the + * legacy `@uifabricshared/*` packages (Button does) and still be considered v1. + */ +export const v1Components = [ + '@fluentui-react-native/avatar', + '@fluentui-react-native/badge', + '@fluentui-react-native/button', + '@fluentui-react-native/checkbox', + '@fluentui-react-native/chip', + '@fluentui-react-native/divider', + '@fluentui-react-native/icon', + '@fluentui-react-native/input', + '@fluentui-react-native/link', + '@fluentui-react-native/menu', + '@fluentui-react-native/notification', + '@fluentui-react-native/persona', + '@fluentui-react-native/persona-coin', + '@fluentui-react-native/radio-group', + '@fluentui-react-native/separator', + '@fluentui-react-native/stack', + '@fluentui-react-native/switch', + '@fluentui-react-native/tablist', + '@fluentui-react-native/text', + '@fluentui-react-native/experimental-activity-indicator', + '@fluentui-react-native/experimental-appearance-additions', + '@fluentui-react-native/experimental-avatar', + '@fluentui-react-native/experimental-checkbox', + '@fluentui-react-native/drawer', + '@fluentui-react-native/dropdown', + '@fluentui-react-native/experimental-expander', + '@fluentui-react-native/experimental-menu-button', + '@fluentui-react-native/overflow', + '@fluentui-react-native/popover', + '@fluentui-react-native/experimental-shadow', + '@fluentui-react-native/experimental-shimmer', + '@fluentui-react-native/spinner', +] as const; + +export type V1Component = (typeof v1Components)[number]; + +/** + * The common Button token functions, re-exported so that platform test files can + * pin them (and individual subsets) via the analyzer. Jest's platform resolution + * selects the matching `..ts` variant per run, so these imports resolve + * to the correct platform-specific tokens automatically. + */ +export { defaultButtonTokens, defaultButtonFontTokens, defaultButtonColorTokens }; + +/** + * Resolve the full set of default Button tokens against a sentinel theme, mapping + * every theme-color leaf back to its semantic slot name (e.g. `colors.buttonBackground`). + * + * The token functions already encode states (disabled/hovered/pressed/focused), + * variants (primary/subtle) and sizes (small/medium/large), so a single snapshot + * of the resolved object pins all of them. + */ +export function resolveButtonTokens(): Record { + return resolveTokensForTheme({}, defaultButtonTokens, defaultButtonFontTokens, defaultButtonColorTokens); +} + +/** + * Resolve just the Button color tokens, so the semantic theme-slot mapping is + * clearly isolated in the snapshot. + */ +export function resolveButtonColorTokens(): Record { + return resolveTokensForTheme({}, defaultButtonColorTokens); +} + +// TODO: rendered-style pinning ("styles with various states" from the folder PLAN) +// is a follow-up. It depends on the agentic-analyzer's component-render / style +// extraction helpers (renderWithTheme / getComputedStyles), which are not +// implemented yet. Once they land, add per-platform tests that render Button in +// each state and pin the computed styles here. diff --git a/packages/components/Button/src/__tests__/validate.test.android.ts b/packages/components/Button/src/__tests__/validate.test.android.ts new file mode 100644 index 0000000000..d6c8cae57c --- /dev/null +++ b/packages/components/Button/src/__tests__/validate.test.android.ts @@ -0,0 +1,11 @@ +import { resolveButtonColorTokens, resolveButtonTokens } from './metadata.ts'; + +describe('Button pinning tests (android)', () => { + it('Button tokens match snapshot', () => { + expect(resolveButtonTokens()).toMatchSnapshot(); + }); + + it('Button color tokens map to semantic theme slots', () => { + expect(resolveButtonColorTokens()).toMatchSnapshot(); + }); +}); diff --git a/packages/components/Button/src/__tests__/validate.test.ios.ts b/packages/components/Button/src/__tests__/validate.test.ios.ts new file mode 100644 index 0000000000..32168a2cc2 --- /dev/null +++ b/packages/components/Button/src/__tests__/validate.test.ios.ts @@ -0,0 +1,11 @@ +import { resolveButtonColorTokens, resolveButtonTokens } from './metadata.ts'; + +describe('Button pinning tests (ios)', () => { + it('Button tokens match snapshot', () => { + expect(resolveButtonTokens()).toMatchSnapshot(); + }); + + it('Button color tokens map to semantic theme slots', () => { + expect(resolveButtonColorTokens()).toMatchSnapshot(); + }); +}); diff --git a/packages/components/Button/src/__tests__/validate.test.macos.ts b/packages/components/Button/src/__tests__/validate.test.macos.ts new file mode 100644 index 0000000000..4e4726e3be --- /dev/null +++ b/packages/components/Button/src/__tests__/validate.test.macos.ts @@ -0,0 +1,11 @@ +import { resolveButtonColorTokens, resolveButtonTokens } from './metadata.ts'; + +describe('Button pinning tests (macos)', () => { + it('Button tokens match snapshot', () => { + expect(resolveButtonTokens()).toMatchSnapshot(); + }); + + it('Button color tokens map to semantic theme slots', () => { + expect(resolveButtonColorTokens()).toMatchSnapshot(); + }); +}); diff --git a/packages/components/Button/src/__tests__/validate.test.win32.ts b/packages/components/Button/src/__tests__/validate.test.win32.ts new file mode 100644 index 0000000000..6bdb9467a2 --- /dev/null +++ b/packages/components/Button/src/__tests__/validate.test.win32.ts @@ -0,0 +1,11 @@ +import { resolveButtonColorTokens, resolveButtonTokens } from './metadata.ts'; + +describe('Button pinning tests (win32)', () => { + it('Button tokens match snapshot', () => { + expect(resolveButtonTokens()).toMatchSnapshot(); + }); + + it('Button color tokens map to semantic theme slots', () => { + expect(resolveButtonColorTokens()).toMatchSnapshot(); + }); +}); diff --git a/packages/components/Button/src/__tests__/validate.test.windows.ts b/packages/components/Button/src/__tests__/validate.test.windows.ts new file mode 100644 index 0000000000..334191b95c --- /dev/null +++ b/packages/components/Button/src/__tests__/validate.test.windows.ts @@ -0,0 +1,11 @@ +import { resolveButtonColorTokens, resolveButtonTokens } from './metadata.ts'; + +describe('Button pinning tests (windows)', () => { + it('Button tokens match snapshot', () => { + expect(resolveButtonTokens()).toMatchSnapshot(); + }); + + it('Button color tokens map to semantic theme slots', () => { + expect(resolveButtonColorTokens()).toMatchSnapshot(); + }); +}); diff --git a/packages/components/Button/tsconfig.json b/packages/components/Button/tsconfig.json index 1209992568..f05119690a 100644 --- a/packages/components/Button/tsconfig.json +++ b/packages/components/Button/tsconfig.json @@ -8,6 +8,9 @@ }, "include": ["src"], "references": [ + { + "path": "../../agentic/agentic-analyzer/tsconfig.json" + }, { "path": "../../utils/adapters/tsconfig.json" }, diff --git a/tsconfig.json b/tsconfig.json index 82e7dc8b85..a44cabf20b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,9 @@ { "path": "apps/win32/tsconfig.json" }, + { + "path": "packages/agentic/agentic-analyzer/tsconfig.json" + }, { "path": "packages/codemods/tsconfig.json" }, diff --git a/yarn.lock b/yarn.lock index fe7547f456..5faf955ba8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2510,6 +2510,33 @@ __metadata: languageName: unknown linkType: soft +"@fluentui-react-native/agentic-analyzer@workspace:*, @fluentui-react-native/agentic-analyzer@workspace:packages/agentic/agentic-analyzer": + version: 0.0.0-use.local + resolution: "@fluentui-react-native/agentic-analyzer@workspace:packages/agentic/agentic-analyzer" + dependencies: + "@babel/core": "catalog:" + "@fluentui-react-native/default-theme": "workspace:*" + "@fluentui-react-native/scripts": "workspace:*" + "@fluentui-react-native/theme": "workspace:*" + "@fluentui-react-native/theme-types": "workspace:*" + "@react-native/babel-preset": "npm:^0.81.0" + "@testing-library/react-native": "npm:^13.2.0" + "@types/react": "npm:~19.1.4" + "@types/react-test-renderer": "npm:^19.1.0" + react: "npm:19.1.4" + react-native: "npm:^0.81.6" + react-test-renderer: "npm:19.1.4" + peerDependencies: + "@types/react": ~18.2.0 || ~19.0.0 || ~19.1.4 + react: 18.2.0 || 19.0.0 || 19.1.4 + react-native: ^0.73.0 || ^0.74.0 || ^0.78.0 || ^0.81.6 + react-test-renderer: 18.2.0 || 19.0.0 || 19.1.4 + peerDependenciesMeta: + "@types/react": + optional: true + languageName: unknown + linkType: soft + "@fluentui-react-native/android-theme@workspace:*, @fluentui-react-native/android-theme@workspace:packages/theming/android-theme": version: 0.0.0-use.local resolution: "@fluentui-react-native/android-theme@workspace:packages/theming/android-theme" @@ -2703,6 +2730,7 @@ __metadata: dependencies: "@babel/core": "catalog:" "@fluentui-react-native/adapters": "workspace:*" + "@fluentui-react-native/agentic-analyzer": "workspace:*" "@fluentui-react-native/experimental-activity-indicator": "workspace:*" "@fluentui-react-native/experimental-shadow": "workspace:*" "@fluentui-react-native/framework": "workspace:*" @@ -6422,6 +6450,13 @@ __metadata: languageName: node linkType: hard +"@jest/diff-sequences@npm:30.4.0": + version: 30.4.0 + resolution: "@jest/diff-sequences@npm:30.4.0" + checksum: 10c0/b4358b1b885098b905cb777f58788ddd45f90c4ebc3ce2c04fb1d4c9516f35ac2d9daef8263cd21c537bd7a52ab320f03e4ba9521677959ae20e3d405356b420 + languageName: node + linkType: hard + "@jest/environment@npm:^29.7.0": version: 29.7.0 resolution: "@jest/environment@npm:29.7.0" @@ -6551,6 +6586,15 @@ __metadata: languageName: node linkType: hard +"@jest/schemas@npm:30.4.1": + version: 30.4.1 + resolution: "@jest/schemas@npm:30.4.1" + dependencies: + "@sinclair/typebox": "npm:^0.34.0" + checksum: 10c0/96f388ebfc1974457fcbde2ad36c40a0b549cba3f624fe8d9d6e5903a152dc75e4043f4ac9ac7668622f2ecb0f9a4dcb9a38edf3bc0d52b82045b2bb2b69b72a + languageName: node + linkType: hard + "@jest/schemas@npm:^29.6.3": version: 29.6.3 resolution: "@jest/schemas@npm:29.6.3" @@ -9322,6 +9366,26 @@ __metadata: languageName: node linkType: hard +"@testing-library/react-native@npm:^13.2.0": + version: 13.3.3 + resolution: "@testing-library/react-native@npm:13.3.3" + dependencies: + jest-matcher-utils: "npm:^30.0.5" + picocolors: "npm:^1.1.1" + pretty-format: "npm:^30.0.5" + redent: "npm:^3.0.0" + peerDependencies: + jest: ">=29.0.0" + react: ">=18.2.0" + react-native: ">=0.71" + react-test-renderer: ">=18.2.0" + peerDependenciesMeta: + jest: + optional: true + checksum: 10c0/ba13066536d5b2c0b625220d4320c6ad1e390c3df4f4b614d859ef467c4974ad52aa79269ae98efdba8f5a074644e3d11583a5485312df5a64387976ecf4225a + languageName: node + linkType: hard + "@tootallnate/quickjs-emscripten@npm:^0.23.0": version: 0.23.0 resolution: "@tootallnate/quickjs-emscripten@npm:0.23.0" @@ -15604,6 +15668,13 @@ __metadata: languageName: node linkType: hard +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + languageName: node + linkType: hard + "index-to-position@npm:^1.1.0": version: 1.2.0 resolution: "index-to-position@npm:1.2.0" @@ -16416,6 +16487,18 @@ __metadata: languageName: node linkType: hard +"jest-diff@npm:30.4.1": + version: 30.4.1 + resolution: "jest-diff@npm:30.4.1" + dependencies: + "@jest/diff-sequences": "npm:30.4.0" + "@jest/get-type": "npm:30.1.0" + chalk: "npm:^4.1.2" + pretty-format: "npm:30.4.1" + checksum: 10c0/787e11f0ea27e94815479d6c5415e4173da1e74bede34c1515b8515fc9d1fe053e2ad25a3c31f9998a7292c186a0e4d395ed82e0e149d57d7708ee6759b442e9 + languageName: node + linkType: hard + "jest-diff@npm:^29.7.0": version: 29.7.0 resolution: "jest-diff@npm:29.7.0" @@ -16528,6 +16611,18 @@ __metadata: languageName: node linkType: hard +"jest-matcher-utils@npm:^30.0.5": + version: 30.4.1 + resolution: "jest-matcher-utils@npm:30.4.1" + dependencies: + "@jest/get-type": "npm:30.1.0" + chalk: "npm:^4.1.2" + jest-diff: "npm:30.4.1" + pretty-format: "npm:30.4.1" + checksum: 10c0/ddbb0c7075def27ba30160883c327cb3fd13f561f5789d00a1edca1b48b0651f8ea23a1c51bcfcb6413a68c47d658bcf47a34701b8a39ce135dd28d87a3117af + languageName: node + linkType: hard + "jest-message-util@npm:30.2.0": version: 30.2.0 resolution: "jest-message-util@npm:30.2.0" @@ -18533,6 +18628,13 @@ __metadata: languageName: node linkType: hard +"min-indent@npm:^1.0.0": + version: 1.0.1 + resolution: "min-indent@npm:1.0.1" + checksum: 10c0/7e207bd5c20401b292de291f02913230cb1163abca162044f7db1d951fa245b174dc00869d40dd9a9f32a885ad6a5f3e767ee104cf278f399cb4e92d3f582d5c + languageName: node + linkType: hard + "minimalistic-assert@npm:^1.0.0": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" @@ -20287,6 +20389,18 @@ __metadata: languageName: node linkType: hard +"pretty-format@npm:30.4.1, pretty-format@npm:^30.0.5": + version: 30.4.1 + resolution: "pretty-format@npm:30.4.1" + dependencies: + "@jest/schemas": "npm:30.4.1" + ansi-styles: "npm:^5.2.0" + react-is-18: "npm:react-is@^18.3.1" + react-is-19: "npm:react-is@^19.2.5" + checksum: 10c0/c7e6633740cd2f6d382f188c00c8b4b3f2bee3cda16db6753471c6bb4b94f76531358d3a7793062a0fb00d72ebfb934e8ae1d4f5ced6bb34c8e7f60996f90076 + languageName: node + linkType: hard + "pretty-format@npm:^26.6.2": version: 26.6.2 resolution: "pretty-format@npm:26.6.2" @@ -20544,6 +20658,20 @@ __metadata: languageName: node linkType: hard +"react-is-18@npm:react-is@^18.3.1, react-is@npm:^18.0.0, react-is@npm:^18.3.1": + version: 18.3.1 + resolution: "react-is@npm:18.3.1" + checksum: 10c0/f2f1e60010c683479e74c63f96b09fb41603527cd131a9959e2aee1e5a8b0caf270b365e5ca77d4a6b18aae659b60a86150bb3979073528877029b35aecd2072 + languageName: node + linkType: hard + +"react-is-19@npm:react-is@^19.2.5": + version: 19.2.7 + resolution: "react-is@npm:19.2.7" + checksum: 10c0/419fe54d5bd7fdf5414a5bb7bd9a1e0e36f9fae28ffb4cb73290fbe342bde15d8584a90d1db62547f6aa03018dce517b178a041abb522136cd4b4b51b4e94c83 + languageName: node + linkType: hard + "react-is@npm:^16.13.1": version: 16.13.1 resolution: "react-is@npm:16.13.1" @@ -20558,13 +20686,6 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^18.0.0, react-is@npm:^18.3.1": - version: 18.3.1 - resolution: "react-is@npm:18.3.1" - checksum: 10c0/f2f1e60010c683479e74c63f96b09fb41603527cd131a9959e2aee1e5a8b0caf270b365e5ca77d4a6b18aae659b60a86150bb3979073528877029b35aecd2072 - languageName: node - linkType: hard - "react-is@npm:^19.1.4": version: 19.2.4 resolution: "react-is@npm:19.2.4" @@ -21087,6 +21208,16 @@ __metadata: languageName: node linkType: hard +"redent@npm:^3.0.0": + version: 3.0.0 + resolution: "redent@npm:3.0.0" + dependencies: + indent-string: "npm:^4.0.0" + strip-indent: "npm:^3.0.0" + checksum: 10c0/d64a6b5c0b50eb3ddce3ab770f866658a2b9998c678f797919ceb1b586bab9259b311407280bd80b804e2a7c7539b19238ae6a2a20c843f1a7fcff21d48c2eae + languageName: node + linkType: hard + "reduce-flatten@npm:^1.0.1": version: 1.0.1 resolution: "reduce-flatten@npm:1.0.1" @@ -22510,6 +22641,15 @@ __metadata: languageName: node linkType: hard +"strip-indent@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-indent@npm:3.0.0" + dependencies: + min-indent: "npm:^1.0.0" + checksum: 10c0/ae0deaf41c8d1001c5d4fbe16cb553865c1863da4fae036683b474fa926af9fc121e155cb3fc57a68262b2ae7d5b8420aa752c97a6428c315d00efe2a3875679 + languageName: node + linkType: hard + "strip-json-comments@npm:5.0.3": version: 5.0.3 resolution: "strip-json-comments@npm:5.0.3"