@rmdes/indiekit-endpoint-site-config 1.0.0-alpha.2 → 1.0.0-alpha.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/lib/controllers/branding.js +1 -3
- package/lib/controllers/features.js +1 -5
- package/lib/controllers/identity.js +1 -3
- package/lib/controllers/layout.js +1 -5
- package/locales/en.json +1 -3
- package/locales/fr.json +1 -3
- package/package.json +1 -1
- package/views/partials/color-picker.njk +22 -7
- package/views/partials/tab-strip.njk +30 -4
- package/views/site-config-branding.njk +300 -65
- package/views/site-config-features.njk +160 -29
- package/views/site-config-identity.njk +162 -32
- package/views/site-config-layout.njk +220 -44
|
@@ -19,7 +19,6 @@ export function brandingRouter(Indiekit) {
|
|
|
19
19
|
config,
|
|
20
20
|
activeTab: "branding",
|
|
21
21
|
curatedFonts: CURATED_FONTS,
|
|
22
|
-
success: req.query.success,
|
|
23
22
|
});
|
|
24
23
|
} catch (error) {
|
|
25
24
|
next(error);
|
|
@@ -86,8 +85,7 @@ export function brandingRouter(Indiekit) {
|
|
|
86
85
|
const updated = await saveSiteConfig(Indiekit, patch, userIdent);
|
|
87
86
|
await writeSiteJson(updated);
|
|
88
87
|
await writeThemeCss(updated);
|
|
89
|
-
|
|
90
|
-
res.redirect(`/site-config/branding?success=${message}`);
|
|
88
|
+
res.redirect("/site-config/branding?saved=1");
|
|
91
89
|
} catch (error) {
|
|
92
90
|
next(error);
|
|
93
91
|
}
|
|
@@ -26,7 +26,6 @@ export function featuresRouter(Indiekit) {
|
|
|
26
26
|
config,
|
|
27
27
|
activeTab: "features",
|
|
28
28
|
flags,
|
|
29
|
-
success: req.query.success,
|
|
30
29
|
});
|
|
31
30
|
} catch (error) {
|
|
32
31
|
next(error);
|
|
@@ -43,10 +42,7 @@ export function featuresRouter(Indiekit) {
|
|
|
43
42
|
const userIdent = Indiekit.config?.publication?.me || "unknown";
|
|
44
43
|
const updated = await saveSiteConfig(Indiekit, { features }, userIdent);
|
|
45
44
|
await writeSiteJson(updated);
|
|
46
|
-
|
|
47
|
-
res.locals.__("siteConfig.common.saved"),
|
|
48
|
-
);
|
|
49
|
-
res.redirect(`/site-config/features?success=${message}`);
|
|
45
|
+
res.redirect("/site-config/features?saved=1");
|
|
50
46
|
} catch (error) {
|
|
51
47
|
next(error);
|
|
52
48
|
}
|
|
@@ -27,7 +27,6 @@ export function identityRouter(Indiekit) {
|
|
|
27
27
|
res.render("site-config-identity", {
|
|
28
28
|
config,
|
|
29
29
|
activeTab: "identity",
|
|
30
|
-
success: req.query.success,
|
|
31
30
|
});
|
|
32
31
|
} catch (error) {
|
|
33
32
|
next(error);
|
|
@@ -50,8 +49,7 @@ export function identityRouter(Indiekit) {
|
|
|
50
49
|
const userIdent = Indiekit.config?.publication?.me || "unknown";
|
|
51
50
|
const updated = await saveSiteConfig(Indiekit, patch, userIdent);
|
|
52
51
|
await writeSiteJson(updated);
|
|
53
|
-
|
|
54
|
-
res.redirect(`/site-config/identity?success=${message}`);
|
|
52
|
+
res.redirect("/site-config/identity?saved=1");
|
|
55
53
|
} catch (error) {
|
|
56
54
|
next(error);
|
|
57
55
|
}
|
|
@@ -15,7 +15,6 @@ export function layoutRouter(Indiekit) {
|
|
|
15
15
|
config,
|
|
16
16
|
activeTab: "layout",
|
|
17
17
|
presets: PRESETS,
|
|
18
|
-
success: req.query.success,
|
|
19
18
|
});
|
|
20
19
|
} catch (error) {
|
|
21
20
|
next(error);
|
|
@@ -53,10 +52,7 @@ export function layoutRouter(Indiekit) {
|
|
|
53
52
|
const userIdent = Indiekit.config?.publication?.me || "unknown";
|
|
54
53
|
const updated = await saveSiteConfig(Indiekit, patch, userIdent);
|
|
55
54
|
await writeSiteJson(updated);
|
|
56
|
-
|
|
57
|
-
res.locals.__("siteConfig.common.saved"),
|
|
58
|
-
);
|
|
59
|
-
res.redirect(`/site-config/layout?success=${message}`);
|
|
55
|
+
res.redirect("/site-config/layout?saved=1");
|
|
60
56
|
} catch (error) {
|
|
61
57
|
next(error);
|
|
62
58
|
}
|
package/locales/en.json
CHANGED
package/locales/fr.json
CHANGED
|
@@ -33,9 +33,7 @@
|
|
|
33
33
|
"fontSans": "Police sans-serif",
|
|
34
34
|
"fontSerif": "Police serif",
|
|
35
35
|
"fontMono": "Police mono",
|
|
36
|
-
"fontHosting": "Hébergement des polices"
|
|
37
|
-
"logo": "Logo",
|
|
38
|
-
"favicon": "Favicon"
|
|
36
|
+
"fontHosting": "Hébergement des polices"
|
|
39
37
|
},
|
|
40
38
|
"layout": {
|
|
41
39
|
"title": "Mise en page",
|
package/package.json
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
{#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
{#
|
|
2
|
+
Nunjucks macro: colorPicker(name, label, value)
|
|
3
|
+
Usage:
|
|
4
|
+
{% import "partials/color-picker.njk" as cp %}
|
|
5
|
+
{{ cp.colorPicker("accentBase", "Accent colour", config.branding.accentBase) }}
|
|
6
|
+
#}
|
|
7
|
+
{% macro colorPicker(name, label, value) %}
|
|
8
|
+
<div class="field sc-color-field">
|
|
9
|
+
<label class="field__label" for="{{ name }}">{{ label }}</label>
|
|
10
|
+
<div class="sc-color-picker">
|
|
11
|
+
<input type="color"
|
|
12
|
+
class="sc-color-picker__swatch"
|
|
13
|
+
id="{{ name }}-swatch"
|
|
14
|
+
value="{{ value or '#888888' }}"
|
|
6
15
|
oninput="document.getElementById('{{ name }}').value = this.value">
|
|
7
|
-
<input
|
|
8
|
-
|
|
16
|
+
<input class="field__input sc-color-picker__hex"
|
|
17
|
+
type="text"
|
|
18
|
+
id="{{ name }}"
|
|
19
|
+
name="{{ name }}"
|
|
20
|
+
value="{{ value or '#888888' }}"
|
|
21
|
+
pattern="^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
|
|
22
|
+
required
|
|
9
23
|
oninput="document.getElementById('{{ name }}-swatch').value = this.value">
|
|
10
24
|
</div>
|
|
11
25
|
</div>
|
|
26
|
+
{% endmacro %}
|
|
@@ -1,14 +1,40 @@
|
|
|
1
|
-
{#
|
|
2
|
-
|
|
1
|
+
{# Tab navigation for site-config admin - server-rendered URL tabs #}
|
|
2
|
+
{# Expects: activeTab (string, one of identity|branding|layout|features) #}
|
|
3
|
+
<style>
|
|
4
|
+
.sc-tab-nav {
|
|
5
|
+
display: flex;
|
|
6
|
+
gap: 0;
|
|
7
|
+
border-bottom: 2px solid var(--color-outline-variant, #ddd);
|
|
8
|
+
margin-block-end: var(--space-xl, 2rem);
|
|
9
|
+
}
|
|
10
|
+
.sc-tab-nav__item {
|
|
11
|
+
padding: var(--space-s, 0.75rem) var(--space-m, 1.25rem);
|
|
12
|
+
text-decoration: none;
|
|
13
|
+
color: var(--color-on-offset, #666);
|
|
14
|
+
font-weight: 500;
|
|
15
|
+
border-bottom: 2px solid transparent;
|
|
16
|
+
margin-bottom: -2px;
|
|
17
|
+
transition: color 0.2s, border-color 0.2s;
|
|
18
|
+
}
|
|
19
|
+
.sc-tab-nav__item:hover {
|
|
20
|
+
color: var(--color-primary, #0066cc);
|
|
21
|
+
}
|
|
22
|
+
.sc-tab-nav__item--active {
|
|
23
|
+
color: var(--color-primary, #0066cc);
|
|
24
|
+
border-bottom-color: var(--color-primary, #0066cc);
|
|
25
|
+
font-weight: 600;
|
|
26
|
+
}
|
|
27
|
+
</style>
|
|
28
|
+
<nav class="sc-tab-nav" aria-label="{{ __('siteConfig.title') }}">
|
|
3
29
|
{% set tabs = [
|
|
4
30
|
{key: 'identity', href: '/site-config/identity'},
|
|
5
31
|
{key: 'branding', href: '/site-config/branding'},
|
|
6
|
-
{key: 'layout',
|
|
32
|
+
{key: 'layout', href: '/site-config/layout'},
|
|
7
33
|
{key: 'features', href: '/site-config/features'}
|
|
8
34
|
] %}
|
|
9
35
|
{% for tab in tabs %}
|
|
10
36
|
<a href="{{ tab.href }}"
|
|
11
|
-
class="
|
|
37
|
+
class="sc-tab-nav__item{% if activeTab == tab.key %} sc-tab-nav__item--active{% endif %}"
|
|
12
38
|
{% if activeTab == tab.key %}aria-current="page"{% endif %}>
|
|
13
39
|
{{ __('siteConfig.tabs.' + tab.key) }}
|
|
14
40
|
</a>
|
|
@@ -1,76 +1,311 @@
|
|
|
1
1
|
{% extends "document.njk" %}
|
|
2
|
+
{% import "partials/color-picker.njk" as cp %}
|
|
2
3
|
|
|
3
4
|
{% block title %}{{ __('siteConfig.branding.title') }}{% endblock %}
|
|
4
5
|
|
|
5
6
|
{% block content %}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
7
|
+
<style>
|
|
8
|
+
.sc-dashboard {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: var(--space-xl, 2rem);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.sc-section {
|
|
15
|
+
background: var(--color-offset, #f5f5f5);
|
|
16
|
+
border-radius: var(--border-radius-small, 0.5rem);
|
|
17
|
+
padding: var(--space-m, 1.5rem);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.sc-section h2 {
|
|
21
|
+
font: var(--font-heading, bold 1.25rem/1.4 sans-serif);
|
|
22
|
+
margin-block-end: var(--space-s, 0.75rem);
|
|
23
|
+
padding-block-end: var(--space-xs, 0.5rem);
|
|
24
|
+
border-block-end: 1px solid var(--color-outline-variant, #ddd);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.sc-section__desc {
|
|
28
|
+
color: var(--color-on-offset, #666);
|
|
29
|
+
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
30
|
+
margin-block-end: var(--space-s, 0.75rem);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.hp-success {
|
|
34
|
+
background: var(--color-success-container, #d4edda);
|
|
35
|
+
border: 1px solid var(--color-success, #28a745);
|
|
36
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
37
|
+
padding: var(--space-s, 0.75rem);
|
|
38
|
+
margin-block-end: var(--space-m, 1rem);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Field grid for two-column layouts */
|
|
42
|
+
.hp-field-grid {
|
|
43
|
+
display: grid;
|
|
44
|
+
grid-template-columns: 1fr 1fr;
|
|
45
|
+
gap: var(--space-m, 1rem) var(--space-s, 0.75rem);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.hp-field-grid .field--full {
|
|
49
|
+
grid-column: 1 / -1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@media (max-width: 640px) {
|
|
53
|
+
.hp-field-grid {
|
|
54
|
+
grid-template-columns: 1fr;
|
|
55
|
+
}
|
|
56
|
+
.hp-field-grid .field--full {
|
|
57
|
+
grid-column: 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Field styling */
|
|
62
|
+
.sc-dashboard .field {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
gap: 0.25rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.sc-dashboard .field__label {
|
|
69
|
+
display: block;
|
|
70
|
+
font: var(--font-caption, 0.75rem/1.4 sans-serif);
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
color: var(--color-on-surface, inherit);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.sc-dashboard .field__input {
|
|
76
|
+
width: 100%;
|
|
77
|
+
padding: var(--space-2xs, 0.375rem) var(--space-xs, 0.5rem);
|
|
78
|
+
border: 1px solid var(--color-outline-variant, #ccc);
|
|
79
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
80
|
+
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
81
|
+
background: var(--color-background, #fff);
|
|
82
|
+
color: var(--color-on-surface, inherit);
|
|
83
|
+
transition: border-color 0.15s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.sc-dashboard .field__input:focus {
|
|
87
|
+
outline: none;
|
|
88
|
+
border-color: var(--color-primary, #0066cc);
|
|
89
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary, #0066cc) 20%, transparent);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.sc-dashboard .field__hint {
|
|
93
|
+
color: var(--color-on-offset, #888);
|
|
94
|
+
font: var(--font-caption, 0.75rem/1.4 sans-serif);
|
|
95
|
+
margin: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Radio group */
|
|
99
|
+
.sc-radio-group {
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-wrap: wrap;
|
|
102
|
+
gap: var(--space-s, 0.75rem);
|
|
103
|
+
margin-block-start: var(--space-xs, 0.5rem);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.sc-radio-label {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: var(--space-2xs, 0.375rem);
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
112
|
+
padding: var(--space-2xs, 0.375rem) var(--space-s, 0.75rem);
|
|
113
|
+
border: 1px solid var(--color-outline-variant, #ccc);
|
|
114
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
115
|
+
background: var(--color-background, #fff);
|
|
116
|
+
transition: border-color 0.15s, background 0.15s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.sc-radio-label:has(input:checked) {
|
|
120
|
+
border-color: var(--color-primary, #0066cc);
|
|
121
|
+
background: color-mix(in srgb, var(--color-primary, #0066cc) 8%, var(--color-background, #fff));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Color picker compound */
|
|
125
|
+
.sc-color-picker {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: var(--space-xs, 0.5rem);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.sc-color-picker__swatch {
|
|
132
|
+
width: 2.5rem;
|
|
133
|
+
height: 2.25rem;
|
|
134
|
+
padding: 2px;
|
|
135
|
+
border: 1px solid var(--color-outline-variant, #ccc);
|
|
136
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
137
|
+
background: var(--color-background, #fff);
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
flex-shrink: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.sc-color-picker .field__input {
|
|
143
|
+
flex: 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Surface tones grid */
|
|
147
|
+
.sc-tones-grid {
|
|
148
|
+
display: grid;
|
|
149
|
+
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
|
|
150
|
+
gap: var(--space-s, 0.75rem);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Two-column layout for branding (form + preview) */
|
|
154
|
+
.sc-branding-layout {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-columns: 1fr 480px;
|
|
157
|
+
gap: var(--space-xl, 2rem);
|
|
158
|
+
align-items: start;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@media (max-width: 1024px) {
|
|
162
|
+
.sc-branding-layout {
|
|
163
|
+
grid-template-columns: 1fr;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.sc-preview-panel {
|
|
168
|
+
position: sticky;
|
|
169
|
+
top: var(--space-m, 1rem);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.sc-preview-panel h3 {
|
|
173
|
+
font: var(--font-heading, bold 1rem/1.4 sans-serif);
|
|
174
|
+
margin-block-end: var(--space-s, 0.75rem);
|
|
175
|
+
color: var(--color-on-surface, inherit);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.sc-preview-iframe {
|
|
179
|
+
width: 100%;
|
|
180
|
+
height: 600px;
|
|
181
|
+
border: 1px solid var(--color-outline-variant, #ccc);
|
|
182
|
+
border-radius: var(--border-radius-small, 0.5rem);
|
|
183
|
+
background: var(--color-background, #fff);
|
|
184
|
+
}
|
|
185
|
+
</style>
|
|
186
|
+
|
|
187
|
+
<header class="page-header">
|
|
188
|
+
<h1 class="page-header__title">{{ __('siteConfig.title') }}</h1>
|
|
189
|
+
</header>
|
|
190
|
+
|
|
191
|
+
{% include "partials/tab-strip.njk" %}
|
|
192
|
+
|
|
193
|
+
{% if request.query.saved %}
|
|
194
|
+
<div class="hp-success">
|
|
195
|
+
<p>{{ __('siteConfig.common.saved') }}</p>
|
|
196
|
+
</div>
|
|
197
|
+
{% endif %}
|
|
198
|
+
|
|
199
|
+
<div class="sc-branding-layout">
|
|
200
|
+
|
|
201
|
+
<form method="post" action="/site-config/branding" class="sc-dashboard">
|
|
202
|
+
|
|
203
|
+
{# Surface Preset #}
|
|
204
|
+
<section class="sc-section">
|
|
205
|
+
<h2>{{ __('siteConfig.branding.surfacePreset') }}</h2>
|
|
206
|
+
<p class="sc-section__desc">Choose a base surface palette or define your own custom tones.</p>
|
|
207
|
+
<div class="sc-radio-group">
|
|
208
|
+
{% for preset in ['warm-stone', 'cool-slate', 'neutral-zinc', 'custom'] %}
|
|
209
|
+
<label class="sc-radio-label">
|
|
210
|
+
<input type="radio" name="surfacePreset" value="{{ preset }}"
|
|
211
|
+
{% if config.branding.surfacePreset == preset %}checked{% endif %}>
|
|
212
|
+
{{ preset }}
|
|
213
|
+
</label>
|
|
214
|
+
{% endfor %}
|
|
215
|
+
</div>
|
|
216
|
+
</section>
|
|
217
|
+
|
|
218
|
+
{# Custom Surface Tones #}
|
|
219
|
+
<section class="sc-section">
|
|
220
|
+
<h2>{{ __('siteConfig.branding.surfaceCustom') }}</h2>
|
|
221
|
+
<p class="sc-section__desc">11 tones from lightest (50) to darkest (950). Only applied when preset is set to "custom".</p>
|
|
222
|
+
<div class="sc-tones-grid">
|
|
223
|
+
{% for tone in [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] %}
|
|
224
|
+
{% set toneVal = config.branding.surfaceCustom[tone] if config.branding.surfaceCustom else '#888888' %}
|
|
225
|
+
{{ cp.colorPicker('surfaceCustom_' + tone, 'surface-' + tone, toneVal) }}
|
|
226
|
+
{% endfor %}
|
|
227
|
+
</div>
|
|
228
|
+
</section>
|
|
229
|
+
|
|
230
|
+
{# Accent Colour #}
|
|
231
|
+
<section class="sc-section">
|
|
232
|
+
<h2>{{ __('siteConfig.branding.accent') }}</h2>
|
|
233
|
+
<p class="sc-section__desc">Base hue used to derive the accent scale.</p>
|
|
234
|
+
<div class="hp-field-grid">
|
|
235
|
+
<div class="field--full">
|
|
236
|
+
{{ cp.colorPicker('accentBase', __('siteConfig.branding.accent'), config.branding.accentBase or '#0066cc') }}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</section>
|
|
240
|
+
|
|
241
|
+
{# Brand Tokens #}
|
|
242
|
+
<section class="sc-section">
|
|
243
|
+
<h2>{{ __('siteConfig.branding.brandTokens') }}</h2>
|
|
244
|
+
<p class="sc-section__desc">Semantic colours for UI states.</p>
|
|
245
|
+
<div class="hp-field-grid">
|
|
246
|
+
{% for token in ['primary', 'link', 'focus', 'success', 'warning', 'danger'] %}
|
|
247
|
+
{{ cp.colorPicker('colors_' + token, __('siteConfig.branding.' + token), config.branding.colors[token] or '#888888') }}
|
|
248
|
+
{% endfor %}
|
|
67
249
|
</div>
|
|
250
|
+
</section>
|
|
251
|
+
|
|
252
|
+
{# Typography #}
|
|
253
|
+
<section class="sc-section">
|
|
254
|
+
<h2>{{ __('siteConfig.branding.typography') }}</h2>
|
|
255
|
+
<div class="hp-field-grid">
|
|
256
|
+
|
|
257
|
+
<div class="field">
|
|
258
|
+
<label class="field__label" for="typography_sans">{{ __('siteConfig.branding.fontSans') }}</label>
|
|
259
|
+
<select class="field__input" id="typography_sans" name="typography_sans">
|
|
260
|
+
{% for f in curatedFonts.sans %}
|
|
261
|
+
<option value="{{ f }}" {% if config.branding.typography.sans == f %}selected{% endif %}>{{ f }}</option>
|
|
262
|
+
{% endfor %}
|
|
263
|
+
</select>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="field">
|
|
267
|
+
<label class="field__label" for="typography_serif">{{ __('siteConfig.branding.fontSerif') }}</label>
|
|
268
|
+
<select class="field__input" id="typography_serif" name="typography_serif">
|
|
269
|
+
{% for f in curatedFonts.serif %}
|
|
270
|
+
<option value="{{ f }}" {% if config.branding.typography.serif == f %}selected{% endif %}>{{ f }}</option>
|
|
271
|
+
{% endfor %}
|
|
272
|
+
</select>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div class="field">
|
|
276
|
+
<label class="field__label" for="typography_mono">{{ __('siteConfig.branding.fontMono') }}</label>
|
|
277
|
+
<select class="field__input" id="typography_mono" name="typography_mono">
|
|
278
|
+
{% for f in curatedFonts.mono %}
|
|
279
|
+
<option value="{{ f }}" {% if config.branding.typography.mono == f %}selected{% endif %}>{{ f }}</option>
|
|
280
|
+
{% endfor %}
|
|
281
|
+
</select>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div class="field">
|
|
285
|
+
<label class="field__label" for="typography_hosting">{{ __('siteConfig.branding.fontHosting') }}</label>
|
|
286
|
+
<select class="field__input" id="typography_hosting" name="typography_hosting">
|
|
287
|
+
<option value="self" {% if config.branding.typography.hosting == 'self' %}selected{% endif %}>Self-hosted</option>
|
|
288
|
+
<option value="bunny" {% if config.branding.typography.hosting == 'bunny' %}selected{% endif %}>Bunny Fonts</option>
|
|
289
|
+
</select>
|
|
290
|
+
</div>
|
|
68
291
|
|
|
69
|
-
<div class="site-config__preview-column">
|
|
70
|
-
<h3>{{ __('siteConfig.common.preview') }}</h3>
|
|
71
|
-
{# TODO Task 10: /site-config/api/preview endpoint not yet implemented; iframe will 404 until then #}
|
|
72
|
-
<iframe src="/site-config/api/preview" class="site-config__preview-iframe" title="{{ __('siteConfig.common.preview') }}"></iframe>
|
|
73
292
|
</div>
|
|
293
|
+
</section>
|
|
294
|
+
|
|
295
|
+
<div class="button-group">
|
|
296
|
+
<button type="submit" class="button button--primary">{{ __('siteConfig.common.save') }}</button>
|
|
74
297
|
</div>
|
|
298
|
+
|
|
75
299
|
</form>
|
|
300
|
+
|
|
301
|
+
{# Live preview panel #}
|
|
302
|
+
<aside class="sc-preview-panel">
|
|
303
|
+
<h3>{{ __('siteConfig.common.preview') }}</h3>
|
|
304
|
+
{# Preview iframe — renders a styled sample page at /site-config/api/preview #}
|
|
305
|
+
<iframe src="/site-config/api/preview"
|
|
306
|
+
class="sc-preview-iframe"
|
|
307
|
+
title="{{ __('siteConfig.common.preview') }}"></iframe>
|
|
308
|
+
</aside>
|
|
309
|
+
|
|
310
|
+
</div>
|
|
76
311
|
{% endblock %}
|
|
@@ -3,33 +3,164 @@
|
|
|
3
3
|
{% block title %}{{ __('siteConfig.features.title') }}{% endblock %}
|
|
4
4
|
|
|
5
5
|
{% block content %}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 %}
|
|
35
166
|
{% endblock %}
|
|
@@ -3,40 +3,170 @@
|
|
|
3
3
|
{% block title %}{{ __('siteConfig.identity.title') }}{% endblock %}
|
|
4
4
|
|
|
5
5
|
{% block content %}
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
<style>
|
|
7
|
+
.sc-dashboard {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: var(--space-xl, 2rem);
|
|
11
|
+
}
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
/* Field grid for two-column layouts */
|
|
41
|
+
.hp-field-grid {
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-columns: 1fr 1fr;
|
|
44
|
+
gap: var(--space-m, 1rem) var(--space-s, 0.75rem);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.hp-field-grid .field--full {
|
|
48
|
+
grid-column: 1 / -1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@media (max-width: 640px) {
|
|
52
|
+
.hp-field-grid {
|
|
53
|
+
grid-template-columns: 1fr;
|
|
54
|
+
}
|
|
55
|
+
.hp-field-grid .field--full {
|
|
56
|
+
grid-column: 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Field styling */
|
|
61
|
+
.sc-dashboard .field {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: 0.25rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.sc-dashboard .field__label {
|
|
68
|
+
display: block;
|
|
69
|
+
font: var(--font-caption, 0.75rem/1.4 sans-serif);
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
color: var(--color-on-surface, inherit);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.sc-dashboard .field__input {
|
|
75
|
+
width: 100%;
|
|
76
|
+
padding: var(--space-2xs, 0.375rem) var(--space-xs, 0.5rem);
|
|
77
|
+
border: 1px solid var(--color-outline-variant, #ccc);
|
|
78
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
79
|
+
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
80
|
+
background: var(--color-background, #fff);
|
|
81
|
+
color: var(--color-on-surface, inherit);
|
|
82
|
+
transition: border-color 0.15s;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.sc-dashboard .field__input:focus {
|
|
86
|
+
outline: none;
|
|
87
|
+
border-color: var(--color-primary, #0066cc);
|
|
88
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary, #0066cc) 20%, transparent);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.sc-dashboard textarea.field__input {
|
|
92
|
+
resize: vertical;
|
|
93
|
+
min-height: 4.5rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.sc-dashboard .field__hint {
|
|
97
|
+
color: var(--color-on-offset, #888);
|
|
98
|
+
font: var(--font-caption, 0.75rem/1.4 sans-serif);
|
|
99
|
+
margin: 0;
|
|
100
|
+
}
|
|
101
|
+
</style>
|
|
102
|
+
|
|
103
|
+
<header class="page-header">
|
|
104
|
+
<h1 class="page-header__title">{{ __('siteConfig.title') }}</h1>
|
|
105
|
+
</header>
|
|
106
|
+
|
|
107
|
+
{% include "partials/tab-strip.njk" %}
|
|
108
|
+
|
|
109
|
+
{% if request.query.saved %}
|
|
110
|
+
<div class="hp-success">
|
|
111
|
+
<p>{{ __('siteConfig.common.saved') }}</p>
|
|
112
|
+
</div>
|
|
113
|
+
{% endif %}
|
|
114
|
+
|
|
115
|
+
<form method="post" action="/site-config/identity" class="sc-dashboard">
|
|
116
|
+
|
|
117
|
+
<section class="sc-section">
|
|
118
|
+
<h2>{{ __('siteConfig.identity.title') }}</h2>
|
|
119
|
+
<div class="hp-field-grid">
|
|
120
|
+
|
|
121
|
+
<div class="field field--full">
|
|
122
|
+
<label class="field__label" for="name">{{ __('siteConfig.identity.name') }}</label>
|
|
123
|
+
<input class="field__input" type="text" id="name" name="name"
|
|
124
|
+
value="{{ config.identity.name or '' }}" required>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div class="field field--full">
|
|
128
|
+
<label class="field__label" for="description">{{ __('siteConfig.identity.description') }}</label>
|
|
129
|
+
<textarea class="field__input" id="description" name="description" rows="2">{{ config.identity.description or '' }}</textarea>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div class="field field--full">
|
|
133
|
+
<label class="field__label" for="tagline">{{ __('siteConfig.identity.tagline') }}</label>
|
|
134
|
+
<input class="field__input" type="text" id="tagline" name="tagline"
|
|
135
|
+
value="{{ config.identity.tagline or '' }}">
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="field">
|
|
139
|
+
<label class="field__label" for="defaultAuthor">{{ __('siteConfig.identity.defaultAuthor') }}</label>
|
|
140
|
+
<input class="field__input" type="text" id="defaultAuthor" name="defaultAuthor"
|
|
141
|
+
value="{{ config.identity.defaultAuthor or '' }}">
|
|
34
142
|
</div>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<
|
|
143
|
+
|
|
144
|
+
<div class="field">
|
|
145
|
+
<label class="field__label" for="defaultOgImage">{{ __('siteConfig.identity.defaultOgImage') }}</label>
|
|
146
|
+
<input class="field__input" type="url" id="defaultOgImage" name="defaultOgImage"
|
|
147
|
+
value="{{ config.identity.defaultOgImage or '' }}">
|
|
38
148
|
</div>
|
|
149
|
+
|
|
150
|
+
<div class="field">
|
|
151
|
+
<label class="field__label" for="locale">{{ __('siteConfig.identity.locale') }}</label>
|
|
152
|
+
<input class="field__input" type="text" id="locale" name="locale"
|
|
153
|
+
value="{{ config.identity.locale or 'en' }}" maxlength="5">
|
|
154
|
+
<p class="field__hint">e.g. en, fr, en-GB</p>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div class="field">
|
|
158
|
+
<label class="field__label" for="timezone">{{ __('siteConfig.identity.timezone') }}</label>
|
|
159
|
+
<input class="field__input" type="text" id="timezone" name="timezone"
|
|
160
|
+
value="{{ config.identity.timezone or 'UTC' }}">
|
|
161
|
+
<p class="field__hint">e.g. UTC, Europe/Paris, America/New_York</p>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
39
164
|
</div>
|
|
40
|
-
|
|
41
|
-
|
|
165
|
+
</section>
|
|
166
|
+
|
|
167
|
+
<div class="button-group">
|
|
168
|
+
<button type="submit" class="button button--primary">{{ __('siteConfig.common.save') }}</button>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
</form>
|
|
42
172
|
{% endblock %}
|
|
@@ -3,53 +3,229 @@
|
|
|
3
3
|
{% block title %}{{ __('siteConfig.layout.title') }}{% endblock %}
|
|
4
4
|
|
|
5
5
|
{% block content %}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
/* Field grid for two-column layouts */
|
|
41
|
+
.hp-field-grid {
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-columns: 1fr 1fr;
|
|
44
|
+
gap: var(--space-m, 1rem) var(--space-s, 0.75rem);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.hp-field-grid .field--full {
|
|
48
|
+
grid-column: 1 / -1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@media (max-width: 640px) {
|
|
52
|
+
.hp-field-grid {
|
|
53
|
+
grid-template-columns: 1fr;
|
|
54
|
+
}
|
|
55
|
+
.hp-field-grid .field--full {
|
|
56
|
+
grid-column: 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Field styling */
|
|
61
|
+
.sc-dashboard .field {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: 0.25rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.sc-dashboard .field__label {
|
|
68
|
+
display: block;
|
|
69
|
+
font: var(--font-caption, 0.75rem/1.4 sans-serif);
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
color: var(--color-on-surface, inherit);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.sc-dashboard .field__input {
|
|
75
|
+
width: 100%;
|
|
76
|
+
padding: var(--space-2xs, 0.375rem) var(--space-xs, 0.5rem);
|
|
77
|
+
border: 1px solid var(--color-outline-variant, #ccc);
|
|
78
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
79
|
+
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
80
|
+
background: var(--color-background, #fff);
|
|
81
|
+
color: var(--color-on-surface, inherit);
|
|
82
|
+
transition: border-color 0.15s;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.sc-dashboard .field__input:focus {
|
|
86
|
+
outline: none;
|
|
87
|
+
border-color: var(--color-primary, #0066cc);
|
|
88
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary, #0066cc) 20%, transparent);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.sc-dashboard .field__hint {
|
|
92
|
+
color: var(--color-on-offset, #888);
|
|
93
|
+
font: var(--font-caption, 0.75rem/1.4 sans-serif);
|
|
94
|
+
margin: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Checkbox toggle */
|
|
98
|
+
.sc-checkbox-label {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: var(--space-xs, 0.5rem);
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Nav items table */
|
|
107
|
+
.sc-nav-table {
|
|
108
|
+
width: 100%;
|
|
109
|
+
border-collapse: collapse;
|
|
110
|
+
}
|
|
31
111
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
112
|
+
.sc-nav-table th {
|
|
113
|
+
text-align: left;
|
|
114
|
+
font: var(--font-caption, 0.75rem/1.4 sans-serif);
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
color: var(--color-on-offset, #666);
|
|
117
|
+
padding: var(--space-xs, 0.5rem);
|
|
118
|
+
border-bottom: 2px solid var(--color-outline-variant, #ddd);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.sc-nav-table td {
|
|
122
|
+
padding: var(--space-xs, 0.5rem);
|
|
123
|
+
border-bottom: 1px solid var(--color-outline-variant, #eee);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.sc-nav-table td:last-child {
|
|
127
|
+
border-bottom-color: transparent;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.sc-nav-table input {
|
|
131
|
+
width: 100%;
|
|
132
|
+
padding: var(--space-2xs, 0.375rem) var(--space-xs, 0.5rem);
|
|
133
|
+
border: 1px solid var(--color-outline-variant, #ccc);
|
|
134
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
135
|
+
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
136
|
+
background: var(--color-background, #fff);
|
|
137
|
+
color: var(--color-on-surface, inherit);
|
|
138
|
+
transition: border-color 0.15s;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.sc-nav-table input:focus {
|
|
142
|
+
outline: none;
|
|
143
|
+
border-color: var(--color-primary, #0066cc);
|
|
144
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary, #0066cc) 20%, transparent);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.sc-nav-table tr:last-child td {
|
|
148
|
+
padding-top: var(--space-s, 0.75rem);
|
|
149
|
+
}
|
|
150
|
+
</style>
|
|
151
|
+
|
|
152
|
+
<header class="page-header">
|
|
153
|
+
<h1 class="page-header__title">{{ __('siteConfig.title') }}</h1>
|
|
154
|
+
</header>
|
|
155
|
+
|
|
156
|
+
{% include "partials/tab-strip.njk" %}
|
|
157
|
+
|
|
158
|
+
{% if request.query.saved %}
|
|
159
|
+
<div class="hp-success">
|
|
160
|
+
<p>{{ __('siteConfig.common.saved') }}</p>
|
|
161
|
+
</div>
|
|
162
|
+
{% endif %}
|
|
163
|
+
|
|
164
|
+
<form method="post" action="/site-config/layout" class="sc-dashboard">
|
|
165
|
+
|
|
166
|
+
{# Layout preset + sidebar #}
|
|
167
|
+
<section class="sc-section">
|
|
168
|
+
<h2>{{ __('siteConfig.layout.title') }}</h2>
|
|
169
|
+
<div class="hp-field-grid">
|
|
170
|
+
|
|
171
|
+
<div class="field">
|
|
172
|
+
<label class="field__label" for="preset">{{ __('siteConfig.layout.preset') }}</label>
|
|
173
|
+
<select class="field__input" id="preset" name="preset">
|
|
174
|
+
{% for p in presets %}
|
|
175
|
+
<option value="{{ p }}" {% if config.layout.preset == p %}selected{% endif %}>{{ p }}</option>
|
|
44
176
|
{% endfor %}
|
|
177
|
+
</select>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="field">
|
|
181
|
+
<span class="field__label">{{ __('siteConfig.layout.sidebarSide') }}</span>
|
|
182
|
+
<select class="field__input" id="sidebarSide" name="sidebarSide">
|
|
183
|
+
<option value="left" {% if config.layout.sidebarSide == 'left' %}selected{% endif %}>Left</option>
|
|
184
|
+
<option value="right" {% if config.layout.sidebarSide == 'right' %}selected{% endif %}>Right</option>
|
|
185
|
+
</select>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="field field--full">
|
|
189
|
+
<label class="sc-checkbox-label">
|
|
190
|
+
<input type="checkbox" name="sidebarEnabled"
|
|
191
|
+
{% if config.layout.sidebarEnabled %}checked{% endif %}>
|
|
192
|
+
{{ __('siteConfig.layout.sidebarEnabled') }}
|
|
193
|
+
</label>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
</div>
|
|
197
|
+
</section>
|
|
198
|
+
|
|
199
|
+
{# Navigation items #}
|
|
200
|
+
<section class="sc-section">
|
|
201
|
+
<h2>{{ __('siteConfig.layout.navItems') }}</h2>
|
|
202
|
+
<p class="sc-section__desc">Add navigation links. Leave empty rows blank — they will be ignored on save. Add a trailing row to create a new item.</p>
|
|
203
|
+
<table class="sc-nav-table">
|
|
204
|
+
<thead>
|
|
205
|
+
<tr>
|
|
206
|
+
<th>Label</th>
|
|
207
|
+
<th>URL</th>
|
|
208
|
+
</tr>
|
|
209
|
+
</thead>
|
|
210
|
+
<tbody>
|
|
211
|
+
{% for item in config.layout.navItems %}
|
|
45
212
|
<tr>
|
|
46
|
-
<td><input type="text" name="navLabel"
|
|
47
|
-
<td><input type="text" name="navUrl" placeholder="/path or https://..."></td>
|
|
213
|
+
<td><input type="text" name="navLabel" value="{{ item.label or '' }}" placeholder="Home"></td>
|
|
214
|
+
<td><input type="text" name="navUrl" value="{{ item.url or '' }}" placeholder="/path or https://..."></td>
|
|
48
215
|
</tr>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
216
|
+
{% endfor %}
|
|
217
|
+
{# Trailing empty row for adding a new entry #}
|
|
218
|
+
<tr>
|
|
219
|
+
<td><input type="text" name="navLabel" value="" placeholder="New label"></td>
|
|
220
|
+
<td><input type="text" name="navUrl" value="" placeholder="/path or https://..."></td>
|
|
221
|
+
</tr>
|
|
222
|
+
</tbody>
|
|
223
|
+
</table>
|
|
224
|
+
</section>
|
|
225
|
+
|
|
226
|
+
<div class="button-group">
|
|
227
|
+
<button type="submit" class="button button--primary">{{ __('siteConfig.common.save') }}</button>
|
|
228
|
+
</div>
|
|
52
229
|
|
|
53
|
-
|
|
54
|
-
</form>
|
|
230
|
+
</form>
|
|
55
231
|
{% endblock %}
|