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 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("&", "&amp;")
206
+ .replaceAll("<", "&lt;")
207
+ .replaceAll(">", "&gt;")
208
+ .replaceAll('"', "&quot;");
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>