@khester/create-dynamics-app 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/bin/create-dynamics-app.js +2 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +102 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/utils/copyTemplate.d.ts +2 -0
  7. package/dist/utils/copyTemplate.d.ts.map +1 -0
  8. package/dist/utils/copyTemplate.js +28 -0
  9. package/dist/utils/copyTemplate.js.map +1 -0
  10. package/dist/utils/initGit.d.ts +2 -0
  11. package/dist/utils/initGit.d.ts.map +1 -0
  12. package/dist/utils/initGit.js +122 -0
  13. package/dist/utils/initGit.js.map +1 -0
  14. package/dist/utils/installDependencies.d.ts +2 -0
  15. package/dist/utils/installDependencies.d.ts.map +1 -0
  16. package/dist/utils/installDependencies.js +40 -0
  17. package/dist/utils/installDependencies.js.map +1 -0
  18. package/dist/utils/updatePackageJson.d.ts +2 -0
  19. package/dist/utils/updatePackageJson.d.ts.map +1 -0
  20. package/dist/utils/updatePackageJson.js +24 -0
  21. package/dist/utils/updatePackageJson.js.map +1 -0
  22. package/package.json +51 -0
  23. package/templates/dynamics-365-starter/README.md +178 -0
  24. package/templates/dynamics-365-starter/package.json +44 -0
  25. package/templates/dynamics-365-starter/public/index.html +18 -0
  26. package/templates/dynamics-365-starter/src/components/ContactForm.css +48 -0
  27. package/templates/dynamics-365-starter/src/components/ContactForm.tsx +241 -0
  28. package/templates/dynamics-365-starter/src/components/ContactManagement.css +86 -0
  29. package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +267 -0
  30. package/templates/dynamics-365-starter/src/index.tsx +40 -0
  31. package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +54 -0
  32. package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +136 -0
  33. package/templates/dynamics-365-starter/src/styles/index.css +104 -0
  34. package/templates/dynamics-365-starter/tsconfig.json +26 -0
  35. package/templates/dynamics-365-starter/webpack.config.js +58 -0
  36. package/templates/power-pages-starter/.env.example +6 -0
  37. package/templates/power-pages-starter/README.md +89 -0
  38. package/templates/power-pages-starter/package.json +42 -0
  39. package/templates/power-pages-starter/public/index.html +18 -0
  40. package/templates/power-pages-starter/src/components/ContactForm.css +84 -0
  41. package/templates/power-pages-starter/src/components/ContactForm.tsx +239 -0
  42. package/templates/power-pages-starter/src/index.tsx +32 -0
  43. package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +139 -0
  44. package/templates/power-pages-starter/src/styles/index.css +76 -0
  45. package/templates/power-pages-starter/tsconfig.json +26 -0
  46. package/templates/power-pages-starter/webpack.config.js +52 -0
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { ContactManagement } from '../components/ContactManagement';
3
+ import { DynamicsProvider } from '../providers/DynamicsProvider';
4
+
5
+ interface PCFContextType {
6
+ // Define PCF context properties based on your needs
7
+ webAPI: any;
8
+ utils: any;
9
+ parameters: any;
10
+ }
11
+
12
+ interface ContactControlWrapperProps {
13
+ context: PCFContextType;
14
+ }
15
+
16
+ /**
17
+ * Wrapper component for integrating ContactManagement with PCF (PowerApps Component Framework)
18
+ * This allows the component to be used as a custom control in Dynamics 365 forms and views
19
+ */
20
+ export const ContactControlWrapper: React.FC<ContactControlWrapperProps> = ({ context }) => {
21
+ // Extract configuration from PCF context
22
+ const baseUrl = context.parameters?.baseUrl?.raw || '';
23
+
24
+ // Create a custom API service that uses PCF's webAPI
25
+ const createPCFApiService = () => ({
26
+ createRecord: async (entityName: string, data: any) => {
27
+ return await context.webAPI.createRecord(entityName, data);
28
+ },
29
+ retrieveRecord: async (entityName: string, id: string, select?: string) => {
30
+ return await context.webAPI.retrieveRecord(entityName, id, select);
31
+ },
32
+ updateRecord: async (entityName: string, id: string, data: any) => {
33
+ return await context.webAPI.updateRecord(entityName, id, data);
34
+ },
35
+ deleteRecord: async (entityName: string, id: string) => {
36
+ return await context.webAPI.deleteRecord(entityName, id);
37
+ },
38
+ retrieveMultiple: async (entityName: string, query?: string) => {
39
+ const fetchXml = query || `<fetch><entity name="${entityName}"/></fetch>`;
40
+ return await context.webAPI.retrieveMultipleRecords(entityName, `?fetchXml=${encodeURIComponent(fetchXml)}`);
41
+ }
42
+ });
43
+
44
+ return (
45
+ <DynamicsProvider baseUrl={baseUrl}>
46
+ <div style={{ width: '100%', height: '100%' }}>
47
+ <ContactManagement />
48
+ </div>
49
+ </DynamicsProvider>
50
+ );
51
+ };
52
+
53
+ // Export for PCF integration
54
+ export default ContactControlWrapper;
@@ -0,0 +1,136 @@
1
+ import React, { createContext, useContext, useState, useEffect } from 'react';
2
+ import { DynamicsApiService } from '@khester1/dynamics-ui-api-client';
3
+
4
+ interface DynamicsContextType {
5
+ apiService: DynamicsApiService | null;
6
+ createRecord: (entityName: string, data: any) => Promise<any>;
7
+ retrieveRecord: (entityName: string, id: string, select?: string) => Promise<any>;
8
+ updateRecord: (entityName: string, id: string, data: any) => Promise<any>;
9
+ deleteRecord: (entityName: string, id: string) => Promise<void>;
10
+ retrieveMultiple: (entityName: string, query?: string) => Promise<any>;
11
+ }
12
+
13
+ const DynamicsContext = createContext<DynamicsContextType | undefined>(undefined);
14
+
15
+ interface DynamicsProviderProps {
16
+ children: React.ReactNode;
17
+ baseUrl?: string;
18
+ accessToken?: string;
19
+ }
20
+
21
+ export const DynamicsProvider: React.FC<DynamicsProviderProps> = ({
22
+ children,
23
+ baseUrl,
24
+ accessToken
25
+ }) => {
26
+ const [apiService, setApiService] = useState<DynamicsApiService | null>(null);
27
+
28
+ useEffect(() => {
29
+ // Initialize the API service
30
+ // In a real D365 environment, you would get these from your authentication flow
31
+ const getAccessToken = async () => {
32
+ return accessToken || process.env.DYNAMICS_ACCESS_TOKEN || '';
33
+ };
34
+
35
+ const service = new DynamicsApiService(
36
+ baseUrl || process.env.DYNAMICS_BASE_URL || 'https://org.crm.dynamics.com',
37
+ getAccessToken
38
+ );
39
+
40
+ setApiService(service);
41
+ }, [baseUrl, accessToken]);
42
+
43
+ const createRecord = async (entityName: string, data: any) => {
44
+ if (!apiService) throw new Error('API service not initialized');
45
+ return await apiService.createRecord(entityName, data);
46
+ };
47
+
48
+ const retrieveRecord = async (entityName: string, id: string, select?: string) => {
49
+ if (!apiService) throw new Error('API service not initialized');
50
+ // Use retrieveMultipleRecords with FetchXML to get a single record
51
+ const selectAttributes = select ? select.split(',').map(attr => attr.trim()) : ['*'];
52
+ const attributes = selectAttributes.map(attr => attr === '*' ? '' : `<attribute name="${attr}" />`).join('');
53
+ const fetchXml = `
54
+ <fetch top="1">
55
+ <entity name="${entityName}">
56
+ ${attributes}
57
+ <filter>
58
+ <condition attribute="${entityName}id" operator="eq" value="${id}" />
59
+ </filter>
60
+ </entity>
61
+ </fetch>
62
+ `;
63
+ const result = await apiService.retrieveMultipleRecords(entityName, fetchXml);
64
+ return result.entities.length > 0 ? result.entities[0] : null;
65
+ };
66
+
67
+ const updateRecord = async (entityName: string, id: string, data: any) => {
68
+ if (!apiService) throw new Error('API service not initialized');
69
+ return await apiService.updateRecord(entityName, id, data);
70
+ };
71
+
72
+ const deleteRecord = async (entityName: string, id: string) => {
73
+ if (!apiService) throw new Error('API service not initialized');
74
+ return await apiService.deleteRecord(entityName, id);
75
+ };
76
+
77
+ const retrieveMultiple = async (entityName: string, query?: string) => {
78
+ if (!apiService) throw new Error('API service not initialized');
79
+ // Convert OData-style query to FetchXML
80
+ let fetchXml = `<fetch>`;
81
+
82
+ if (query) {
83
+ // Parse basic OData query parameters
84
+ const selectMatch = query.match(/\$select=([^&]*)/i);
85
+ const orderByMatch = query.match(/\$orderby=([^&]*)/i);
86
+ const topMatch = query.match(/\$top=(\d+)/i);
87
+
88
+ if (topMatch) {
89
+ fetchXml = `<fetch top="${topMatch[1]}">`;
90
+ }
91
+
92
+ fetchXml += `<entity name="${entityName}">`;
93
+
94
+ if (selectMatch) {
95
+ const attributes = selectMatch[1].split(',').map(attr => attr.trim());
96
+ attributes.forEach(attr => {
97
+ fetchXml += `<attribute name="${attr}" />`;
98
+ });
99
+ }
100
+
101
+ if (orderByMatch) {
102
+ const [field, direction] = orderByMatch[1].split(' ');
103
+ fetchXml += `<order attribute="${field.trim()}" descending="${direction?.toLowerCase() === 'desc'}" />`;
104
+ }
105
+
106
+ fetchXml += `</entity></fetch>`;
107
+ } else {
108
+ fetchXml = `<fetch><entity name="${entityName}"></entity></fetch>`;
109
+ }
110
+
111
+ return await apiService.retrieveMultipleRecords(entityName, fetchXml);
112
+ };
113
+
114
+ const value: DynamicsContextType = {
115
+ apiService,
116
+ createRecord,
117
+ retrieveRecord,
118
+ updateRecord,
119
+ deleteRecord,
120
+ retrieveMultiple,
121
+ };
122
+
123
+ return (
124
+ <DynamicsContext.Provider value={value}>
125
+ {children}
126
+ </DynamicsContext.Provider>
127
+ );
128
+ };
129
+
130
+ export const useDynamicsApi = (): DynamicsContextType => {
131
+ const context = useContext(DynamicsContext);
132
+ if (context === undefined) {
133
+ throw new Error('useDynamicsApi must be used within a DynamicsProvider');
134
+ }
135
+ return context;
136
+ };
@@ -0,0 +1,104 @@
1
+ /* Global Styles for Dynamics 365 App */
2
+ * {
3
+ box-sizing: border-box;
4
+ }
5
+
6
+ body {
7
+ margin: 0;
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
9
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
10
+ sans-serif;
11
+ -webkit-font-smoothing: antialiased;
12
+ -moz-osx-font-smoothing: grayscale;
13
+ background-color: #f5f5f5;
14
+ color: #323130;
15
+ }
16
+
17
+ .app {
18
+ min-height: 100vh;
19
+ display: flex;
20
+ flex-direction: column;
21
+ background-color: #faf9f8;
22
+ }
23
+
24
+ .app-header {
25
+ background: linear-gradient(135deg, #0078d4 0%, #106ebe 100%);
26
+ color: white;
27
+ padding: 16px 24px;
28
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
29
+ }
30
+
31
+ .app-header h1 {
32
+ margin: 0 0 4px 0;
33
+ font-size: 24px;
34
+ font-weight: 600;
35
+ }
36
+
37
+ .app-header p {
38
+ margin: 0;
39
+ font-size: 14px;
40
+ opacity: 0.9;
41
+ }
42
+
43
+ .app-main {
44
+ flex: 1;
45
+ padding: 0;
46
+ max-width: 1200px;
47
+ margin: 0 auto;
48
+ width: 100%;
49
+ background-color: white;
50
+ margin-top: 16px;
51
+ margin-bottom: 16px;
52
+ border-radius: 8px;
53
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
54
+ }
55
+
56
+ .app-footer {
57
+ background-color: #f3f2f1;
58
+ border-top: 1px solid #edebe9;
59
+ padding: 12px 16px;
60
+ text-align: center;
61
+ color: #605e5c;
62
+ font-size: 12px;
63
+ }
64
+
65
+ .app-footer p {
66
+ margin: 0;
67
+ }
68
+
69
+ /* Dynamics 365 specific styles */
70
+ .ms-Panel {
71
+ box-shadow: 0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108);
72
+ }
73
+
74
+ .ms-DetailsList {
75
+ border-radius: 0;
76
+ }
77
+
78
+ .ms-DetailsHeader {
79
+ background-color: #f8f8f8;
80
+ border-bottom: 1px solid #edebe9;
81
+ }
82
+
83
+ .ms-DetailsRow:hover {
84
+ background-color: #f3f2f1;
85
+ }
86
+
87
+ .ms-DetailsRow.is-selected {
88
+ background-color: #deecf9;
89
+ }
90
+
91
+ @media (max-width: 768px) {
92
+ .app-header {
93
+ padding: 12px 16px;
94
+ }
95
+
96
+ .app-header h1 {
97
+ font-size: 20px;
98
+ }
99
+
100
+ .app-main {
101
+ margin: 8px;
102
+ border-radius: 4px;
103
+ }
104
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "es6"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "esModuleInterop": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "strict": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "module": "esnext",
17
+ "moduleResolution": "node",
18
+ "resolveJsonModule": true,
19
+ "isolatedModules": true,
20
+ "noEmit": true,
21
+ "jsx": "react-jsx"
22
+ },
23
+ "include": [
24
+ "src"
25
+ ]
26
+ }
@@ -0,0 +1,58 @@
1
+ const path = require('path');
2
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
3
+
4
+ module.exports = (env, argv) => {
5
+ const isProduction = argv.mode === 'production';
6
+
7
+ return {
8
+ entry: './src/index.tsx',
9
+ output: {
10
+ path: path.resolve(__dirname, 'dist'),
11
+ filename: isProduction ? '[name].[contenthash].js' : '[name].js',
12
+ clean: true,
13
+ publicPath: '/',
14
+ library: 'DynamicsApp',
15
+ libraryTarget: 'umd',
16
+ },
17
+ resolve: {
18
+ extensions: ['.tsx', '.ts', '.js', '.jsx'],
19
+ },
20
+ module: {
21
+ rules: [
22
+ {
23
+ test: /\.tsx?$/,
24
+ use: 'ts-loader',
25
+ exclude: /node_modules/,
26
+ },
27
+ {
28
+ test: /\.css$/i,
29
+ use: ['style-loader', 'css-loader'],
30
+ },
31
+ ],
32
+ },
33
+ plugins: [
34
+ new HtmlWebpackPlugin({
35
+ template: './public/index.html',
36
+ filename: 'index.html',
37
+ }),
38
+ ],
39
+ devServer: {
40
+ static: {
41
+ directory: path.join(__dirname, 'public'),
42
+ },
43
+ port: 3000,
44
+ open: true,
45
+ hot: true,
46
+ historyApiFallback: true,
47
+ },
48
+ externals: isProduction ? {
49
+ 'react': 'React',
50
+ 'react-dom': 'ReactDOM'
51
+ } : {},
52
+ optimization: {
53
+ splitChunks: {
54
+ chunks: 'all',
55
+ },
56
+ },
57
+ };
58
+ };
@@ -0,0 +1,6 @@
1
+ # Power Pages Configuration
2
+ PORTAL_URL=https://your-portal.powerappsportals.com
3
+
4
+ # Optional: API Configuration
5
+ API_VERSION=v9.0
6
+ ENABLE_LOGGING=false
@@ -0,0 +1,89 @@
1
+ # Power Pages Application
2
+
3
+ This is a Power Pages application built with Dynamics UI Kit components.
4
+
5
+ ## Getting Started
6
+
7
+ 1. **Environment Setup**
8
+ ```bash
9
+ cp .env.example .env
10
+ ```
11
+ Edit `.env` and set your `PORTAL_URL` to your Power Pages portal URL.
12
+
13
+ 2. **Install Dependencies**
14
+ ```bash
15
+ npm install
16
+ ```
17
+
18
+ 3. **Development**
19
+ ```bash
20
+ npm run dev
21
+ ```
22
+ This starts the development server at http://localhost:3000
23
+
24
+ 4. **Build for Production**
25
+ ```bash
26
+ npm run build
27
+ ```
28
+
29
+ ## Project Structure
30
+
31
+ ```
32
+ src/
33
+ ├── components/ # React components
34
+ │ ├── ContactForm.tsx # Main contact form component
35
+ │ └── ContactForm.css # Component styles
36
+ ├── providers/ # Context providers
37
+ │ └── PowerPagesProvider.tsx # Power Pages API provider
38
+ ├── styles/ # Global styles
39
+ │ └── index.css # Global CSS
40
+ └── index.tsx # Application entry point
41
+ ```
42
+
43
+ ## Features
44
+
45
+ - **Contact Management**: Create and manage contacts using Dynamics UI Kit components
46
+ - **Power Pages Integration**: Seamless integration with Power Pages API
47
+ - **Responsive Design**: Mobile-friendly responsive layout
48
+ - **Type Safety**: Full TypeScript support
49
+
50
+ ## Deployment
51
+
52
+ To deploy your application to Power Pages:
53
+
54
+ 1. Build the application:
55
+ ```bash
56
+ npm run build
57
+ ```
58
+
59
+ 2. Upload the `dist` folder contents to your Power Pages portal's web files
60
+
61
+ 3. Create a new web page in Power Pages and reference your built JavaScript and CSS files
62
+
63
+ ## Customization
64
+
65
+ ### Adding New Components
66
+
67
+ 1. Create your component in `src/components/`
68
+ 2. Use Dynamics UI Kit components for consistent styling
69
+ 3. Import and use the `usePowerPagesApi` hook for data operations
70
+
71
+ ### Styling
72
+
73
+ - Global styles are in `src/styles/index.css`
74
+ - Component-specific styles should be co-located with components
75
+ - Follow the existing CSS naming conventions
76
+
77
+ ## Available Scripts
78
+
79
+ - `npm run dev` - Start development server
80
+ - `npm run build` - Build for production
81
+ - `npm run typecheck` - Type check without building
82
+ - `npm run clean` - Clean build directory
83
+ - `npm run lint` - Lint code
84
+
85
+ ## Learn More
86
+
87
+ - [Dynamics UI Kit Documentation](https://github.com/your-org/dynamics-ui-kit)
88
+ - [Power Pages Documentation](https://docs.microsoft.com/power-pages/)
89
+ - [React Documentation](https://reactjs.org/)
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "power-pages-app",
3
+ "version": "1.0.0",
4
+ "description": "Power Pages application built with Dynamics UI Kit",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "webpack --mode=production",
8
+ "dev": "webpack serve --mode=development",
9
+ "typecheck": "tsc --noEmit",
10
+ "clean": "rimraf dist",
11
+ "lint": "eslint src --ext .ts,.tsx"
12
+ },
13
+ "dependencies": {
14
+ "@khester1/dynamics-ui-components": "^1.0.0",
15
+ "react": "^18.2.0",
16
+ "react-dom": "^18.2.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^18.2.0",
20
+ "@types/react-dom": "^18.2.0",
21
+ "css-loader": "^6.8.1",
22
+ "html-webpack-plugin": "^5.5.3",
23
+ "style-loader": "^3.3.3",
24
+ "ts-loader": "^9.5.1",
25
+ "typescript": "^5.3.3",
26
+ "webpack": "^5.89.0",
27
+ "webpack-cli": "^5.1.4",
28
+ "webpack-dev-server": "^4.15.1"
29
+ },
30
+ "browserslist": {
31
+ "production": [
32
+ ">0.2%",
33
+ "not dead",
34
+ "not op_mini all"
35
+ ],
36
+ "development": [
37
+ "last 1 chrome version",
38
+ "last 1 firefox version",
39
+ "last 1 safari version"
40
+ ]
41
+ }
42
+ }
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <meta name="theme-color" content="#0078d4" />
8
+ <meta
9
+ name="description"
10
+ content="Power Pages application built with Dynamics UI Kit"
11
+ />
12
+ <title>Power Pages App - Dynamics UI Kit</title>
13
+ </head>
14
+ <body>
15
+ <noscript>You need to enable JavaScript to run this app.</noscript>
16
+ <div id="root"></div>
17
+ </body>
18
+ </html>
@@ -0,0 +1,84 @@
1
+ .contact-form {
2
+ max-width: 800px;
3
+ margin: 0 auto;
4
+ padding: 24px;
5
+ background: #ffffff;
6
+ border-radius: 8px;
7
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
8
+ }
9
+
10
+ .contact-form__header {
11
+ margin-bottom: 24px;
12
+ }
13
+
14
+ .contact-form__title {
15
+ margin: 0;
16
+ font-size: 24px;
17
+ font-weight: 600;
18
+ color: #323130;
19
+ }
20
+
21
+ .contact-form__content {
22
+ margin-bottom: 24px;
23
+ }
24
+
25
+ .contact-form__row {
26
+ display: grid;
27
+ grid-template-columns: 1fr 1fr;
28
+ gap: 16px;
29
+ margin-bottom: 16px;
30
+ }
31
+
32
+ .contact-form__field {
33
+ display: flex;
34
+ flex-direction: column;
35
+ }
36
+
37
+ .contact-form__actions {
38
+ display: flex;
39
+ gap: 12px;
40
+ justify-content: flex-start;
41
+ margin-top: 24px;
42
+ }
43
+
44
+ .contact-form__success {
45
+ padding: 12px 16px;
46
+ margin-bottom: 16px;
47
+ background-color: #dff6dd;
48
+ border: 1px solid #107c10;
49
+ border-radius: 4px;
50
+ color: #107c10;
51
+ font-weight: 500;
52
+ }
53
+
54
+ .contact-form__error {
55
+ padding: 12px 16px;
56
+ margin-bottom: 16px;
57
+ background-color: #fef7f1;
58
+ border: 1px solid #d13438;
59
+ border-radius: 4px;
60
+ color: #d13438;
61
+ font-weight: 500;
62
+ }
63
+
64
+ .contact-form__auth-notice {
65
+ margin-top: 16px;
66
+ padding: 12px 16px;
67
+ background-color: #fff4ce;
68
+ border: 1px solid #ffb900;
69
+ border-radius: 4px;
70
+ color: #8a6d00;
71
+ text-align: center;
72
+ }
73
+
74
+ @media (max-width: 768px) {
75
+ .contact-form {
76
+ padding: 16px;
77
+ margin: 16px;
78
+ }
79
+
80
+ .contact-form__row {
81
+ grid-template-columns: 1fr;
82
+ gap: 12px;
83
+ }
84
+ }