Add contribution page and Docker deployment
This commit is contained in:
235
src/pages/ContributePage.tsx
Normal file
235
src/pages/ContributePage.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
const repoUrl =
|
||||
import.meta.env.VITE_PLIMI_REPO_URL || "https://github.com/your-org/plimi";
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: "Create a tool folder",
|
||||
copy: "Add a new directory under src/tools with an index.ts plugin definition, a run.ts runner, and a run.test.ts test file.",
|
||||
},
|
||||
{
|
||||
title: "Describe the tool",
|
||||
copy: "The manifest declares the id, name, category, tags, input type, output type, permissions, and offline readiness.",
|
||||
},
|
||||
{
|
||||
title: "Implement the runner",
|
||||
copy: "The runner receives typed input, normalized options, and a context for progress, cancellation, and logging.",
|
||||
},
|
||||
{
|
||||
title: "Register and test",
|
||||
copy: "Import the plugin in the registry, add it to the list, then run the unit tests and production build.",
|
||||
},
|
||||
];
|
||||
|
||||
const apiItems = [
|
||||
{
|
||||
name: "manifest",
|
||||
detail: "Required metadata used by the directory, routing, generated input UI, result display, and permission labels.",
|
||||
},
|
||||
{
|
||||
name: "input",
|
||||
detail: "Supports none, text, files, text-or-files, or grouped fields with per-field labels, placeholders, limits, and examples.",
|
||||
},
|
||||
{
|
||||
name: "optionsSchema",
|
||||
detail: "Optional generated controls for text, number, boolean, select, and slider values.",
|
||||
},
|
||||
{
|
||||
name: "examples",
|
||||
detail: "Optional shared Try example API. Existing manifest.example and field.example values are automatically converted.",
|
||||
},
|
||||
{
|
||||
name: "run",
|
||||
detail: "The async function that performs the work in the browser and returns text, json, table, or downloadable files.",
|
||||
},
|
||||
{
|
||||
name: "customUi",
|
||||
detail: "Optional React UI for complex tools such as image editors, canvas workflows, and richer file interactions.",
|
||||
},
|
||||
];
|
||||
|
||||
const code = `export const myToolPlugin: PlimiPlugin<MyOptions> = {
|
||||
manifest: {
|
||||
id: "my-tool",
|
||||
name: "My Tool",
|
||||
description: "Runs locally in the browser.",
|
||||
category: "developer",
|
||||
version: "1.0.0",
|
||||
tags: ["example"],
|
||||
input: { type: "text", placeholder: "Paste input..." },
|
||||
output: { type: "text" },
|
||||
offlineReady: true,
|
||||
},
|
||||
optionsSchema: {
|
||||
fields: [
|
||||
{ type: "boolean", key: "uppercase", label: "Uppercase", defaultValue: false }
|
||||
],
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
label: "Try example",
|
||||
input: { text: "Hello Plimi" },
|
||||
options: { uppercase: true },
|
||||
},
|
||||
],
|
||||
capabilities: { worker: false, cancelable: false },
|
||||
permissions: { network: "none", fileSystem: "none", clipboard: "none" },
|
||||
run: runMyTool,
|
||||
};`;
|
||||
|
||||
function ExternalLink({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex w-full items-center justify-center rounded-[12px] bg-[var(--p-accent)] px-5 py-3.5 font-sans text-sm font-semibold tracking-tight text-[var(--p-accent-ink)] no-underline transition-[filter] hover:brightness-110 sm:w-auto"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContributePage() {
|
||||
return (
|
||||
<div className="mx-auto flex max-w-[1120px] flex-col gap-10 pb-16 animate-plimi-slide">
|
||||
<section className="grid grid-cols-1 gap-7 md:grid-cols-[0.95fr_1.05fr] md:items-end">
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="font-mono text-[11px] uppercase tracking-[0.12em] text-[var(--p-muted)]">
|
||||
contribute tools
|
||||
</span>
|
||||
<h1 className="m-0 max-w-[640px] font-sans text-4xl font-bold leading-[1.02] tracking-tight text-[var(--p-text)] text-balance md:text-[56px]">
|
||||
Add useful tools to Plimi.
|
||||
</h1>
|
||||
<p className="m-0 max-w-[610px] text-base leading-relaxed text-[var(--p-muted)] text-pretty">
|
||||
Plimi V1 supports contributions through the source code. Tools are added as internal
|
||||
plugins, reviewed in Git, and shipped with the app so they remain fast, typed, and
|
||||
browser-local.
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<ExternalLink href={repoUrl}>Open Git repository</ExternalLink>
|
||||
<Link
|
||||
to="/tools"
|
||||
className="inline-flex w-full items-center justify-center rounded-[12px] border border-[var(--p-border)] bg-[var(--p-chip)] px-5 py-3.5 font-sans text-sm font-semibold tracking-tight text-[var(--p-text)] no-underline transition-colors hover:bg-[var(--p-border)] sm:w-auto"
|
||||
>
|
||||
Browse existing tools
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[20px] border border-[var(--p-border)] bg-[var(--p-surface)] p-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
<span className="font-mono text-[11px] uppercase tracking-[0.12em] text-[var(--p-muted)]">
|
||||
V1 contribution model
|
||||
</span>
|
||||
<div className="rounded-[14px] border border-[var(--p-border)] bg-[var(--p-surface-2)] p-4">
|
||||
<p className="m-0 text-sm leading-relaxed text-[var(--p-muted)]">
|
||||
External plugin installation is not available in V1. To contribute, fork the repo,
|
||||
add the tool inside <code className="text-[var(--p-text)]">src/tools</code>, register
|
||||
it, test it, and open a pull request.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[14px] border border-[var(--p-border)] bg-[var(--p-bg)] p-4">
|
||||
<div className="mb-2 font-mono text-[10px] uppercase tracking-[0.12em] text-[var(--p-muted)]">
|
||||
repo env variable
|
||||
</div>
|
||||
<code className="break-all font-mono text-[13px] text-[var(--p-text)]">
|
||||
VITE_PLIMI_REPO_URL={repoUrl}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
{steps.map((step, index) => (
|
||||
<article
|
||||
key={step.title}
|
||||
className="flex min-h-[190px] flex-col justify-between gap-5 rounded-[16px] border border-[var(--p-border)] bg-[var(--p-surface)] p-5"
|
||||
>
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-[var(--p-chip)] font-mono text-[11px] text-[var(--p-muted)]">
|
||||
{index + 1}
|
||||
</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="m-0 font-sans text-[16px] font-semibold tracking-tight text-[var(--p-text)]">
|
||||
{step.title}
|
||||
</h2>
|
||||
<p className="m-0 text-sm leading-relaxed text-[var(--p-muted)]">{step.copy}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-1 gap-6 rounded-[20px] border border-[var(--p-border)] bg-[var(--p-surface-2)] p-5 md:grid-cols-[0.72fr_1.28fr] md:p-7">
|
||||
<div className="flex flex-col gap-3">
|
||||
<span className="font-mono text-[11px] uppercase tracking-[0.12em] text-[var(--p-muted)]">
|
||||
plugin API
|
||||
</span>
|
||||
<h2 className="m-0 font-sans text-2xl font-bold tracking-tight text-[var(--p-text)]">
|
||||
A tool is a small typed contract.
|
||||
</h2>
|
||||
<p className="m-0 text-sm leading-relaxed text-[var(--p-muted)]">
|
||||
Most tools only need a manifest, an optional options schema, and a runner. Plimi can
|
||||
generate the input panel, options controls, example action, and result panel from that
|
||||
contract.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{apiItems.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="rounded-[14px] border border-[var(--p-border)] bg-[var(--p-surface)] p-4"
|
||||
>
|
||||
<h3 className="m-0 font-mono text-[12px] font-semibold text-[var(--p-text)]">
|
||||
{item.name}
|
||||
</h3>
|
||||
<p className="mb-0 mt-2 text-[13px] leading-relaxed text-[var(--p-muted)]">
|
||||
{item.detail}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-1 gap-6 md:grid-cols-[1fr_1fr]">
|
||||
<div className="flex flex-col gap-3 rounded-[20px] border border-[var(--p-border)] bg-[var(--p-surface)] p-5 md:p-6">
|
||||
<span className="font-mono text-[11px] uppercase tracking-[0.12em] text-[var(--p-muted)]">
|
||||
minimal shape
|
||||
</span>
|
||||
<pre className="m-0 overflow-x-auto rounded-[14px] border border-[var(--p-border)] bg-[var(--p-bg)] p-4 text-[12px] leading-relaxed text-[var(--p-text)]">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 rounded-[20px] border border-[var(--p-border)] bg-[var(--p-surface)] p-5 md:p-6">
|
||||
<span className="font-mono text-[11px] uppercase tracking-[0.12em] text-[var(--p-muted)]">
|
||||
contribution checklist
|
||||
</span>
|
||||
<ul className="m-0 flex list-none flex-col gap-3 p-0">
|
||||
{[
|
||||
"Keep execution local. Do not add backend calls for tool processing.",
|
||||
"Prefer generated UI unless the workflow needs a custom canvas or preview.",
|
||||
"Add focused tests for parsing, validation, edge cases, and output format.",
|
||||
"Declare permissions honestly, especially network, clipboard, and file access.",
|
||||
"Run pnpm test and pnpm build before opening the pull request.",
|
||||
].map((item) => (
|
||||
<li key={item} className="flex gap-3 text-sm leading-relaxed text-[var(--p-muted)]">
|
||||
<span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--p-accent)]" />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -192,7 +192,7 @@ export function HowItWorksPage() {
|
||||
</div>
|
||||
<Link
|
||||
to="/tools"
|
||||
className="inline-flex items-center justify-center rounded-[10px] bg-[var(--p-accent)] px-5 py-3 font-sans text-sm font-semibold tracking-tight text-[var(--p-accent-ink)] no-underline transition-[filter] hover:brightness-110"
|
||||
className="inline-flex w-full items-center justify-center rounded-[12px] bg-[var(--p-accent)] px-5 py-3.5 font-sans text-sm font-semibold tracking-tight text-[var(--p-accent-ink)] no-underline transition-[filter] hover:brightness-110 md:w-auto md:rounded-[10px] md:py-3"
|
||||
>
|
||||
Browse tools
|
||||
</Link>
|
||||
|
||||
@@ -26,20 +26,29 @@ export function ToolDetailPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-0 md:p-10 animate-plimi-fade"
|
||||
<div
|
||||
onClick={handleClose}
|
||||
role="presentation"
|
||||
className="fixed inset-0 z-50 flex items-end justify-center p-0 md:items-center md:p-10 animate-plimi-fade"
|
||||
style={{
|
||||
background: 'color-mix(in oklab, var(--p-bg) 70%, transparent)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-full md:w-[min(900px,100%)] h-[92%] md:h-auto md:max-h-full bg-[var(--p-surface)] rounded-t-[20px] md:rounded-[24px] border-[1.5px] border-[var(--p-border)] flex flex-col animate-plimi-slide self-end md:self-center overflow-hidden"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={plugin.manifest.name}
|
||||
className="w-full md:w-[min(900px,100%)] h-[92dvh] md:h-auto md:max-h-[calc(100dvh-5rem)] bg-[var(--p-surface)] rounded-t-[20px] md:rounded-[24px] border-[1.5px] border-[var(--p-border)] flex flex-col animate-plimi-slide overflow-hidden"
|
||||
style={{
|
||||
boxShadow: '0 40px 80px -30px var(--p-shadow-soft)',
|
||||
}}
|
||||
>
|
||||
{/* Grab handle — signals the sheet is dismissable on touch */}
|
||||
<div className="md:hidden flex justify-center pt-2.5 pb-1 shrink-0">
|
||||
<span className="h-1 w-9 rounded-full bg-[var(--p-border)]" />
|
||||
</div>
|
||||
<ToolShell plugin={plugin} onClose={handleClose} dark={dark} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ export function ToolsPage() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-7 max-w-[1200px] mx-auto pb-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1.1fr_1fr] gap-12 items-end">
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1.1fr_1fr] gap-7 md:gap-12 items-end">
|
||||
<div className="flex flex-col gap-3.5">
|
||||
<span className="font-mono text-[11px] text-[var(--p-muted)] tracking-[0.12em] uppercase">
|
||||
your digital pencil case
|
||||
@@ -127,7 +127,7 @@ export function ToolsPage() {
|
||||
{tools.map((t) => {
|
||||
const flatIdx = filtered.indexOf(t);
|
||||
return (
|
||||
<ToolTile key={t.manifest.id} plugin={t} onClick={handleOpenTool} focused={flatIdx === safeFocusedIndex} dark={dark} />
|
||||
<ToolTile key={t.manifest.id} plugin={t} onClick={handleOpenTool} focused={flatIdx === safeFocusedIndex} dark={dark} index={flatIdx} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -140,12 +140,9 @@ export function ToolsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mt-4">
|
||||
{filtered.map((t) => {
|
||||
const flatIdx = filtered.indexOf(t);
|
||||
return (
|
||||
<ToolTile key={t.manifest.id} plugin={t} onClick={handleOpenTool} focused={flatIdx === safeFocusedIndex} dark={dark} />
|
||||
);
|
||||
})}
|
||||
{filtered.map((t, flatIdx) => (
|
||||
<ToolTile key={t.manifest.id} plugin={t} onClick={handleOpenTool} focused={flatIdx === safeFocusedIndex} dark={dark} index={flatIdx} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user