Skip to content

Commit e7a3c51

Browse files
authored
feat(ui): add commit-generator index.html Introduce a single-page Conventional Commits builder for crafting Git commit messages. Includes type/scope selection, imperative subject, optional body, and structured footers (BREAKING CHANGE, API-Endpoint, Issue-Id/Defect-Id, Task-Id, Assigned-By). Provides live preview, one-click copy (Clipboard API), download as .txt, and a git commit command preview. Adds a Credits modal and an inline HTML comment for attribution. Credits-UI: One-numan (https://github.com/one-numan) Project: commit-generator
1 parent b061e26 commit e7a3c51

File tree

1 file changed

+316
-0
lines changed

1 file changed

+316
-0
lines changed

index.html

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<!-- Code credits: One-numan (https://github.com/one-numan) -->
5+
<meta charset="utf-8" />
6+
<title>Conventional Commit Builder (Single Page)</title>
7+
<meta name="viewport" content="width=device-width,initial-scale=1" />
8+
<style>
9+
:root {
10+
--bg:#0f172a; --card:#111827; --border:#1f2937; --text:#e5e7eb;
11+
--muted:#9ca3af; --accent:#22c55e; --accent2:#60a5fa; --danger:#f97316;
12+
--ink:#0b1220;
13+
}
14+
* { box-sizing: border-box; }
15+
html, body { margin:0; padding:0; background:var(--bg); color:var(--text); font:14px/1.45 system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial, sans-serif; }
16+
.wrap { min-height:100vh; display:grid; grid-template-rows:auto 1fr auto; }
17+
header, main, footer { padding:18px; }
18+
header { background:var(--card); border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; }
19+
h1 { margin:0; font-size:20px; }
20+
.muted { color:var(--muted); }
21+
.btn, button, a.button { display:inline-block; text-decoration:none; background:var(--ink); border:1px solid var(--border); color:var(--text); padding:10px 12px; border-radius:8px; cursor:pointer; }
22+
.primary { background:var(--accent); color:#08130c; border-color:#16a34a; font-weight:700; }
23+
.secondary { background:var(--accent2); color:#071224; border-color:#3b82f6; font-weight:700; }
24+
.warn { background:var(--danger); color:#1b0e07; border-color:#ea580c; font-weight:700; }
25+
.grid { display:grid; grid-template-columns:1fr; gap:14px; max-width:980px; margin:18px auto; }
26+
@media (min-width: 840px) { .grid { grid-template-columns: 1fr 1fr; } }
27+
.card { background:var(--card); border:1px solid var(--border); border-radius:12px; padding:14px; }
28+
label { display:block; font-weight:600; margin:8px 0 6px; }
29+
input[type="text"], textarea, select { width:100%; background:var(--ink); color:var(--text); border:1px solid var(--border); border-radius:8px; padding:10px 12px; outline:none; }
30+
input[type="text"]:focus, textarea:focus, select:focus { border-color: var(--accent2); }
31+
textarea { min-height:110px; resize:vertical; }
32+
.row { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
33+
.hint { color:var(--muted); font-size:12px; margin-top:6px; }
34+
.checkbox { display:flex; align-items:center; gap:10px; margin-top:10px; }
35+
pre, code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
36+
pre { margin:0; padding:12px; background:var(--ink); border:1px solid var(--border); border-radius:8px; overflow:auto; white-space:pre-wrap; word-break:break-word; }
37+
.preview-header { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px; }
38+
.badge { font-size:12px; padding:2px 8px; background:var(--ink); border:1px solid var(--border); border-radius:999px; color:var(--muted); }
39+
.actions { display:flex; gap:10px; flex-wrap:wrap; margin-top:12px; }
40+
footer { background:var(--card); border-top:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; }
41+
.small { font-size:12px; color:var(--muted); }
42+
43+
/* Modal */
44+
.modal-backdrop {
45+
position: fixed; inset: 0; background: rgba(0,0,0,.6);
46+
display: none; align-items: center; justify-content: center; z-index: 50;
47+
}
48+
.modal {
49+
width: min(560px, 92vw); background: var(--card); border:1px solid var(--border);
50+
border-radius: 12px; padding: 16px;
51+
}
52+
.modal header { padding:0 0 10px 0; border:0; background:transparent; }
53+
.modal .close { float:right; }
54+
</style>
55+
</head>
56+
<body>
57+
<div class="wrap">
58+
<header>
59+
<h1>Conventional Commit Builder</h1>
60+
<div class="actions">
61+
<button id="creditsBtn" class="button">Credits</button>
62+
<a class="button" href="https://github.com/one-numan" target="_blank" rel="noopener noreferrer">GitHub</a>
63+
</div>
64+
</header>
65+
66+
<main>
67+
<div class="grid">
68+
<div class="card">
69+
<div class="row">
70+
<div>
71+
<label for="type">Type</label>
72+
<select id="type">
73+
<option>feat</option><option>fix</option><option>docs</option>
74+
<option>style</option><option>refactor</option><option>perf</option>
75+
<option>test</option><option>build</option><option>ci</option>
76+
<option>chore</option><option>revert</option>
77+
</select>
78+
<div class="hint">Format: type(scope): subject</div>
79+
</div>
80+
<div>
81+
<label for="scope">Scope (optional)</label>
82+
<input id="scope" type="text" placeholder="e.g., auth, api, parser" />
83+
<div class="hint">Example: feat(auth): add login</div>
84+
</div>
85+
</div>
86+
87+
<label for="subject">Subject (short, imperative)</label>
88+
<input id="subject" type="text" placeholder="Add JWT-based login" />
89+
<div class="hint">Keep concise and imperative.</div>
90+
91+
<label for="body">Body (optional)</label>
92+
<textarea id="body" placeholder="Explain what and why, not how..."></textarea>
93+
94+
<div class="checkbox">
95+
<input id="breaking" type="checkbox" />
96+
<label for="breaking" style="margin:0;">BREAKING CHANGE</label>
97+
</div>
98+
<input id="breakingDetails" type="text" placeholder="Describe the breaking change (footer)" />
99+
100+
<hr style="border-color:#1f2937; margin:14px 0;" />
101+
102+
<!-- Optional custom footers -->
103+
<div class="checkbox">
104+
<input id="apiInclude" type="checkbox" />
105+
<label for="apiInclude" style="margin:0;">API Endpoint (optional)</label>
106+
</div>
107+
<input id="apiEndpoint" type="text" placeholder="e.g., GET /v1/users/:id" />
108+
109+
<div class="checkbox" style="margin-top:12px;">
110+
<input id="issueInclude" type="checkbox" />
111+
<label for="issueInclude" style="margin:0;">Issue ID or Defect ID (optional)</label>
112+
</div>
113+
<div class="row">
114+
<select id="issueType">
115+
<option value="Issue-Id">Issue-Id</option>
116+
<option value="Defect-Id">Defect-Id</option>
117+
</select>
118+
<input id="issueNumber" type="text" placeholder="#123 or ABC-456" />
119+
</div>
120+
121+
<div class="checkbox" style="margin-top:12px;">
122+
<input id="taskInclude" type="checkbox" />
123+
<label for="taskInclude" style="margin:0;">Task Id and Assigned By (optional)</label>
124+
</div>
125+
<div class="row">
126+
<input id="taskId" type="text" placeholder="Task-12345" />
127+
<input id="assignedBy" type="text" placeholder="Assigned by: Name" />
128+
</div>
129+
<div class="hint" style="margin-top:8px;">Footers: API-Endpoint, Issue-Id/Defect-Id, Task-Id, Assigned-By.</div>
130+
</div>
131+
132+
<div class="card">
133+
<div class="preview-header">
134+
<strong>Preview</strong>
135+
<span class="badge" id="charCount">0 chars</span>
136+
</div>
137+
<pre id="preview">(message will appear here)</pre>
138+
139+
<div class="actions">
140+
<button class="primary" id="copyBtn">Copy commit</button>
141+
<button class="secondary" id="downloadBtn">Download .txt</button>
142+
<button class="warn" id="gitBtn">Show git commit command</button>
143+
</div>
144+
145+
<div class="gitcmd" id="gitCmdWrap" style="display:none; margin-top:10px;">
146+
<label>git commit command</label>
147+
<pre id="gitCmd"></pre>
148+
</div>
149+
</div>
150+
</div>
151+
</main>
152+
153+
<footer>
154+
<div class="small">Based on Conventional Commits format.</div>
155+
<a class="small" href="https://www.conventionalcommits.org/en/v1.0.0/" target="_blank" rel="noopener noreferrer">Spec</a>
156+
</footer>
157+
</div>
158+
159+
<!-- Credits modal -->
160+
<div id="creditsModal" class="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="creditsTitle">
161+
<div class="modal">
162+
<header>
163+
<strong id="creditsTitle">Credits</strong>
164+
<button class="button close" id="creditsClose" aria-label="Close">Close</button>
165+
</header>
166+
<div>
167+
<p>Code credits go to <strong>One‑numan</strong><a href="https://github.com/one-numan" target="_blank" rel="noopener noreferrer">github.com/one-numan</a>.</p>
168+
<p class="muted">External links open in a new tab with rel="noopener noreferrer" for security and privacy.</p>
169+
</div>
170+
</div>
171+
</div>
172+
173+
<script>
174+
const els = {
175+
type: document.getElementById('type'),
176+
scope: document.getElementById('scope'),
177+
subject: document.getElementById('subject'),
178+
body: document.getElementById('body'),
179+
breaking: document.getElementById('breaking'),
180+
breakingDetails: document.getElementById('breakingDetails'),
181+
apiInclude: document.getElementById('apiInclude'),
182+
apiEndpoint: document.getElementById('apiEndpoint'),
183+
issueInclude: document.getElementById('issueInclude'),
184+
issueType: document.getElementById('issueType'),
185+
issueNumber: document.getElementById('issueNumber'),
186+
taskInclude: document.getElementById('taskInclude'),
187+
taskId: document.getElementById('taskId'),
188+
assignedBy: document.getElementById('assignedBy'),
189+
preview: document.getElementById('preview'),
190+
charCount: document.getElementById('charCount'),
191+
copyBtn: document.getElementById('copyBtn'),
192+
downloadBtn: document.getElementById('downloadBtn'),
193+
gitBtn: document.getElementById('gitBtn'),
194+
gitCmd: document.getElementById('gitCmd'),
195+
gitCmdWrap: document.getElementById('gitCmdWrap'),
196+
creditsBtn: document.getElementById('creditsBtn'),
197+
creditsModal: document.getElementById('creditsModal'),
198+
creditsClose: document.getElementById('creditsClose'),
199+
};
200+
201+
function buildMessage() {
202+
const type = els.type.value.trim();
203+
const scope = els.scope.value.trim();
204+
const subject = els.subject.value.trim();
205+
const body = els.body.value.replace(/\r\n/g, '\\n').trim();
206+
const breaking = els.breaking.checked;
207+
const breakingDetails = els.breakingDetails.value.trim();
208+
209+
if (!type || !subject) return '(message will appear here)';
210+
211+
const hdrScope = scope ? `(${scope})` : '';
212+
const bang = breaking ? '!' : '';
213+
const header = `${type}${hdrScope}${bang}: ${subject}`;
214+
215+
const parts = [header];
216+
217+
if (body) {
218+
parts.push('');
219+
parts.push(body);
220+
}
221+
222+
const footers = [];
223+
if (breaking && breakingDetails) {
224+
footers.push(`BREAKING CHANGE: ${breakingDetails}`);
225+
}
226+
if (els.apiInclude.checked && els.apiEndpoint.value.trim()) {
227+
footers.push(`API-Endpoint: ${els.apiEndpoint.value.trim()}`);
228+
}
229+
if (els.issueInclude.checked && els.issueNumber.value.trim()) {
230+
footers.push(`${els.issueType.value}: ${els.issueNumber.value.trim()}`);
231+
}
232+
if (els.taskInclude.checked) {
233+
const t = els.taskId.value.trim();
234+
const a = els.assignedBy.value.trim();
235+
if (t) footers.push(`Task-Id: ${t}`);
236+
if (a) footers.push(`Assigned-By: ${a}`);
237+
}
238+
if (footers.length) {
239+
if (!body) parts.push('');
240+
parts.push('');
241+
parts.push(...footers);
242+
}
243+
244+
return parts.join('\\n');
245+
}
246+
247+
function update() {
248+
const msg = buildMessage();
249+
els.preview.textContent = msg;
250+
els.charCount.textContent = `${msg.length} chars`;
251+
}
252+
253+
function escapeForDoubleQuotes(str) {
254+
return str.replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\"');
255+
}
256+
257+
function buildGitCommitCommand() {
258+
const msg = buildMessage();
259+
const lines = msg.split('\\n');
260+
const subject = lines || '';
261+
const rest = lines.slice(1).join('\\n').trim();
262+
263+
const parts = [];
264+
parts.push(`git commit -m "${escapeForDoubleQuotes(subject)}"`);
265+
if (rest) parts.push(`-m "${escapeForDoubleQuotes(rest)}"`);
266+
return parts.join(' ');
267+
}
268+
269+
['change','keyup','input'].forEach(evt => {
270+
[
271+
'type','scope','subject','body','breaking','breakingDetails',
272+
'apiInclude','apiEndpoint','issueInclude','issueType','issueNumber',
273+
'taskInclude','taskId','assignedBy'
274+
].forEach(id => document.getElementById(id).addEventListener(evt, update));
275+
});
276+
update();
277+
278+
// Clipboard copy (secure context recommended)
279+
els.copyBtn.addEventListener('click', async () => {
280+
const msg = buildMessage();
281+
try {
282+
await navigator.clipboard.writeText(msg);
283+
els.copyBtn.textContent = 'Copied!';
284+
setTimeout(() => (els.copyBtn.textContent = 'Copy commit'), 1200);
285+
} catch (e) {
286+
alert('Clipboard copy failed. Select and copy manually.');
287+
}
288+
});
289+
290+
// Download as .txt
291+
els.downloadBtn.addEventListener('click', () => {
292+
const msg = buildMessage();
293+
const blob = new Blob([msg], { type: 'text/plain;charset=utf-8' });
294+
const url = URL.createObjectURL(blob);
295+
const a = document.createElement('a');
296+
a.href = url; a.download = 'commit-message.txt';
297+
document.body.appendChild(a); a.click(); a.remove();
298+
URL.revokeObjectURL(url);
299+
});
300+
301+
// Show git commit command
302+
els.gitBtn.addEventListener('click', () => {
303+
els.gitCmdWrap.style.display = 'block';
304+
els.gitCmd.textContent = buildGitCommitCommand();
305+
});
306+
307+
// Credits modal
308+
function openCredits() { els.creditsModal.style.display = 'flex'; }
309+
function closeCredits() { els.creditsModal.style.display = 'none'; }
310+
els.creditsBtn.addEventListener('click', openCredits);
311+
els.creditsClose.addEventListener('click', closeCredits);
312+
els.creditsModal.addEventListener('click', (e) => { if (e.target === els.creditsModal) closeCredits(); });
313+
window.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeCredits(); });
314+
</script>
315+
</body>
316+
</html>

0 commit comments

Comments
 (0)