Public: anyone can read this; the crew can change it.

Settings › Features speaks the home page's language #9700

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/9682-settings-features-speaks-the-home-page-s → main 6 commits fork

  • Merged. mason/9682-settings-features-speaks-the-home-page-s was merged into main as 0a3afd9.

Review

Sign in to review this change.
2 files changed +28 −32
docs/design/areas/features.md added +6
@@ -0,0 +1,6 @@
1 # Features (Settings › Features)
2
3 `src/web/pages/features.ts` adds no component class. The list and a feature's page are a heading over rows, as the
4 home page is: a `<section class="settings-block">` with an `<h2>` (a `.bar` when a badge sits beside it) over a
5 `.setting-list` of `row()`s. No card around the list or a feature's form, and no icon in a row's lead. Classes used are
6 named in `docs/design/areas/settings.md`.
src/web/pages/features.ts +22 −32
@@ -23,7 +23,7 @@ import { notFoundPage, settingsLayout, signInFirst } from "../chrome.ts";
2323 import { readForm, type Form } from "../form.ts";
2424 import { html, raw, when } from "../html.ts";
2525 import { flashTo, page } from "../layout.ts";
26 import { badge, btn, card, empty, icon, row } from "../ui.ts";
26 import { badge, btn, empty, row } from "../ui.ts";
2727 import { accountSections } from "./origins.ts";
2828
2929 function requireAdmin(ctx: Ctx): Response | undefined {
@@ -54,7 +54,6 @@ export function switchNote(f: Pick<FeatureView, "name" | "absorbed">): string {
5454 function featureRow(f: FeatureView) {
5555 const base = `/settings/features/${encodeURIComponent(f.slug)}`;
5656 return row({
57 lead: icon("rocket"),
5857 title: html`${f.name} <span class="muted small mono">${f.repo}</span>
5958 ${f.absorbed ? badge("absorbed", { tone: "success" }) : null}
6059 ${f.pending !== null ? badge("declaration changed: review", { tone: "attn" }) : null}`,
@@ -89,25 +88,19 @@ export async function featuresPage(ctx: Ctx): Promise<Response> {
8988 const listed = await timePhase(ctx.request, "features", () => listFeatures(ctx.app, ctx.auth.user));
9089 if (!listed.ok) return notFoundPage(ctx);
9190 const rendering = performance.now();
92 const body = html`${card(
93 listed.value.length === 0
94 ? html`<div class="card-body">
95 ${empty({
91 const body = html`<section class="settings-block">
92 <h2>Features</h2>
93 ${listed.value.length === 0
94 ? empty({
9695 icon: "rocket",
9796 title: "No app declares a feature yet",
9897 note: 'An app declares one with a "feature" block in its forge.json: a name, a page and typed settings. Absorbing it puts it in the forge\'s nav and keeps its settings here.',
9998 action: html`<p><a href="${DOCS}">How an app declares a feature</a></p>`,
100 })}
101 </div>`
102 : html`<ul class="list setting-list">
103 ${listed.value.map(featureRow)}
104 </ul>`,
105 {
106 head: html`${icon("rocket")}
107 <h2>Features</h2>`,
108 pad: false,
109 },
110 )}`;
99 })
100 : html`<ul class="list setting-list">
101 ${listed.value.map(featureRow)}
102 </ul>`}
103 </section>`;
111104 const response = shell(ctx, "Features", body);
112105 notePhase(ctx.request, "render", performance.now() - rendering);
113106 return response;
@@ -177,31 +170,28 @@ function featureView(ctx: Ctx, f: FeatureView, refused?: Refused): Response {
177170 const bad = refused?.key === shown.key;
178171 const s = refused !== undefined && shown.key in refused.typed ? { ...shown, value: typedValue(shown, refused.typed[shown.key]) } : shown;
179172 return row({
180 lead: icon("settings"),
181173 title: html`${s.label} <span class="muted small mono">${s.env}</span>`,
182174 meta: html`${bad ? html`<span class="danger" role="alert">${refused.message}</span><br />` : null}${s.help ?? ""}${when(s.help !== null, () => html`<br />`)}Default
183175 <span class="mono">${s.default === "" ? "empty" : s.default}</span>${s.set ? "" : " · not changed"}`,
184176 trail: input(s, bad),
185177 });
186178 });
187 const body = html`${card(
188 f.settings.length === 0
189 ? html`<div class="card-body"><p class="muted small">${f.problem ?? "This feature declares no settings."}</p></div>`
179 const body = html`<section class="settings-block">
180 <div class="bar">
181 <h2>${f.name}</h2>
182 ${f.absorbed ? badge("absorbed", { tone: "success" }) : badge("not absorbed", { variant: "outline" })}
183 </div>
184 ${f.settings.length === 0
185 ? html`<p class="muted small">${f.problem ?? "This feature declares no settings."}</p>`
190186 : html`<form method="post" action="${base}">
191187 <ul class="list setting-list">${settings}</ul>
192 <div class="card-body">
188 <p>
193189 ${f.absorbed
194190 ? btn("Save", { variant: "primary", label: `Save ${f.name}'s settings` })
195 : html`<p class="muted small">Absorb ${f.name} to change its settings.</p>`}
196 </div>
197 </form>`,
198 {
199 head: html`${icon("rocket")}
200 <h2>${f.name}</h2>
201 ${f.absorbed ? badge("absorbed", { tone: "success" }) : badge("not absorbed", { variant: "outline" })}`,
202 pad: false,
203 },
204 )}
191 : html`<span class="muted small">Absorb ${f.name} to change its settings.</span>`}
192 </p>
193 </form>`}
194 </section>
205195 <p class="muted small">
206196 Saving restarts ${f.name}: its process reads each setting as an environment variable, named on the right.
207197 <a href="/settings/features">All features</a>

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs