prototype-prd-vite-plugin 0.1.0
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/LICENSE +21 -0
- package/README.md +93 -0
- package/client/overlay.css +174 -0
- package/client/overlay.js +209 -0
- package/examples/basic/index.html +19 -0
- package/examples/basic/package-lock.json +887 -0
- package/examples/basic/package.json +12 -0
- package/examples/basic/src/main.js +1 -0
- package/examples/basic/src/style.css +45 -0
- package/examples/basic/vite.config.js +10 -0
- package/index.js +83 -0
- package/package.json +50 -0
- package/src/config.js +27 -0
- package/src/files.js +74 -0
- package/src/http.js +25 -0
- package/src/middleware.js +112 -0
- package/src/openai.js +78 -0
- package/src/paths.js +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# prototype-prd-vite-plugin
|
|
2
|
+
|
|
3
|
+
Local-first PRD workbench for Vite prototype projects.
|
|
4
|
+
|
|
5
|
+
During `vite dev`, this plugin injects a small `PRD` button into the page. The button opens a right-side drawer where you can edit Markdown, preview it, save a local draft, export a project PRD, and optionally trigger AI generation from your local dev server.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -D prototype-prd-vite-plugin
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
// vite.config.js
|
|
17
|
+
import { defineConfig } from "vite";
|
|
18
|
+
import prototypePrd from "prototype-prd-vite-plugin";
|
|
19
|
+
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
plugins: [
|
|
22
|
+
prototypePrd()
|
|
23
|
+
]
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The overlay only runs in Vite dev mode. It is disabled for production builds by default.
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
prototypePrd({
|
|
33
|
+
draftDir: ".prototype-prd",
|
|
34
|
+
draftFile: "current.md",
|
|
35
|
+
exportDir: "docs/prd",
|
|
36
|
+
defaultTitle: "Product Requirements Document",
|
|
37
|
+
ai: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
model: "gpt-4.1-mini",
|
|
40
|
+
baseURL: "https://api.openai.com/v1"
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Storage Model
|
|
46
|
+
|
|
47
|
+
Drafts are local-first:
|
|
48
|
+
|
|
49
|
+
- Draft file: `.prototype-prd/current.md`
|
|
50
|
+
- Export directory: `docs/prd`
|
|
51
|
+
- Export file shape: `YYYY-MM-DD-<slug>.md`
|
|
52
|
+
|
|
53
|
+
If you want private drafts, keep `.prototype-prd/` in `.gitignore`. Exported PRDs under `docs/prd/` are intended to be committed and reviewed with the project.
|
|
54
|
+
|
|
55
|
+
## AI Generation
|
|
56
|
+
|
|
57
|
+
AI generation is manual. The plugin never calls an external API on startup or while you type. It only calls AI after you click `Generate Markdown`.
|
|
58
|
+
|
|
59
|
+
Set your API key locally before starting Vite:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
OPENAI_API_KEY=your_key npm run dev
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The key is read only by the Vite dev server middleware. It is not sent to the browser, not written by this plugin, and not printed.
|
|
66
|
+
|
|
67
|
+
By default the plugin calls the OpenAI Responses API at:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
POST https://api.openai.com/v1/responses
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
You can set `ai.baseURL` for a compatible local gateway or proxy.
|
|
74
|
+
|
|
75
|
+
## Privacy And Safety
|
|
76
|
+
|
|
77
|
+
- No production build injection by default.
|
|
78
|
+
- No browser-side OpenAI API calls.
|
|
79
|
+
- No API key creation or storage.
|
|
80
|
+
- No automatic network requests for editing, saving, previewing, or exporting.
|
|
81
|
+
- File access is constrained to the Vite project root.
|
|
82
|
+
- AI context is limited to current PRD Markdown, current page URL, and notes you enter.
|
|
83
|
+
|
|
84
|
+
## Local Development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm test
|
|
88
|
+
npm pack --dry-run
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Example
|
|
92
|
+
|
|
93
|
+
See `examples/basic` for a minimal Vite app configuration.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
--prd-ink: #1f1c17;
|
|
3
|
+
--prd-paper: #fbf5e8;
|
|
4
|
+
--prd-panel: #fffaf0;
|
|
5
|
+
--prd-line: #ddccb1;
|
|
6
|
+
--prd-accent: #c9792b;
|
|
7
|
+
--prd-accent-dark: #8f4b1d;
|
|
8
|
+
--prd-muted: #74695d;
|
|
9
|
+
--prd-shadow: 0 24px 80px rgba(48, 35, 18, 0.22);
|
|
10
|
+
color: var(--prd-ink);
|
|
11
|
+
font-family: "Avenir Next", "Gill Sans", "Trebuchet MS", sans-serif;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
* {
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.toggle {
|
|
19
|
+
position: fixed;
|
|
20
|
+
right: 22px;
|
|
21
|
+
bottom: 22px;
|
|
22
|
+
z-index: 2147483646;
|
|
23
|
+
border: 1px solid rgba(255, 255, 255, 0.26);
|
|
24
|
+
border-radius: 999px;
|
|
25
|
+
background: #1f1c17;
|
|
26
|
+
color: #fff7e7;
|
|
27
|
+
box-shadow: 0 16px 40px rgba(31, 28, 23, 0.32);
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
font-weight: 800;
|
|
30
|
+
letter-spacing: 0.08em;
|
|
31
|
+
padding: 13px 18px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.drawer {
|
|
35
|
+
position: fixed;
|
|
36
|
+
inset: 0 0 0 auto;
|
|
37
|
+
z-index: 2147483647;
|
|
38
|
+
width: min(520px, 100vw);
|
|
39
|
+
transform: translateX(105%);
|
|
40
|
+
transition: transform 180ms ease;
|
|
41
|
+
background:
|
|
42
|
+
radial-gradient(circle at top left, rgba(201, 121, 43, 0.16), transparent 38%),
|
|
43
|
+
linear-gradient(180deg, var(--prd-panel), var(--prd-paper));
|
|
44
|
+
border-left: 1px solid var(--prd-line);
|
|
45
|
+
box-shadow: var(--prd-shadow);
|
|
46
|
+
display: grid;
|
|
47
|
+
grid-template-rows: auto auto 1fr auto;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.drawer.open {
|
|
51
|
+
transform: translateX(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.header,
|
|
55
|
+
.footer {
|
|
56
|
+
padding: 16px;
|
|
57
|
+
border-bottom: 1px solid var(--prd-line);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.footer {
|
|
61
|
+
border-bottom: 0;
|
|
62
|
+
border-top: 1px solid var(--prd-line);
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-wrap: wrap;
|
|
65
|
+
gap: 8px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.title-row {
|
|
69
|
+
align-items: center;
|
|
70
|
+
display: flex;
|
|
71
|
+
gap: 12px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.title-row h2 {
|
|
75
|
+
flex: 1;
|
|
76
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
77
|
+
font-size: 22px;
|
|
78
|
+
line-height: 1.1;
|
|
79
|
+
margin: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.status {
|
|
83
|
+
color: var(--prd-muted);
|
|
84
|
+
font-size: 12px;
|
|
85
|
+
margin-top: 8px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.tabs {
|
|
89
|
+
display: flex;
|
|
90
|
+
gap: 8px;
|
|
91
|
+
padding: 12px 16px 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
button {
|
|
95
|
+
border: 1px solid var(--prd-line);
|
|
96
|
+
border-radius: 999px;
|
|
97
|
+
background: rgba(255, 255, 255, 0.64);
|
|
98
|
+
color: var(--prd-ink);
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
font: inherit;
|
|
101
|
+
font-weight: 700;
|
|
102
|
+
padding: 9px 12px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
button.primary {
|
|
106
|
+
background: var(--prd-accent);
|
|
107
|
+
border-color: var(--prd-accent-dark);
|
|
108
|
+
color: white;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
button.active {
|
|
112
|
+
background: var(--prd-ink);
|
|
113
|
+
border-color: var(--prd-ink);
|
|
114
|
+
color: var(--prd-paper);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.content {
|
|
118
|
+
min-height: 0;
|
|
119
|
+
overflow: auto;
|
|
120
|
+
padding: 16px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
textarea,
|
|
124
|
+
input {
|
|
125
|
+
width: 100%;
|
|
126
|
+
border: 1px solid var(--prd-line);
|
|
127
|
+
border-radius: 16px;
|
|
128
|
+
background: rgba(255, 255, 255, 0.76);
|
|
129
|
+
color: var(--prd-ink);
|
|
130
|
+
font: 14px/1.55 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
131
|
+
outline: none;
|
|
132
|
+
padding: 13px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
textarea {
|
|
136
|
+
min-height: 58vh;
|
|
137
|
+
resize: vertical;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
label {
|
|
141
|
+
color: var(--prd-muted);
|
|
142
|
+
display: grid;
|
|
143
|
+
font-size: 12px;
|
|
144
|
+
gap: 6px;
|
|
145
|
+
margin-bottom: 12px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.preview {
|
|
149
|
+
background: rgba(255, 255, 255, 0.62);
|
|
150
|
+
border: 1px solid var(--prd-line);
|
|
151
|
+
border-radius: 20px;
|
|
152
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
153
|
+
line-height: 1.65;
|
|
154
|
+
padding: 18px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.preview h1,
|
|
158
|
+
.preview h2,
|
|
159
|
+
.preview h3 {
|
|
160
|
+
line-height: 1.15;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.hint {
|
|
164
|
+
color: var(--prd-muted);
|
|
165
|
+
font-size: 13px;
|
|
166
|
+
line-height: 1.5;
|
|
167
|
+
margin: 0 0 12px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@media (max-width: 640px) {
|
|
171
|
+
.drawer {
|
|
172
|
+
width: 100vw;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
const API = "/__prototype_prd__";
|
|
2
|
+
const CSS_URL = "/__prototype_prd__/client/overlay.css";
|
|
3
|
+
|
|
4
|
+
const host = document.createElement("prototype-prd-overlay");
|
|
5
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
6
|
+
document.documentElement.append(host);
|
|
7
|
+
|
|
8
|
+
shadow.innerHTML = `
|
|
9
|
+
<link rel="stylesheet" href="${CSS_URL}">
|
|
10
|
+
<button class="toggle" type="button">PRD</button>
|
|
11
|
+
<aside class="drawer" aria-label="PRD workbench">
|
|
12
|
+
<header class="header">
|
|
13
|
+
<div class="title-row">
|
|
14
|
+
<h2>Prototype PRD</h2>
|
|
15
|
+
<button class="close" type="button" aria-label="Close PRD panel">Close</button>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="status">Loading local draft...</div>
|
|
18
|
+
</header>
|
|
19
|
+
<nav class="tabs">
|
|
20
|
+
<button class="tab active" type="button" data-tab="edit">Edit</button>
|
|
21
|
+
<button class="tab" type="button" data-tab="preview">Preview</button>
|
|
22
|
+
<button class="tab" type="button" data-tab="generate">Generate</button>
|
|
23
|
+
</nav>
|
|
24
|
+
<main class="content"></main>
|
|
25
|
+
<footer class="footer">
|
|
26
|
+
<button class="primary save" type="button">Save draft</button>
|
|
27
|
+
<button class="export" type="button">Export docs</button>
|
|
28
|
+
<button class="copy" type="button">Copy Markdown</button>
|
|
29
|
+
</footer>
|
|
30
|
+
</aside>
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const state = {
|
|
34
|
+
open: false,
|
|
35
|
+
tab: "edit",
|
|
36
|
+
title: "Product Requirements Document",
|
|
37
|
+
markdown: "",
|
|
38
|
+
aiConfigured: false
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const elements = {
|
|
42
|
+
toggle: shadow.querySelector(".toggle"),
|
|
43
|
+
drawer: shadow.querySelector(".drawer"),
|
|
44
|
+
close: shadow.querySelector(".close"),
|
|
45
|
+
status: shadow.querySelector(".status"),
|
|
46
|
+
tabs: [...shadow.querySelectorAll(".tab")],
|
|
47
|
+
content: shadow.querySelector(".content"),
|
|
48
|
+
save: shadow.querySelector(".save"),
|
|
49
|
+
export: shadow.querySelector(".export"),
|
|
50
|
+
copy: shadow.querySelector(".copy")
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
elements.toggle.addEventListener("click", () => setOpen(true));
|
|
54
|
+
elements.close.addEventListener("click", () => setOpen(false));
|
|
55
|
+
elements.save.addEventListener("click", saveDraft);
|
|
56
|
+
elements.export.addEventListener("click", exportDocs);
|
|
57
|
+
elements.copy.addEventListener("click", copyMarkdown);
|
|
58
|
+
elements.tabs.forEach((tab) => {
|
|
59
|
+
tab.addEventListener("click", () => {
|
|
60
|
+
state.tab = tab.dataset.tab;
|
|
61
|
+
render();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
boot();
|
|
66
|
+
|
|
67
|
+
async function boot() {
|
|
68
|
+
try {
|
|
69
|
+
const appState = await request("/state");
|
|
70
|
+
const draft = await request("/draft");
|
|
71
|
+
state.title = appState.title;
|
|
72
|
+
state.aiConfigured = appState.ai.configured;
|
|
73
|
+
state.markdown = draft.markdown;
|
|
74
|
+
setStatus(draft.exists ? "Loaded local draft." : "Started from PRD template.");
|
|
75
|
+
render();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
setStatus(error.message);
|
|
78
|
+
render();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function setOpen(open) {
|
|
83
|
+
state.open = open;
|
|
84
|
+
elements.drawer.classList.toggle("open", open);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function render() {
|
|
88
|
+
elements.tabs.forEach((tab) => {
|
|
89
|
+
tab.classList.toggle("active", tab.dataset.tab === state.tab);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (state.tab === "edit") {
|
|
93
|
+
elements.content.innerHTML = `<textarea aria-label="PRD Markdown"></textarea>`;
|
|
94
|
+
const textarea = elements.content.querySelector("textarea");
|
|
95
|
+
textarea.value = state.markdown;
|
|
96
|
+
textarea.addEventListener("input", () => {
|
|
97
|
+
state.markdown = textarea.value;
|
|
98
|
+
setStatus("Unsaved changes.");
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (state.tab === "preview") {
|
|
104
|
+
elements.content.innerHTML = `<article class="preview">${renderMarkdown(state.markdown)}</article>`;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
elements.content.innerHTML = `
|
|
109
|
+
<p class="hint">AI generation only runs when you click the button. The request is sent from the local Vite dev server, not the browser.</p>
|
|
110
|
+
<label>Instruction
|
|
111
|
+
<input class="instruction" value="Generate or improve this PRD from the current draft.">
|
|
112
|
+
</label>
|
|
113
|
+
<label>Product notes
|
|
114
|
+
<textarea class="notes" style="min-height:120px" placeholder="Optional notes about this prototype"></textarea>
|
|
115
|
+
</label>
|
|
116
|
+
<button class="primary generate" type="button">Generate Markdown</button>
|
|
117
|
+
`;
|
|
118
|
+
elements.content.querySelector(".generate").addEventListener("click", generate);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function saveDraft() {
|
|
122
|
+
try {
|
|
123
|
+
await request("/draft", {
|
|
124
|
+
method: "PUT",
|
|
125
|
+
body: { markdown: state.markdown }
|
|
126
|
+
});
|
|
127
|
+
setStatus("Draft saved locally.");
|
|
128
|
+
} catch (error) {
|
|
129
|
+
setStatus(error.message);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function exportDocs() {
|
|
134
|
+
try {
|
|
135
|
+
const result = await request("/export", {
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: { markdown: state.markdown, title: state.title }
|
|
138
|
+
});
|
|
139
|
+
setStatus(`Exported ${result.fileName}.`);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
setStatus(error.message);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function copyMarkdown() {
|
|
146
|
+
await navigator.clipboard.writeText(state.markdown);
|
|
147
|
+
setStatus("Markdown copied.");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function generate() {
|
|
151
|
+
const instruction = elements.content.querySelector(".instruction").value;
|
|
152
|
+
const notes = elements.content.querySelector(".notes").value;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
setStatus("Generating with AI...");
|
|
156
|
+
const result = await request("/ai/generate", {
|
|
157
|
+
method: "POST",
|
|
158
|
+
body: {
|
|
159
|
+
markdown: state.markdown,
|
|
160
|
+
instruction,
|
|
161
|
+
notes,
|
|
162
|
+
pageUrl: window.location.href
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
state.markdown = result.markdown;
|
|
166
|
+
state.tab = "edit";
|
|
167
|
+
setStatus("AI generated Markdown. Review before saving.");
|
|
168
|
+
render();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
setStatus(error.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function request(path, options = {}) {
|
|
175
|
+
const response = await fetch(`${API}${path}`, {
|
|
176
|
+
method: options.method || "GET",
|
|
177
|
+
headers: options.body ? { "content-type": "application/json" } : undefined,
|
|
178
|
+
body: options.body ? JSON.stringify(options.body) : undefined
|
|
179
|
+
});
|
|
180
|
+
const payload = await response.json();
|
|
181
|
+
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
throw new Error(payload?.error?.message || "Prototype PRD request failed.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return payload;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function setStatus(message) {
|
|
190
|
+
elements.status.textContent = message;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function renderMarkdown(markdown) {
|
|
194
|
+
return escapeHtml(markdown)
|
|
195
|
+
.replace(/^# (.*)$/gm, "<h1>$1</h1>")
|
|
196
|
+
.replace(/^## (.*)$/gm, "<h2>$1</h2>")
|
|
197
|
+
.replace(/^### (.*)$/gm, "<h3>$1</h3>")
|
|
198
|
+
.replace(/^\- (.*)$/gm, "<li>$1</li>")
|
|
199
|
+
.replace(/\n{2,}/g, "</p><p>")
|
|
200
|
+
.replace(/^(?!<h|<li|<\/p>)(.+)$/gm, "<p>$1</p>");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function escapeHtml(value) {
|
|
204
|
+
return String(value ?? "")
|
|
205
|
+
.replaceAll("&", "&")
|
|
206
|
+
.replaceAll("<", "<")
|
|
207
|
+
.replaceAll(">", ">")
|
|
208
|
+
.replaceAll('"', """);
|
|
209
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Prototype PRD Example</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main class="hero">
|
|
10
|
+
<p class="eyebrow">Prototype</p>
|
|
11
|
+
<h1>Checkout flow concept</h1>
|
|
12
|
+
<p>
|
|
13
|
+
A small Vite page used to test the local PRD workbench overlay.
|
|
14
|
+
</p>
|
|
15
|
+
<button>Start checkout</button>
|
|
16
|
+
</main>
|
|
17
|
+
<script type="module" src="/src/main.js"></script>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|