@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 +29 -0
- package/patches.ts +7 -0
- package/template/webApp/src/__inherit__appLayout.tsx +9 -0
- package/template/webApp/src/components/Footer.test.tsx +44 -0
- package/template/webApp/src/components/Footer.tsx +71 -0
- package/template/webApp/src/components/Hero.test.tsx +48 -0
- package/template/webApp/src/components/Hero.tsx +61 -0
- package/template/webApp/src/components/PromptCard.test.tsx +71 -0
- package/template/webApp/src/components/PromptCard.tsx +307 -0
- package/template/webApp/src/components/PromptHighlight.test.tsx +87 -0
- package/template/webApp/src/components/PromptHighlight.tsx +75 -0
- package/template/webApp/src/index.tsx +135 -0
- package/template/webApp/src/pages/__delete__About.tsx +7 -0
- package/template/webApp/src/routes.tsx +22 -0
- package/template/webApp/src/styles/__append__global.css +176 -0
- package/template/webApp/src/testCmp.tsx +9 -0
- package/tsconfig.app.json +39 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +41 -0
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,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,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,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,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
|
+
});
|