diff --git a/.github/ISSUE_TEMPLATE/showcase.yml b/.github/ISSUE_TEMPLATE/showcase.yml new file mode 100644 index 0000000..5ccf5b6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/showcase.yml @@ -0,0 +1,44 @@ +name: Add a site to the showcase +description: Have you adopted solid-route-progress? Tell us where. +title: 'Showcase: ' +labels: [showcase] +body: + - type: markdown + attributes: + value: | + Prefer to send it yourself? [Add a file on GitHub](https://github.com/kecan0406/solid-route-progress/new/main?filename=www%2Fshowcase%2Fmy-site.json&value=%7B%0A++%2F%2F+Delete+the+%2F%2F+lines+before+you+propose+the+change.%0A++%2F%2F+The+site%27s+domain+or+name.%0A++%22name%22%3A+%22%22%2C%0A++%2F%2F+Starting+with+https%3A%2F%2F%0A++%22url%22%3A+%22https%3A%2F%2F%22%2C%0A++%2F%2F+One+line+on+what+the+site+is.%0A++%22description%22%3A+%22%22%0A++%2F%2F+Optional+logo%3A+drop+the+file+in+a+comment+on+your+pull+request%2C+and+we+add+these.%0A++%2F%2F+%22image%22%3A+%22%2Fimg%2Fshowcase%2Fyour-site.svg%22%2C%0A++%2F%2F+%22credit%22%3A+%7B%0A++%2F%2F+++%22text%22%3A+%22Logo+by+Jane+Doe%22%2C%0A++%2F%2F+++%22url%22%3A+%22https%3A%2F%2Fexample.com%2Fjane%22%2C%0A++%2F%2F+++%22license%22%3A+%22CC+BY+4.0%22%2C%0A++%2F%2F+++%22licenseUrl%22%3A+%22https%3A%2F%2Fcreativecommons.org%2Flicenses%2Fby%2F4.0%2F%22%0A++%2F%2F+%7D%0A%7D%0A) and GitHub opens the pull request for you. + - type: input + id: name + attributes: + label: Name + description: The site's domain or name. + placeholder: example.com + validations: + required: true + - type: input + id: url + attributes: + label: URL + placeholder: https://example.com/ + validations: + required: true + - type: input + id: description + attributes: + label: Description + description: One line on what the site is. + placeholder: A social network for hackers. + validations: + required: true + - type: textarea + id: mark + attributes: + label: Mark + description: Optional. Drag your logo here (SVG or PNG), and say where it comes from and under which license. + - type: checkboxes + id: live + attributes: + label: Confirm + options: + - label: The site runs solid-route-progress in production. + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 56fe4f0..3da6e14 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,5 @@ + ## What changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c7db27e..80a7e81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,8 @@ # Contributing Thanks for taking the time to contribute. This guide covers what you need to set up the project and -what a pull request should include. +what a pull request should include. To add your site to the showcase, skip to +[Add a site to the showcase](#add-a-site-to-the-showcase): it takes one small file and no setup. ## Prerequisites @@ -73,6 +74,35 @@ page with `pnpm build:www`. - If your change affects the published package, run `pnpm changeset` and describe it for the release notes. Docs-only and tooling-only changes do not need one. +## Add a site to the showcase + +The [showcase](https://solid-route-progress.vercel.app/showcase) lists sites that run +solid-route-progress in production. Each site is one JSON file in `www/showcase/`: + +```json +{ + "name": "hackers.pub", + "url": "https://hackers.pub/", + "description": "ActivityPub-enabled social network for hackers." +} +``` + +`image` is optional: a mark shown left of the name, as a file in `www/public/img/showcase/` +referenced as `/img/showcase/`. You do not have to add it yourself. Drop your logo (SVG or PNG) +in a comment on your pull request, or in the showcase issue form, and say where it comes from and +under which license. We commit the file, note the license in the `README.md` next to it, and add a +`credit` (the maker and the license, each with a link) that the card shows under the site. + +The quickest way needs no clone: +[add the file on GitHub](https://github.com/kecan0406/solid-route-progress/new/main?filename=www%2Fshowcase%2Fmy-site.json&value=%7B%0A++%2F%2F+Delete+the+%2F%2F+lines+before+you+propose+the+change.%0A++%2F%2F+The+site%27s+domain+or+name.%0A++%22name%22%3A+%22%22%2C%0A++%2F%2F+Starting+with+https%3A%2F%2F%0A++%22url%22%3A+%22https%3A%2F%2F%22%2C%0A++%2F%2F+One+line+on+what+the+site+is.%0A++%22description%22%3A+%22%22%0A++%2F%2F+Optional+logo%3A+drop+the+file+in+a+comment+on+your+pull+request%2C+and+we+add+these.%0A++%2F%2F+%22image%22%3A+%22%2Fimg%2Fshowcase%2Fyour-site.svg%22%2C%0A++%2F%2F+%22credit%22%3A+%7B%0A++%2F%2F+++%22text%22%3A+%22Logo+by+Jane+Doe%22%2C%0A++%2F%2F+++%22url%22%3A+%22https%3A%2F%2Fexample.com%2Fjane%22%2C%0A++%2F%2F+++%22license%22%3A+%22CC+BY+4.0%22%2C%0A++%2F%2F+++%22licenseUrl%22%3A+%22https%3A%2F%2Fcreativecommons.org%2Flicenses%2Fby%2F4.0%2F%22%0A++%2F%2F+%7D%0A%7D%0A), +fill in the three fields, and propose the change. GitHub forks the repository and opens the pull +request for you. If you would rather fill in a form, use the +[showcase issue](https://github.com/kecan0406/solid-route-progress/issues/new?template=showcase.yml) +instead. + +CI builds the site, and the build fails with the file and the field if an entry does not fit. A +showcase entry needs no tests, docs, or changeset. + ## Releasing Releases are automated with changesets and npm trusted publishing, so nobody publishes from a diff --git a/www/public/img/showcase/README.md b/www/public/img/showcase/README.md new file mode 100644 index 0000000..3d8e99d --- /dev/null +++ b/www/public/img/showcase/README.md @@ -0,0 +1,13 @@ +# Showcase images + +Each file is a site's mark, shown left of its name on the showcase card (`image` in +`www/showcase/.json`). Every image keeps its own license, so note it here. + +## hackers-pub.svg + +The cat motif from [hackers-pub/visual-identity](https://github.com/hackers-pub/visual-identity), +`exports/pubnyan-normal-border.svg`, unmodified. + +Visual Identity of Hackers' Pub © 2025 by Bak Eunji, licensed under +[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). The credit is on the card, from +`credit` in `www/showcase/hackers-pub.json`. The license grants no trademark rights. diff --git a/www/public/img/showcase/hackers-pub.svg b/www/public/img/showcase/hackers-pub.svg new file mode 100644 index 0000000..8806c7b --- /dev/null +++ b/www/public/img/showcase/hackers-pub.svg @@ -0,0 +1,6443 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/showcase.ts b/www/showcase.ts new file mode 100644 index 0000000..8a6f61c --- /dev/null +++ b/www/showcase.ts @@ -0,0 +1,107 @@ +import type { Plugin } from 'vite' +import type { ShowcaseEntry } from './src/showcase.ts' + +const dir = new URL('./showcase/', import.meta.url) +const pathOf = (url: URL) => decodeURIComponent(url.pathname) +const publicDir = new URL('./public/', import.meta.url) +const FIELDS = ['name', 'url', 'description', 'image', 'credit'] +const CREDIT_FIELDS = ['text', 'url', 'license', 'licenseUrl'] +/** Where a card's picture lives: a file in `public/img/showcase/`, referenced by its URL. */ +const IMAGE = /^\/img\/showcase\/[\w.-]+\.(?:svg|png|webp|jpe?g|avif)$/ + +const isUrl = (value: unknown) => { + try { + return typeof value === 'string' && new URL(value).protocol === 'https:' + } catch { + return false + } +} + +/** + * One `showcase/.json`, checked. An entry that does not fit fails the build with the file + * and the field, so a contributor who edited it on github.com learns what to change from CI. + */ +export function parseEntry(file: string, text: string): ShowcaseEntry { + const fail = (why: string) => new Error(`showcase/${file}: ${why}`) + let data: unknown + try { + data = JSON.parse(text) + } catch (error) { + // the template GitHub opens carries `//` comments that JSON cannot hold + if (/^\s*\/\//m.test(text)) throw fail('delete the // comment lines') + throw fail(`not valid JSON (${(error as Error).message})`) + } + if (typeof data !== 'object' || data === null || Array.isArray(data)) { + throw fail('expected one JSON object') + } + const entry = data as Record + for (const key of Object.keys(entry)) { + if (!FIELDS.includes(key)) + throw fail(`unknown field "${key}" (the fields are ${FIELDS.join(', ')})`) + } + for (const key of ['name', 'description']) { + const value = entry[key] + if (typeof value !== 'string' || !value.trim()) + throw fail(`"${key}" must be a non-empty string`) + } + if (!isUrl(entry.url)) throw fail('"url" must be an https:// URL') + if (entry.image !== undefined && !(typeof entry.image === 'string' && IMAGE.test(entry.image))) { + throw fail('"image" must be a file in www/public/img/showcase/, like "/img/showcase/site.svg"') + } + if (entry.credit !== undefined) { + if (entry.image === undefined) throw fail('"credit" credits the "image", so it needs one') + const credit = entry.credit as Record | null + if (typeof credit !== 'object' || credit === null || Array.isArray(credit)) { + throw fail('"credit" must be an object') + } + for (const key of Object.keys(credit)) { + if (!CREDIT_FIELDS.includes(key)) { + throw fail(`unknown "credit" field "${key}" (the fields are ${CREDIT_FIELDS.join(', ')})`) + } + } + for (const key of ['text', 'license']) { + const value = credit[key] + if (typeof value !== 'string' || !value.trim()) { + throw fail(`"credit.${key}" must be a non-empty string`) + } + } + for (const key of ['url', 'licenseUrl']) { + if (!isUrl(credit[key])) throw fail(`"credit.${key}" must be an https:// URL`) + } + } + return entry as unknown as ShowcaseEntry +} + +/** + * `virtual:showcase`: the sites in `showcase/*.json`, one file per site, ordered by file name. + * A new site is a new file, so two pull requests never touch the same lines. + */ +export function showcase(): Plugin { + const id = 'virtual:showcase' + + return { + name: 'showcase', + resolveId: (source) => (source === id ? `\0${id}` : undefined), + async load(loaded) { + if (loaded !== `\0${id}`) return + this.addWatchFile(pathOf(dir)) + const files = (await this.fs.readdir(pathOf(dir))) + .filter((file) => file.endsWith('.json')) + .sort() + const entries = await Promise.all( + files.map(async (file) => { + const path = pathOf(new URL(file, dir)) + this.addWatchFile(path) + const entry = parseEntry(file, await this.fs.readFile(path, { encoding: 'utf8' })) + if (entry.image) { + await this.fs.stat(pathOf(new URL(`.${entry.image}`, publicDir))).catch(() => { + throw new Error(`showcase/${file}: "image" ${entry.image} is not in www/public`) + }) + } + return entry + }), + ) + return `export default ${JSON.stringify(entries)}` + }, + } +} diff --git a/www/showcase/hackers-pub.json b/www/showcase/hackers-pub.json new file mode 100644 index 0000000..65ad16c --- /dev/null +++ b/www/showcase/hackers-pub.json @@ -0,0 +1,12 @@ +{ + "name": "hackers.pub", + "url": "https://hackers.pub/", + "description": "ActivityPub-enabled social network for hackers.", + "image": "/img/showcase/hackers-pub.svg", + "credit": { + "text": "Hackers' Pub cat by Bak Eunji", + "url": "https://github.com/hackers-pub/visual-identity", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0/" + } +} diff --git a/www/src/app.css b/www/src/app.css index d57cace..33227fc 100644 --- a/www/src/app.css +++ b/www/src/app.css @@ -119,6 +119,47 @@ } } +/* /showcase previews: a bar drifting like the library's — fast out of the gate, a long crawl, a + hop to the end, a fade. Plain CSS on `translate`; nothing here is driven by the package. */ +.showcase-bar::before { + content: ''; + position: absolute; + inset: 0; + background: var(--primary); + translate: -100% 0; + animation: showcase-drift 7s infinite; +} +@media (prefers-reduced-motion: reduce) { + .showcase-bar::before { + animation: none; + translate: -38% 0; + } +} +@keyframes showcase-drift { + 0% { + translate: -100% 0; + opacity: 1; + animation-timing-function: cubic-bezier(0.08, 0.6, 0.2, 1); + } + 78% { + translate: -9% 0; + opacity: 1; + animation-timing-function: ease-in; + } + 86% { + translate: 0 0; + opacity: 1; + } + 93% { + translate: 0 0; + opacity: 0; + } + 100% { + translate: -100% 0; + opacity: 0; + } +} + /* `--destructive` is sized for a filled button; a 3px bar at its dark value (L 0.396) is too dim to read as a failure, so the dark half is lifted. */ .ex-error[data-error] { diff --git a/www/src/components/CodeCard.tsx b/www/src/components/CodeCard.tsx index a64d4f7..64c7274 100644 --- a/www/src/components/CodeCard.tsx +++ b/www/src/components/CodeCard.tsx @@ -1,6 +1,7 @@ import { createSignal, Show } from 'solid-js' import { copyText } from '~/clipboard' -import { Highlight, plain } from '~/highlight' +import { CodeFile } from '~/components/CodeFile' +import { plain } from '~/highlight' import { usageText } from '~/playground' /** The code the playground produces. Every control lands in it, so one snippet is enough. */ @@ -14,9 +15,12 @@ export function CodeCard() { } return ( -
-
- app.tsx + -
-
-        
-      
-
+ } + /> ) } diff --git a/www/src/components/CodeFile.tsx b/www/src/components/CodeFile.tsx new file mode 100644 index 0000000..14e13ff --- /dev/null +++ b/www/src/components/CodeFile.tsx @@ -0,0 +1,26 @@ +import type { JSX } from 'solid-js' +import { Highlight, type Lang } from '~/highlight' + +/** + * A code card: the file's name in a header (with an `action` beside it), then the highlighted + * source. The landing's `CodeCard` and the showcase's entry sample share it. + */ +export function CodeFile(props: { + file: string + text: string + lang: Lang + action?: JSX.Element + class?: string +}) { + return ( +
+
+ {props.file} + {props.action} +
+
+        
+      
+
+ ) +} diff --git a/www/src/components/Footer.tsx b/www/src/components/Footer.tsx new file mode 100644 index 0000000..767a871 --- /dev/null +++ b/www/src/components/Footer.tsx @@ -0,0 +1,26 @@ +import { A } from '@solidjs/router' +import { REPO } from '~/links' + +const LINK = 'underline underline-offset-4 hover:text-foreground' + +/** The landing page's and /showcase's footer: license, the site's pages, and the version. */ +export function Footer() { + return ( + + ) +} diff --git a/www/src/components/Header.tsx b/www/src/components/Header.tsx index b48b098..398b8be 100644 --- a/www/src/components/Header.tsx +++ b/www/src/components/Header.tsx @@ -2,6 +2,9 @@ import { A } from '@solidjs/router' import { createSignal, onMount, Show } from 'solid-js' import { REPO } from '~/links' +const LINK = + 'rounded-lg px-3 py-2.5 text-[14px] font-medium text-muted-foreground hover:text-foreground' + const effectiveDark = () => document.documentElement.dataset.theme ? document.documentElement.dataset.theme === 'dark' @@ -32,17 +35,13 @@ export function Header() {