@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.
@@ -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
- const message = encodeURIComponent(res.locals.__("siteConfig.common.saved"));
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
- const message = encodeURIComponent(
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
- const message = encodeURIComponent(res.locals.__("siteConfig.common.saved"));
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
- const message = encodeURIComponent(
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
@@ -33,9 +33,7 @@
33
33
  "fontSans": "Sans-serif font",
34
34
  "fontSerif": "Serif font",
35
35
  "fontMono": "Monospace font",
36
- "fontHosting": "Font hosting",
37
- "logo": "Logo",
38
- "favicon": "Favicon"
36
+ "fontHosting": "Font hosting"
39
37
  },
40
38
  "layout": {
41
39
  "title": "Layout",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-site-config",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.3",
4
4
  "type": "module",
5
5
  "description": "Site identity, branding, layout, and feature flag configuration for Indiekit",
6
6
  "main": "index.js",
@@ -1,11 +1,26 @@
1
- {# locals: name (string), label (string), value (hex string) #}
2
- <div class="form-field form-field--color">
3
- <label for="{{ name }}">{{ label }}</label>
4
- <div class="color-picker">
5
- <input type="color" class="color-picker__swatch" id="{{ name }}-swatch" value="{{ value }}"
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 type="text" class="color-picker__hex" id="{{ name }}" name="{{ name }}"
8
- value="{{ value }}" pattern="^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$" required
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
- {# locals: activeTab (string, one of identity|branding|layout|features) #}
2
- <nav class="site-config__tabs" aria-label="{{ __('siteConfig.title') }}">
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', href: '/site-config/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="site-config__tab {% if activeTab == tab.key %}site-config__tab--active{% endif %}"
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
- <h1>{{ __('siteConfig.title') }}</h1>
7
- {% include "partials/tab-strip.njk" %}
8
-
9
- <form method="post" action="/site-config/branding" class="site-config__form site-config__form--branding">
10
- <div class="site-config__columns">
11
- <div class="site-config__form-column">
12
- <fieldset>
13
- <legend>{{ __('siteConfig.branding.surfacePreset') }}</legend>
14
- <div class="form-radio-group">
15
- {% for preset in ['warm-stone', 'cool-slate', 'neutral-zinc', 'custom'] %}
16
- <label class="form-radio">
17
- <input type="radio" name="surfacePreset" value="{{ preset }}"
18
- {% if config.branding.surfacePreset == preset %}checked{% endif %}>
19
- <span>{{ preset }}</span>
20
- </label>
21
- {% endfor %}
22
- </div>
23
- </fieldset>
24
-
25
- <fieldset class="surface-custom" data-show-when="surfacePreset=custom">
26
- <legend>{{ __('siteConfig.branding.surfaceCustom') }}</legend>
27
- {% for tone in [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] %}
28
- {% set v = config.branding.surfaceCustom and config.branding.surfaceCustom[tone] or '#888888' %}
29
- {% include "partials/color-picker.njk" with { name: 'surfaceCustom_' + tone, label: 'surface-' + tone, value: v } %}
30
- {% endfor %}
31
- </fieldset>
32
-
33
- <fieldset>
34
- <legend>{{ __('siteConfig.branding.accent') }}</legend>
35
- {% include "partials/color-picker.njk" with { name: 'accentBase', label: __('siteConfig.branding.accent'), value: config.branding.accentBase } %}
36
- </fieldset>
37
-
38
- <fieldset>
39
- <legend>{{ __('siteConfig.branding.brandTokens') }}</legend>
40
- {% for token in ['primary', 'link', 'focus', 'success', 'warning', 'danger'] %}
41
- {% include "partials/color-picker.njk" with { name: 'colors_' + token, label: __('siteConfig.branding.' + token), value: config.branding.colors[token] } %}
42
- {% endfor %}
43
- </fieldset>
44
-
45
- <fieldset>
46
- <legend>{{ __('siteConfig.branding.typography') }}</legend>
47
- {% for cat in ['sans', 'serif', 'mono'] %}
48
- <div class="form-field">
49
- <label for="typography_{{ cat }}">{{ __('siteConfig.branding.font' + cat | capitalize) }}</label>
50
- <select id="typography_{{ cat }}" name="typography_{{ cat }}">
51
- {% for f in curatedFonts[cat] %}
52
- <option value="{{ f }}" {% if config.branding.typography[cat] == f %}selected{% endif %}>{{ f }}</option>
53
- {% endfor %}
54
- </select>
55
- </div>
56
- {% endfor %}
57
- <div class="form-field">
58
- <label for="typography_hosting">{{ __('siteConfig.branding.fontHosting') }}</label>
59
- <select id="typography_hosting" name="typography_hosting">
60
- <option value="self" {% if config.branding.typography.hosting == 'self' %}selected{% endif %}>self-hosted</option>
61
- <option value="bunny" {% if config.branding.typography.hosting == 'bunny' %}selected{% endif %}>Bunny Fonts</option>
62
- </select>
63
- </div>
64
- </fieldset>
65
-
66
- <button type="submit" class="button">{{ __('siteConfig.common.save') }}</button>
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
- <h1>{{ __('siteConfig.title') }}</h1>
7
- {% include "partials/tab-strip.njk" %}
8
-
9
- {% if flags.length == 0 %}
10
- <p class="empty-state">{{ __('siteConfig.features.empty') }}</p>
11
- {% else %}
12
- <form method="post" action="/site-config/features" class="site-config__form">
13
- {% set currentCategory = '' %}
14
- {% for flag in flags %}
15
- {% if flag.category != currentCategory %}
16
- {% if not loop.first %}</fieldset>{% endif %}
17
- <fieldset>
18
- <legend>{{ flag.category }}</legend>
19
- {% set currentCategory = flag.category %}
20
- {% endif %}
21
- <div class="form-field">
22
- <label>
23
- <input type="checkbox" name="feature_{{ flag.key }}"
24
- {% if config.features[flag.key] %}checked{% elif flag.default %}checked{% endif %}>
25
- <strong>{{ flag.label }}</strong>
26
- {% if flag.description %}<span class="hint">{{ flag.description }}</span>{% endif %}
27
- {% if flag.requiresRestart %}<span class="badge">Restart required</span>{% endif %}
28
- </label>
29
- </div>
30
- {% endfor %}
31
- </fieldset>
32
- <button type="submit" class="button">{{ __('siteConfig.common.save') }}</button>
33
- </form>
34
- {% endif %}
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
- <h1>{{ __('siteConfig.title') }}</h1>
7
- {% include "partials/tab-strip.njk" %}
6
+ <style>
7
+ .sc-dashboard {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: var(--space-xl, 2rem);
11
+ }
8
12
 
9
- <form method="post" action="/site-config/identity" class="site-config__form">
10
- <div class="form-field">
11
- <label for="name">{{ __('siteConfig.identity.name') }}</label>
12
- <input type="text" id="name" name="name" value="{{ config.identity.name }}" required>
13
- </div>
14
- <div class="form-field">
15
- <label for="description">{{ __('siteConfig.identity.description') }}</label>
16
- <textarea id="description" name="description" rows="2">{{ config.identity.description }}</textarea>
17
- </div>
18
- <div class="form-field">
19
- <label for="tagline">{{ __('siteConfig.identity.tagline') }}</label>
20
- <input type="text" id="tagline" name="tagline" value="{{ config.identity.tagline }}">
21
- </div>
22
- <div class="form-field">
23
- <label for="defaultAuthor">{{ __('siteConfig.identity.defaultAuthor') }}</label>
24
- <input type="text" id="defaultAuthor" name="defaultAuthor" value="{{ config.identity.defaultAuthor }}">
25
- </div>
26
- <div class="form-field">
27
- <label for="defaultOgImage">{{ __('siteConfig.identity.defaultOgImage') }}</label>
28
- <input type="url" id="defaultOgImage" name="defaultOgImage" value="{{ config.identity.defaultOgImage }}">
29
- </div>
30
- <div class="form-row">
31
- <div class="form-field">
32
- <label for="locale">{{ __('siteConfig.identity.locale') }}</label>
33
- <input type="text" id="locale" name="locale" value="{{ config.identity.locale }}" maxlength="5">
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
- <div class="form-field">
36
- <label for="timezone">{{ __('siteConfig.identity.timezone') }}</label>
37
- <input type="text" id="timezone" name="timezone" value="{{ config.identity.timezone }}">
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
- <button type="submit" class="button">{{ __('siteConfig.common.save') }}</button>
41
- </form>
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
- <h1>{{ __('siteConfig.title') }}</h1>
7
- {% include "partials/tab-strip.njk" %}
8
-
9
- <form method="post" action="/site-config/layout" class="site-config__form">
10
- <div class="form-field">
11
- <label for="preset">{{ __('siteConfig.layout.preset') }}</label>
12
- <select id="preset" name="preset">
13
- {% for p in presets %}
14
- <option value="{{ p }}" {% if config.layout.preset == p %}selected{% endif %}>{{ p }}</option>
15
- {% endfor %}
16
- </select>
17
- </div>
18
- <div class="form-field">
19
- <label>
20
- <input type="checkbox" name="sidebarEnabled" {% if config.layout.sidebarEnabled %}checked{% endif %}>
21
- {{ __('siteConfig.layout.sidebarEnabled') }}
22
- </label>
23
- </div>
24
- <div class="form-field">
25
- <label for="sidebarSide">{{ __('siteConfig.layout.sidebarSide') }}</label>
26
- <select id="sidebarSide" name="sidebarSide">
27
- <option value="left" {% if config.layout.sidebarSide == 'left' %}selected{% endif %}>left</option>
28
- <option value="right" {% if config.layout.sidebarSide == 'right' %}selected{% endif %}>right</option>
29
- </select>
30
- </div>
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
- <fieldset>
33
- <legend>{{ __('siteConfig.layout.navItems') }}</legend>
34
- <table class="form-table">
35
- <thead>
36
- <tr><th>Label</th><th>URL</th></tr>
37
- </thead>
38
- <tbody>
39
- {% for item in config.layout.navItems %}
40
- <tr>
41
- <td><input type="text" name="navLabel" value="{{ item.label }}"></td>
42
- <td><input type="text" name="navUrl" value="{{ item.url }}"></td>
43
- </tr>
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" placeholder="New label"></td>
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
- </tbody>
50
- </table>
51
- </fieldset>
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
- <button type="submit" class="button">{{ __('siteConfig.common.save') }}</button>
54
- </form>
230
+ </form>
55
231
  {% endblock %}