@salesforce/webapp-template-app-react-vibe-coding-starter-experimental 1.3.3

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 (107) hide show
  1. package/LICENSE.txt +82 -0
  2. package/dist/.a4drules/build-validation.md +81 -0
  3. package/dist/.a4drules/code-quality.md +150 -0
  4. package/dist/.a4drules/graphql/tools/knowledge/lds-explore-graphql-schema.md +227 -0
  5. package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-mutationquery.md +211 -0
  6. package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-readquery.md +185 -0
  7. package/dist/.a4drules/graphql/tools/knowledge/lds-guide-graphql.md +205 -0
  8. package/dist/.a4drules/graphql/tools/schemas/shared.graphqls +1150 -0
  9. package/dist/.a4drules/graphql.md +98 -0
  10. package/dist/.a4drules/images.md +13 -0
  11. package/dist/.a4drules/react.md +361 -0
  12. package/dist/.a4drules/react_image_processing.md +45 -0
  13. package/dist/.a4drules/typescript.md +224 -0
  14. package/dist/.forceignore +15 -0
  15. package/dist/.husky/pre-commit +4 -0
  16. package/dist/.prettierignore +11 -0
  17. package/dist/.prettierrc +17 -0
  18. package/dist/CHANGELOG.md +11 -0
  19. package/dist/README.md +18 -0
  20. package/dist/config/project-scratch-def.json +13 -0
  21. package/dist/force-app/main/default/digitalExperienceConfigs/appreactvibecodingstarter1.digitalExperienceConfig +8 -0
  22. package/dist/force-app/main/default/digitalExperiences/site/appreactvibecodingstarter1/appreactvibecodingstarter1.digitalExperience-meta.xml +11 -0
  23. package/dist/force-app/main/default/digitalExperiences/site/appreactvibecodingstarter1/sfdc_cms__site/appreactvibecodingstarter1/_meta.json +5 -0
  24. package/dist/force-app/main/default/digitalExperiences/site/appreactvibecodingstarter1/sfdc_cms__site/appreactvibecodingstarter1/content.json +10 -0
  25. package/dist/force-app/main/default/networks/appreactvibecodingstarter.network +60 -0
  26. package/dist/force-app/main/default/package.xml +20 -0
  27. package/dist/force-app/main/default/sites/appreactvibecodingstarter.site +31 -0
  28. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/.prettierignore +9 -0
  29. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/.prettierrc +11 -0
  30. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/appreactvibecodingstarter.webapplication-meta.xml +7 -0
  31. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/eslint.config.js +113 -0
  32. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/index.html +13 -0
  33. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/package.json +42 -0
  34. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/api/graphql-operations-types.ts +127 -0
  35. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/api/utils/query/highRevenueAccountsQuery.graphql +29 -0
  36. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/app.tsx +16 -0
  37. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/appLayout.tsx +9 -0
  38. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/assets/icons/book.svg +3 -0
  39. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/assets/icons/copy.svg +4 -0
  40. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/assets/icons/rocket.svg +3 -0
  41. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/assets/icons/star.svg +3 -0
  42. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/assets/images/codey-1.png +0 -0
  43. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/assets/images/codey-2.png +0 -0
  44. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/assets/images/codey-3.png +0 -0
  45. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/assets/images/vibe-codey.svg +194 -0
  46. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/AccountsTable.tsx +134 -0
  47. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/Footer.test.tsx +35 -0
  48. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/Footer.tsx +68 -0
  49. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/Hero.test.tsx +44 -0
  50. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/Hero.tsx +61 -0
  51. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/PromptCard.test.tsx +57 -0
  52. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/PromptCard.tsx +301 -0
  53. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/PromptHighlight.test.tsx +85 -0
  54. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/PromptHighlight.tsx +73 -0
  55. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/alerts/status-alert.tsx +45 -0
  56. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/auth/authentication-route.tsx +21 -0
  57. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/auth/private-route.tsx +36 -0
  58. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/footers/footer-link.tsx +36 -0
  59. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/forms/auth-form.tsx +79 -0
  60. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/forms/submit-button.tsx +49 -0
  61. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/layout/card-layout.tsx +23 -0
  62. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/layout/centered-page-layout.tsx +73 -0
  63. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/layout/loading-page.tsx +46 -0
  64. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/alert.tsx +65 -0
  65. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/button.tsx +56 -0
  66. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/card.tsx +77 -0
  67. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/field.tsx +111 -0
  68. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/index.ts +71 -0
  69. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/input.tsx +19 -0
  70. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/label.tsx +19 -0
  71. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/pagination.tsx +99 -0
  72. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/select.tsx +151 -0
  73. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/skeleton.tsx +7 -0
  74. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/spinner.tsx +21 -0
  75. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/table.tsx +114 -0
  76. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/components/ui/tabs.tsx +115 -0
  77. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/context/AuthContext.tsx +83 -0
  78. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/hooks/form.tsx +116 -0
  79. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/index.tsx +229 -0
  80. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/lib/utils.ts +6 -0
  81. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/pages/ChangePassword.tsx +105 -0
  82. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/pages/ForgotPassword.tsx +67 -0
  83. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/pages/Home.tsx +12 -0
  84. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/pages/Login.tsx +84 -0
  85. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/pages/NotFound.tsx +18 -0
  86. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/pages/Profile.tsx +146 -0
  87. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/pages/Register.tsx +117 -0
  88. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/pages/ResetPassword.tsx +101 -0
  89. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/routes.tsx +71 -0
  90. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/styles/global.css +270 -0
  91. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/testCmp.tsx +9 -0
  92. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/utils/authenticationConfig.ts +52 -0
  93. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/src/utils/helpers.ts +161 -0
  94. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/tsconfig.json +36 -0
  95. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/tsconfig.node.json +13 -0
  96. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/vite-env.d.ts +1 -0
  97. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/vite.config.ts +82 -0
  98. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/vitest-env.d.ts +2 -0
  99. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/vitest.config.ts +11 -0
  100. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/vitest.setup.ts +1 -0
  101. package/dist/force-app/main/default/webapplications/appreactvibecodingstarter/webapplication.json +7 -0
  102. package/dist/jest.config.js +6 -0
  103. package/dist/package.json +37 -0
  104. package/dist/scripts/apex/hello.apex +10 -0
  105. package/dist/scripts/soql/account.soql +6 -0
  106. package/dist/sfdx-project.json +12 -0
  107. package/package.json +34 -0
