diff --git a/home.tsx b/home.tsx index 11e10f3..cf5bb8a 100644 --- a/home.tsx +++ b/home.tsx @@ -1,95 +1,7 @@ -import { renderToString } from "preact-render-to-string"; -import { PluginData, PluginsData, readInfoFile } from "./readInfoFile.js"; +import { renderHomeHtml } from "./homeView.jsx"; +import { readInfoFile } from "./readInfoFile.js"; export async function renderHome(origin: string, ctx?: ExecutionContext) { - const content = await renderContent(origin, ctx); - return ` - - - - - - Plugins - dprint - - - - - ${content} - - -`; -} - -async function renderContent(origin: string, ctx?: ExecutionContext) { const pluginsData = await readInfoFile(origin, ctx); - const section = ( -
-

Plugins

- {renderPlugins(pluginsData)} -

- Helpful commands: -

-

-

- Documentation -

-
- ); - return renderToString(section); -} - -function renderPlugins(data: PluginsData) { - return ( -
-
-
Name
-
Latest URL
-
Downloads (30d)
-
-
- {data.latest.map((plugin) => renderPlugin(plugin))} -
- ); -} - -function renderPlugin(plugin: PluginData) { - return ( -
-
{plugin.name}
-
{plugin.url}
-
{plugin.downloadCount.allVersions?.toLocaleString("en-US")}
-
- -
-
- ); + return renderHomeHtml(pluginsData); } diff --git a/homeView.tsx b/homeView.tsx new file mode 100644 index 0000000..7f2ca52 --- /dev/null +++ b/homeView.tsx @@ -0,0 +1,178 @@ +import { renderToString } from "preact-render-to-string"; +import type { PluginData, PluginsData } from "./readInfoFile.js"; + +// renders the full home page document for the given plugins data. kept free of +// any server/data imports so it can be rendered in isolation (e.g. previews). +export function renderHomeHtml(pluginsData: PluginsData) { + const content = renderToString(renderPage(pluginsData)); + return ` + + + + + + + Plugins - dprint + + + + + + + + ${content} + + +`; +} + +function renderPage(pluginsData: PluginsData) { + return ( +
+
+ + dprint plugin docs +
+ {renderPlugins(pluginsData)} + + {renderCommands()} +
+ ); +} + +// builds the lowercased string the search filters against: name, url, version, +// description, config key, and every file extension / file name / exec command +// the plugin handles. +function pluginSearchText(plugin: PluginData) { + const parts: (string | undefined)[] = [ + plugin.name, + plugin.url, + plugin.version, + plugin.description, + plugin.configKey, + ...(plugin.keywords ?? []), + ...(plugin.fileExtensions ?? []), + ...(plugin.fileNames ?? []), + ]; + for (const item of plugin.configItems ?? []) { + parts.push(...(item.match?.fileExtensions ?? [])); + for (const command of item.config?.commands ?? []) { + parts.push(command.command, ...(command.exts ?? [])); + } + } + return parts.filter(Boolean).join(" ").toLowerCase(); +} + +function renderCommands() { + const commands: { cmd: string; desc: string }[] = [ + { cmd: "dprint config update", desc: "Automatically updates the plugins in a config file." }, + { cmd: "dprint add", desc: "Adds one of these plugins via a multi-select prompt." }, + { cmd: "dprint add ", desc: "Adds a plugin by name." }, + { cmd: "dprint add /", desc: "Adds a plugin by GitHub repo." }, + ]; + return ( +
+

Helpful commands

+
    + {commands.map((c) => ( +
  • + {c.cmd} — {c.desc} +
  • + ))} +
