Release Git Tools 0.1.0
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { AppShell } from "@add-ideas/toolbox-shell-react";
|
||||
import "@add-ideas/toolbox-shell-react/styles.css";
|
||||
import "./styles.css";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||
import { HelpDialog } from "./components/HelpDialog";
|
||||
import { manifest } from "./toolbox/manifest";
|
||||
|
||||
const Workbench = lazy(async () => ({
|
||||
default: (await import("./components/Workbench")).Workbench,
|
||||
}));
|
||||
|
||||
export function App() {
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<AppShell
|
||||
app={manifest}
|
||||
manifestUrl="./toolbox-app.json"
|
||||
helpAction={{ onClick: () => setHelpOpen(true) }}
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<p className="loading" role="status">
|
||||
Preparing Git Tools…
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<Workbench />
|
||||
</Suspense>
|
||||
</AppShell>
|
||||
<HelpDialog open={helpOpen} onClose={() => setHelpOpen(false)} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
|
||||
export class ErrorBoundary extends Component<
|
||||
{ children: ReactNode },
|
||||
{ error?: Error }
|
||||
> {
|
||||
state: { error?: Error } = {};
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { error };
|
||||
}
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
console.error("Application failure", error, info);
|
||||
}
|
||||
render() {
|
||||
if (this.state.error)
|
||||
return (
|
||||
<main className="fatal">
|
||||
<h1>Git Tools could not continue</h1>
|
||||
<p>{this.state.error.message}</p>
|
||||
<button type="button" onClick={() => location.reload()}>
|
||||
Reload
|
||||
</button>
|
||||
</main>
|
||||
);
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function HelpDialog({
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const dialog = useRef<HTMLDialogElement>(null);
|
||||
useEffect(() => {
|
||||
const node = dialog.current;
|
||||
if (!node) return;
|
||||
if (open && !node.open) node.showModal();
|
||||
if (!open && node.open) node.close();
|
||||
}, [open]);
|
||||
return (
|
||||
<dialog
|
||||
ref={dialog}
|
||||
className="help-dialog"
|
||||
onClose={onClose}
|
||||
onCancel={onClose}
|
||||
aria-labelledby="help-title"
|
||||
>
|
||||
<div className="dialog-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Local-first help</p>
|
||||
<h2 id="help-title">About Git Tools</h2>
|
||||
</div>
|
||||
<button type="button" onClick={onClose} aria-label="Close help">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<p>Inspect and author Git interchange formats locally in the browser.</p>
|
||||
<p>
|
||||
All processing is performed in this browser. Imported data is treated as
|
||||
untrusted and bounded before parsing.
|
||||
</p>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,760 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { triggerBlobDownload } from "@add-ideas/toolbox-helpers";
|
||||
import {
|
||||
CONVENTIONAL_TYPES,
|
||||
buildConventionalCommit,
|
||||
compareSemVer,
|
||||
createUnifiedPatch,
|
||||
explainGitignore,
|
||||
lintConventionalCommit,
|
||||
normalizeChangelog,
|
||||
parseSemVer,
|
||||
parseUnifiedPatch,
|
||||
satisfiesSemVer,
|
||||
type IgnoreResult,
|
||||
type PatchDocument,
|
||||
} from "../git/tools";
|
||||
|
||||
const patchSample = `diff --git a/src/example.ts b/src/example.ts
|
||||
index 18d541c..d7042aa 100644
|
||||
--- a/src/example.ts
|
||||
+++ b/src/example.ts
|
||||
@@ -1,4 +1,5 @@
|
||||
export function greet(name: string) {
|
||||
- return "Hello " + name;
|
||||
+ const value = name.trim();
|
||||
+ return "Hello " + value;
|
||||
}
|
||||
|
||||
`;
|
||||
const beforeSample = `export function greet(name: string) {
|
||||
return "Hello " + name;
|
||||
}
|
||||
`;
|
||||
const afterSample = `export function greet(name: string) {
|
||||
const value = name.trim();
|
||||
return "Hello " + value;
|
||||
}
|
||||
`;
|
||||
const ignoreSample = `# Build output
|
||||
dist/
|
||||
*.log
|
||||
!.keep.log
|
||||
cache/
|
||||
!cache/README.md
|
||||
`;
|
||||
const changelogSample = `# Changelog
|
||||
|
||||
## 1.2.0 - 2026-08-30
|
||||
|
||||
### fixed
|
||||
|
||||
- Handle empty input
|
||||
- Handle empty input
|
||||
|
||||
### Added
|
||||
|
||||
- Add patch inspector
|
||||
|
||||
## Unreleased
|
||||
|
||||
### changed
|
||||
|
||||
- Improve diagnostics
|
||||
`;
|
||||
type Tab = "patch" | "author" | "ignore" | "versions" | "commit" | "changelog";
|
||||
|
||||
function download(value: string, filename: string, type = "text/plain") {
|
||||
triggerBlobDownload(
|
||||
new Blob([value], { type: `${type};charset=utf-8` }),
|
||||
filename,
|
||||
);
|
||||
}
|
||||
|
||||
export function Workbench() {
|
||||
const [tab, setTab] = useState<Tab>("patch");
|
||||
const [error, setError] = useState("");
|
||||
const [patchSource, setPatchSource] = useState(patchSample);
|
||||
const [patch, setPatch] = useState<PatchDocument>(() =>
|
||||
parseUnifiedPatch(patchSample),
|
||||
);
|
||||
const [before, setBefore] = useState(beforeSample);
|
||||
const [after, setAfter] = useState(afterSample);
|
||||
const [path, setPath] = useState("src/example.ts");
|
||||
const [context, setContext] = useState(3);
|
||||
const [authored, setAuthored] = useState(() =>
|
||||
createUnifiedPatch(beforeSample, afterSample, "src/example.ts", 3),
|
||||
);
|
||||
const [ignoreSource, setIgnoreSource] = useState(ignoreSample);
|
||||
const [ignorePaths, setIgnorePaths] = useState(
|
||||
"dist/app.js\nnotes.log\n.keep.log\ncache/README.md\nsrc/main.ts",
|
||||
);
|
||||
const [ignoreResults, setIgnoreResults] = useState<IgnoreResult[]>(() =>
|
||||
explainGitignore(ignoreSample, ignorePaths.split("\n")),
|
||||
);
|
||||
const [versions, setVersions] = useState(
|
||||
"1.2.3\n1.3.0-beta.1\n1.3.0\n2.0.0\n0.9.8",
|
||||
);
|
||||
const [range, setRange] = useState("^1.2.0");
|
||||
const [message, setMessage] = useState(
|
||||
"feat(git): add local patch inspection",
|
||||
);
|
||||
const [type, setType] = useState("feat");
|
||||
const [scope, setScope] = useState("git");
|
||||
const [breaking, setBreaking] = useState(false);
|
||||
const [subject, setSubject] = useState("add local patch inspection");
|
||||
const [body, setBody] = useState("");
|
||||
const [breakingDescription, setBreakingDescription] = useState("");
|
||||
const [references, setReferences] = useState("");
|
||||
const [changelog, setChangelog] = useState(changelogSample);
|
||||
const [normalized, setNormalized] = useState(() =>
|
||||
normalizeChangelog(changelogSample),
|
||||
);
|
||||
const linted = useMemo(() => lintConventionalCommit(message), [message]);
|
||||
const versionRows = useMemo(
|
||||
() =>
|
||||
versions
|
||||
.split(/\r?\n/u)
|
||||
.filter((value) => value.trim())
|
||||
.slice(0, 1_000)
|
||||
.map((value) => {
|
||||
try {
|
||||
const parsed = parseSemVer(value);
|
||||
return {
|
||||
value,
|
||||
parsed,
|
||||
matches: satisfiesSemVer(value, range),
|
||||
error: "",
|
||||
};
|
||||
} catch (reason) {
|
||||
return {
|
||||
value,
|
||||
parsed: null,
|
||||
matches: false,
|
||||
error:
|
||||
reason instanceof Error ? reason.message : "Invalid version.",
|
||||
};
|
||||
}
|
||||
})
|
||||
.sort((left, right) =>
|
||||
left.parsed && right.parsed
|
||||
? compareSemVer(right.parsed, left.parsed)
|
||||
: left.parsed
|
||||
? -1
|
||||
: 1,
|
||||
),
|
||||
[versions, range],
|
||||
);
|
||||
|
||||
const inspectPatch = () => {
|
||||
try {
|
||||
setPatch(parseUnifiedPatch(patchSource));
|
||||
setError("");
|
||||
} catch (reason) {
|
||||
setError(
|
||||
reason instanceof Error ? reason.message : "Patch inspection failed.",
|
||||
);
|
||||
}
|
||||
};
|
||||
const authorPatch = () => {
|
||||
try {
|
||||
setAuthored(createUnifiedPatch(before, after, path, context));
|
||||
setError("");
|
||||
} catch (reason) {
|
||||
setError(
|
||||
reason instanceof Error ? reason.message : "Patch authoring failed.",
|
||||
);
|
||||
}
|
||||
};
|
||||
const testIgnore = () => {
|
||||
try {
|
||||
setIgnoreResults(
|
||||
explainGitignore(ignoreSource, ignorePaths.split(/\r?\n/u)),
|
||||
);
|
||||
setError("");
|
||||
} catch (reason) {
|
||||
setError(
|
||||
reason instanceof Error ? reason.message : ".gitignore test failed.",
|
||||
);
|
||||
}
|
||||
};
|
||||
const buildCommit = () => {
|
||||
try {
|
||||
setMessage(
|
||||
buildConventionalCommit({
|
||||
type,
|
||||
scope,
|
||||
breaking,
|
||||
subject,
|
||||
body,
|
||||
breakingDescription,
|
||||
issues: references,
|
||||
}),
|
||||
);
|
||||
setError("");
|
||||
} catch (reason) {
|
||||
setError(
|
||||
reason instanceof Error ? reason.message : "Commit builder failed.",
|
||||
);
|
||||
}
|
||||
};
|
||||
const runChangelog = () => {
|
||||
try {
|
||||
setNormalized(normalizeChangelog(changelog));
|
||||
setError("");
|
||||
} catch (reason) {
|
||||
setError(
|
||||
reason instanceof Error
|
||||
? reason.message
|
||||
: "Changelog normalization failed.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="workbench">
|
||||
<header className="hero">
|
||||
<div>
|
||||
<p className="eyebrow">Repository interchange workbench</p>
|
||||
<h1>Git Tools</h1>
|
||||
<p>
|
||||
Inspect and author patches, explain ignore rules, reason about
|
||||
versions, and normalize release text entirely in this browser.
|
||||
</p>
|
||||
</div>
|
||||
<span className="privacy-pill">Browser-local</span>
|
||||
</header>
|
||||
{error && (
|
||||
<p className="alert" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<nav className="workspace-tabs" aria-label="Git workspaces">
|
||||
{(
|
||||
[
|
||||
"patch",
|
||||
"author",
|
||||
"ignore",
|
||||
"versions",
|
||||
"commit",
|
||||
"changelog",
|
||||
] as const
|
||||
).map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === value}
|
||||
onClick={() => setTab(value)}
|
||||
>
|
||||
{value === "ignore"
|
||||
? ".gitignore"
|
||||
: value[0]!.toUpperCase() + value.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{tab === "patch" && (
|
||||
<div className="split-layout">
|
||||
<section className="panel workspace">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Unified / Git patch</p>
|
||||
<h2>Patch source</h2>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
aria-label="Patch source"
|
||||
value={patchSource}
|
||||
onChange={(event) => setPatchSource(event.target.value)}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div className="actions">
|
||||
<button type="button" className="primary" onClick={inspectPatch}>
|
||||
Inspect patch
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
download(patchSource, "changes.patch", "text/x-diff")
|
||||
}
|
||||
>
|
||||
Download .patch
|
||||
</button>
|
||||
</div>
|
||||
<p className="muted">
|
||||
The last valid inspection remains visible when later input is
|
||||
malformed. Applying patches is intentionally out of scope.
|
||||
</p>
|
||||
</section>
|
||||
<section className="panel workspace">
|
||||
<div className="facts">
|
||||
<div>
|
||||
<span>Files</span>
|
||||
<strong>{patch.files.length}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Added lines</span>
|
||||
<strong className="added">+{patch.added}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Deleted lines</span>
|
||||
<strong className="deleted">−{patch.deleted}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Diagnostics</span>
|
||||
<strong>{patch.diagnostics.length}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-list">
|
||||
{patch.files.map((file, fileIndex) => (
|
||||
<article key={`${file.oldPath}-${file.newPath}-${fileIndex}`}>
|
||||
<header>
|
||||
<div>
|
||||
<strong>{file.newPath}</strong>
|
||||
<span>
|
||||
{file.status}
|
||||
{file.oldPath !== file.newPath
|
||||
? ` · from ${file.oldPath}`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<b className="added">+{file.added}</b>{" "}
|
||||
<b className="deleted">−{file.deleted}</b>
|
||||
</span>
|
||||
</header>
|
||||
{file.binary ? (
|
||||
<p>Binary content marker; no line payload interpreted.</p>
|
||||
) : (
|
||||
file.hunks.map((hunk) => (
|
||||
<details key={hunk.header}>
|
||||
<summary>
|
||||
{hunk.header} ·{" "}
|
||||
{hunk.validCounts ? "counts valid" : "count mismatch"}
|
||||
</summary>
|
||||
<div
|
||||
className="patch-lines"
|
||||
role="table"
|
||||
aria-label={hunk.header}
|
||||
>
|
||||
{hunk.lines.slice(0, 2_000).map((line, index) => (
|
||||
<div
|
||||
key={`${line.oldLine}-${line.newLine}-${index}`}
|
||||
data-kind={line.kind}
|
||||
role="row"
|
||||
>
|
||||
<span>{line.oldLine ?? ""}</span>
|
||||
<span>{line.newLine ?? ""}</span>
|
||||
<code>
|
||||
{line.kind === "add"
|
||||
? "+"
|
||||
: line.kind === "delete"
|
||||
? "-"
|
||||
: line.kind === "context"
|
||||
? " "
|
||||
: ""}
|
||||
{line.text}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
))
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
{patch.diagnostics.length > 0 && (
|
||||
<ul className="diagnostics">
|
||||
{patch.diagnostics.map((item, index) => (
|
||||
<li key={`${item}-${index}`}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === "author" && (
|
||||
<section className="panel workspace">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Before → after</p>
|
||||
<h2>Unified patch authoring</h2>
|
||||
</div>
|
||||
<button type="button" className="primary" onClick={authorPatch}>
|
||||
Generate patch
|
||||
</button>
|
||||
</div>
|
||||
<div className="author-controls">
|
||||
<label className="field">
|
||||
<span>Repository-relative path</span>
|
||||
<input
|
||||
value={path}
|
||||
onChange={(event) => setPath(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Context lines</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="20"
|
||||
value={context}
|
||||
onChange={(event) => setContext(Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="triple-editors">
|
||||
<label className="field">
|
||||
<span>Before</span>
|
||||
<textarea
|
||||
aria-label="Before text"
|
||||
value={before}
|
||||
onChange={(event) => setBefore(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>After</span>
|
||||
<textarea
|
||||
aria-label="After text"
|
||||
value={after}
|
||||
onChange={(event) => setAfter(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Generated patch</span>
|
||||
<textarea
|
||||
aria-label="Generated patch"
|
||||
value={authored}
|
||||
readOnly
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!authored}
|
||||
onClick={() =>
|
||||
download(authored, "authored.patch", "text/x-diff")
|
||||
}
|
||||
>
|
||||
Download generated patch
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!authored}
|
||||
onClick={() => {
|
||||
setPatchSource(authored);
|
||||
if (authored) setPatch(parseUnifiedPatch(authored));
|
||||
setTab("patch");
|
||||
}}
|
||||
>
|
||||
Inspect generated patch
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{tab === "ignore" && (
|
||||
<div className="split-layout">
|
||||
<section className="panel workspace">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Root .gitignore</p>
|
||||
<h2>Rules and candidates</h2>
|
||||
</div>
|
||||
<button type="button" className="primary" onClick={testIgnore}>
|
||||
Explain matches
|
||||
</button>
|
||||
</div>
|
||||
<label className="field">
|
||||
<span>.gitignore contents</span>
|
||||
<textarea
|
||||
aria-label=".gitignore source"
|
||||
value={ignoreSource}
|
||||
onChange={(event) => setIgnoreSource(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Paths, one per line; end directories with /</span>
|
||||
<textarea
|
||||
aria-label="Paths to test"
|
||||
value={ignorePaths}
|
||||
onChange={(event) => setIgnorePaths(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
<section className="panel workspace">
|
||||
<div className="result-list">
|
||||
{ignoreResults.map((result, index) => (
|
||||
<article
|
||||
key={`${result.path}-${index}`}
|
||||
data-ignored={result.ignored}
|
||||
>
|
||||
<header>
|
||||
<code>{result.path}</code>
|
||||
<strong>{result.ignored ? "ignored" : "included"}</strong>
|
||||
</header>
|
||||
<p>{result.explanation}</p>
|
||||
{result.matched.length > 0 && (
|
||||
<ol>
|
||||
{result.matched.map((match) => (
|
||||
<li key={match.line}>
|
||||
Line {match.line}: <code>{match.source}</code> →{" "}
|
||||
{match.outcome}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<p className="muted">
|
||||
This evaluates one repository-root ignore file. Nested .gitignore
|
||||
files, global excludes, index state, and submodule boundaries are
|
||||
not loaded.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === "versions" && (
|
||||
<div className="split-layout">
|
||||
<section className="panel workspace">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<p className="eyebrow">SemVer 2.0 precedence</p>
|
||||
<h2>Compare and filter</h2>
|
||||
</div>
|
||||
</div>
|
||||
<label className="field">
|
||||
<span>Versions, one per line</span>
|
||||
<textarea
|
||||
aria-label="Semantic versions"
|
||||
value={versions}
|
||||
onChange={(event) => setVersions(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Basic range</span>
|
||||
<input
|
||||
aria-label="Semantic version range"
|
||||
value={range}
|
||||
onChange={(event) => setRange(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<p className="muted">
|
||||
Supports exact/partial/wildcard versions, </≤/>/≥
|
||||
comparators, whitespace AND, || OR, hyphen, tilde, and caret
|
||||
ranges. Build metadata does not affect precedence.
|
||||
</p>
|
||||
</section>
|
||||
<section className="panel workspace">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version (descending)</th>
|
||||
<th>Normalized</th>
|
||||
<th>Range</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{versionRows.map((row, index) => (
|
||||
<tr key={`${row.value}-${index}`}>
|
||||
<td>
|
||||
<code>{row.value}</code>
|
||||
</td>
|
||||
<td>
|
||||
{row.error ||
|
||||
(row.parsed
|
||||
? `${row.parsed.major}.${row.parsed.minor}.${row.parsed.patch}${row.parsed.prerelease.length ? `-${row.parsed.prerelease.join(".")}` : ""}`
|
||||
: "")}
|
||||
</td>
|
||||
<td>
|
||||
{row.error
|
||||
? "invalid"
|
||||
: row.matches
|
||||
? "matches"
|
||||
: "outside"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === "commit" && (
|
||||
<div className="split-layout">
|
||||
<section className="panel workspace">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Conventional Commits</p>
|
||||
<h2>Message builder</h2>
|
||||
</div>
|
||||
<button type="button" className="primary" onClick={buildCommit}>
|
||||
Build message
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-grid">
|
||||
<label className="field">
|
||||
<span>Type</span>
|
||||
<select
|
||||
value={type}
|
||||
onChange={(event) => setType(event.target.value)}
|
||||
>
|
||||
{CONVENTIONAL_TYPES.map((value) => (
|
||||
<option key={value}>{value}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Scope</span>
|
||||
<input
|
||||
value={scope}
|
||||
onChange={(event) => setScope(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field wide">
|
||||
<span>Subject</span>
|
||||
<input
|
||||
value={subject}
|
||||
onChange={(event) => setSubject(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="check wide">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={breaking}
|
||||
onChange={(event) => setBreaking(event.target.checked)}
|
||||
/>{" "}
|
||||
Breaking change
|
||||
</label>
|
||||
<label className="field wide">
|
||||
<span>Body</span>
|
||||
<textarea
|
||||
value={body}
|
||||
onChange={(event) => setBody(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field wide">
|
||||
<span>Breaking change description</span>
|
||||
<input
|
||||
value={breakingDescription}
|
||||
onChange={(event) =>
|
||||
setBreakingDescription(event.target.value)
|
||||
}
|
||||
disabled={!breaking}
|
||||
/>
|
||||
</label>
|
||||
<label className="field wide">
|
||||
<span>Issue references</span>
|
||||
<input
|
||||
value={references}
|
||||
onChange={(event) => setReferences(event.target.value)}
|
||||
placeholder="#123, PROJ-4"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
<section className="panel workspace">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Live lint</p>
|
||||
<h2>{linted.valid ? "Structurally valid" : "Needs changes"}</h2>
|
||||
</div>
|
||||
<span>{linted.breaking ? "Breaking" : "Compatible"}</span>
|
||||
</div>
|
||||
<textarea
|
||||
aria-label="Commit message"
|
||||
value={message}
|
||||
onChange={(event) => setMessage(event.target.value)}
|
||||
/>
|
||||
<dl className="metadata">
|
||||
<div>
|
||||
<dt>Type / scope</dt>
|
||||
<dd>
|
||||
{linted.type || "—"} / {linted.scope || "—"}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Subject</dt>
|
||||
<dd>{linted.subject || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Footers</dt>
|
||||
<dd>{linted.footers.join("; ") || "—"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{linted.issues.length ? (
|
||||
<ul className="diagnostics">
|
||||
{linted.issues.map((issue) => (
|
||||
<li key={issue.message} data-severity={issue.severity}>
|
||||
{issue.severity}: {issue.message}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="success">No configured structural issues.</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === "changelog" && (
|
||||
<div className="split-layout">
|
||||
<section className="panel workspace">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Keep a Changelog shape</p>
|
||||
<h2>Changelog source</h2>
|
||||
</div>
|
||||
<button type="button" className="primary" onClick={runChangelog}>
|
||||
Normalize
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
aria-label="Changelog source"
|
||||
value={changelog}
|
||||
onChange={(event) => setChangelog(event.target.value)}
|
||||
/>
|
||||
</section>
|
||||
<section className="panel workspace">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Deterministic output</p>
|
||||
<h2>
|
||||
{normalized.releases} releases · {normalized.deduplicated}{" "}
|
||||
duplicates removed
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
download(normalized.output, "CHANGELOG.md", "text/markdown")
|
||||
}
|
||||
>
|
||||
Download Markdown
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
aria-label="Normalized changelog"
|
||||
value={normalized.output}
|
||||
readOnly
|
||||
/>
|
||||
{normalized.diagnostics.length > 0 && (
|
||||
<ul className="diagnostics">
|
||||
{normalized.diagnostics.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+1002
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
if ("serviceWorker" in navigator && import.meta.env.PROD) {
|
||||
window.addEventListener("load", () => {
|
||||
const url = new URL("./sw.js", document.baseURI);
|
||||
void navigator.serviceWorker
|
||||
.register(url, { scope: new URL("./", document.baseURI).pathname })
|
||||
.catch(() => undefined);
|
||||
});
|
||||
}
|
||||
+575
@@ -0,0 +1,575 @@
|
||||
:root {
|
||||
--toolbox-background: #f6f7fb;
|
||||
--toolbox-surface: #fff;
|
||||
--toolbox-surface-soft: #eff1f7;
|
||||
--toolbox-text: #202332;
|
||||
--toolbox-muted: #656b7d;
|
||||
--toolbox-border: #d9dce7;
|
||||
--toolbox-accent: #5b4ec4;
|
||||
--toolbox-accent-hover: #493caf;
|
||||
--toolbox-accent-soft: #ece9ff;
|
||||
--toolbox-accent-contrast: #fff;
|
||||
--toolbox-focus: #137d75;
|
||||
--toolbox-danger: #b42342;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html {
|
||||
min-width: 20rem;
|
||||
min-height: 100%;
|
||||
background: var(--toolbox-background);
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
body {
|
||||
min-width: 20rem;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background: var(--toolbox-background);
|
||||
color: var(--toolbox-text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
button,
|
||||
.button {
|
||||
min-height: 2.55rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.55rem 0.8rem;
|
||||
border: 1px solid var(--toolbox-border);
|
||||
border-radius: 0.65rem;
|
||||
background: var(--toolbox-surface);
|
||||
color: var(--toolbox-text);
|
||||
font-weight: 720;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover:not(:disabled),
|
||||
.button:hover {
|
||||
border-color: var(--toolbox-accent);
|
||||
background: var(--toolbox-surface-soft);
|
||||
}
|
||||
:where(button, input, select, textarea, a):focus-visible {
|
||||
outline: 3px solid color-mix(in srgb, var(--toolbox-focus) 42%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.58rem 0.7rem;
|
||||
border: 1px solid var(--toolbox-border);
|
||||
border-radius: 0.62rem;
|
||||
background: var(--toolbox-surface);
|
||||
color: var(--toolbox-text);
|
||||
}
|
||||
textarea {
|
||||
min-height: 10rem;
|
||||
resize: vertical;
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
line-height: 1.48;
|
||||
}
|
||||
.toolbox-shell__main {
|
||||
width: min(100%, 90rem);
|
||||
padding: clamp(0.75rem, 1.8vw, 1.5rem);
|
||||
}
|
||||
.workbench {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
.hero,
|
||||
.panel {
|
||||
border: 1px solid var(--toolbox-border);
|
||||
border-radius: 0.9rem;
|
||||
background: var(--toolbox-surface);
|
||||
box-shadow: 0 8px 28px rgb(30 36 70 / 4%);
|
||||
}
|
||||
.hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
padding: clamp(1.1rem, 3vw, 2rem);
|
||||
}
|
||||
.hero h1,
|
||||
.panel h2,
|
||||
.panel h3,
|
||||
.help-dialog h2,
|
||||
.fatal h1 {
|
||||
margin: 0;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
.hero p:not(.eyebrow) {
|
||||
max-width: 52rem;
|
||||
margin: 0.55rem 0 0;
|
||||
color: var(--toolbox-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.eyebrow {
|
||||
margin: 0 0 0.3rem;
|
||||
color: var(--toolbox-accent);
|
||||
font-size: 0.69rem;
|
||||
font-weight: 820;
|
||||
letter-spacing: 0.115em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.privacy-pill {
|
||||
flex: 0 0 auto;
|
||||
padding: 0.38rem 0.62rem;
|
||||
border-radius: 999px;
|
||||
background: var(--toolbox-accent-soft);
|
||||
color: var(--toolbox-accent);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 760;
|
||||
}
|
||||
.panel {
|
||||
padding: 1rem;
|
||||
}
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: end;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
.capability-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.capability-grid article {
|
||||
padding: 0.9rem;
|
||||
border: 1px solid var(--toolbox-border);
|
||||
border-radius: 0.72rem;
|
||||
background: var(--toolbox-surface-soft);
|
||||
}
|
||||
.capability-grid p {
|
||||
margin: 0.4rem 0 0;
|
||||
color: var(--toolbox-muted);
|
||||
line-height: 1.48;
|
||||
}
|
||||
.workspace-tabs {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 0.2rem;
|
||||
}
|
||||
.workspace-tabs button[aria-selected="true"] {
|
||||
border-color: var(--toolbox-accent);
|
||||
background: var(--toolbox-accent);
|
||||
color: var(--toolbox-accent-contrast);
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.field > span {
|
||||
font-size: 0.76rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
.muted {
|
||||
color: var(--toolbox-muted);
|
||||
}
|
||||
.result {
|
||||
padding: 0.8rem;
|
||||
border: 1px solid var(--toolbox-border);
|
||||
border-radius: 0.68rem;
|
||||
background: var(--toolbox-surface-soft);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.loading,
|
||||
.fatal {
|
||||
width: min(100% - 2rem, 60rem);
|
||||
margin: 2rem auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
.help-dialog {
|
||||
width: min(36rem, calc(100% - 2rem));
|
||||
border: 1px solid var(--toolbox-border);
|
||||
border-radius: 0.9rem;
|
||||
background: var(--toolbox-surface);
|
||||
color: var(--toolbox-text);
|
||||
}
|
||||
.help-dialog::backdrop {
|
||||
background: rgb(20 24 45 / 55%);
|
||||
}
|
||||
.dialog-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
.workspace {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
.editor-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.editor-grid textarea {
|
||||
min-height: 22rem;
|
||||
}
|
||||
.encoding-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(10rem, 16rem) minmax(12rem, 1fr);
|
||||
gap: 0.7rem;
|
||||
align-items: end;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.primary {
|
||||
border-color: var(--toolbox-accent);
|
||||
background: var(--toolbox-accent);
|
||||
color: var(--toolbox-accent-contrast);
|
||||
}
|
||||
.file-button {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.file-button input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.check {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
min-height: 2.55rem;
|
||||
}
|
||||
.check input {
|
||||
width: auto;
|
||||
min-height: auto;
|
||||
}
|
||||
.inventory {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
|
||||
gap: 0.45rem;
|
||||
margin: 0;
|
||||
}
|
||||
.inventory div {
|
||||
padding: 0.55rem;
|
||||
border-radius: 0.55rem;
|
||||
background: var(--toolbox-surface-soft);
|
||||
}
|
||||
.inventory dt {
|
||||
color: var(--toolbox-muted);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
.inventory dd {
|
||||
margin: 0.15rem 0 0;
|
||||
}
|
||||
.steps {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.steps li {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(12rem, 0.8fr) minmax(12rem, 1.4fr) auto;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
padding: 0.6rem;
|
||||
border: 1px solid var(--toolbox-border);
|
||||
border-radius: 0.65rem;
|
||||
}
|
||||
.step-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.step-actions button {
|
||||
min-width: 2.55rem;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
.warning,
|
||||
.error,
|
||||
.notice {
|
||||
margin: 0;
|
||||
padding: 0.7rem;
|
||||
border: 1px solid var(--toolbox-border);
|
||||
border-radius: 0.65rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.warning {
|
||||
border-color: #d9a72e;
|
||||
background: #fff8df;
|
||||
color: #725000;
|
||||
}
|
||||
.error {
|
||||
border-color: var(--toolbox-danger);
|
||||
color: var(--toolbox-danger);
|
||||
}
|
||||
.notice {
|
||||
background: var(--toolbox-surface-soft);
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 0.55rem;
|
||||
border-bottom: 1px solid var(--toolbox-border);
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
color: var(--toolbox-muted);
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
details {
|
||||
border: 1px solid var(--toolbox-border);
|
||||
border-radius: 0.65rem;
|
||||
}
|
||||
summary {
|
||||
padding: 0.65rem;
|
||||
cursor: pointer;
|
||||
font-weight: 750;
|
||||
}
|
||||
.recipe {
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
padding: 0 0.65rem 0.65rem;
|
||||
}
|
||||
.alert,
|
||||
.success {
|
||||
margin: 0;
|
||||
padding: 0.75rem 0.9rem;
|
||||
border: 1px solid var(--toolbox-border);
|
||||
border-radius: 0.68rem;
|
||||
}
|
||||
.alert {
|
||||
border-color: var(--toolbox-danger);
|
||||
color: var(--toolbox-danger);
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--toolbox-danger) 7%,
|
||||
var(--toolbox-surface)
|
||||
);
|
||||
}
|
||||
.success {
|
||||
color: #187147;
|
||||
background: color-mix(in srgb, #2ba46a 10%, var(--toolbox-surface));
|
||||
}
|
||||
.split-layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
.split-layout textarea {
|
||||
min-height: 29rem;
|
||||
}
|
||||
.facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(6rem, 1fr));
|
||||
gap: 0.55rem;
|
||||
}
|
||||
.facts > div {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
padding: 0.7rem;
|
||||
border-radius: 0.6rem;
|
||||
background: var(--toolbox-surface-soft);
|
||||
}
|
||||
.facts span,
|
||||
.file-list header span,
|
||||
.file-list article > p,
|
||||
.metadata dt {
|
||||
color: var(--toolbox-muted);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
.added {
|
||||
color: #187147;
|
||||
}
|
||||
.deleted {
|
||||
color: var(--toolbox-danger);
|
||||
}
|
||||
.file-list,
|
||||
.result-list {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.file-list > article,
|
||||
.result-list > article {
|
||||
border: 1px solid var(--toolbox-border);
|
||||
border-radius: 0.7rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.file-list article > header,
|
||||
.result-list article > header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.6rem;
|
||||
align-items: start;
|
||||
padding: 0.7rem;
|
||||
background: var(--toolbox-surface-soft);
|
||||
}
|
||||
.file-list header > div {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
.file-list article > p,
|
||||
.result-list article > p,
|
||||
.result-list article > ol {
|
||||
margin: 0;
|
||||
padding: 0.7rem;
|
||||
}
|
||||
.patch-lines {
|
||||
max-height: 25rem;
|
||||
overflow: auto;
|
||||
font:
|
||||
0.78rem/1.45 ui-monospace,
|
||||
SFMono-Regular,
|
||||
Consolas,
|
||||
monospace;
|
||||
}
|
||||
.patch-lines > div {
|
||||
display: grid;
|
||||
grid-template-columns: 3.3rem 3.3rem minmax(max-content, 1fr);
|
||||
min-width: max-content;
|
||||
}
|
||||
.patch-lines > div > span {
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-right: 1px solid var(--toolbox-border);
|
||||
color: var(--toolbox-muted);
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
}
|
||||
.patch-lines code {
|
||||
padding: 0.1rem 0.45rem;
|
||||
white-space: pre;
|
||||
}
|
||||
.patch-lines [data-kind="add"] {
|
||||
background: color-mix(in srgb, #2ba46a 14%, var(--toolbox-surface));
|
||||
}
|
||||
.patch-lines [data-kind="delete"] {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--toolbox-danger) 11%,
|
||||
var(--toolbox-surface)
|
||||
);
|
||||
}
|
||||
.diagnostics {
|
||||
max-height: 18rem;
|
||||
overflow: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.diagnostics [data-severity="error"] {
|
||||
color: var(--toolbox-danger);
|
||||
}
|
||||
.author-controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(14rem, 1fr) minmax(8rem, 12rem);
|
||||
gap: 0.7rem;
|
||||
}
|
||||
.triple-editors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.8rem;
|
||||
}
|
||||
.triple-editors textarea {
|
||||
min-height: 30rem;
|
||||
}
|
||||
.result-list [data-ignored="true"] {
|
||||
border-left: 5px solid var(--toolbox-danger);
|
||||
}
|
||||
.result-list [data-ignored="false"] {
|
||||
border-left: 5px solid #2ba46a;
|
||||
}
|
||||
.result-list code,
|
||||
table code {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 0.65rem;
|
||||
border-bottom: 1px solid var(--toolbox-border);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
color: var(--toolbox-muted);
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.form-grid textarea {
|
||||
min-height: 8rem;
|
||||
}
|
||||
.metadata {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin: 0;
|
||||
}
|
||||
.metadata > div {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(7rem, 0.3fr) minmax(0, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.metadata dd {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@media (max-width: 62rem) {
|
||||
.editor-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.split-layout,
|
||||
.triple-editors {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@media (max-width: 48rem) {
|
||||
.steps li {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@media (max-width: 42rem) {
|
||||
.hero {
|
||||
flex-direction: column;
|
||||
}
|
||||
.privacy-pill {
|
||||
order: -1;
|
||||
}
|
||||
.facts,
|
||||
.author-controls,
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.wide {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { afterEach } from "vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://git.add-ideas.de/lotobo/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json",
|
||||
"schemaVersion": 1,
|
||||
"id": "de.add-ideas.git-tools",
|
||||
"name": "Git Tools",
|
||||
"version": "0.1.0",
|
||||
"description": "Inspect and author Git interchange formats locally in the browser.",
|
||||
"entry": "./",
|
||||
"icon": "./favicon.svg",
|
||||
"categories": ["developer", "text", "productivity"],
|
||||
"tags": [
|
||||
"git",
|
||||
"patch",
|
||||
"diff",
|
||||
"gitignore",
|
||||
"semver",
|
||||
"conventional-commits",
|
||||
"changelog"
|
||||
],
|
||||
"integration": {
|
||||
"contextVersion": 1,
|
||||
"launchModes": ["navigate", "new-tab"],
|
||||
"embedding": "unsupported"
|
||||
},
|
||||
"requirements": {
|
||||
"secureContext": false,
|
||||
"workers": false,
|
||||
"indexedDb": false,
|
||||
"crossOriginIsolated": false,
|
||||
"topLevelContext": false
|
||||
},
|
||||
"privacy": {
|
||||
"processing": "local",
|
||||
"fileUploads": true,
|
||||
"telemetry": false,
|
||||
"label": "Inputs stay in this browser; nothing is uploaded."
|
||||
},
|
||||
"source": {
|
||||
"repository": "https://git.add-ideas.de/lotobo/git-tools",
|
||||
"license": "GPL-3.0-or-later"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"id": "source",
|
||||
"label": "Source",
|
||||
"url": "https://git.add-ideas.de/lotobo/git-tools"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { defineToolboxApp, parseToolboxApp } from "@add-ideas/toolbox-contract";
|
||||
import source from "./manifest.source.json";
|
||||
|
||||
export const manifest = defineToolboxApp(parseToolboxApp(source));
|
||||
@@ -0,0 +1 @@
|
||||
export const APP_VERSION = "0.1.0";
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user