Add contribution page and Docker deployment

This commit is contained in:
achraf
2026-06-02 20:33:09 +02:00
parent be635b1828
commit 4f1af76658
22 changed files with 791 additions and 573 deletions

View 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>
);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>