+
+ ); +} + +function renderPlugins(data: PluginsData) { + return ( +
+
+
Plugin
+
Latest URL
+
Downloads (30d)
+
+
+ {data.latest.map((plugin) => renderPlugin(plugin))} +
+ ); +} + +function renderPlugin(plugin: PluginData) { + return ( +
+
+ + {plugin.name} + {plugin.version ? {plugin.version} : null} +
+
+ {plugin.url} +
+
+ Downloads (30d) + {plugin.downloadCount.allVersions?.toLocaleString("en-US")} +
+
+ +
+
+ ); +} diff --git a/info.json b/info.json index b30e2be..644a3f5 100644 --- a/info.json +++ b/info.json @@ -7,6 +7,13 @@ "description": "TypeScript/JavaScript code formatter.", "selected": true, "configKey": "typescript", + "keywords": [ + "javascript", + "typescript", + "node", + "deno", + "ecmascript" + ], "fileExtensions": [ "ts", "tsx", @@ -26,6 +33,10 @@ "description": "JSON/JSONC code formatter.", "selected": true, "configKey": "json", + "keywords": [ + "json", + "jsonc" + ], "fileExtensions": [ "json", "jsonc" @@ -39,6 +50,11 @@ "description": "Markdown code formatter.", "selected": true, "configKey": "markdown", + "keywords": [ + "markdown", + "commonmark", + "gfm" + ], "fileExtensions": [ "md", "mkd", @@ -54,6 +70,10 @@ "description": "TOML code formatter.", "selected": true, "configKey": "toml", + "keywords": [ + "toml", + "cargo" + ], "fileExtensions": [ "toml" ], @@ -64,6 +84,11 @@ "description": "Dockerfile code formatter.", "selected": false, "configKey": "dockerfile", + "keywords": [ + "docker", + "dockerfile", + "container" + ], "fileExtensions": [ "dockerfile" ], @@ -77,6 +102,13 @@ "description": "Biome (JS/TS/JSON) wrapper plugin.", "selected": false, "configKey": "biome", + "keywords": [ + "biome", + "rome", + "javascript", + "typescript", + "json" + ], "fileExtensions": [ "ts", "tsx", @@ -98,6 +130,12 @@ "description": "Oxc (JS/TS) wrapper plugin.", "selected": false, "configKey": "oxc", + "keywords": [ + "oxc", + "oxlint", + "javascript", + "typescript" + ], "fileExtensions": [ "ts", "tsx", @@ -117,6 +155,11 @@ "description": "Mago (PHP) wrapper plugin.", "selected": false, "configKey": "mago", + "keywords": [ + "php", + "mago", + "laravel" + ], "fileExtensions": [ "php" ], @@ -127,6 +170,12 @@ "description": "Ruff (Python) wrapper plugin.", "selected": false, "configKey": "ruff", + "keywords": [ + "python", + "ruff", + "black", + "flake8" + ], "fileExtensions": [ "py", "pyi" @@ -138,16 +187,45 @@ "description": "Jupyter notebook code block formatter.", "selected": false, "configKey": "jupyter", + "keywords": [ + "jupyter", + "notebook", + "ipython", + "python" + ], "fileExtensions": [ "ipynb" ], "configExcludes": [] }, + { + "name": "jakebailey/gofumpt", + "description": "Gofumpt (Go) wrapper plugin.", + "selected": false, + "configKey": "gofumpt", + "keywords": [ + "go", + "golang", + "gofumpt", + "gofmt" + ], + "fileExtensions": [ + "go" + ], + "configExcludes": [] + }, { "name": "g-plane/malva", "description": "CSS, SCSS, Sass and Less formatter.", "selected": true, "configKey": "malva", + "keywords": [ + "css", + "scss", + "sass", + "less", + "stylesheet" + ], "fileExtensions": [ "css", "scss", @@ -163,6 +241,22 @@ "description": "HTML, Vue, Svelte, Astro, Angular, Jinja, Twig, Nunjucks, and Vento formatter.", "selected": true, "configKey": "markup", + "keywords": [ + "html", + "vue", + "svelte", + "astro", + "angular", + "jinja", + "twig", + "nunjucks", + "vento", + "handlebars", + "mustache", + "xml", + "markup", + "template" + ], "fileExtensions": [ "html", "vue", @@ -192,6 +286,12 @@ "description": "YAML formatter.", "selected": true, "configKey": "yaml", + "keywords": [ + "yaml", + "kubernetes", + "k8s", + "config" + ], "fileExtensions": [ "yaml", "yml" @@ -203,6 +303,11 @@ "description": "GraphQL formatter.", "selected": false, "configKey": "graphql", + "keywords": [ + "graphql", + "apollo", + "schema" + ], "fileExtensions": [ "graphql", "gql" @@ -214,6 +319,12 @@ "description": "CSS, SCSS, and Less formatter that never reinterprets your styles.", "selected": false, "configKey": "css", + "keywords": [ + "css", + "scss", + "less", + "stylesheet" + ], "fileExtensions": [ "css", "scss", @@ -228,6 +339,15 @@ "description": "HTML, XML, SVG, Vue, Svelte, and Astro formatter that preserves rendering-critical whitespace.", "selected": false, "configKey": "markup", + "keywords": [ + "html", + "xml", + "svg", + "vue", + "svelte", + "astro", + "markup" + ], "fileExtensions": [ "html", "htm", @@ -244,6 +364,14 @@ "description": "Dialect-agnostic SQL formatter.", "selected": false, "configKey": "sql", + "keywords": [ + "sql", + "mysql", + "postgres", + "postgresql", + "sqlite", + "database" + ], "fileExtensions": [ "sql" ], @@ -254,6 +382,12 @@ "description": "Quarto, Pandoc, R Markdown, and Markdown formatter.", "selected": false, "configKey": "panache", + "keywords": [ + "quarto", + "pandoc", + "rmarkdown", + "markdown" + ], "fileExtensions": [ "md", "qmd", @@ -272,6 +406,14 @@ "description": "Swift code formatter (SwiftFormat wrapper).", "selected": false, "configKey": "swiftformat", + "keywords": [ + "swift", + "swiftformat", + "ios", + "macos", + "apple", + "xcode" + ], "fileExtensions": [ "swift" ], @@ -282,6 +424,18 @@ "description": "Prettier wrapper plugin (consider faster dedicated plugins for JS/TS, CSS, etc.).", "selected": false, "configKey": "prettier", + "keywords": [ + "prettier", + "javascript", + "typescript", + "css", + "html", + "vue", + "yaml", + "graphql", + "markdown", + "mdx" + ], "fileExtensions": [ "js", "jsx", @@ -314,6 +468,15 @@ "description": "C# and Visual Basic code formatter (Roslyn).", "selected": false, "configKey": "roslyn", + "keywords": [ + "c#", + "csharp", + "visual basic", + "vb", + "dotnet", + ".net", + "roslyn" + ], "fileExtensions": [ "cs", "vb" @@ -325,6 +488,20 @@ "description": "Formats code using formatting CLIs installed on the host machine (rustfmt, clang-format, shfmt, etc.).", "selected": false, "configKey": "exec", + "keywords": [ + "rust", + "rustfmt", + "c", + "c++", + "cpp", + "clang", + "clang-format", + "shell", + "bash", + "shfmt", + "zig", + "cli" + ], "fileExtensions": [], "configExcludes": [], "defaultConfig": { diff --git a/readInfoFile.ts b/readInfoFile.ts index d623ae2..3081b29 100644 --- a/readInfoFile.ts +++ b/readInfoFile.ts @@ -16,6 +16,20 @@ export interface PluginData { currentVersion: number; allVersions: number; }; + // descriptive fields carried over from info.json, used to index the search + description?: string; + configKey?: string; + keywords?: string[]; + fileExtensions?: string[]; + fileNames?: string[]; + // present on plugins like exec where the real extensions/commands live in + // per-match config rather than top-level fileExtensions + configItems?: PluginConfigItem[]; +} + +interface PluginConfigItem { + match?: { fileExtensions?: string[] }; + config?: { commands?: { command?: string; exts?: string[] }[] }; } interface CacheEntry { diff --git a/style.css b/style.css index 97fe339..1dcc371 100644 --- a/style.css +++ b/style.css @@ -1,68 +1,296 @@ -html { - background-color: #1e1e1e; - color: #fff; +/* ===================== base / theme ===================== */ + +:root { + --accent: #8b93a1; + --max: 1320px; +} + +* { + box-sizing: border-box; } -body, -html { +html, +body { margin: 0; padding: 0; } body { + font-family: "IBM Plex Mono", ui-monospace, Menlo, Monaco, Consolas, "Courier New", monospace; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background: #1e1e1e; + color: #c2c8d0; + min-height: 100vh; +} + +::selection { + background: #3a3f49; + color: #fff; +} + +a { + text-decoration: none; + color: inherit; +} + +/* ===================== page ===================== */ + +.page { + max-width: var(--max); + margin: 0 auto; + padding: 26px 28px 64px; +} + +.topbar { display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 14px; + margin-bottom: 22px; +} +.plugin-search { flex: 1; - flex-direction: column; - font-family: - "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", - "Helvetica Neue", sans-serif; + min-width: 200px; + max-width: 360px; + font-family: inherit; + font-size: 14px; + color: #e8e8ea; + background: #181a1e; + border: 1px solid #33373f; + border-radius: 8px; + padding: 9px 13px; + outline: none; + transition: border-color 0.15s; } - -a, -a:visited, -a:hover { +.plugin-search::placeholder { + color: #6e7681; +} +.plugin-search:focus { + border-color: #4d5564; +} +.docs-link { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #cdd2da; + border: 1px solid #33373f; + border-radius: 8px; + padding: 8px 14px; + transition: color 0.15s, border-color 0.15s; +} +.docs-link:hover { + border-color: #4d5564; color: #fff; } +.docs-link span { + color: #6e7681; +} -h1 { - padding: 0; - margin: 0; - margin-bottom: 10px; +/* ===================== registry table ===================== */ + +.plugin-table { + background: #181a1e; + border: 1px solid #33373f; + border-radius: 14px; + overflow: hidden; +} +.plugin-table-head, +.plugin-row { + display: grid; + grid-template-columns: minmax(260px, max-content) minmax(0, 2fr) minmax(120px, max-content) auto; + align-items: center; + gap: 18px 24px; + padding: 14px 22px; +} +.plugin-table-head { + background: #1f2228; + border-bottom: 1px solid #2a2e35; + font-size: 11.5px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #6e7681; +} +.plugin-table-head .num-col { + text-align: right; +} +.plugin-row { + border-top: 1px solid #23262d; + transition: background 0.15s; +} +.plugin-row:first-of-type { + border-top: none; +} +.plugin-row:hover { + background: #1d2025; +} +.plugin-row[hidden] { + display: none; } -#content { - background-color: #4d5564; - align-self: center; - padding: 20px; - margin-top: 20px; - border: 1px solid #000; +.no-matches { + background: #181a1e; + border: 1px solid #33373f; + border-radius: 14px; + padding: 30px 22px; + margin-top: 12px; + color: #6e7681; + font-size: 14px; + text-align: center; } -#plugins { - display: table; - border-spacing: 10px; +.col-name { + display: flex; + align-items: center; + gap: 11px; + min-width: 0; +} +.col-name .swatch { + width: 8px; + height: 8px; + border-radius: 2px; + flex-shrink: 0; + background: var(--accent); +} +.col-name .name-text { + font-size: 14.5px; + font-weight: 600; + color: #e8e8ea; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.col-name .version-tag { + flex-shrink: 0; + font-size: 11px; + color: var(--accent); + background: #23262d; + border: 1px solid #2f333b; + border-radius: 999px; + padding: 2px 8px; } -#plugins-header { - display: table-row; +.col-url { + min-width: 0; +} +.col-url code { + display: block; + font-family: inherit; + font-size: 13px; + color: #c2c8d0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -#plugins-header div { - display: table-cell; - font-weight: bold; +.col-downloads { + font-size: 13.5px; + color: #a1a9b5; + text-align: right; + white-space: nowrap; +} +.col-downloads .dl-label { + display: none; + color: #6e7681; } -.plugin { - display: table-row; +.col-action { + text-align: right; +} +.copy-btn { + background: #23262d; + border: 1px solid #383d47; + border-radius: 8px; + cursor: pointer; + font-family: inherit; + font-size: 12px; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #9aa1ad; + padding: 7px 14px; + transition: color 0.15s, border-color 0.15s, background 0.15s; +} +.copy-btn:hover { + border-color: #4d5564; + color: #fff; +} +.copy-btn.copied { + color: #a7d39b; + border-color: #3f5340; } -.plugin div { - display: table-cell; +@media (max-width: 820px) { + .plugin-table-head { + display: none; + } + .plugin-row { + grid-template-columns: 1fr auto; + grid-template-areas: + "name action" + "url url" + "downloads downloads"; + gap: 10px 14px; + padding: 18px 20px; + } + .col-name { + grid-area: name; + } + .col-action { + grid-area: action; + } + .col-url { + grid-area: url; + } + .col-url code { + white-space: normal; + word-break: break-all; + background: #0f1114; + border: 1px solid #2a2e35; + border-radius: 8px; + padding: 10px 12px; + font-size: 12.5px; + } + .col-downloads { + grid-area: downloads; + text-align: left; + } + .col-downloads .dl-label { + display: inline; + } } -code { - background-color: #333; - border-radius: 3px; - display: inline-block; - padding: 2px 5px; +/* ===================== helpful commands ===================== */ + +.commands { + margin-top: 44px; +} +.commands h2 { + margin: 0 0 16px; + font-size: 16px; + font-weight: 600; + color: #f0f1f3; +} +.commands ul { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + gap: 10px; +} +.commands li { + font-size: 14px; + line-height: 1.6; + color: #a1a9b5; +} +.commands code { + font-family: inherit; + font-size: 13px; + color: #fff; + background: #181a1e; + border: 1px solid #2a2e35; + border-radius: 6px; + padding: 3px 8px; }