From c4cf8c4e7cf9242a156dc2cc484b346948184158 Mon Sep 17 00:00:00 2001 From: DanailH Date: Mon, 12 Jan 2026 22:10:02 +0200 Subject: [PATCH 01/12] Implement latest price changes proposal --- .github/copilot-instructions.md | 193 ++++++ docs/pages/pricing.tsx | 6 +- .../pricing/InfoPrioritySupport.tsx | 6 +- .../components/pricing/MultiAppContext.tsx | 20 + ...tySupportSwitch.tsx => MultiAppSwitch.tsx} | 37 +- docs/src/components/pricing/PricingCards.tsx | 549 ++++++++++-------- docs/src/components/pricing/PricingTable.tsx | 10 +- .../pricing/PrioritySupportContext.tsx | 23 - 8 files changed, 538 insertions(+), 306 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 docs/src/components/pricing/MultiAppContext.tsx rename docs/src/components/pricing/{PrioritySupportSwitch.tsx => MultiAppSwitch.tsx} (67%) delete mode 100644 docs/src/components/pricing/PrioritySupportContext.tsx diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000000000..34bc939012ef85 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,193 @@ +# Material UI Copilot Instructions + +## Project Overview + +Material UI is a monorepo containing multiple packages (Material UI, Joy UI, System, Base) with ~80+ React components implementing Google's Material Design and custom design systems. Uses pnpm workspaces, Lerna for versioning, and Nx for build orchestration. + +## Repository Structure + +- **`packages/`** - Core packages: `mui-material` (main), `mui-system` (styling), `mui-joy` (experimental), `mui-base`, `mui-utils` +- **`docs/`** - Next.js documentation site with interactive component demos +- **`examples/`** - Template projects (Next.js, Vite, CRA, etc.) +- **`test/`** - Regression tests, e2e tests, bundle size tracking + +## Key Build & Test Commands + +```bash +pnpm build # Build all packages (excluding docs) +pnpm test:unit # Run unit tests with Vitest +pnpm test:node # Unit tests (node environment) +pnpm test:browser # Unit tests (browser environment) +pnpm test:e2e # End-to-end tests +pnpm test:regressions # Visual regression tests +pnpm docs:dev # Dev server for docs +pnpm eslint # Lint workspace +pnpm prettier # Format code +``` + +## Component Architecture Patterns + +### File Structure Per Component + +Each component follows this structure (e.g., `Button`): + +``` +Button/ +├── Button.d.ts # Type definitions +├── Button.spec.tsx # Type tests (expectType assertions) +├── buttonClasses.ts # Class definitions & utilities +├── index.ts # Main export +``` + +### Required TypeScript Conventions + +**Props Interface** - Use `interface` not `type`: + +- Export `{ComponentName}Props` from component +- Export `{ComponentName}Classes` from `{component}Classes.ts` +- Document `sx` prop: `sx?: SxProps` + +**CSS Classes** - Prefixed with MUI namespace: + +```ts +// buttonClasses.ts +export interface ButtonClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied if variant="text". */ + text: string; +} + +export function getButtonUtilityClass(slot: string) { + return generateUtilityClass('MuiButton', slot); +} +``` + +**Utility Function** - Generate class names from state: + +```ts +const useUtilityClasses = (ownerState: ButtonProps) => { + const { variant, disabled, classes } = ownerState; + return composeClasses( + { root: ['root', variant, disabled && 'disabled'] }, + getButtonUtilityClass, + classes, + ); +}; +``` + +### Slot-Based Props System + +Components use `slotProps` pattern for sub-component customization: + +```tsx + @@ -224,29 +284,28 @@ export function PlanPrice(props: PlanPriceProps) { } if (plan === 'premium') { - const premiumAnnualValue = 588; + const premiumAnnualValue = 599; - const premiumPerpetualValue = premiumAnnualValue * perpetualMultiplier; + const premiumPerpetualValue = 1318; const premiumMonthlyValueForAnnual = premiumAnnualValue / 12; - const premiumAnnualValueWithPrioritySupport = premiumAnnualValue + 399; - const premiumPerpetualValueWithPrioritySupport = premiumPerpetualValue + 399; - const premiumMonthlyValueForAnnualWithPrioritySupport = 82; // premiumAnnualValueWithPrioritySupport / 12; + const premiumAnnualValueWithPrioritySupport = 999; + const premiumPerpetualValueWithPrioritySupport = 2198; + const premiumMonthlyValueForAnnualWithPrioritySupport = + Math.round((premiumAnnualValueWithPrioritySupport / 12) * 100) / 100; const priceExplanation = getPriceExplanation( - prioritySupport - ? premiumMonthlyValueForAnnualWithPrioritySupport - : premiumMonthlyValueForAnnual, + multiApp ? premiumMonthlyValueForAnnualWithPrioritySupport : premiumMonthlyValueForAnnual, ); let premiumDisplayedValue: number = premiumAnnualValue; - if (annual && prioritySupport) { + if (annual && multiApp) { premiumDisplayedValue = premiumAnnualValueWithPrioritySupport; - } else if (!annual && prioritySupport) { + } else if (!annual && multiApp) { premiumDisplayedValue = premiumPerpetualValueWithPrioritySupport; - } else if (annual && !prioritySupport) { + } else if (annual && !multiApp) { premiumDisplayedValue = premiumAnnualValue; - } else if (!annual && !prioritySupport) { + } else if (!annual && !multiApp) { premiumDisplayedValue = premiumPerpetualValue; } @@ -283,24 +342,22 @@ export function PlanPrice(props: PlanPriceProps) { minHeight: planPriceMinHeight, }} > - { - - {priceExplanation} - - } + + {priceExplanation} + - ); } - // else enterprise - return ( - + if (plan === 'enterprise') { + const enterpriseAnnualValue = 1399; + const enterprisePerpetualValue = 3078; + const enterpriseMonthlyValueForAnnual = enterpriseAnnualValue / 12; + + const priceExplanation = getPriceExplanation(enterpriseMonthlyValueForAnnual); + + const enterpriseDisplayedValue = annual ? enterpriseAnnualValue : enterprisePerpetualValue; + + return ( - + + {formatCurrency(enterpriseDisplayedValue)} + + + + {priceUnit} + + + - Custom pricing - - - - + {priceExplanation} + + + - - - ); + ); + } + + return null; } function getHref(annual: boolean, prioritySupport: boolean): string { @@ -494,6 +554,131 @@ export function PlanNameDisplay({ ); } +interface PricingCardWrapperProps { + plan: PlanName; + highlighted?: boolean; +} + +function PricingCardWrapper({ plan, highlighted = false }: PricingCardWrapperProps) { + const [multiApp, setMultiApp] = React.useState(false); + + const handleMultiAppChange = (event: React.ChangeEvent) => { + setMultiApp(event.target.checked); + }; + + const MultiAppDescription = + 'Choose this option if you need to use MUI X across multiple applications within your organization.'; + + const tooltipProps = { + enterDelay: 400, + enterNextDelay: 50, + enterTouchDelay: 500, + placement: 'top' as const, + describeChild: true, + slotProps: { + tooltip: { + sx: { + fontSize: 12, + }, + }, + }, + }; + + return ( + ({ + display: 'flex', + border: '1px solid', + borderColor: highlighted ? 'primary.200' : 'divider', + borderRadius: 1, + flexDirection: 'column', + gap: 3, + py: 3, + px: 2, + flex: '1 1 0px', + ...(highlighted && { + background: `${(theme.vars || theme).palette.gradients.linearSubtle}`, + boxShadow: '0px 2px 12px 0px rgba(234, 237, 241, 0.3) inset', + ...theme.applyDarkStyles({ + borderColor: `${alpha(theme.palette.primary[700], 0.4)}`, + boxShadow: '0px 2px 12px 0px rgba(0, 0, 0, 0.25) inset', + }), + }), + })} + > + + + + + {plan !== 'community' && plan !== 'enterprise' && ( + ({ + border: '1px solid', + borderColor: 'primary.100', + borderRadius: 1, + padding: 2, + ...theme.applyDarkStyles({ + borderColor: `${alpha(theme.palette.primary[700], 0.4)}`, + }), + })} + > + + } + label={ + + + Multi App License + + + + + + } + sx={{ + mb: 0.5, + ml: 0, + mr: 0, + display: 'flex', + justifyContent: 'space-between', + width: '100%', + '& .MuiFormControlLabel-label': { + marginRight: 'auto', + }, + }} + labelPlacement="start" + /> + + + )} + {plan !== 'community' && plan !== 'enterprise' && } + + {getPlanFeatures(plan, multiApp).map((feature, index) => ( + + + + ))} + + + ); +} + export default function PricingCards() { return ( @@ -509,150 +694,10 @@ export default function PricingCards() { maxWidth: '100%', }} > - - - - - - - - {planInfo.community.features.map((feature, index) => ( - - - - ))} - - - - - - - - - - - {planInfo.pro.features.map((feature, index) => ( - - - - ))} - - - - ({ - display: 'flex', - border: '1px solid', - borderColor: 'primary.200', - borderRadius: 1, - flexDirection: 'column', - gap: 3, - py: 3, - px: 2, - flex: '1 1 0px', - background: `${(theme.vars || theme).palette.gradients.linearSubtle}`, - boxShadow: '0px 2px 12px 0px rgba(234, 237, 241, 0.3) inset', - ...theme.applyDarkStyles({ - borderColor: `${alpha(theme.palette.primary[700], 0.4)}`, - boxShadow: '0px 2px 12px 0px rgba(0, 0, 0, 0.25) inset', - }), - })} - > - - - - - - {planInfo.premium.features.map((feature, index) => ( - - - - ))} - - - - - - - - - - - {planInfo.enterprise.features.map((feature, index) => ( - - - - ))} - - + + + + ); diff --git a/docs/src/components/pricing/PricingTable.tsx b/docs/src/components/pricing/PricingTable.tsx index 4063303dd933d5..5861f001471cbf 100644 --- a/docs/src/components/pricing/PricingTable.tsx +++ b/docs/src/components/pricing/PricingTable.tsx @@ -16,7 +16,7 @@ import { Link } from '@mui/docs/Link'; import IconImage from 'docs/src/components/icon/IconImage'; import { useLicenseModel } from 'docs/src/components/pricing/LicenseModelContext'; import SupportAgentIcon from '@mui/icons-material/SupportAgent'; -import { PrioritySupportSwitchTable } from 'docs/src/components/pricing/PrioritySupportSwitch'; +import { MultiAppSwitchTable } from 'docs/src/components/pricing/MultiAppSwitch'; import InfoPrioritySupport from 'docs/src/components/pricing/InfoPrioritySupport'; import { PlanName, planInfo } from './PricingCards'; @@ -840,7 +840,7 @@ const proData: Record = { 'response-time': no, 'pre-screening': no, 'issue-escalation': no, - 'security-questionnaire': , + 'security-questionnaire': , }; const premiumData: Record = { @@ -938,13 +938,13 @@ const premiumData: Record = { // Support 'core-support': , 'x-support': , - 'priority-support': , + 'multi-app': , 'tech-advisory': pending, 'support-duration': , 'response-time': , 'pre-screening': , 'issue-escalation': , - 'security-questionnaire': , + 'security-questionnaire': , 'customer-success': no, }; @@ -1050,7 +1050,7 @@ const enterpriseData: Record = { 'response-time': , 'pre-screening': , 'issue-escalation': , - 'security-questionnaire': , + 'security-questionnaire': yes, }; function RowCategory(props: BoxProps) { diff --git a/docs/src/components/pricing/PrioritySupportContext.tsx b/docs/src/components/pricing/PrioritySupportContext.tsx deleted file mode 100644 index 0e5435354483e3..00000000000000 --- a/docs/src/components/pricing/PrioritySupportContext.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react'; - -const PrioritySupport = React.createContext<{ - prioritySupport: boolean; - setPrioritySupport: React.Dispatch>; -}>(undefined as any); - -if (process.env.NODE_ENV !== 'production') { - PrioritySupport.displayName = 'PrioritySupport'; -} - -export function PrioritySupportProvider(props: any) { - const [prioritySupport, setPrioritySupport] = React.useState(false); - const value = React.useMemo( - () => ({ prioritySupport, setPrioritySupport }), - [prioritySupport, setPrioritySupport], - ); - return {props.children}; -} - -export function usePrioritySupport() { - return React.useContext(PrioritySupport); -} From 7114b1123d371f8d1dd7bd225f5356d9cf3fe676 Mon Sep 17 00:00:00 2001 From: DanailH Date: Mon, 12 Jan 2026 22:15:33 +0200 Subject: [PATCH 02/12] remove unneeded file --- .github/copilot-instructions.md | 193 -------------------------------- 1 file changed, 193 deletions(-) delete mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 34bc939012ef85..00000000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,193 +0,0 @@ -# Material UI Copilot Instructions - -## Project Overview - -Material UI is a monorepo containing multiple packages (Material UI, Joy UI, System, Base) with ~80+ React components implementing Google's Material Design and custom design systems. Uses pnpm workspaces, Lerna for versioning, and Nx for build orchestration. - -## Repository Structure - -- **`packages/`** - Core packages: `mui-material` (main), `mui-system` (styling), `mui-joy` (experimental), `mui-base`, `mui-utils` -- **`docs/`** - Next.js documentation site with interactive component demos -- **`examples/`** - Template projects (Next.js, Vite, CRA, etc.) -- **`test/`** - Regression tests, e2e tests, bundle size tracking - -## Key Build & Test Commands - -```bash -pnpm build # Build all packages (excluding docs) -pnpm test:unit # Run unit tests with Vitest -pnpm test:node # Unit tests (node environment) -pnpm test:browser # Unit tests (browser environment) -pnpm test:e2e # End-to-end tests -pnpm test:regressions # Visual regression tests -pnpm docs:dev # Dev server for docs -pnpm eslint # Lint workspace -pnpm prettier # Format code -``` - -## Component Architecture Patterns - -### File Structure Per Component - -Each component follows this structure (e.g., `Button`): - -``` -Button/ -├── Button.d.ts # Type definitions -├── Button.spec.tsx # Type tests (expectType assertions) -├── buttonClasses.ts # Class definitions & utilities -├── index.ts # Main export -``` - -### Required TypeScript Conventions - -**Props Interface** - Use `interface` not `type`: - -- Export `{ComponentName}Props` from component -- Export `{ComponentName}Classes` from `{component}Classes.ts` -- Document `sx` prop: `sx?: SxProps` - -**CSS Classes** - Prefixed with MUI namespace: - -```ts -// buttonClasses.ts -export interface ButtonClasses { - /** Styles applied to the root element. */ - root: string; - /** Styles applied if variant="text". */ - text: string; -} - -export function getButtonUtilityClass(slot: string) { - return generateUtilityClass('MuiButton', slot); -} -``` - -**Utility Function** - Generate class names from state: - -```ts -const useUtilityClasses = (ownerState: ButtonProps) => { - const { variant, disabled, classes } = ownerState; - return composeClasses( - { root: ['root', variant, disabled && 'disabled'] }, - getButtonUtilityClass, - classes, - ); -}; -``` - -### Slot-Based Props System - -Components use `slotProps` pattern for sub-component customization: - -```tsx - + + ); +} + interface PlanPriceProps { plan: 'community' | 'pro' | 'premium' | 'enterprise'; multiApp?: boolean; @@ -132,54 +253,28 @@ export function PlanPrice(props: PlanPriceProps) { const { licenseModel } = useLicenseModel(); const annual = licenseModel === 'annual'; - const planPriceMinHeight = 24; + if (plan === 'community') { return ( - - - - $0 - - - - - Free forever! - - - - + ); } const priceUnit = annual ? '/ year / dev' : '/ dev'; const getPriceExplanation = (displayedValue: number) => { if (annual) { - return `Equivalent to $${displayedValue.toFixed(2)} / month / dev`; + return ( + + Equivalent to ${displayedValue.toFixed(2)} / month / dev + + ); } return ''; }; @@ -213,7 +308,6 @@ export function PlanPrice(props: PlanPriceProps) { } else { priceForExplanation = perpetualValue; } - const priceExplanation = getPriceExplanation(priceForExplanation); let mainDisplayValue: number = annual ? annualValue : perpetualValue; if (annual && multiApp) { @@ -227,60 +321,18 @@ export function PlanPrice(props: PlanPriceProps) { } return ( - - - - {formatCurrency(mainDisplayValue)} - - - - {priceUnit} - - - - - {priceExplanation} - - - - + ); } if (plan === 'premium') { const premiumAnnualValue = 599; - const premiumPerpetualValue = 1318; const premiumMonthlyValueForAnnual = premiumAnnualValue / 12; @@ -289,10 +341,6 @@ export function PlanPrice(props: PlanPriceProps) { const premiumMonthlyValueForAnnualWithPrioritySupport = Math.round((premiumAnnualValueWithPrioritySupport / 12) * 100) / 100; - const priceExplanation = getPriceExplanation( - multiApp ? premiumMonthlyValueForAnnualWithPrioritySupport : premiumMonthlyValueForAnnual, - ); - let premiumDisplayedValue: number = premiumAnnualValue; if (annual && multiApp) { premiumDisplayedValue = premiumAnnualValueWithPrioritySupport; @@ -305,62 +353,14 @@ export function PlanPrice(props: PlanPriceProps) { } return ( - - - - {formatCurrency(premiumDisplayedValue)} - - - - {priceUnit} - - - - - {priceExplanation} - - - - + ); } @@ -369,60 +369,15 @@ export function PlanPrice(props: PlanPriceProps) { const enterprisePerpetualValue = 2798; const enterpriseMonthlyValueForAnnual = enterpriseAnnualValue / 12; - const priceExplanation = getPriceExplanation(enterpriseMonthlyValueForAnnual); - - const enterpriseDisplayedValue = annual ? enterpriseAnnualValue : enterprisePerpetualValue; - return ( - - - - {formatCurrency(enterpriseDisplayedValue)} - - - - {priceUnit} - - - - - {priceExplanation} - - - - + ); } @@ -447,13 +402,13 @@ export function FeatureItem({ feature, idPrefix }: { feature: Feature; idPrefix? {feature.icon === 'check' && ( - + )} {feature.icon === 'support' && ( )} - - {feature.text} - {feature.highlight && ( - - {feature.highlight} +
+ + {feature.primaryLabel} + + {feature.secondaryLabel && ( + + {feature.secondaryLabel} )} - {feature.text2 && ` ${feature.text2}`} - +
); } @@ -514,38 +461,40 @@ export function PlanNameDisplay({ }) { const { title, iconName, description } = planInfo[plan]; return ( - - - {title} - + + + + + {title} + + + {!disableDescription && ( {description} )} - +
); } @@ -565,7 +514,7 @@ function PricingCardWrapper({ plan, highlighted = false }: PricingCardWrapperPro borderColor: highlighted ? 'primary.200' : 'divider', borderRadius: 1, flexDirection: 'column', - gap: 3, + gap: 2, py: 3, px: 2, flex: '1 1 0px', @@ -579,27 +528,27 @@ function PricingCardWrapper({ plan, highlighted = false }: PricingCardWrapperPro }), })} > - + - {plan !== 'community' && plan !== 'enterprise' && ( - - )} - {plan !== 'community' && plan !== 'enterprise' && } - + {plan !== 'community' && plan !== 'enterprise' && } + + {plan !== 'community' && ( + + Everything in {getPreviousPlanName(plan)} plan and... + + )} + {getPlanFeatures(plan, multiApp).map((feature, index) => ( - - - + ))} diff --git a/docs/src/components/pricing/PricingList.tsx b/docs/src/components/pricing/PricingList.tsx index d9a77de4b69b5c..04445a3e8c8edb 100644 --- a/docs/src/components/pricing/PricingList.tsx +++ b/docs/src/components/pricing/PricingList.tsx @@ -8,10 +8,12 @@ import PricingTable from 'docs/src/components/pricing/PricingTable'; import { PlanPrice, PlanNameDisplay, - planInfo, FeatureItem, + getPlanFeatures, } from 'docs/src/components/pricing/PricingCards'; import LicenseModelSwitch from 'docs/src/components/pricing/LicenseModelSwitch'; +import MultiAppSwitch from 'docs/src/components/pricing/MultiAppSwitch'; +import { useMultiApp } from 'docs/src/components/pricing/MultiAppContext'; const Plan = React.forwardRef< HTMLDivElement, @@ -21,33 +23,36 @@ const Plan = React.forwardRef< unavailable?: boolean; } & PaperProps >(function Plan({ plan, unavailable, sx, ...props }, ref) { - const { features } = planInfo[plan]; + const { multiApp } = useMultiApp(); + const features = getPlanFeatures(plan, multiApp); return ( - - {(plan === 'pro' || plan === 'premium') && } - - - {features.map((feature, index) => ( - - + {(plan === 'pro' || plan === 'premium') && ( + + - ))} + )} + + {plan !== 'community' && plan !== 'enterprise' && } + + {features.map((feature, index) => ( + + ))} + ); }); From 46cf57441e958e868a8410f1e3708422c8529279 Mon Sep 17 00:00:00 2001 From: DanailH Date: Fri, 20 Feb 2026 13:29:00 +0200 Subject: [PATCH 11/12] PR review --- docs/src/components/pricing/MultiAppSwitch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/components/pricing/MultiAppSwitch.tsx b/docs/src/components/pricing/MultiAppSwitch.tsx index 8e6a2a15552ee4..913f19978e935d 100644 --- a/docs/src/components/pricing/MultiAppSwitch.tsx +++ b/docs/src/components/pricing/MultiAppSwitch.tsx @@ -74,7 +74,7 @@ export function MultiAppSwitchTable() { setMultiApp(event.target.checked); }; const MultiAppDescription = - 'Choose this option if you need to use MUI X across multiple applications within your organization.'; + 'Choose this option if you expect to use MUI X across multiple applications within your organization.'; const tooltipProps = { enterDelay: 400, From fe62ff2c30dbbc4964ed3bdfa76e47f94789aa19 Mon Sep 17 00:00:00 2001 From: DanailH Date: Fri, 20 Feb 2026 13:34:47 +0200 Subject: [PATCH 12/12] more PR comments --- docs/src/components/pricing/PricingCards.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/components/pricing/PricingCards.tsx b/docs/src/components/pricing/PricingCards.tsx index 98044a12bbbefa..6f597b8907e25f 100644 --- a/docs/src/components/pricing/PricingCards.tsx +++ b/docs/src/components/pricing/PricingCards.tsx @@ -152,7 +152,6 @@ export const multiAppPlanInfo: Partial> = { primaryLabel: 5+ {highlightText('Premium')} features, icon: 'check', }, - { primaryLabel: 'Self served up to 15 seats', icon: 'check' }, ], };