@sfdc-webapps/app-vibe-coding-starter 1.0.2

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.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@sfdc-webapps/app-vibe-coding-starter",
3
+ "version": "1.0.2",
4
+ "description": "Vibe coding starter app template",
5
+ "license": "ISC",
6
+ "author": "",
7
+ "type": "module",
8
+ "main": "index.js",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "scripts": {
13
+ "dev": "cd dist/digitalExperiences/webApplications/reference-app-vibe-coding-starter && yarn && yarn dev",
14
+ "test-patch": "npx tsx ../cli/src/index.ts apply-patches packages/reference-app-vibe-coding-starter packages/base-react-app packages/reference-app-vibe-coding-starter/dist --clean",
15
+ "watch": "npx tsx ../cli/src/index.ts watch-patches packages/reference-app-vibe-coding-starter packages/base-react-app packages/reference-app-vibe-coding-starter/dist --clean"
16
+ },
17
+ "devDependencies": {
18
+ "@testing-library/jest-dom": "^6.6.3",
19
+ "@testing-library/react": "^16.1.0",
20
+ "@testing-library/user-event": "^14.5.2",
21
+ "@types/react": "^19.2.7",
22
+ "@types/react-dom": "^19.2.3",
23
+ "@vitest/ui": "^2.1.8",
24
+ "jsdom": "^25.0.1",
25
+ "react-dom": "^19.2.1",
26
+ "react-router": "^7.10.1",
27
+ "vitest": "^2.1.8"
28
+ }
29
+ }
package/patches.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { Feature } from '../cli/src/types.js';
2
+
3
+ const feature: Feature = {
4
+ dependencies: ['packages/feature-authentication'],
5
+ };
6
+
7
+ export default feature;
@@ -0,0 +1,9 @@
1
+ import { Outlet } from "react-router";
2
+
3
+ export default function AppLayout() {
4
+ return (
5
+ <>
6
+ <Outlet />
7
+ </>
8
+ );
9
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import Footer from './Footer';
4
+
5
+ describe('Footer', () => {
6
+ it('renders the section title', () => {
7
+ render(<Footer />);
8
+
9
+ expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
10
+ 'Helpful content for your first build'
11
+ );
12
+ });
13
+
14
+ it('renders all resource links', () => {
15
+ render(<Footer />);
16
+
17
+ const links = screen.getAllByRole('link');
18
+ expect(links).toHaveLength(4);
19
+
20
+ expect(screen.getByRole('link', { name: /walkthroughs/i })).toHaveAttribute(
21
+ 'href',
22
+ '#walkthroughs'
23
+ );
24
+ expect(screen.getByRole('link', { name: /docs/i })).toHaveAttribute(
25
+ 'href',
26
+ '#docs'
27
+ );
28
+ expect(screen.getByRole('link', { name: /examples/i })).toHaveAttribute(
29
+ 'href',
30
+ '#examples'
31
+ );
32
+ expect(screen.getByRole('link', { name: /xx/i })).toHaveAttribute(
33
+ 'href',
34
+ '#more'
35
+ );
36
+ });
37
+
38
+ it('includes icons with each link', () => {
39
+ const { container } = render(<Footer />);
40
+
41
+ const images = container.querySelectorAll('img');
42
+ expect(images.length).toBeGreaterThan(0);
43
+ });
44
+ });
@@ -0,0 +1,71 @@
1
+ import type React from 'react';
2
+ import bookIcon from '@assets/icons/book.svg';
3
+ import rocketIcon from '@assets/icons/rocket.svg';
4
+ import starIcon from '@assets/icons/star.svg';
5
+
6
+ export default function Footer() {
7
+ return (
8
+ <section style={styles.resourcesSection}>
9
+ <h2 style={styles.resourcesTitle}>
10
+ Helpful content for your first build
11
+ </h2>
12
+ <div style={styles.resourcesLinks}>
13
+ <a href="#walkthroughs" style={styles.resourceLink}>
14
+ <img src={starIcon} alt="" width="16" height="16" />
15
+ <span>Walkthroughs</span>
16
+ </a>
17
+ <a href="#docs" style={styles.resourceLink}>
18
+ <img src={bookIcon} alt="" width="16" height="16" />
19
+ <span>Docs</span>
20
+ </a>
21
+ <a href="#examples" style={styles.resourceLink}>
22
+ <img src={rocketIcon} alt="" width="16" height="16" />
23
+ <span>Examples</span>
24
+ </a>
25
+ <a href="#more" style={styles.resourceLink}>
26
+ <img src={rocketIcon} alt="" width="16" height="16" />
27
+ <span>XX</span>
28
+ </a>
29
+ </div>
30
+ </section>
31
+ );
32
+ }
33
+
34
+ const styles: Record<string, React.CSSProperties> = {
35
+ resourcesSection: {
36
+ position: 'relative',
37
+ background:
38
+ 'linear-gradient(rgba(255, 255, 255, 0.01), rgba(255, 255, 255, 0.5))',
39
+ border: '2px solid #055fff',
40
+ borderRadius: '100px',
41
+ padding: '56px 64px',
42
+ marginTop: '64px',
43
+ marginBottom: '4rem',
44
+ display: 'flex',
45
+ flexDirection: 'column',
46
+ gap: '24px',
47
+ backdropFilter: 'blur(24px)',
48
+ WebkitBackdropFilter: 'blur(24px)',
49
+ },
50
+ resourcesTitle: {
51
+ fontSize: '24px',
52
+ fontWeight: 600,
53
+ lineHeight: 'normal',
54
+ color: '#002775',
55
+ margin: 0,
56
+ },
57
+ resourcesLinks: {
58
+ display: 'flex',
59
+ gap: '64px',
60
+ alignItems: 'center',
61
+ },
62
+ resourceLink: {
63
+ display: 'flex',
64
+ alignItems: 'center',
65
+ gap: '8px',
66
+ fontSize: '16px',
67
+ color: '#002775',
68
+ textDecoration: 'none',
69
+ transition: 'opacity 0.2s ease',
70
+ },
71
+ };
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import Hero from './Hero';
4
+
5
+ describe('Hero', () => {
6
+ it('renders heading and default image', () => {
7
+ render(<Hero heading="Welcome to Vibe Coding" />);
8
+
9
+ expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(
10
+ 'Welcome to Vibe Coding'
11
+ );
12
+ expect(screen.getByRole('img')).toBeInTheDocument();
13
+ });
14
+
15
+ it('renders with subtext when provided', () => {
16
+ render(<Hero heading="Test" subtext="This is a subtext" />);
17
+
18
+ expect(screen.getByText('Test')).toBeInTheDocument();
19
+ expect(screen.getByText('This is a subtext')).toBeInTheDocument();
20
+ });
21
+
22
+ it('accepts custom image and alt text', () => {
23
+ render(
24
+ <Hero heading="Test" image="/custom-image.png" imageAlt="Custom Alt" />
25
+ );
26
+
27
+ const img = screen.getByAltText('Custom Alt');
28
+ expect(img).toHaveAttribute('src', '/custom-image.png');
29
+ });
30
+
31
+ it('supports JSX content in heading and subtext', () => {
32
+ const heading = (
33
+ <>
34
+ Welcome <strong>Hero</strong>
35
+ </>
36
+ );
37
+ const subtext = (
38
+ <span>
39
+ Get <em>started</em>
40
+ </span>
41
+ );
42
+
43
+ render(<Hero heading={heading} subtext={subtext} />);
44
+
45
+ expect(screen.getByText('Hero')).toBeInTheDocument();
46
+ expect(screen.getByText('started')).toBeInTheDocument();
47
+ });
48
+ });
@@ -0,0 +1,61 @@
1
+ import type React from 'react';
2
+ import vibeCodey from '@assets/images/vibe-codey.svg';
3
+
4
+ interface HeroProps {
5
+ heading: React.ReactNode;
6
+ subtext?: React.ReactNode;
7
+ image?: string;
8
+ imageAlt?: string;
9
+ }
10
+
11
+ export default function Hero({
12
+ heading,
13
+ subtext,
14
+ image = vibeCodey,
15
+ imageAlt = 'Agentforce',
16
+ }: HeroProps) {
17
+ return (
18
+ <section style={styles.heroSection}>
19
+ <div style={styles.heroImageContainer}>
20
+ <img src={image} alt={imageAlt} style={styles.heroImage} />
21
+ </div>
22
+ <h1 style={styles.heroHeading}>{heading}</h1>
23
+ {subtext && <p style={styles.heroSubtext}>{subtext}</p>}
24
+ </section>
25
+ );
26
+ }
27
+
28
+ const styles: Record<string, React.CSSProperties> = {
29
+ heroSection: {
30
+ display: 'flex',
31
+ flexDirection: 'column',
32
+ alignItems: 'center',
33
+ paddingTop: '4rem',
34
+ paddingBottom: '4rem',
35
+ textAlign: 'center',
36
+ },
37
+ heroImageContainer: {
38
+ marginBottom: '2rem',
39
+ position: 'relative',
40
+ },
41
+ heroImage: {
42
+ width: '100%',
43
+ maxWidth: '383px',
44
+ height: 'auto',
45
+ },
46
+ heroHeading: {
47
+ fontSize: '4rem',
48
+ fontWeight: 700,
49
+ lineHeight: 1.2,
50
+ marginBottom: '1.5rem',
51
+ color: 'var(--on-surface-3)',
52
+ maxWidth: '800px',
53
+ },
54
+ heroSubtext: {
55
+ fontSize: '1.25rem',
56
+ lineHeight: 1.6,
57
+ color: 'var(--on-surface-3)',
58
+ maxWidth: '100%',
59
+ margin: '0 auto',
60
+ },
61
+ };
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import PromptCard from './PromptCard';
5
+
6
+ describe('PromptCard', () => {
7
+ beforeEach(() => {
8
+ // Mock clipboard API for tests
9
+ Object.defineProperty(navigator, 'clipboard', {
10
+ value: {
11
+ writeText: vi.fn(() => Promise.resolve()),
12
+ },
13
+ writable: true,
14
+ configurable: true,
15
+ });
16
+
17
+ // Mock isSecureContext to true so the modern clipboard API is used
18
+ Object.defineProperty(window, 'isSecureContext', {
19
+ value: true,
20
+ writable: true,
21
+ configurable: true,
22
+ });
23
+ });
24
+
25
+ it('renders title, description, and prompt text', () => {
26
+ render(
27
+ <PromptCard
28
+ title="Create a component"
29
+ description="Build a React component"
30
+ promptText="Generate a button component"
31
+ />
32
+ );
33
+
34
+ expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
35
+ 'Create a component'
36
+ );
37
+ expect(screen.getByText('Build a React component')).toBeInTheDocument();
38
+ expect(screen.getByText('Generate a button component')).toBeInTheDocument();
39
+ });
40
+
41
+ it('renders a clickable copy button', async () => {
42
+ const user = userEvent.setup();
43
+
44
+ render(
45
+ <PromptCard
46
+ title="Test"
47
+ description="Description"
48
+ promptText="Copy this text"
49
+ />
50
+ );
51
+
52
+ const button = screen.getByRole('button', { name: /copy prompt/i });
53
+ expect(button).toBeInTheDocument();
54
+
55
+ await expect(user.click(button)).resolves.not.toThrow();
56
+ });
57
+
58
+ it('handles long prompt text', () => {
59
+ const longPrompt = 'A'.repeat(500);
60
+
61
+ render(
62
+ <PromptCard
63
+ title="Test"
64
+ description="Description"
65
+ promptText={longPrompt}
66
+ />
67
+ );
68
+
69
+ expect(screen.getByText(longPrompt)).toBeInTheDocument();
70
+ });
71
+ });
@@ -0,0 +1,307 @@
1
+ import type React from 'react';
2
+ import copyIcon from '@assets/icons/copy.svg';
3
+
4
+ interface PromptCardProps {
5
+ title: string;
6
+ description: string;
7
+ promptText: string;
8
+ }
9
+
10
+ interface ListItem {
11
+ content: string;
12
+ subItems: string[];
13
+ }
14
+
15
+ function renderMarkdown(text: string): React.ReactNode {
16
+ const lines = text.split('\n');
17
+ const elements: React.ReactNode[] = [];
18
+ let bulletItems: string[] = [];
19
+ let numberedItems: ListItem[] = [];
20
+
21
+ const renderInlineMarkdown = (line: string): React.ReactNode => {
22
+ // Handle bold text with **
23
+ const parts: React.ReactNode[] = [];
24
+ const boldRegex = /\*\*(.+?)\*\*/g;
25
+ let lastIndex = 0;
26
+ let match;
27
+
28
+ while ((match = boldRegex.exec(line)) !== null) {
29
+ if (match.index > lastIndex) {
30
+ parts.push(line.slice(lastIndex, match.index));
31
+ }
32
+ parts.push(
33
+ <strong key={match.index} style={markdownStyles.bold}>
34
+ {match[1]}
35
+ </strong>
36
+ );
37
+ lastIndex = match.index + match[0].length;
38
+ }
39
+
40
+ if (lastIndex < line.length) {
41
+ parts.push(line.slice(lastIndex));
42
+ }
43
+
44
+ return parts.length > 0 ? parts : line;
45
+ };
46
+
47
+ const flushBulletList = () => {
48
+ if (bulletItems.length > 0) {
49
+ elements.push(
50
+ <ul key={`ul-${elements.length}`} style={markdownStyles.list}>
51
+ {bulletItems.map((item, i) => (
52
+ <li key={i} style={markdownStyles.listItem}>
53
+ {renderInlineMarkdown(item)}
54
+ </li>
55
+ ))}
56
+ </ul>
57
+ );
58
+ bulletItems = [];
59
+ }
60
+ };
61
+
62
+ const flushNumberedList = () => {
63
+ if (numberedItems.length > 0) {
64
+ elements.push(
65
+ <ol key={`ol-${elements.length}`} style={markdownStyles.numberedList}>
66
+ {numberedItems.map((item, i) => (
67
+ <li key={i} style={markdownStyles.listItem}>
68
+ {renderInlineMarkdown(item.content)}
69
+ {item.subItems.length > 0 && (
70
+ <ul style={markdownStyles.nestedList}>
71
+ {item.subItems.map((subItem, j) => (
72
+ <li key={j} style={markdownStyles.listItem}>
73
+ {renderInlineMarkdown(subItem)}
74
+ </li>
75
+ ))}
76
+ </ul>
77
+ )}
78
+ </li>
79
+ ))}
80
+ </ol>
81
+ );
82
+ numberedItems = [];
83
+ }
84
+ };
85
+
86
+ const flushAllLists = () => {
87
+ flushBulletList();
88
+ flushNumberedList();
89
+ };
90
+
91
+ for (let i = 0; i < lines.length; i++) {
92
+ const line = lines[i];
93
+
94
+ // H3 headers (###)
95
+ if (line.startsWith('### ')) {
96
+ flushAllLists();
97
+ elements.push(
98
+ <h4 key={`h3-${i}`} style={markdownStyles.h3}>
99
+ {line.slice(4)}
100
+ </h4>
101
+ );
102
+ continue;
103
+ }
104
+
105
+ // H2 headers (##)
106
+ if (line.startsWith('## ')) {
107
+ flushAllLists();
108
+ elements.push(
109
+ <h3 key={`h2-${i}`} style={markdownStyles.h2}>
110
+ {line.slice(3)}
111
+ </h3>
112
+ );
113
+ continue;
114
+ }
115
+
116
+ // Numbered list items (1. 2. etc)
117
+ const numberedMatch = line.match(/^\d+\.\s+(.+)$/);
118
+ if (numberedMatch) {
119
+ flushBulletList();
120
+ numberedItems.push({ content: numberedMatch[1], subItems: [] });
121
+ continue;
122
+ }
123
+
124
+ // Indented sub-items under numbered list ( - item)
125
+ const indentedBulletMatch = line.match(/^\s{2,}[-*]\s+(.+)$/);
126
+ if (indentedBulletMatch && numberedItems.length > 0) {
127
+ numberedItems[numberedItems.length - 1].subItems.push(
128
+ indentedBulletMatch[1]
129
+ );
130
+ continue;
131
+ }
132
+
133
+ // Top-level unordered list items (- or *)
134
+ const bulletMatch = line.match(/^[-*]\s+(.+)$/);
135
+ if (bulletMatch) {
136
+ flushNumberedList();
137
+ bulletItems.push(bulletMatch[1]);
138
+ continue;
139
+ }
140
+
141
+ // Empty lines
142
+ if (line.trim() === '') {
143
+ // Don't flush numbered lists on empty lines - they may have more items
144
+ flushBulletList();
145
+ continue;
146
+ }
147
+
148
+ // Regular paragraph
149
+ flushAllLists();
150
+ elements.push(
151
+ <p key={`p-${i}`} style={markdownStyles.paragraph}>
152
+ {renderInlineMarkdown(line)}
153
+ </p>
154
+ );
155
+ }
156
+
157
+ flushAllLists();
158
+ return elements;
159
+ }
160
+
161
+ const markdownStyles: Record<string, React.CSSProperties> = {
162
+ h2: {
163
+ fontSize: '13px',
164
+ fontWeight: 700,
165
+ color: 'var(--on-surface-3)',
166
+ margin: '8px 0 4px 0',
167
+ },
168
+ h3: {
169
+ fontSize: '12px',
170
+ fontWeight: 600,
171
+ color: 'var(--on-surface-3)',
172
+ margin: '8px 0 4px 0',
173
+ },
174
+ paragraph: {
175
+ margin: '0 0 4px 0',
176
+ },
177
+ list: {
178
+ margin: '4px 0',
179
+ paddingLeft: '16px',
180
+ listStyleType: 'disc',
181
+ },
182
+ numberedList: {
183
+ margin: '4px 0',
184
+ paddingLeft: '18px',
185
+ listStyleType: 'decimal',
186
+ },
187
+ nestedList: {
188
+ margin: '2px 0 2px 0',
189
+ paddingLeft: '16px',
190
+ listStyleType: 'disc',
191
+ },
192
+ listItem: {
193
+ margin: '2px 0',
194
+ },
195
+ bold: {
196
+ fontWeight: 600,
197
+ },
198
+ };
199
+
200
+ export default function PromptCard({
201
+ title,
202
+ description,
203
+ promptText,
204
+ }: PromptCardProps) {
205
+ const handleCopy = () => {
206
+ // Try modern clipboard API first
207
+ if (navigator.clipboard && window.isSecureContext) {
208
+ navigator.clipboard.writeText(promptText).catch(() => {
209
+ // Fallback if modern API fails
210
+ fallbackCopy(promptText);
211
+ });
212
+ } else {
213
+ // Use fallback method for restricted environments like VSCode tabs
214
+ fallbackCopy(promptText);
215
+ }
216
+ };
217
+
218
+ const fallbackCopy = (text: string) => {
219
+ const textArea = document.createElement('textarea');
220
+ textArea.value = text;
221
+ textArea.style.position = 'fixed';
222
+ textArea.style.left = '-999999px';
223
+ textArea.style.top = '-999999px';
224
+ document.body.appendChild(textArea);
225
+ textArea.focus();
226
+ textArea.select();
227
+ document.execCommand('copy');
228
+ textArea.remove();
229
+ };
230
+
231
+ return (
232
+ <div style={styles.promptContent}>
233
+ <h2 style={styles.promptTitle}>{title}</h2>
234
+ <p style={styles.promptDescription}>{description}</p>
235
+ <div style={styles.promptField}>
236
+ <div style={styles.promptFieldInner}>
237
+ <div style={styles.promptText}>{renderMarkdown(promptText)}</div>
238
+ <button
239
+ type="button"
240
+ style={styles.copyButton}
241
+ aria-label="Copy prompt"
242
+ onClick={handleCopy}
243
+ >
244
+ <img src={copyIcon} alt="Copy" width="20" height="20" />
245
+ </button>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ );
250
+ }
251
+
252
+ const styles: Record<string, React.CSSProperties> = {
253
+ promptContent: {
254
+ flex: 1,
255
+ display: 'flex',
256
+ flexDirection: 'column',
257
+ gap: '10px',
258
+ minWidth: 0,
259
+ },
260
+ promptTitle: {
261
+ fontSize: '36px',
262
+ fontWeight: 600,
263
+ lineHeight: 1.03,
264
+ color: 'var(--on-surface-3)',
265
+ margin: 0,
266
+ },
267
+ promptDescription: {
268
+ fontSize: '16px',
269
+ lineHeight: 1.45,
270
+ color: 'var(--on-surface-3)',
271
+ margin: 0,
272
+ },
273
+ promptField: {
274
+ border: '2px solid #055fff',
275
+ borderRadius: '20px',
276
+ overflow: 'hidden',
277
+ },
278
+ promptFieldInner: {
279
+ display: 'flex',
280
+ alignItems: 'flex-start',
281
+ gap: '8px',
282
+ padding: '18px',
283
+ },
284
+ promptText: {
285
+ flex: 1,
286
+ fontSize: '12px',
287
+ fontWeight: 500,
288
+ lineHeight: 1.4,
289
+ color: 'var(--on-surface-2)',
290
+ margin: 0,
291
+ minHeight: '45px',
292
+ maxHeight: '150px',
293
+ overflow: 'auto',
294
+ },
295
+ copyButton: {
296
+ background: 'rgba(3,45,96,0.17)',
297
+ border: 'none',
298
+ borderRadius: '58px',
299
+ padding: '8px',
300
+ cursor: 'pointer',
301
+ display: 'flex',
302
+ alignItems: 'center',
303
+ justifyContent: 'center',
304
+ flexShrink: 0,
305
+ transition: 'background 0.2s ease',
306
+ },
307
+ };
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import PromptHighlight from './PromptHighlight';
4
+
5
+ describe('PromptHighlight', () => {
6
+ const defaultProps = {
7
+ image: '/test-image.png',
8
+ imageAlt: 'Test Image',
9
+ };
10
+
11
+ it('renders children and image', () => {
12
+ render(
13
+ <PromptHighlight {...defaultProps}>
14
+ <div>Test Content</div>
15
+ </PromptHighlight>
16
+ );
17
+
18
+ expect(screen.getByText('Test Content')).toBeInTheDocument();
19
+ expect(screen.getByAltText('Test Image')).toBeInTheDocument();
20
+ });
21
+
22
+ it('positions image on the right by default', () => {
23
+ const { container } = render(
24
+ <PromptHighlight {...defaultProps}>
25
+ <div data-testid="content">Content</div>
26
+ </PromptHighlight>
27
+ );
28
+
29
+ const layout = container.firstChild as HTMLElement;
30
+ const children = Array.from(layout.children);
31
+
32
+ expect(children[0]).toContainElement(screen.getByTestId('content'));
33
+ expect(children[1]).toContainElement(screen.getByAltText('Test Image'));
34
+ });
35
+
36
+ it('positions image on the left when specified', () => {
37
+ const { container } = render(
38
+ <PromptHighlight {...defaultProps} imagePosition="left">
39
+ <div data-testid="content">Content</div>
40
+ </PromptHighlight>
41
+ );
42
+
43
+ const layout = container.firstChild as HTMLElement;
44
+ const children = Array.from(layout.children);
45
+
46
+ expect(children[0]).toContainElement(screen.getByAltText('Test Image'));
47
+ expect(children[1]).toContainElement(screen.getByTestId('content'));
48
+ });
49
+
50
+ it('supports landscape and square image sizes', () => {
51
+ const { rerender } = render(
52
+ <PromptHighlight {...defaultProps} imageSize="landscape">
53
+ <div>Content</div>
54
+ </PromptHighlight>
55
+ );
56
+
57
+ let img = screen.getByAltText('Test Image');
58
+ expect(img.parentElement).toBeInTheDocument();
59
+
60
+ rerender(
61
+ <PromptHighlight {...defaultProps} imageSize="square">
62
+ <div>Content</div>
63
+ </PromptHighlight>
64
+ );
65
+
66
+ img = screen.getByAltText('Test Image');
67
+ expect(img.parentElement).toBeInTheDocument();
68
+ });
69
+
70
+ it('renders complex children content', () => {
71
+ render(
72
+ <PromptHighlight {...defaultProps}>
73
+ <div>
74
+ <h2>Heading</h2>
75
+ <p>Paragraph</p>
76
+ <button type="button">Click me</button>
77
+ </div>
78
+ </PromptHighlight>
79
+ );
80
+
81
+ expect(screen.getByText('Heading')).toBeInTheDocument();
82
+ expect(screen.getByText('Paragraph')).toBeInTheDocument();
83
+ expect(
84
+ screen.getByRole('button', { name: 'Click me' })
85
+ ).toBeInTheDocument();
86
+ });
87
+ });
@@ -0,0 +1,75 @@
1
+ import type React from 'react';
2
+
3
+ interface PromptHighlightProps {
4
+ children: React.ReactNode;
5
+ image: string | React.ReactNode;
6
+ imageAlt: string;
7
+ imagePosition?: 'left' | 'right';
8
+ imageSize?: 'landscape' | 'square';
9
+ }
10
+
11
+ export default function PromptHighlight({
12
+ children,
13
+ image,
14
+ imageAlt,
15
+ imagePosition = 'right',
16
+ imageSize = 'landscape',
17
+ }: PromptHighlightProps) {
18
+ const imageContainerStyle =
19
+ imageSize === 'square'
20
+ ? styles.promptImageContainerSquare
21
+ : styles.promptImageContainer;
22
+
23
+ const imageElement =
24
+ typeof image === 'string' ? (
25
+ <div style={imageContainerStyle}>
26
+ <img src={image} alt={imageAlt} style={styles.promptImage} />
27
+ </div>
28
+ ) : (
29
+ <div style={imageContainerStyle}>{image}</div>
30
+ );
31
+
32
+ return (
33
+ <div style={styles.layout}>
34
+ {imagePosition === 'left' ? (
35
+ <>
36
+ {imageElement}
37
+ {children}
38
+ </>
39
+ ) : (
40
+ <>
41
+ {children}
42
+ {imageElement}
43
+ </>
44
+ )}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ const styles: Record<string, React.CSSProperties> = {
50
+ layout: {
51
+ display: 'flex',
52
+ alignItems: 'center',
53
+ gap: '20px',
54
+ width: '100%',
55
+ },
56
+ promptImageContainer: {
57
+ flexShrink: 0,
58
+ width: '480px',
59
+ height: '250px',
60
+ overflow: 'hidden',
61
+ borderRadius: '8px',
62
+ },
63
+ promptImageContainerSquare: {
64
+ flexShrink: 0,
65
+ width: '371px',
66
+ height: '371px',
67
+ overflow: 'hidden',
68
+ borderRadius: '8px',
69
+ },
70
+ promptImage: {
71
+ width: '100%',
72
+ height: '100%',
73
+ objectFit: 'contain',
74
+ },
75
+ };
@@ -0,0 +1,135 @@
1
+ import codey1 from '@assets/images/codey-1.png';
2
+ import codey3 from '@assets/images/codey-3.png';
3
+ // import Footer from '@components/Footer';
4
+ import Hero from '@components/Hero';
5
+ import PromptCard from '@components/PromptCard';
6
+ import PromptHighlight from '@components/PromptHighlight';
7
+
8
+ import '@styles/global.css';
9
+
10
+ const styles: Record<string, React.CSSProperties> = {
11
+ container: {
12
+ position: 'relative',
13
+ width: '100%',
14
+ minHeight: '100vh',
15
+ overflow: 'hidden',
16
+ background:
17
+ 'linear-gradient(180deg, #EDF4FF 0%, #F6F2FB 50%, #E5B9FE 100%)',
18
+ },
19
+ content: {
20
+ position: 'relative',
21
+ zIndex: 1,
22
+ width: '100%',
23
+ maxWidth: '1100px',
24
+ margin: '0 auto',
25
+ padding: '0 2rem',
26
+ },
27
+ agentforceText: {
28
+ background: 'linear-gradient(135deg, #5867E8 0%, #E5B9FE 100%)',
29
+ WebkitBackgroundClip: 'text',
30
+ WebkitTextFillColor: 'transparent',
31
+ backgroundClip: 'text',
32
+ },
33
+ newProjectLink: {
34
+ color: 'var(--on-surface-3)',
35
+ textDecoration: 'underline',
36
+ fontWeight: 500,
37
+ },
38
+ promptsSection: {
39
+ display: 'flex',
40
+ flexDirection: 'column',
41
+ gap: '100px',
42
+ paddingBottom: '4rem',
43
+ },
44
+ toast: {
45
+ position: 'fixed' as const,
46
+ bottom: '2rem',
47
+ left: '50%',
48
+ transform: 'translateX(-50%)',
49
+ background: '#c62828',
50
+ color: 'white',
51
+ padding: '1rem 2rem',
52
+ borderRadius: '8px',
53
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
54
+ zIndex: 1000,
55
+ display: 'flex',
56
+ alignItems: 'center',
57
+ gap: '0.75rem',
58
+ animation: 'slideUp 0.3s ease',
59
+ },
60
+ toastCloseButton: {
61
+ background: 'transparent',
62
+ border: 'none',
63
+ color: 'white',
64
+ cursor: 'pointer',
65
+ fontSize: '1.25rem',
66
+ padding: '0',
67
+ lineHeight: 1,
68
+ },
69
+ };
70
+
71
+ export default function Home(): React.ReactElement {
72
+
73
+ return (
74
+ <div style={styles.container}>
75
+ <div style={styles.content}>
76
+ <Hero
77
+ heading={
78
+ <>
79
+ Try Building React with{' '}
80
+ <span style={styles.agentforceText}>Agentforce</span>
81
+ </>
82
+ }
83
+ subtext={
84
+ <>
85
+ Get started with the example prompts below or start fresh with a{' '}
86
+ <a href="#new-project" style={styles.newProjectLink}>
87
+ new project
88
+ </a>
89
+ </>
90
+ }
91
+ />
92
+
93
+ <section style={styles.promptsSection}>
94
+ <PromptHighlight
95
+ image={codey1}
96
+ imageAlt="Dark Mode Example"
97
+ imagePosition="right"
98
+ imageSize="landscape"
99
+ >
100
+ <PromptCard
101
+ title="Create a Dark Mode"
102
+ description="Instantly add a Dark Mode switch that updates your app's background, text, and accent colors."
103
+ promptText="Add a Dark Mode to my app that updates background, text, and accent colors when toggled, with a smooth, polished animation for the transition.
104
+
105
+ ### Dark Mode Toggle
106
+
107
+ A sliding switch should be placed in the top right of the index page. The switch should slide between sun and moon icons. When the moon icon is selected, Dark Mode should be displayed, and when the sun icon is selected, Light Mode (the current color scheme) should be selected.
108
+
109
+ ### Colors
110
+
111
+ The word **Agentforce** in the heading is a gradient. The app's background is also a gradient. In Dark Mode these two gradients should complement each other, as they do in light mode. Also keep in mind the colors of the various SVG images on the page. These images need to look good in Dark mode.
112
+
113
+ Be sure to also change the footer background. Remember to change every component's foreground and background color to appropriately contrast with the Dark mode background."
114
+ />
115
+ </PromptHighlight>
116
+
117
+ <PromptHighlight
118
+ image={codey3}
119
+ imageAlt="Animations Example"
120
+ imagePosition="right"
121
+ imageSize="square"
122
+ >
123
+ <PromptCard
124
+ title="Add animations to give personality"
125
+ description="Bring your UI to life and give it personality with animations. Send the prompt to the agent to see the magic happen."
126
+ promptText="Animate the sections of this page to fly in from the sides on load. when you finish reload the page to show the animations"
127
+ />
128
+ </PromptHighlight>
129
+ </section>
130
+
131
+ {/* <Footer /> */}
132
+ </div>
133
+ </div>
134
+ );
135
+ }
@@ -0,0 +1,7 @@
1
+ export default function About() {
2
+ return (
3
+ <div>
4
+ <h1>About</h1>
5
+ </div>
6
+ );
7
+ }
@@ -0,0 +1,22 @@
1
+ import type { RouteObject } from "react-router";
2
+ import Home from ".";
3
+ import AppLayout from "./__inherit__appLayout";
4
+ import About from "./pages/__delete__about";
5
+
6
+ export const routes: RouteObject[] = [
7
+ {
8
+ path: '/',
9
+ element: <AppLayout/>,
10
+ children: [
11
+ {
12
+ index: true,
13
+ element: <Home />,
14
+ handle: { showInNavigation: true, label: 'Home' }
15
+ },
16
+ {
17
+ path: '__delete__about',
18
+ element: <About />,
19
+ }
20
+ ]
21
+ }
22
+ ]
@@ -0,0 +1,176 @@
1
+
2
+ :root {
3
+ /* Salesforce Design Tokens - Colors */
4
+ --constant-white: #ffffff;
5
+ --vibrant-purple-95: #f6f2fb;
6
+ --electric-blue-50: #066afe;
7
+ --electric-blue-40: #0250d9;
8
+ --electric-blue-20: #002775;
9
+ --electric-blue-95: #edf4ff;
10
+ --vibrant-violet-80: #e5b9fe;
11
+ --indigo-50: #5867e8;
12
+ --indigo-30: #2f2cb7;
13
+ --indigo-90: #e0e5f8;
14
+ --hot-orange-65: #ff784f;
15
+ --hot-orange-80: #feb9a5;
16
+ --aubergine: #df9ff4;
17
+ --soft-contrast-gray: #f3f3f3;
18
+ --on-surface-3: #03234d;
19
+ --on-surface-2: #2e2e2e;
20
+
21
+ /* Typography */
22
+ font-family:
23
+ 'SF Pro',
24
+ -apple-system,
25
+ BlinkMacSystemFont,
26
+ 'Segoe UI',
27
+ Roboto,
28
+ sans-serif;
29
+ line-height: 1.5;
30
+ font-weight: 400;
31
+
32
+ /* Color scheme */
33
+ color-scheme: light;
34
+ color: var(--on-surface-3);
35
+ background-color: var(--constant-white);
36
+
37
+ /* Rendering optimizations */
38
+ font-synthesis: none;
39
+ text-rendering: optimizeLegibility;
40
+ -webkit-font-smoothing: antialiased;
41
+ -moz-osx-font-smoothing: grayscale;
42
+ text-size-adjust: 100%;
43
+ }
44
+
45
+ * {
46
+ box-sizing: border-box;
47
+ }
48
+
49
+ body {
50
+ margin: 0;
51
+ min-width: 320px;
52
+ min-height: 100vh;
53
+ overflow-x: hidden;
54
+ }
55
+
56
+ #root {
57
+ width: 100%;
58
+ margin: 0 auto;
59
+ }
60
+
61
+ h1,
62
+ h2,
63
+ h3,
64
+ h4,
65
+ h5,
66
+ h6 {
67
+ margin: 0 0 1rem 0;
68
+ }
69
+
70
+ a {
71
+ font-weight: 500;
72
+ color: var(--electric-blue-50);
73
+ text-decoration: inherit;
74
+ transition: color 0.2s ease-in-out;
75
+ }
76
+
77
+ a:hover {
78
+ color: var(--electric-blue-40);
79
+ }
80
+
81
+ a:focus-visible {
82
+ outline: 2px solid var(--electric-blue-50);
83
+ outline-offset: 2px;
84
+ border-radius: 2px;
85
+ }
86
+
87
+ button {
88
+ border-radius: 8px;
89
+ border: 1px solid transparent;
90
+ padding: 0.6em 1.2em;
91
+ font-size: 1em;
92
+ font-weight: 500;
93
+ font-family: inherit;
94
+ background-color: var(--electric-blue-50);
95
+ color: white;
96
+ cursor: pointer;
97
+ transition: all 0.2s ease-in-out;
98
+ }
99
+
100
+ button:hover {
101
+ background-color: var(--electric-blue-40);
102
+ transform: translateY(-1px);
103
+ }
104
+
105
+ button:active {
106
+ transform: translateY(0);
107
+ }
108
+
109
+ button:focus,
110
+ button:focus-visible {
111
+ outline: 2px solid var(--electric-blue-50);
112
+ outline-offset: 2px;
113
+ }
114
+
115
+ button:disabled {
116
+ opacity: 0.5;
117
+ cursor: not-allowed;
118
+ transform: none;
119
+ }
120
+
121
+ code {
122
+ background-color: rgba(255, 255, 255, 0.1);
123
+ padding: 0.2em 0.4em;
124
+ border-radius: 4px;
125
+ font-family: 'Courier New', Courier, monospace;
126
+ font-size: 0.9em;
127
+ }
128
+
129
+ pre {
130
+ background-color: rgba(255, 255, 255, 0.05);
131
+ padding: 1em;
132
+ border-radius: 8px;
133
+ overflow-x: auto;
134
+ }
135
+
136
+ pre code {
137
+ background-color: transparent;
138
+ padding: 0;
139
+ }
140
+
141
+ /* Smooth scrolling for better UX */
142
+ html {
143
+ scroll-behavior: smooth;
144
+ }
145
+
146
+ @media (prefers-reduced-motion: reduce) {
147
+ html {
148
+ scroll-behavior: auto;
149
+ }
150
+
151
+ *,
152
+ *::before,
153
+ *::after {
154
+ animation-duration: 0.01ms !important;
155
+ animation-iteration-count: 1 !important;
156
+ transition-duration: 0.01ms !important;
157
+ }
158
+ }
159
+
160
+ /* Image optimization */
161
+ img {
162
+ max-width: 100%;
163
+ height: auto;
164
+ }
165
+
166
+ /* Focus styles for accessibility */
167
+ :focus-visible {
168
+ outline: 2px solid var(--electric-blue-50);
169
+ outline-offset: 2px;
170
+ }
171
+
172
+ /* Selection styling */
173
+ ::selection {
174
+ background-color: var(--electric-blue-50);
175
+ color: white;
176
+ }
@@ -0,0 +1,9 @@
1
+ import AppLayout from "./__inherit__appLayout";
2
+
3
+ export default function TestCmp() {
4
+ return (
5
+ <div>
6
+ <AppLayout />
7
+ </div>
8
+ );
9
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true,
26
+
27
+ /* Path mapping */
28
+ "baseUrl": ".",
29
+ "paths": {
30
+ "@/*": ["./template/webApp/src/*"],
31
+ "@api/*": ["./template/webApp/src/api/*"],
32
+ "@components/*": ["./template/webApp/src/components/*"],
33
+ "@utils/*": ["./template/webApp/src/utils/*"],
34
+ "@styles/*": ["./template/webApp/src/styles/*"],
35
+ "@assets/*": ["./template/webApp/src/assets/*"]
36
+ }
37
+ },
38
+ "include": ["./template/webApp/src"]
39
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import path from 'path';
4
+ import { resolve } from 'path';
5
+ import tailwindcss from '@tailwindcss/vite';
6
+
7
+ export default defineConfig(({ mode }) => {
8
+ return {
9
+ plugins: [tailwindcss(), react()],
10
+
11
+ // Build configuration
12
+ build: {
13
+ outDir: resolve(__dirname, 'dist'),
14
+ assetsDir: 'assets',
15
+ sourcemap: false,
16
+ },
17
+
18
+ // Resolve aliases (shared between build and test)
19
+ resolve: {
20
+ alias: {
21
+ '@': path.resolve(__dirname, './template/src'),
22
+ '@api': path.resolve(__dirname, './template/src/api'),
23
+ '@components': path.resolve(__dirname, './template/src/components'),
24
+ '@utils': path.resolve(__dirname, './template/src/utils'),
25
+ '@styles': path.resolve(__dirname, './template/src/styles'),
26
+ '@assets': path.resolve(__dirname, './template/src/assets'),
27
+ },
28
+ },
29
+
30
+ // Vitest configuration
31
+ test: {
32
+ root: resolve(__dirname),
33
+ environment: 'jsdom',
34
+ include: [
35
+ 'template/src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
36
+ 'template/src/**/__tests__/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
37
+ ],
38
+ globals: true,
39
+ },
40
+ };
41
+ });