@rmdes/indiekit-endpoint-site-config 1.0.0-beta.1 → 1.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +2 -2
- package/lib/render/write-site-json.js +0 -1
- package/lib/storage/backfill-identity.js +65 -0
- package/lib/storage/defaults-site.js +3 -9
- package/locales/en.json +1 -6
- package/locales/fr.json +1 -6
- package/package.json +2 -2
- package/views/partials/tab-strip.njk +2 -3
- package/views/site-config-identity.njk +26 -26
- package/lib/controllers/features.js +0 -52
- package/views/site-config-features.njk +0 -166
package/index.js
CHANGED
|
@@ -7,12 +7,12 @@ import { brandingRouter } from "./lib/controllers/branding.js";
|
|
|
7
7
|
import { homepageRouter } from "./lib/controllers/homepage.js";
|
|
8
8
|
import { blogRouter } from "./lib/controllers/blog.js";
|
|
9
9
|
import { navigationRouter } from "./lib/controllers/navigation.js";
|
|
10
|
-
import { featuresRouter } from "./lib/controllers/features.js";
|
|
11
10
|
import { apiRouter } from "./lib/controllers/api.js";
|
|
12
11
|
|
|
13
12
|
import { getSiteConfig } from "./lib/storage/get-site-config.js";
|
|
14
13
|
import { getHomepageConfig } from "./lib/storage/get-homepage-config.js";
|
|
15
14
|
import { maybeSeedFromEnv } from "./lib/storage/seed-from-env.js";
|
|
15
|
+
import { maybeBackfillIdentity } from "./lib/storage/backfill-identity.js";
|
|
16
16
|
|
|
17
17
|
import { writeThemeCss } from "./lib/render/write-theme-css.js";
|
|
18
18
|
import { writeCriticalCss } from "./lib/render/write-critical-css.js";
|
|
@@ -77,13 +77,13 @@ export default class SiteConfigEndpoint {
|
|
|
77
77
|
protectedRouter.use("/homepage", homepageRouter(Indiekit));
|
|
78
78
|
protectedRouter.use("/blog", blogRouter(Indiekit));
|
|
79
79
|
protectedRouter.use("/navigation", navigationRouter(Indiekit));
|
|
80
|
-
protectedRouter.use("/features", featuresRouter(Indiekit));
|
|
81
80
|
|
|
82
81
|
this.routes = protectedRouter;
|
|
83
82
|
|
|
84
83
|
// First-boot seed + initial file write
|
|
85
84
|
try {
|
|
86
85
|
await maybeSeedFromEnv(Indiekit);
|
|
86
|
+
await maybeBackfillIdentity(Indiekit);
|
|
87
87
|
const config = await getSiteConfig(Indiekit);
|
|
88
88
|
const homepage = await getHomepageConfig(Indiekit);
|
|
89
89
|
await writeThemeCss(config);
|
|
@@ -47,7 +47,6 @@ export function renderSiteJson(config) {
|
|
|
47
47
|
identity: config.identity,
|
|
48
48
|
branding: config.branding,
|
|
49
49
|
navigation: config.navigation,
|
|
50
|
-
features: config.features,
|
|
51
50
|
updatedAt: config.updatedAt,
|
|
52
51
|
};
|
|
53
52
|
const replacer = (key, value) => (PRIVATE_KEYS.has(key) ? undefined : value);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time backfill: copy homepageConfig.identity → siteConfig.identity
|
|
3
|
+
* when the latter is empty/stub.
|
|
4
|
+
*
|
|
5
|
+
* The legacy @rmdes/indiekit-endpoint-homepage plugin persisted identity
|
|
6
|
+
* inside the homepageConfig collection. The unified plugin reads identity
|
|
7
|
+
* from siteConfig.identity. Without a backfill, operators upgrading from
|
|
8
|
+
* homepage v1.0.24 → site-config v1.0.0-beta.2 would see an empty Identity
|
|
9
|
+
* tab even though the data is sitting one collection over.
|
|
10
|
+
*
|
|
11
|
+
* Heuristic for "empty/stub": none of name, avatar, bio populated.
|
|
12
|
+
* If those three are all empty/missing, we consider the identity stub-shaped
|
|
13
|
+
* and backfill from homepageConfig.identity if a richer version exists.
|
|
14
|
+
*
|
|
15
|
+
* Idempotent: once siteConfig.identity has data, the function no-ops.
|
|
16
|
+
*
|
|
17
|
+
* @module storage/backfill-identity
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { saveSiteConfig } from "./save-site-config.js";
|
|
21
|
+
|
|
22
|
+
function isStubIdentity(identity) {
|
|
23
|
+
if (!identity || typeof identity !== "object") return true;
|
|
24
|
+
// The three load-bearing h-card fields. If none populated, treat as stub.
|
|
25
|
+
const name = (identity.name || "").trim();
|
|
26
|
+
const avatar = (identity.avatar || "").trim();
|
|
27
|
+
const bio = (identity.bio || "").trim();
|
|
28
|
+
return name === "" && avatar === "" && bio === "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasRichIdentity(identity) {
|
|
32
|
+
if (!identity || typeof identity !== "object") return false;
|
|
33
|
+
// Source must have at least one of the three load-bearing fields
|
|
34
|
+
return Boolean(
|
|
35
|
+
(identity.name || "").trim() ||
|
|
36
|
+
(identity.avatar || "").trim() ||
|
|
37
|
+
(identity.bio || "").trim()
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* If siteConfig.identity is stub-shaped AND homepageConfig.identity has
|
|
43
|
+
* rich data, copy the rich identity over. Idempotent.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} Indiekit - Indiekit application instance
|
|
46
|
+
* @returns {Promise<boolean>} true if backfill ran, false otherwise
|
|
47
|
+
*/
|
|
48
|
+
export async function maybeBackfillIdentity(Indiekit) {
|
|
49
|
+
const db = Indiekit.database;
|
|
50
|
+
if (!db) return false;
|
|
51
|
+
|
|
52
|
+
const siteDoc = await db.collection("siteConfig").findOne({ _id: "primary" });
|
|
53
|
+
if (!siteDoc) return false; // seed-from-env handles greenfield install
|
|
54
|
+
if (!isStubIdentity(siteDoc.identity)) return false;
|
|
55
|
+
|
|
56
|
+
const homepageDoc = await db.collection("homepageConfig").findOne({ _id: "homepage" });
|
|
57
|
+
if (!homepageDoc || !hasRichIdentity(homepageDoc.identity)) return false;
|
|
58
|
+
|
|
59
|
+
// Copy the rich identity over. saveSiteConfig handles the deepMerge + replaceOne
|
|
60
|
+
// so we preserve any other siteConfig keys (branding, navigation).
|
|
61
|
+
await saveSiteConfig(Indiekit, { identity: homepageDoc.identity }, "backfill-from-homepage");
|
|
62
|
+
|
|
63
|
+
console.log("[site-config] backfilled identity from homepageConfig (one-time migration)");
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Default values for siteConfig collection (identity, branding, navigation
|
|
2
|
+
* Default values for siteConfig collection (identity, branding, navigation).
|
|
3
3
|
* Singleton _id: "primary".
|
|
4
4
|
*
|
|
5
5
|
* Schema v3 (unification):
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* - layout subtree removed entirely (replaced by homepageConfig + navigation)
|
|
8
8
|
* - navigation.items[] added (was siteConfig.layout.navItems[])
|
|
9
9
|
* - branding subtree unchanged from v2 (Path D)
|
|
10
|
-
* - features subtree
|
|
10
|
+
* - features subtree DROPPED in v3.x (was unused; per-plugin config lives in
|
|
11
|
+
* each plugin's own admin UI, plugin loadout lives in plugins.yaml).
|
|
11
12
|
*
|
|
12
13
|
* Frozen for immutability — never mutate this object directly.
|
|
13
14
|
* @module storage/defaults-site
|
|
@@ -77,13 +78,6 @@ export const DEFAULTS_SITE = Object.freeze({
|
|
|
77
78
|
navigation: Object.freeze({
|
|
78
79
|
items: Object.freeze([]),
|
|
79
80
|
}),
|
|
80
|
-
features: Object.freeze({
|
|
81
|
-
webmentions: true,
|
|
82
|
-
syndication: true,
|
|
83
|
-
activitypub: false,
|
|
84
|
-
search: true,
|
|
85
|
-
rss: true,
|
|
86
|
-
}),
|
|
87
81
|
});
|
|
88
82
|
|
|
89
83
|
// Legacy export name for any code path that hasn't been updated yet.
|
package/locales/en.json
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
"branding": "Branding",
|
|
8
8
|
"homepage": "Homepage",
|
|
9
9
|
"blog": "Blog",
|
|
10
|
-
"navigation": "Navigation"
|
|
11
|
-
"features": "Feature Flags"
|
|
10
|
+
"navigation": "Navigation"
|
|
12
11
|
},
|
|
13
12
|
"common": {
|
|
14
13
|
"save": "Save",
|
|
@@ -218,10 +217,6 @@
|
|
|
218
217
|
"sidebarEnabled": "Sidebar enabled",
|
|
219
218
|
"sidebarSide": "Sidebar side",
|
|
220
219
|
"navItems": "Navigation items"
|
|
221
|
-
},
|
|
222
|
-
"features": {
|
|
223
|
-
"title": "Feature Flags",
|
|
224
|
-
"empty": "No plugins declare a feature flag yet."
|
|
225
220
|
}
|
|
226
221
|
}
|
|
227
222
|
}
|
package/locales/fr.json
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
"branding": "Identité visuelle",
|
|
8
8
|
"homepage": "Accueil",
|
|
9
9
|
"blog": "Blog",
|
|
10
|
-
"navigation": "Navigation"
|
|
11
|
-
"features": "Fonctionnalités"
|
|
10
|
+
"navigation": "Navigation"
|
|
12
11
|
},
|
|
13
12
|
"common": {
|
|
14
13
|
"save": "Enregistrer",
|
|
@@ -218,10 +217,6 @@
|
|
|
218
217
|
"sidebarEnabled": "Barre latérale activée",
|
|
219
218
|
"sidebarSide": "Côté de la barre latérale",
|
|
220
219
|
"navItems": "Éléments de navigation"
|
|
221
|
-
},
|
|
222
|
-
"features": {
|
|
223
|
-
"title": "Fonctionnalités",
|
|
224
|
-
"empty": "Aucun plugin ne déclare encore de drapeau de fonctionnalité."
|
|
225
220
|
}
|
|
226
221
|
}
|
|
227
222
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-site-config",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Site identity, branding,
|
|
5
|
+
"description": "Site identity, branding, and navigation configuration for Indiekit",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.js"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{# Tab navigation for site-config admin - server-rendered URL tabs #}
|
|
2
|
-
{# Expects: activeTab (string, one of identity|branding|homepage|blog|navigation
|
|
2
|
+
{# Expects: activeTab (string, one of identity|branding|homepage|blog|navigation) #}
|
|
3
3
|
<style>
|
|
4
4
|
.sc-tab-nav {
|
|
5
5
|
display: flex;
|
|
@@ -31,8 +31,7 @@
|
|
|
31
31
|
{key: 'branding', href: '/site-config/branding'},
|
|
32
32
|
{key: 'homepage', href: '/site-config/homepage'},
|
|
33
33
|
{key: 'blog', href: '/site-config/blog'},
|
|
34
|
-
{key: 'navigation', href: '/site-config/navigation'}
|
|
35
|
-
{key: 'features', href: '/site-config/features'}
|
|
34
|
+
{key: 'navigation', href: '/site-config/navigation'}
|
|
36
35
|
] %}
|
|
37
36
|
{% for tab in tabs %}
|
|
38
37
|
<a href="{{ tab.href }}"
|
|
@@ -224,34 +224,34 @@
|
|
|
224
224
|
<h2>{{ __("siteConfig.identity.profile.legend") }}</h2>
|
|
225
225
|
<div class="hp-field-grid">
|
|
226
226
|
<div class="field">
|
|
227
|
-
<label class="field__label" for="
|
|
228
|
-
<input class="field__input" type="text" id="
|
|
227
|
+
<label class="field__label" for="name">{{ __("siteConfig.identity.profile.name.label") }}</label>
|
|
228
|
+
<input class="field__input" type="text" id="name" name="name" value="{{ config.identity.name or '' }}">
|
|
229
229
|
<p class="field__hint">{{ __("siteConfig.identity.profile.name.hint") }}</p>
|
|
230
230
|
</div>
|
|
231
231
|
<div class="field">
|
|
232
|
-
<label class="field__label" for="
|
|
233
|
-
<input class="field__input" type="text" id="
|
|
232
|
+
<label class="field__label" for="title">{{ __("siteConfig.identity.profile.title.label") }}</label>
|
|
233
|
+
<input class="field__input" type="text" id="title" name="title" value="{{ config.identity.title or '' }}">
|
|
234
234
|
<p class="field__hint">{{ __("siteConfig.identity.profile.title.hint") }}</p>
|
|
235
235
|
</div>
|
|
236
236
|
<div class="field field--full">
|
|
237
|
-
<label class="field__label" for="
|
|
238
|
-
<input class="field__input" type="url" id="
|
|
237
|
+
<label class="field__label" for="avatar">{{ __("siteConfig.identity.profile.avatar.label") }}</label>
|
|
238
|
+
<input class="field__input" type="url" id="avatar" name="avatar" value="{{ config.identity.avatar or '' }}">
|
|
239
239
|
<p class="field__hint">{{ __("siteConfig.identity.profile.avatar.hint") }}</p>
|
|
240
240
|
</div>
|
|
241
241
|
<div class="field">
|
|
242
|
-
<label class="field__label" for="
|
|
243
|
-
<input class="field__input" type="text" id="
|
|
242
|
+
<label class="field__label" for="pronoun">{{ __("siteConfig.identity.profile.pronoun.label") }}</label>
|
|
243
|
+
<input class="field__input" type="text" id="pronoun" name="pronoun" value="{{ config.identity.pronoun or '' }}">
|
|
244
244
|
<p class="field__hint">{{ __("siteConfig.identity.profile.pronoun.hint") }}</p>
|
|
245
245
|
</div>
|
|
246
246
|
<div class="field"></div>
|
|
247
247
|
<div class="field field--full">
|
|
248
|
-
<label class="field__label" for="
|
|
249
|
-
<textarea class="field__input" id="
|
|
248
|
+
<label class="field__label" for="bio">{{ __("siteConfig.identity.profile.bio.label") }}</label>
|
|
249
|
+
<textarea class="field__input" id="bio" name="bio" rows="3">{{ config.identity.bio or '' }}</textarea>
|
|
250
250
|
<p class="field__hint">{{ __("siteConfig.identity.profile.bio.hint") }}</p>
|
|
251
251
|
</div>
|
|
252
252
|
<div class="field field--full">
|
|
253
|
-
<label class="field__label" for="
|
|
254
|
-
<textarea class="field__input" id="
|
|
253
|
+
<label class="field__label" for="description">{{ __("siteConfig.identity.profile.description.label") }}</label>
|
|
254
|
+
<textarea class="field__input" id="description" name="description" rows="3">{{ config.identity.description or '' }}</textarea>
|
|
255
255
|
<p class="field__hint">{{ __("siteConfig.identity.profile.description.hint") }}</p>
|
|
256
256
|
</div>
|
|
257
257
|
</div>
|
|
@@ -262,17 +262,17 @@
|
|
|
262
262
|
<h2>{{ __("siteConfig.identity.location.legend") }}</h2>
|
|
263
263
|
<div class="hp-field-grid">
|
|
264
264
|
<div class="field">
|
|
265
|
-
<label class="field__label" for="
|
|
266
|
-
<input class="field__input" type="text" id="
|
|
265
|
+
<label class="field__label" for="locality">{{ __("siteConfig.identity.location.locality.label") }}</label>
|
|
266
|
+
<input class="field__input" type="text" id="locality" name="locality" value="{{ config.identity.locality or '' }}">
|
|
267
267
|
<p class="field__hint">{{ __("siteConfig.identity.location.locality.hint") }}</p>
|
|
268
268
|
</div>
|
|
269
269
|
<div class="field">
|
|
270
|
-
<label class="field__label" for="
|
|
271
|
-
<input class="field__input" type="text" id="
|
|
270
|
+
<label class="field__label" for="country">{{ __("siteConfig.identity.location.country.label") }}</label>
|
|
271
|
+
<input class="field__input" type="text" id="country" name="country" value="{{ config.identity.country or '' }}">
|
|
272
272
|
</div>
|
|
273
273
|
<div class="field">
|
|
274
|
-
<label class="field__label" for="
|
|
275
|
-
<input class="field__input" type="text" id="
|
|
274
|
+
<label class="field__label" for="org">{{ __("siteConfig.identity.location.org.label") }}</label>
|
|
275
|
+
<input class="field__input" type="text" id="org" name="org" value="{{ config.identity.org or '' }}">
|
|
276
276
|
<p class="field__hint">{{ __("siteConfig.identity.location.org.hint") }}</p>
|
|
277
277
|
</div>
|
|
278
278
|
</div>
|
|
@@ -283,17 +283,17 @@
|
|
|
283
283
|
<h2>{{ __("siteConfig.identity.contact.legend") }}</h2>
|
|
284
284
|
<div class="hp-field-grid">
|
|
285
285
|
<div class="field field--full">
|
|
286
|
-
<label class="field__label" for="
|
|
287
|
-
<input class="field__input" type="url" id="
|
|
286
|
+
<label class="field__label" for="url">{{ __("siteConfig.identity.contact.url.label") }}</label>
|
|
287
|
+
<input class="field__input" type="url" id="url" name="url" value="{{ config.identity.url or '' }}">
|
|
288
288
|
<p class="field__hint">{{ __("siteConfig.identity.contact.url.hint") }}</p>
|
|
289
289
|
</div>
|
|
290
290
|
<div class="field">
|
|
291
|
-
<label class="field__label" for="
|
|
292
|
-
<input class="field__input" type="email" id="
|
|
291
|
+
<label class="field__label" for="email">{{ __("siteConfig.identity.contact.email.label") }}</label>
|
|
292
|
+
<input class="field__input" type="email" id="email" name="email" value="{{ config.identity.email or '' }}">
|
|
293
293
|
</div>
|
|
294
294
|
<div class="field">
|
|
295
|
-
<label class="field__label" for="
|
|
296
|
-
<input class="field__input" type="url" id="
|
|
295
|
+
<label class="field__label" for="keyUrl">{{ __("siteConfig.identity.contact.keyUrl.label") }}</label>
|
|
296
|
+
<input class="field__input" type="url" id="keyUrl" name="keyUrl" value="{{ config.identity.keyUrl or '' }}">
|
|
297
297
|
<p class="field__hint">{{ __("siteConfig.identity.contact.keyUrl.hint") }}</p>
|
|
298
298
|
</div>
|
|
299
299
|
</div>
|
|
@@ -303,8 +303,8 @@
|
|
|
303
303
|
<section class="hp-section">
|
|
304
304
|
<h2>{{ __("siteConfig.identity.categories.legend") }}</h2>
|
|
305
305
|
<div class="field">
|
|
306
|
-
<label class="field__label" for="
|
|
307
|
-
<input class="field__input" type="text" id="
|
|
306
|
+
<label class="field__label" for="categories">{{ __("siteConfig.identity.categories.tags.label") }}</label>
|
|
307
|
+
<input class="field__input" type="text" id="categories" name="categories" value="{{ config.identity.categories | join(', ') if config.identity.categories else '' }}">
|
|
308
308
|
<p class="field__hint">{{ __("siteConfig.identity.categories.tags.hint") }}</p>
|
|
309
309
|
</div>
|
|
310
310
|
</section>
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import { getSiteConfig } from "../storage/get-site-config.js";
|
|
3
|
-
import { saveSiteConfig } from "../storage/save-site-config.js";
|
|
4
|
-
import { writeSiteJson } from "../render/write-site-json.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Discover feature flags from loaded plugins by reading `plugin.featureFlag`.
|
|
8
|
-
* Plugins without the capability are skipped.
|
|
9
|
-
*/
|
|
10
|
-
export function discoverFlags(Indiekit) {
|
|
11
|
-
const plugins = Indiekit.config?.plugins || [];
|
|
12
|
-
return plugins
|
|
13
|
-
.map((p) => p.featureFlag)
|
|
14
|
-
.filter(Boolean)
|
|
15
|
-
.sort((a, b) => (a.category || "").localeCompare(b.category || ""));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function featuresRouter(Indiekit) {
|
|
19
|
-
const router = express.Router();
|
|
20
|
-
|
|
21
|
-
router.get("/", async (req, res, next) => {
|
|
22
|
-
try {
|
|
23
|
-
const config = await getSiteConfig(Indiekit);
|
|
24
|
-
const flags = discoverFlags(Indiekit);
|
|
25
|
-
res.render("site-config-features", {
|
|
26
|
-
config,
|
|
27
|
-
activeTab: "features",
|
|
28
|
-
flags,
|
|
29
|
-
});
|
|
30
|
-
} catch (error) {
|
|
31
|
-
next(error);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
router.post("/", async (req, res, next) => {
|
|
36
|
-
try {
|
|
37
|
-
const flags = discoverFlags(Indiekit);
|
|
38
|
-
const features = {};
|
|
39
|
-
for (const flag of flags) {
|
|
40
|
-
features[flag.key] = req.body[`feature_${flag.key}`] === "on";
|
|
41
|
-
}
|
|
42
|
-
const userIdent = Indiekit.config?.publication?.me || "unknown";
|
|
43
|
-
const updated = await saveSiteConfig(Indiekit, { features }, userIdent);
|
|
44
|
-
await writeSiteJson(updated);
|
|
45
|
-
res.redirect("/site-config/features?saved=1");
|
|
46
|
-
} catch (error) {
|
|
47
|
-
next(error);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return router;
|
|
52
|
-
}
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
{% extends "document.njk" %}
|
|
2
|
-
|
|
3
|
-
{% block title %}{{ __('siteConfig.features.title') }}{% endblock %}
|
|
4
|
-
|
|
5
|
-
{% block content %}
|
|
6
|
-
<style>
|
|
7
|
-
.sc-dashboard {
|
|
8
|
-
display: flex;
|
|
9
|
-
flex-direction: column;
|
|
10
|
-
gap: var(--space-xl, 2rem);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
.sc-section {
|
|
14
|
-
background: var(--color-offset, #f5f5f5);
|
|
15
|
-
border-radius: var(--border-radius-small, 0.5rem);
|
|
16
|
-
padding: var(--space-m, 1.5rem);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
.sc-section h2 {
|
|
20
|
-
font: var(--font-heading, bold 1.25rem/1.4 sans-serif);
|
|
21
|
-
margin-block-end: var(--space-s, 0.75rem);
|
|
22
|
-
padding-block-end: var(--space-xs, 0.5rem);
|
|
23
|
-
border-block-end: 1px solid var(--color-outline-variant, #ddd);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.sc-section__desc {
|
|
27
|
-
color: var(--color-on-offset, #666);
|
|
28
|
-
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
29
|
-
margin-block-end: var(--space-s, 0.75rem);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.hp-success {
|
|
33
|
-
background: var(--color-success-container, #d4edda);
|
|
34
|
-
border: 1px solid var(--color-success, #28a745);
|
|
35
|
-
border-radius: var(--border-radius-small, 0.25rem);
|
|
36
|
-
padding: var(--space-s, 0.75rem);
|
|
37
|
-
margin-block-end: var(--space-m, 1rem);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/* Empty state */
|
|
41
|
-
.sc-empty-state {
|
|
42
|
-
text-align: center;
|
|
43
|
-
padding: var(--space-xl, 2rem) var(--space-m, 1rem);
|
|
44
|
-
color: var(--color-on-offset, #888);
|
|
45
|
-
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
46
|
-
background: var(--color-offset, #f5f5f5);
|
|
47
|
-
border: 1px dashed var(--color-outline-variant, #ddd);
|
|
48
|
-
border-radius: var(--border-radius-small, 0.5rem);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.sc-empty-state p {
|
|
52
|
-
margin: 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/* Flag rows */
|
|
56
|
-
.sc-flag-row {
|
|
57
|
-
display: flex;
|
|
58
|
-
align-items: flex-start;
|
|
59
|
-
gap: var(--space-s, 0.75rem);
|
|
60
|
-
padding: var(--space-s, 0.75rem) 0;
|
|
61
|
-
border-bottom: 1px solid var(--color-outline-variant, #eee);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.sc-flag-row:last-child {
|
|
65
|
-
border-bottom: none;
|
|
66
|
-
padding-bottom: 0;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.sc-flag-row:first-child {
|
|
70
|
-
padding-top: 0;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.sc-flag-label {
|
|
74
|
-
display: flex;
|
|
75
|
-
flex-direction: column;
|
|
76
|
-
gap: 0.125rem;
|
|
77
|
-
flex: 1;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.sc-flag-label strong {
|
|
81
|
-
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
82
|
-
font-weight: 600;
|
|
83
|
-
color: var(--color-on-surface, inherit);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.sc-flag-hint {
|
|
87
|
-
font: var(--font-caption, 0.75rem/1.4 sans-serif);
|
|
88
|
-
color: var(--color-on-offset, #888);
|
|
89
|
-
margin: 0;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.sc-restart-badge {
|
|
93
|
-
display: inline-flex;
|
|
94
|
-
align-items: center;
|
|
95
|
-
font: var(--font-caption, 0.7rem/1.4 sans-serif);
|
|
96
|
-
font-weight: 600;
|
|
97
|
-
color: var(--color-warning, #856404);
|
|
98
|
-
background: color-mix(in srgb, var(--color-warning, #856404) 12%, var(--color-background, #fff));
|
|
99
|
-
border: 1px solid color-mix(in srgb, var(--color-warning, #856404) 30%, transparent);
|
|
100
|
-
border-radius: var(--border-radius-small, 0.25rem);
|
|
101
|
-
padding: 0.1rem 0.4rem;
|
|
102
|
-
white-space: nowrap;
|
|
103
|
-
}
|
|
104
|
-
</style>
|
|
105
|
-
|
|
106
|
-
<header class="page-header">
|
|
107
|
-
<h1 class="page-header__title">{{ __('siteConfig.title') }}</h1>
|
|
108
|
-
</header>
|
|
109
|
-
|
|
110
|
-
{% include "partials/tab-strip.njk" %}
|
|
111
|
-
|
|
112
|
-
{% if request.query.saved %}
|
|
113
|
-
<div class="hp-success">
|
|
114
|
-
<p>{{ __('siteConfig.common.saved') }}</p>
|
|
115
|
-
</div>
|
|
116
|
-
{% endif %}
|
|
117
|
-
|
|
118
|
-
{% if flags.length == 0 %}
|
|
119
|
-
|
|
120
|
-
<div class="sc-empty-state">
|
|
121
|
-
<p>{{ __('siteConfig.features.empty') }}</p>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
{% else %}
|
|
125
|
-
|
|
126
|
-
<form method="post" action="/site-config/features" class="sc-dashboard">
|
|
127
|
-
|
|
128
|
-
{% set currentCategory = '' %}
|
|
129
|
-
{% for flag in flags %}
|
|
130
|
-
|
|
131
|
-
{% if flag.category != currentCategory %}
|
|
132
|
-
{% set currentCategory = flag.category %}
|
|
133
|
-
<section class="sc-section">
|
|
134
|
-
<h2>{{ flag.category }}</h2>
|
|
135
|
-
{% endif %}
|
|
136
|
-
|
|
137
|
-
<div class="sc-flag-row">
|
|
138
|
-
<input type="checkbox"
|
|
139
|
-
id="feature_{{ flag.key }}"
|
|
140
|
-
name="feature_{{ flag.key }}"
|
|
141
|
-
{% if config.features[flag.key] %}checked{% elif flag.default %}checked{% endif %}>
|
|
142
|
-
<label class="sc-flag-label" for="feature_{{ flag.key }}">
|
|
143
|
-
<strong>{{ flag.label }}</strong>
|
|
144
|
-
{% if flag.description %}
|
|
145
|
-
<p class="sc-flag-hint">{{ flag.description }}</p>
|
|
146
|
-
{% endif %}
|
|
147
|
-
{% if flag.requiresRestart %}
|
|
148
|
-
<span class="sc-restart-badge">Restart required</span>
|
|
149
|
-
{% endif %}
|
|
150
|
-
</label>
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
{% if loop.last or flags[loop.index].category != flag.category %}
|
|
154
|
-
</section>
|
|
155
|
-
{% endif %}
|
|
156
|
-
|
|
157
|
-
{% endfor %}
|
|
158
|
-
|
|
159
|
-
<div class="button-group">
|
|
160
|
-
<button type="submit" class="button button--primary">{{ __('siteConfig.common.save') }}</button>
|
|
161
|
-
</div>
|
|
162
|
-
|
|
163
|
-
</form>
|
|
164
|
-
|
|
165
|
-
{% endif %}
|
|
166
|
-
{% endblock %}
|