@@ -0,0 +1,134 @@
1
+ import React from "react";
2
+
3
+ export default function AccountsTable() {
4
+ // TODO: Add data fetching logic here
5
+ // const [accounts, setAccounts] = useState([]);
6
+ // const [loading, setLoading] = useState(true);
7
+ // const [error, setError] = useState<string | null>(null);
8
+
9
+ // Placeholder: Replace with actual data from GraphQL query
10
+ const hasData = false;
11
+
12
+ if (!hasData) {
13
+ return (
14
+ <div style={styles.placeholderContainer}>
15
+ <div style={styles.placeholderText}>Your table will appear here</div>
16
+ </div>
17
+ );
18
+ }
19
+
20
+ return (
21
+ <div style={styles.tableContainer}>
22
+ <div style={styles.columnContainer}>
23
+ {/* Account Name Column */}
24
+ <div style={styles.column}>
25
+ <div style={styles.headerCell}>
26
+ <div style={styles.cellText}>Account Name</div>
27
+ </div>
28
+ {/* TODO: Map over accounts data here */}
29
+ {/* Example: accounts.map((account) => (
30
+ <div key={account.Id} style={styles.dataCell}>
31
+ <div style={styles.cellText}>{account.Name.value || '-'}</div>
32
+ </div>
33
+ )) */}
34
+ </div>
35
+
36
+ {/* Billing City Column */}
37
+ <div style={styles.column}>
38
+ <div style={styles.headerCell}>
39
+ <div style={styles.cellText}>Billing City</div>
40
+ </div>
41
+ {/* TODO: Map over accounts data here */}
42
+ </div>
43
+
44
+ {/* Industry Column */}
45
+ <div style={styles.column}>
46
+ <div style={styles.headerCell}>
47
+ <div style={styles.cellText}>Industry</div>
48
+ </div>
49
+ {/* TODO: Map over accounts data here */}
50
+ </div>
51
+
52
+ {/* Annual Revenue Column */}
53
+ <div style={styles.column}>
54
+ <div style={styles.headerCell}>
55
+ <div style={styles.cellText}>Annual Revenue</div>
56
+ </div>
57
+ {/* TODO: Map over accounts data here */}
58
+ </div>
59
+ </div>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ const styles: Record<string, React.CSSProperties> = {
65
+ tableContainer: {
66
+ width: "100%",
67
+ height: "100%",
68
+ overflow: "auto",
69
+ borderRadius: "10px",
70
+ },
71
+ columnContainer: {
72
+ display: "flex",
73
+ gap: "2px",
74
+ alignItems: "flex-start",
75
+ width: "100%",
76
+ },
77
+ column: {
78
+ display: "flex",
79
+ flexDirection: "column",
80
+ gap: "2px",
81
+ flex: "1 1 0",
82
+ minWidth: "0",
83
+ },
84
+ headerCell: {
85
+ background: "#e5e3ff",
86
+ borderRight: "2px solid #d1cef9",
87
+ borderBottom: "2px solid #d1cef9",
88
+ display: "flex",
89
+ alignItems: "center",
90
+ gap: "10px",
91
+ height: "36px",
92
+ padding: "11px",
93
+ overflow: "hidden",
94
+ boxSizing: "border-box",
95
+ },
96
+ dataCell: {
97
+ background: "white",
98
+ borderRight: "2px solid #d1cef9",
99
+ borderBottom: "2px solid #d1cef9",
100
+ display: "flex",
101
+ alignItems: "center",
102
+ gap: "10px",
103
+ height: "36px",
104
+ padding: "11px",
105
+ overflow: "hidden",
106
+ boxSizing: "border-box",
107
+ },
108
+ cellText: {
109
+ fontFamily: "Inter, sans-serif",
110
+ fontWeight: 400,
111
+ fontSize: "14px",
112
+ color: "#2b2b2b",
113
+ letterSpacing: "0.14px",
114
+ overflow: "hidden",
115
+ textOverflow: "ellipsis",
116
+ whiteSpace: "nowrap",
117
+ lineHeight: "1.15",
118
+ flex: "1 1 0",
119
+ },
120
+ placeholderContainer: {
121
+ position: "absolute",
122
+ top: "50%",
123
+ left: "50%",
124
+ transform: "translate(-50%, -50%)",
125
+ },
126
+ placeholderText: {
127
+ fontFamily: "Inter, sans-serif",
128
+ fontStyle: "italic",
129
+ fontSize: "13px",
130
+ color: "#717171",
131
+ textAlign: "center",
132
+ letterSpacing: "0.13px",
133
+ },
134
+ };
@@ -0,0 +1,35 @@
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("href", "#docs");
25
+ expect(screen.getByRole("link", { name: /examples/i })).toHaveAttribute("href", "#examples");
26
+ expect(screen.getByRole("link", { name: /xx/i })).toHaveAttribute("href", "#more");
27
+ });
28
+
29
+ it("includes icons with each link", () => {
30
+ const { container } = render(<Footer />);
31
+
32
+ const images = container.querySelectorAll("img");
33
+ expect(images.length).toBeGreaterThan(0);
34
+ });
35
+ });
@@ -0,0 +1,68 @@
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}>Helpful content for your first build</h2>
10
+ <div style={styles.resourcesLinks}>
11
+ <a href="#walkthroughs" style={styles.resourceLink}>
12
+ <img src={starIcon} alt="" width="16" height="16" />
13
+ <span>Walkthroughs</span>
14
+ </a>
15
+ <a href="#docs" style={styles.resourceLink}>
16
+ <img src={bookIcon} alt="" width="16" height="16" />
17
+ <span>Docs</span>
18
+ </a>
19
+ <a href="#examples" style={styles.resourceLink}>
20
+ <img src={rocketIcon} alt="" width="16" height="16" />
21
+ <span>Examples</span>
22
+ </a>
23
+ <a href="#more" style={styles.resourceLink}>
24
+ <img src={rocketIcon} alt="" width="16" height="16" />
25
+ <span>XX</span>
26
+ </a>
27
+ </div>
28
+ </section>
29
+ );
30
+ }
31
+
32
+ const styles: Record<string, React.CSSProperties> = {
33
+ resourcesSection: {
34
+ position: "relative",
35
+ background: "linear-gradient(rgba(255, 255, 255, 0.01), rgba(255, 255, 255, 0.5))",
36
+ border: "2px solid #055fff",
37
+ borderRadius: "100px",
38
+ padding: "56px 64px",
39
+ marginTop: "64px",
40
+ marginBottom: "4rem",
41
+ display: "flex",
42
+ flexDirection: "column",
43
+ gap: "24px",
44
+ backdropFilter: "blur(24px)",
45
+ WebkitBackdropFilter: "blur(24px)",
46
+ },
47
+ resourcesTitle: {
48
+ fontSize: "24px",
49
+ fontWeight: 600,
50
+ lineHeight: "normal",
51
+ color: "#002775",
52
+ margin: 0,
53
+ },
54
+ resourcesLinks: {
55
+ display: "flex",
56
+ gap: "64px",
57
+ alignItems: "center",
58
+ },
59
+ resourceLink: {
60
+ display: "flex",
61
+ alignItems: "center",
62
+ gap: "8px",
63
+ fontSize: "16px",
64
+ color: "#002775",
65
+ textDecoration: "none",
66
+ transition: "opacity 0.2s ease",
67
+ },
68
+ };
@@ -0,0 +1,44 @@
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("Welcome to Vibe Coding");
10
+ expect(screen.getByRole("img")).toBeInTheDocument();
11
+ });
12
+
13
+ it("renders with subtext when provided", () => {
14
+ render(<Hero heading="Test" subtext="This is a subtext" />);
15
+
16
+ expect(screen.getByText("Test")).toBeInTheDocument();
17
+ expect(screen.getByText("This is a subtext")).toBeInTheDocument();
18
+ });
19
+
20
+ it("accepts custom image and alt text", () => {
21
+ render(<Hero heading="Test" image="/custom-image.png" imageAlt="Custom Alt" />);
22
+
23
+ const img = screen.getByAltText("Custom Alt");
24
+ expect(img).toHaveAttribute("src", "/custom-image.png");
25
+ });
26
+
27
+ it("supports JSX content in heading and subtext", () => {
28
+ const heading = (
29
+ <>
30
+ Welcome <strong>Hero</strong>
31
+ </>
32
+ );
33
+ const subtext = (
34
+ <span>
35
+ Get <em>started</em>
36
+ </span>
37
+ );
38
+
39
+ render(<Hero heading={heading} subtext={subtext} />);
40
+
41
+ expect(screen.getByText("Hero")).toBeInTheDocument();
42
+ expect(screen.getByText("started")).toBeInTheDocument();
43
+ });
44
+ });
@@ -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,57 @@
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("Create a component");
35
+ expect(screen.getByText("Build a React component")).toBeInTheDocument();
36
+ expect(screen.getByText("Generate a button component")).toBeInTheDocument();
37
+ });
38
+
39
+ it("renders a clickable copy button", async () => {
40
+ const user = userEvent.setup();
41
+
42
+ render(<PromptCard title="Test" description="Description" promptText="Copy this text" />);
43
+
44
+ const button = screen.getByRole("button", { name: /copy prompt/i });
45
+ expect(button).toBeInTheDocument();
46
+
47
+ await expect(user.click(button)).resolves.not.toThrow();
48
+ });
49
+
50
+ it("handles long prompt text", () => {
51
+ const longPrompt = "A".repeat(500);
52
+
53
+ render(<PromptCard title="Test" description="Description" promptText={longPrompt} />);
54
+
55
+ expect(screen.getByText(longPrompt)).toBeInTheDocument();
56
+ });
57
+ });
@@ -0,0 +1,301 @@
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(indentedBulletMatch[1]);
128
+ continue;
129
+ }
130
+
131
+ // Top-level unordered list items (- or *)
132
+ const bulletMatch = line.match(/^[-*]\s+(.+)$/);
133
+ if (bulletMatch) {
134
+ flushNumberedList();
135
+ bulletItems.push(bulletMatch[1]);
136
+ continue;
137
+ }
138
+
139
+ // Empty lines
140
+ if (line.trim() === "") {
141
+ // Don't flush numbered lists on empty lines - they may have more items
142
+ flushBulletList();
143
+ continue;
144
+ }
145
+
146
+ // Regular paragraph
147
+ flushAllLists();
148
+ elements.push(
149
+ <p key={`p-${i}`} style={markdownStyles.paragraph}>
150
+ {renderInlineMarkdown(line)}
151
+ </p>,
152
+ );
153
+ }
154
+
155
+ flushAllLists();
156
+ return elements;
157
+ }
158
+
159
+ const markdownStyles: Record<string, React.CSSProperties> = {
160
+ h2: {
161
+ fontSize: "13px",
162
+ fontWeight: 700,
163
+ color: "var(--on-surface-3)",
164
+ margin: "8px 0 4px 0",
165
+ },
166
+ h3: {
167
+ fontSize: "12px",
168
+ fontWeight: 600,
169
+ color: "var(--on-surface-3)",
170
+ margin: "8px 0 4px 0",
171
+ },
172
+ paragraph: {
173
+ margin: "0 0 4px 0",
174
+ },
175
+ list: {
176
+ margin: "4px 0",
177
+ paddingLeft: "16px",
178
+ listStyleType: "disc",
179
+ },
180
+ numberedList: {
181
+ margin: "4px 0",
182
+ paddingLeft: "18px",
183
+ listStyleType: "decimal",
184
+ },
185
+ nestedList: {
186
+ margin: "2px 0 2px 0",
187
+ paddingLeft: "16px",
188
+ listStyleType: "disc",
189
+ },
190
+ listItem: {
191
+ margin: "2px 0",
192
+ },
193
+ bold: {
194
+ fontWeight: 600,
195
+ },
196
+ };
197
+
198
+ export default function PromptCard({ title, description, promptText }: PromptCardProps) {
199
+ const handleCopy = () => {
200
+ // Try modern clipboard API first
201
+ if (navigator.clipboard && window.isSecureContext) {
202
+ navigator.clipboard.writeText(promptText).catch(() => {
203
+ // Fallback if modern API fails
204
+ fallbackCopy(promptText);
205
+ });
206
+ } else {
207
+ // Use fallback method for restricted environments like VSCode tabs
208
+ fallbackCopy(promptText);
209
+ }
210
+ };
211
+
212
+ const fallbackCopy = (text: string) => {
213
+ const textArea = document.createElement("textarea");
214
+ textArea.value = text;
215
+ textArea.style.position = "fixed";
216
+ textArea.style.left = "-999999px";
217
+ textArea.style.top = "-999999px";
218
+ document.body.appendChild(textArea);
219
+ textArea.focus();
220
+ textArea.select();
221
+ document.execCommand("copy");
222
+ textArea.remove();
223
+ };
224
+
225
+ return (
226
+ <div style={styles.promptContent}>
227
+ <h2 style={styles.promptTitle}>{title}</h2>
228
+ <p style={styles.promptDescription}>{description}</p>
229
+ <div style={styles.promptField}>
230
+ <div style={styles.promptFieldInner}>
231
+ <div style={styles.promptText}>{renderMarkdown(promptText)}</div>
232
+ <button
233
+ type="button"
234
+ style={styles.copyButton}
235
+ aria-label="Copy prompt"
236
+ onClick={handleCopy}
237
+ >
238
+ <img src={copyIcon} alt="Copy" width="20" height="20" />
239
+ </button>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ );
244
+ }
245
+
246
+ const styles: Record<string, React.CSSProperties> = {
247
+ promptContent: {
248
+ flex: 1,
249
+ display: "flex",
250
+ flexDirection: "column",
251
+ gap: "10px",
252
+ minWidth: 0,
253
+ },
254
+ promptTitle: {
255
+ fontSize: "36px",
256
+ fontWeight: 600,
257
+ lineHeight: 1.03,
258
+ color: "var(--on-surface-3)",
259
+ margin: 0,
260
+ },
261
+ promptDescription: {
262
+ fontSize: "16px",
263
+ lineHeight: 1.45,
264
+ color: "var(--on-surface-3)",
265
+ margin: 0,
266
+ },
267
+ promptField: {
268
+ border: "2px solid #055fff",
269
+ borderRadius: "20px",
270
+ overflow: "hidden",
271
+ },
272
+ promptFieldInner: {
273
+ display: "flex",
274
+ alignItems: "flex-start",
275
+ gap: "8px",
276
+ padding: "18px",
277
+ },
278
+ promptText: {
279
+ flex: 1,
280
+ fontSize: "12px",
281
+ fontWeight: 500,
282
+ lineHeight: 1.4,
283
+ color: "var(--on-surface-2)",
284
+ margin: 0,
285
+ minHeight: "45px",
286
+ maxHeight: "150px",
287
+ overflow: "auto",
288
+ },
289
+ copyButton: {
290
+ background: "rgba(3,45,96,0.17)",
291
+ border: "none",
292
+ borderRadius: "58px",
293
+ padding: "8px",
294
+ cursor: "pointer",
295
+ display: "flex",
296
+ alignItems: "center",
297
+ justifyContent: "center",
298
+ flexShrink: 0,
299
+ transition: "background 0.2s ease",
300
+ },
301
+ };