waybetter-for-x/apps/web/store/submit/index.html
2026-10-01 06:01:13 +07:00

327 lines
25 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- seo -->
<meta name="theme-color" content="#07080c" />
<link rel="icon" type="image/svg+xml" href="/brand/wb-icon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/brand/wb-icon-32.png" />
<link rel="apple-touch-icon" href="/brand/wb-icon-256.png" />
<script>document.documentElement.classList.add('js');</script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Geist:wght@300..900&family=Geist+Mono:wght@400;500;600&family=Instrument+Serif:ital@1&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="../../src/base.css" />
<link rel="stylesheet" href="../../src/gallery.css" />
<link rel="stylesheet" href="../../src/store.css" />
</head>
<body>
<a data-i18n="site.common.skipToContent" class="skip" href="#main">Skip to content</a>
<header class="top">
<div class="wrap top-row">
<a class="mark" href="../" aria-label="wayBetter Store">
<img class="wb" src="/brand/wb-mark-simple.svg" alt="" width="43" height="24" />
<span class="mark-name" translate="no">wayBetter <span class="mark-sub">Store</span></span>
</a>
<nav data-i18n-aria-label="site.common.primary" class="top-nav" aria-label="Primary">
<a href="../../" class="keep keep-sm nav-family">wayBetter</a>
<a data-i18n="site.common.store" href="../" class="keep">Store</a>
<a data-i18n="site.submit.nav.make" href="#make">Make</a>
<a data-i18n="site.submit.nav.add" href="#folder">Add</a>
<a data-i18n="site.submit.nav.check" href="#checks">Check</a>
<a data-i18n="site.submit.nav.send" href="#send">Send</a>
<a data-i18n="site.submit.nav.catalogFormat" href="#format">Catalog format</a>
<a data-i18n="site.common.languages" href="#languages">Languages</a>
<a data-i18n="site.common.source" data-i18n-aria-label="site.common.sourceOn" href="%REPO_URL%" class="src-link" aria-label="Source on %FORGE_HOST%"
><img class="src-logo" src="/git-crt-logo.svg" alt="" width="16" height="16" />Source</a
>
</nav>
<!-- lang:top -->
</div>
<div class="top-progress" aria-hidden="true"></div>
</header>
<main id="main">
<section class="st-hero st-hero-slim">
<div class="st-hero-wash" aria-hidden="true"></div>
<div class="wrap">
<nav data-i18n-aria-label="site.submit.hero.breadcrumb" class="it-crumbs" aria-label="Breadcrumb">
<ol><li><a data-i18n="site.common.store" href="../">Store</a></li><li data-i18n="site.common.publishYourOwn" aria-current="page">Publish your own</li></ol>
</nav>
<p data-i18n="site.submit.hero.aPullRequestIsA" class="kicker"><span class="kicker-dot"></span><span class="foss" translate="no">(FOSS)</span> A pull request is a submission</p>
<h1>
<span data-i18n="site.submit.hero.publishYour" class="h1-line">Publish your</span>
<span class="h1-line"><em data-i18n="site.submit.hero.own" class="grad">own.</em></span>
</h1>
<p data-i18n="site.submit.hero.aThemeIsOneJson" class="st-lede">
A theme is one JSON file. A plugin is one JavaScript file. Make it, try it in the app,
then add it to the Store: one folder in one pull request, or the file in an issue if
you do not use git. Both need a free %FORGE_HOST% account, and nothing else.
</p>
<div class="ctas">
<a data-i18n="site.submit.hero.theFiveSteps" class="btn btn-solid" href="#make">
The five steps
<svg class="arrow" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8h9M8.5 4.5 12 8l-3.5 3.5" /></svg>
</a>
<a data-i18n="site.submit.hero.theRepositoryOn" class="btn btn-ghost src-link" href="%REPO_URL%"><img class="src-logo" src="/git-crt-logo.svg" alt="" width="16" height="16" />The repository on %FORGE_HOST%</a>
</div>
<!-- repo:note -->
</div>
</section>
<section class="sub-sec" id="make">
<div class="wrap sub-grid">
<header class="sub-head">
<span class="sub-n">1</span>
<h2 data-i18n="site.submit.make.makeIt">Make it</h2>
<p data-i18n="site.submit.make.startFromAWorkingFile">Start from a working file. The scaffolder writes one; the app&rsquo;s own Make section (wayBetter &rarr; Make) has examples and the live theme values.</p>
</header>
<div class="sub-body">
<div class="sub-two">
<article class="sub-card">
<h3 data-i18n="site.submit.make.aTheme">A theme</h3>
<p data-i18n="site.submit.make.colorsFontsCornersDensityLayout">Colors, fonts, corners, density, layout, background, effects. Three colors are enough; the rest is derived and text is kept readable.</p>
<button data-i18n-aria-label="site.common.copyTheCommand" class="inline-cmd" type="button" data-copy='npm create waybetter theme -- --name "My Theme"' aria-label="Copy the command">
<span class="ps" aria-hidden="true">$</span> <span class="cmd-text" translate="no">npm create waybetter theme</span>
<span data-i18n="site.common.copy" class="copy-btn" aria-hidden="true" data-copy-label>Copy</span>
</button>
<p data-i18n="site.submit.make.orBuildItInThe" class="sub-fine">Or build it in the app: Appearance &rarr; Theme Builder &rarr; Export. <a href="%REPO_FILE:docs/THEMES.md%">THEMES.md</a> lists everything a theme can set.</p>
</article>
<article class="sub-card">
<h3 data-i18n="site.submit.make.aPlugin">A plugin</h3>
<p data-i18n="site.submit.make.aClassWithAndAnd">A class with <code>start()</code> and <code>stop()</code>, and a header that says who made it and what it uses.</p>
<button data-i18n-aria-label="site.common.copyTheCommand" class="inline-cmd" type="button" data-copy='npm create waybetter plugin -- --name "My Plugin"' aria-label="Copy the command">
<span class="ps" aria-hidden="true">$</span> <span class="cmd-text" translate="no">npm create waybetter plugin</span>
<span data-i18n="site.common.copy" class="copy-btn" aria-hidden="true" data-copy-label>Copy</span>
</button>
<p data-i18n="site.submit.make.addForATypescriptProject" class="sub-fine">Add <code>--ts</code> for a TypeScript project. <a href="%REPO_FILE:docs/PLUGINS.md%">PLUGINS.md</a> has the API, the permissions and live reload.</p>
</article>
</div>
<div class="code">
<div class="code-bar"><span class="dots" aria-hidden="true"><i></i><i></i><i></i></span><span class="file">hide-trends.plugin.js</span><button data-i18n="site.common.copy" class="copy-btn" type="button" data-copy-from="sub-plugin">Copy</button></div>
<pre id="sub-plugin" tabindex="0"><code><span class="tk-c">/**
* @name Hide Trends
* @description Hides the trends box in the right column, so the timeline is the only thing moving.
* @version 1.0.0
* @author you
* @permissions network:none
*/</span>
<span class="tk-t">module</span>.<span class="tk-t">exports</span> = <span class="tk-k">class</span> <span class="tk-f">HideTrends</span> {
<span class="tk-f">start</span>() { wayBetter.api.<span class="tk-f">injectCSS</span>(<span class="tk-s">'hide-trends'</span>, <span class="tk-s">'[data-testid="sidebarColumn"] section { display: none }'</span>); }
<span class="tk-f">stop</span>() { wayBetter.api.<span class="tk-f">clearCSS</span>(<span class="tk-s">'hide-trends'</span>); }
};</code></pre>
</div>
<p data-i18n="site.submit.make.theStoreReadsAPlugin" class="sub-fine">The Store reads a plugin&rsquo;s name, author, version, description and permissions from this header, and a theme&rsquo;s name, author and description from its JSON. The install screen shows the same values, so the two can never disagree.</p>
</div>
</div>
</section>
<section class="sub-sec" id="try">
<div class="wrap sub-grid">
<header class="sub-head">
<span class="sub-n">2</span>
<h2 data-i18n="site.submit.try.tryIt">Try it</h2>
<p data-i18n="site.submit.try.theStoreIsOptionalA">The Store is optional. A file that works is already shareable.</p>
</header>
<div class="sub-body">
<ul class="sub-list">
<li data-i18n="site.submit.try.dropTheFileOnThe"><b>Drop the file on the wayBetter window.</b> You get the same install screen a Store item gets: name, author, version, SHA-256, permissions, the code.</li>
<li data-i18n="site.submit.try.turnOnDeveloperModeSettings"><b>Turn on developer mode</b> (Settings &rarr; Developer) while you work: a saved plugin, theme file or <code>custom.css</code> reloads in every tab at once.</li>
<li data-i18n="site.submit.try.shareItWithoutTheStore"><b>Share it without the Store.</b> Send the file, or a <code>waybetter://install</code> link to an https URL on waybetter.fyi, cdn1.crt.fyi or a raw file on %FORGE_HOST%. It is never installed without the install screen.</li>
</ul>
</div>
</div>
</section>
<section class="sub-sec" id="folder">
<div class="wrap sub-grid">
<header class="sub-head">
<span class="sub-n">3</span>
<h2 data-i18n="site.submit.folder.addOneFolder">Add one folder</h2>
<p data-i18n="site.submit.folder.everyStoreItemIsA">Every Store item is a folder under <code>store/&lt;app&gt;/</code> in the repository. The folder name is the item&rsquo;s id and its address here: <code>/store/plugins/&lt;id&gt;/</code>.</p>
</header>
<div class="sub-body">
<div class="code">
<div class="code-bar"><span class="dots" aria-hidden="true"><i></i><i></i><i></i></span><span data-i18n="site.submit.folder.storeXWaybetterForX" class="file">store/x/ &mdash; wayBetter for X&rsquo;s items</span></div>
<pre tabindex="0"><code>store/x/plugins/<span class="tk-f">hide-trends</span>/
hide-trends.plugin.js <span class="tk-c">the plugin, exactly as it installs</span>
item.json <span class="tk-c">{ "category": "focus", "tags": ["trends", "sidebar"] }</span>
preview.svg <span class="tk-c">optional, 640×400 (.svg or .png); drawn for you without one</span>
store/x/themes/<span class="tk-f">midnight-mint</span>/
theme.json <span class="tk-c">the theme, as Appearance → Export writes it</span>
item.json <span class="tk-c">{ "version": "1.0.0" }</span>
preview.svg <span class="tk-c">optional; drawn from the theme without one</span></code></pre>
</div>
<dl class="sub-defs">
<div><dt><code>category</code></dt><dd data-i18n="site.submit.folder.pluginsOrThemesOrTaken">Plugins: <code>reading</code>, <code>writing</code>, <code>filters</code>, <code>focus</code> or <code>tools</code>. Themes: <code>dark</code> or <code>light</code>, taken from the theme&rsquo;s <code>mode</code> when you leave it out.</dd></div>
<div><dt><code>tags</code></dt><dd data-i18n="site.submit.folder.upTo12WordsPeople">Up to 12 words people would search for.</dd></div>
<div><dt><code>version</code></dt><dd data-i18n="site.submit.folder.themesOnlyAPlugins">Themes only (a plugin&rsquo;s is in its header). Raise it when you change the file.</dd></div>
<div><dt data-i18n="site.submit.folder.description">Description</dt><dd data-i18n="site.submit.folder.oneOrTwoSentencesThat">One or two sentences that say what it does, 50 characters or more. The first 160 become the search-engine description of its page.</dd></div>
</dl>
<p data-i18n="site.submit.folder.thatIsTheWholeSubmission" class="sub-fine">That is the whole submission. The page, the card, the search entry, the link preview, the structured data and the sitemap entry are generated from the signed catalog when the site builds. <a href="%REPO_FILE:store/README.md%">store/README.md</a> has the full rules.</p>
</div>
</div>
</section>
<section class="sub-sec" id="checks">
<div class="wrap sub-grid">
<header class="sub-head">
<span class="sub-n">4</span>
<h2 data-i18n="site.submit.checks.runTheChecks">Run the checks</h2>
<p data-i18n="site.submit.checks.thereIsNoHostedCi">There is no hosted CI. The checks are commands you run before you send, and a maintainer runs again before merging.</p>
</header>
<div class="sub-body">
<div class="code">
<div class="code-bar"><span class="dots" aria-hidden="true"><i></i><i></i><i></i></span><span data-i18n="site.submit.checks.inYourClone" class="file">in your clone</span><button data-i18n="site.common.copy" class="copy-btn" type="button" data-copy-from="sub-checks">Copy</button></div>
<pre id="sub-checks" tabindex="0"><code>git submodule update --init
bun install --frozen-lockfile
bun run check:store <span class="tk-c"># your folder, against the Store's rules</span>
bun run test <span class="tk-c"># everything else, including the site's SEO test</span></code></pre>
</div>
<ul class="sub-checks">
<li><b data-i18n="site.submit.checks.idsAndNames">Ids and names</b><span data-i18n="site.submit.checks.folderIdUpTo40">folder id (<code>a-z 0-9 -</code>, up to 40), <code>name.plugin.js</code>, no duplicates in an app</span></li>
<li><b data-i18n="site.submit.checks.size">Size</b><span data-i18n="site.submit.checks.itemFilesUpTo2">item files up to 2 MiB, previews up to 300 KB at 16:10</span></li>
<li><b data-i18n="site.submit.checks.theHeader">The header</b><span data-i18n="site.submit.checks.aDescriptionAndALine"><code>@name</code>, <code>@version</code>, a description, and a <code>@permissions</code> line that names <code>network</code> or <code>network:none</code></span></li>
<li><b data-i18n="site.submit.checks.honestPermissions">Honest permissions</b><span data-i18n="site.submit.checks.isRefusedWhenTheCode"><code>network:none</code> is refused when the code calls <code>fetch</code>, <code>XMLHttpRequest</code>, <code>WebSocket</code>, <code>EventSource</code> or <code>sendBeacon</code>; gated API calls must be declared</span></li>
<li><b data-i18n="site.submit.checks.noRemoteCode">No remote code</b><span data-i18n="site.submit.checks.noOrNoScriptsOr">no <code>eval</code>, <code>new Function</code> or <code>import()</code>; no scripts or outside files in a preview; no <code>@import</code> or outside URLs in a theme&rsquo;s CSS</span></li>
<li><b data-i18n="site.submit.checks.theCatalog">The catalog</b><span data-i18n="site.submit.checks.theItemsBuildIntoA">the items build into a catalog the apps accept (the framework&rsquo;s own rules), and the published catalog still matches its signature and hashes</span></li>
<li><b data-i18n="site.submit.checks.searchAndSharing">Search and sharing</b><span data-i18n="site.submit.checks.pageTitleUpTo60">page title up to 60 characters, description 50 to 160, both unique</span></li>
</ul>
</div>
</div>
</section>
<section class="sub-sec" id="send">
<div class="wrap sub-grid">
<header class="sub-head">
<span class="sub-n">5</span>
<h2 data-i18n="site.submit.send.sendIt">Send it</h2>
<p data-i18n="site.submit.send.twoWaysInBothNeed">Two ways in. Both need a free <a href="%REPO_SIGNUP%">%FORGE_HOST% account</a>: anyone can sign up, and a maintainer approves each new account by hand.</p>
</header>
<div class="sub-body">
<div class="sub-two">
<article class="sub-card sub-card-hi">
<h3 data-i18n="site.submit.send.pullRequest">Pull request</h3>
<ol>
<li data-i18n="site.submit.send.forkTheRepositoryOn">Fork the repository on %FORGE_HOST%.</li>
<li data-i18n="site.submit.send.addYourFolderOnA">Add your folder on a branch and run the checks.</li>
<li data-i18n="site.submit.send.openThePullRequestThe">Open the pull request. The template asks what it does and what it uses.</li>
</ol>
<div class="ctas">
<a data-i18n="site.submit.send.fork" class="btn btn-solid btn-sm" href="%REPO_FORK%">Fork</a>
<a data-i18n="site.submit.send.pullRequests" class="btn btn-ghost btn-sm" href="%REPO_PULLS%">Pull requests</a>
</div>
</article>
<article class="sub-card">
<h3 data-i18n="site.submit.send.noGitOpenAnIssue">No git? Open an issue</h3>
<ol>
<li data-i18n="site.submit.send.chooseTheThemeOrPlugin">Choose the Theme or Plugin template.</li>
<li data-i18n="site.submit.send.attachTheFileAndA">Attach the file (and a preview, if you have one).</li>
<li data-i18n="site.submit.send.aMaintainerAddsTheFolder">A maintainer adds the folder for you.</li>
</ol>
<div class="ctas">
<a data-i18n="site.submit.send.newIssue" class="btn btn-ghost btn-sm" href="%REPO_ISSUE%">New issue</a>
</div>
</article>
</div>
<div class="st-honest sub-sign">
<h3 data-i18n="site.submit.send.thenAMaintainerSigns">Then a maintainer signs</h3>
<p data-i18n="site.submit.send.theStoresSigningKey">The Store&rsquo;s signing key is offline by design, so merging is not publishing. After the merge a maintainer reads the code, runs <code>waybetter store build</code> and <code>waybetter store sign</code> on the machine that holds the key, and pushes. The site rebuilds, your item gets its page, and every app sees it the next time it reads the signed catalog.</p>
<p data-i18n="site.submit.send.anythingInTheSignedCatalog">Anything in the signed catalog is vouched for by that key, which is why a person reads every plugin before it is signed.</p>
</div>
</div>
</div>
</section>
<section class="sub-sec" id="format">
<div class="wrap sub-grid">
<header class="sub-head">
<span class="sub-n sub-n-alt">{ }</span>
<h2 data-i18n="site.submit.format.theCatalogFormat">The catalog format</h2>
<p data-i18n="site.submit.format.oneFileForEveryWaybetter">One file for every wayBetter app. You never edit it by hand: it is built from the folders.</p>
</header>
<div class="sub-body">
<div class="code">
<div class="code-bar"><span class="dots" aria-hidden="true"><i></i><i></i><i></i></span><span data-i18n="site.submit.format.catalogJsonFormat2" class="file">catalog.json (format 2)</span><a data-i18n="site.common.open" class="copy-btn it-raw" href="../catalog.json">Open</a></div>
<pre tabindex="0"><code>{ <span class="tk-p">"format"</span>: <span class="tk-n">2</span>, <span class="tk-p">"name"</span>: <span class="tk-s">"wayBetter Store"</span>, <span class="tk-p">"generatedAt"</span>: <span class="tk-s">"<!-- store:generated -->T…Z"</span>, <span class="tk-p">"items"</span>: [
{ <span class="tk-p">"apps"</span>: [<span class="tk-s">"x"</span>], <span class="tk-p">"kind"</span>: <span class="tk-s">"plugin"</span>, <span class="tk-p">"id"</span>: <span class="tk-s">"thread-reader"</span>, <span class="tk-p">"name"</span>: <span class="tk-s">"Thread Reader"</span>,
<span class="tk-p">"author"</span>: <span class="tk-s">"wayBetter"</span>, <span class="tk-p">"version"</span>: <span class="tk-s">"1.2.0"</span>, <span class="tk-p">"description"</span>: <span class="tk-s">"…"</span>,
<span class="tk-p">"category"</span>: <span class="tk-s">"reading"</span>, <span class="tk-p">"tags"</span>: [<span class="tk-s">"threads"</span>],
<span class="tk-p">"url"</span>: <span class="tk-s">"https://waybetter.fyi/store/plugins/bt-thread-reader.plugin.js"</span>,
<span class="tk-p">"file"</span>: <span class="tk-s">"bt-thread-reader.plugin.js"</span>, <span class="tk-p">"sha256"</span>: <span class="tk-s">"&lt;64 hex&gt;"</span>, <span class="tk-p">"size"</span>: <span class="tk-n">10961</span>,
<span class="tk-p">"preview"</span>: <span class="tk-s">"https://waybetter.fyi/store/previews/plugin-thread-reader.svg"</span>,
<span class="tk-p">"permissions"</span>: [<span class="tk-s">"ui"</span>, <span class="tk-s">"clipboard"</span>, <span class="tk-s">"commands"</span>, <span class="tk-s">"network:none"</span>] }
] }</code></pre>
</div>
<ul class="sub-list">
<li data-i18n="site.submit.format.signedCatalogJsonSigIs"><b>Signed.</b> <a href="../catalog.json.sig">catalog.json.sig</a> is an Ed25519 signature over the exact bytes of <a href="../catalog.json">catalog.json</a>. Apps verify it with the public key compiled into them, then check every download&rsquo;s size and SHA-256.</li>
<li data-i18n="site.submit.format.strictUnknownFieldsMoreThan"><b>Strict.</b> Unknown fields, more than 500 items or a file over 1 MiB refuse the whole catalog. <a href="%REPO_SCHEMA%">store-catalog.schema.json</a> is the JSON Schema; <a href="%REPO_STORE_DOC%">STORE.md</a> in the framework has every rule.</li>
<li data-i18n="site.submit.format.sharedEachItemNamesIts"><b>Shared.</b> Each item names its <code>apps</code>. An app verifies the whole file and keeps only its own items.</li>
</ul>
</div>
</div>
</section>
<section class="sub-sec" id="languages">
<div class="wrap sub-grid">
<header class="sub-head">
<span class="sub-n sub-n-alt">Aa</span>
<h2 data-i18n="site.common.languages">Languages</h2>
<p data-i18n="site.submit.languages.notStoreItemsAll51">Not Store items: all 51 languages are built into the app and loaded on demand.</p>
</header>
<div class="sub-body">
<ul class="sub-list">
<li data-i18n="site.submit.languages.fixOrFinishATranslation"><b>Fix or finish a translation</b> in <code>apps/desktop/locales/&lt;tag&gt;.json</code>, by pull request or by an issue with the changed file attached.</li>
<li data-i18n="site.submit.languages.checkItWithKeysPlaceholders"><b>Check it</b> with <code>bun run i18n:check</code>: keys, placeholders, plural forms, no markup. It must print <code>0 error(s)</code>.</li>
<li data-i18n="site.submit.languages.itShipsWithTheNext"><b>It ships with the next app update</b>, not through the Store. <a href="%REPO_FILE:docs/TRANSLATING.md%">TRANSLATING.md</a> has the message syntax and how to see your words in the app.</li>
</ul>
</div>
</div>
</section>
<section class="section section-tight st-publish">
<div class="wrap">
<div class="next-cta">
<div>
<h3 data-i18n="site.submit.page.seeWhatIsAlreadyThere">See what is <em class="grad">already there.</em></h3>
<p data-i18n="site.submit.page.everyItemInTheStore">Every item in the Store shows its code. The bundled plugins are the best examples of how to use the API well.</p>
</div>
<div class="ctas">
<a data-i18n="site.submit.page.browsePlugins" class="btn btn-solid" href="../?kind=plugin">Browse plugins</a>
<a data-i18n="site.submit.page.browseThemes" class="btn btn-ghost" href="../?kind=theme">Browse themes</a>
</div>
</div>
</div>
</section>
</main>
<footer class="foot">
<div class="wrap foot-row">
<a data-i18n-aria-label="site.common.waybetterHome" class="mark mark-sm" href="../../" aria-label="wayBetter home">
<img class="wb" src="/brand/wb-mark-simple.svg" alt="" width="43" height="24" loading="lazy" />
<span class="mark-name" translate="no">wayBetter</span>
</a>
<p data-i18n="site.common.madeByCrtkafaGpl3" class="foot-legal">
Made by <a href="https://x.com/CRTkafa">@CRTkafa</a> &middot; GPL-3.0-or-later &middot;
Not affiliated with the apps it extends.
</p>
<nav data-i18n-aria-label="site.common.footer" class="foot-nav" aria-label="Footer">
<a data-i18n="site.common.store" href="../">Store</a>
<a data-i18n="site.common.publishYourOwn" href="./">Publish your own</a>
<a href="../../x/">wayBetter for X</a>
<a href="../catalog.json">catalog.json</a>
<a data-i18n="site.common.source" data-i18n-aria-label="site.common.sourceOn" class="src-link" href="%REPO_URL%" aria-label="Source on %FORGE_HOST%"><img class="src-logo" src="/git-crt-logo.svg" alt="" width="16" height="16" />Source</a>
</nav>
<!-- lang:foot -->
</div>
</footer>
<script type="module" src="../../src/submit.ts"></script>
</body>
</html>