@fluid-app/fluid-cli-theme-dev 0.1.41 → 0.1.43
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/dist/skills/themes-settings-schema/SKILL.md +283 -0
- package/dist/skills/themes-settings-schema/references/css-variables.md +99 -0
- package/dist/skills/themes-settings-schema/references/option-groups.md +66 -0
- package/dist/skills/themes-settings-schema/references/typography-presets.md +138 -0
- package/package.json +2 -2
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: themes-settings-schema
|
|
3
|
+
description: |
|
|
4
|
+
Authors and audits `config/settings_schema.json` in a Fluid theme so the Visual Builder
|
|
5
|
+
actually resolves it — text presets group into cards, fonts and weights link to the
|
|
6
|
+
presets that use them, and rich-text blocks pick up theme typography. Use when writing a
|
|
7
|
+
theme's global settings from scratch, when the Themes panel renders a group as a flat
|
|
8
|
+
list of unrelated controls instead of preset cards, when a font or weight dropdown is
|
|
9
|
+
empty or shows the wrong names, when changing a theme setting does not move the preview,
|
|
10
|
+
or when the rich-text preset picker shows no presets. Covers the exact rules the builder
|
|
11
|
+
classifies settings by (what becomes a text preset, what counts as a font or a weight),
|
|
12
|
+
the `role` / `font_family_ref` / `font_weight_ref` linkage, the CSS-variable contract in
|
|
13
|
+
`layouts/theme.liquid` that makes any of it reach the page, `option_group` tokens that
|
|
14
|
+
feed section dropdowns, and the keys the builder writes back into your schema. Companion
|
|
15
|
+
to `themes-review` — that one reviews a whole theme, this one gets the settings schema
|
|
16
|
+
right.
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Settings Schema Authoring
|
|
20
|
+
|
|
21
|
+
`config/settings_schema.json` has two readers, and they are stricter than the file format suggests:
|
|
22
|
+
|
|
23
|
+
1. **The Visual Builder's Themes panel** — turns the schema into editing UI, and groups typography into per-preset cards.
|
|
24
|
+
2. **The rich-text preset picker** — resolves each text preset into a concrete font size, weight and family so a merchant can apply "H1" to a paragraph.
|
|
25
|
+
|
|
26
|
+
Both infer structure from **ids, types, labels and a few reserved keys**. A schema that lints fine can still render as a flat wall of controls, or produce presets that do nothing. This skill is the contract.
|
|
27
|
+
|
|
28
|
+
> Prerequisite: [`themes-review/references/setting-types.md`](../themes-review/references/setting-types.md) lists every valid `type`, and [`themes-review/references/global-settings.md`](../themes-review/references/global-settings.md) covers the globals pipeline generally. This skill covers what the *builder* additionally requires.
|
|
29
|
+
|
|
30
|
+
## Work in this order
|
|
31
|
+
|
|
32
|
+
1. **Groups** — get the group names right; a few carry behaviour.
|
|
33
|
+
2. **Fonts and weights** — declare the theme's typefaces and weight vocabulary once.
|
|
34
|
+
3. **Text presets** — declare sizes and link each to a font and a weight.
|
|
35
|
+
4. **CSS variables** — wire every setting into `layouts/theme.liquid`, or none of it reaches the page.
|
|
36
|
+
5. **Audit** — run the checks at the bottom.
|
|
37
|
+
|
|
38
|
+
Read [`references/typography-presets.md`](references/typography-presets.md) before step 3 and [`references/css-variables.md`](references/css-variables.md) before step 4. They hold the exact resolution rules; the summaries here are not sufficient to author against.
|
|
39
|
+
|
|
40
|
+
## Step 1 — Groups
|
|
41
|
+
|
|
42
|
+
The outer array is **grouped, not flat**. Each object with a `name` becomes one accordion in the panel.
|
|
43
|
+
|
|
44
|
+
| Group `name` | Behaviour |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `typography` | **The only group rendered as text-preset cards.** Any other name renders a flat list, however the settings are shaped. |
|
|
47
|
+
| `custom_font_sizes`, `custom_colors` | Hidden from the panel. Builder-managed — presets a merchant creates from the rich-text toolbar land here. Do not hand-author them. |
|
|
48
|
+
| `theme_info` | Metadata only, no `settings` array. |
|
|
49
|
+
| anything else | Flat list of controls, in schema order. |
|
|
50
|
+
|
|
51
|
+
Two things the panel silently drops, which surprise most authors:
|
|
52
|
+
|
|
53
|
+
- **A setting without a string `id`, `type` *and* `label` is discarded.** That includes every `{"type": "header", "content": "…"}` — headers render in section schemas but **not** in the Themes panel. Do not rely on them to separate groups of settings; use a real group instead.
|
|
54
|
+
- **A group whose settings all get discarded is dropped entirely.**
|
|
55
|
+
|
|
56
|
+
## Step 2 — Fonts and weights
|
|
57
|
+
|
|
58
|
+
Declare each typeface the theme offers as one `font_picker`, and each weight in the theme's vocabulary as its own setting.
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"name": "fonts",
|
|
63
|
+
"settings": [
|
|
64
|
+
{
|
|
65
|
+
"type": "font_picker",
|
|
66
|
+
"id": "font_family_heading",
|
|
67
|
+
"label": "Heading font",
|
|
68
|
+
"default": "Reckless Neue",
|
|
69
|
+
"options": [
|
|
70
|
+
{ "value": "Reckless Neue", "label": "Reckless Neue" },
|
|
71
|
+
{ "value": "Basis Grotesque", "label": "Basis Grotesque" }
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"type": "font_picker",
|
|
76
|
+
"id": "font_family_body",
|
|
77
|
+
"label": "Body font",
|
|
78
|
+
"default": "Basis Grotesque",
|
|
79
|
+
"options": [
|
|
80
|
+
{ "value": "Reckless Neue", "label": "Reckless Neue" },
|
|
81
|
+
{ "value": "Basis Grotesque", "label": "Basis Grotesque" }
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Name fonts by role in the id, not in the label.** The panel labels a font in a preset's picker from its **id** with the `font_family_` prefix stripped — `font_family_heading` shows as "Heading". This exists because themes habitually label both of their font settings "Font family", which tells a merchant nothing about which is which. Give ids meaning:
|
|
89
|
+
|
|
90
|
+
| Id | Shows in the preset picker as |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `font_family_heading` | Heading |
|
|
93
|
+
| `font_family_body` | Body |
|
|
94
|
+
| `font_family_display` | Display |
|
|
95
|
+
| `brand_typeface` | falls back to the label — no recognisable prefix |
|
|
96
|
+
|
|
97
|
+
`font_family_heading` and `font_family_body` are additionally **reserved**: they are what a preset falls back to when it declares no explicit font. Define at least one of them.
|
|
98
|
+
|
|
99
|
+
Weights are a vocabulary, one setting per step:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"name": "font_weights",
|
|
104
|
+
"settings": [
|
|
105
|
+
{ "type": "range", "id": "font_weight_light", "label": "Light", "min": 100, "max": 900, "step": 100, "default": 300 },
|
|
106
|
+
{ "type": "range", "id": "font_weight_normal", "label": "Normal", "min": 100, "max": 900, "step": 100, "default": 400 },
|
|
107
|
+
{ "type": "range", "id": "font_weight_bold", "label": "Bold", "min": 100, "max": 900, "step": 100, "default": 700 }
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Weights are labelled from their **label**, not their id — "Light", "Bold" are already the names a merchant wants.
|
|
113
|
+
|
|
114
|
+
## Step 3 — Text presets
|
|
115
|
+
|
|
116
|
+
A text preset is a **font-size setting** that links to a font and a weight. Put them in the `typography` group.
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"name": "typography",
|
|
121
|
+
"settings": [
|
|
122
|
+
{
|
|
123
|
+
"type": "range",
|
|
124
|
+
"id": "font_size_h1",
|
|
125
|
+
"label": "H1",
|
|
126
|
+
"min": 32, "max": 120, "step": 1,
|
|
127
|
+
"unit": "px",
|
|
128
|
+
"default": 64,
|
|
129
|
+
"role": "heading",
|
|
130
|
+
"font_family_ref": "font_family_heading",
|
|
131
|
+
"font_weight_ref": "font_weight_bold"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"type": "range",
|
|
135
|
+
"id": "font_size_base",
|
|
136
|
+
"label": "Base",
|
|
137
|
+
"min": 14, "max": 20, "step": 0.5,
|
|
138
|
+
"unit": "px",
|
|
139
|
+
"default": 16,
|
|
140
|
+
"role": "body",
|
|
141
|
+
"font_family_ref": "font_family_body",
|
|
142
|
+
"font_weight_ref": "font_weight_normal"
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| Key | Why |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| `unit` | Appended to the size. Omitted means `px`. |
|
|
151
|
+
| `role` | `heading` or `body`. Picks the default weight (bold / normal) and the fallback font. Without it, an id starting `font_size_h` is read as a heading and everything else as body. **Always set it explicitly** — the id convention is a legacy fallback. |
|
|
152
|
+
| `font_family_ref` | Id of the font setting this preset uses. Resolves **anywhere in the schema**, so the font may live in another group. |
|
|
153
|
+
| `font_weight_ref` | Id of the weight setting this preset uses. Same resolution. |
|
|
154
|
+
|
|
155
|
+
**Refs are authoritative.** A `font_family_ref` pointing at an id that does not exist gives the preset **no font at all** — it does not fall back to the role font. Typos fail loudly rather than silently doing something plausible. A dangling `font_weight_ref` does fall back to the role weight.
|
|
156
|
+
|
|
157
|
+
Both refs are optional. Omit them and the preset derives its font from `role` (`heading` → `font_family_heading`, `body` → `font_family_body`, each falling back to the other when only one exists) and its weight from `role` (`bold` / `normal`). Explicit refs are better: they survive an id rename showing up as a visible break rather than a silent change in meaning.
|
|
158
|
+
|
|
159
|
+
Full rules, including exactly what does and does not become a preset: [`references/typography-presets.md`](references/typography-presets.md).
|
|
160
|
+
|
|
161
|
+
## Step 4 — CSS variables
|
|
162
|
+
|
|
163
|
+
**A schema on its own changes nothing.** `layouts/theme.liquid` must publish each setting as a CSS custom property, and the theme's CSS must consume it:
|
|
164
|
+
|
|
165
|
+
```liquid
|
|
166
|
+
{% style %}
|
|
167
|
+
:root {
|
|
168
|
+
--font_family_heading: {{ settings.font_family_heading }};
|
|
169
|
+
--font_family_body: {{ settings.font_family_body }};
|
|
170
|
+
--font_weight_bold: {{ settings.font_weight_bold }};
|
|
171
|
+
--font_size_h1: {{ settings.font_size_h1 }}px;
|
|
172
|
+
}
|
|
173
|
+
{% endstyle %}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Two rules that are easy to miss:
|
|
177
|
+
|
|
178
|
+
- **The builder matches the exact form `--anything: {{ settings.<id> }}`** inside a `{% style %}` or `<style>` block. A setting with no such declaration still edits fine, but presets referencing it cannot link to a variable, so rich-text content gets a baked literal instead of something that follows later edits.
|
|
179
|
+
- **Name the variable after the setting id.** Any name works for *reading*, but the builder writes and removes `--<id>` when it manages a variable for you. Matching keeps hand-authored and builder-authored declarations from diverging.
|
|
180
|
+
|
|
181
|
+
Details, including the per-preset alias variables the builder maintains and why repointing a preset needs them: [`references/css-variables.md`](references/css-variables.md).
|
|
182
|
+
|
|
183
|
+
## Step 5 — Section dropdowns from theme settings
|
|
184
|
+
|
|
185
|
+
A theme setting can publish itself into a named token list that **section** settings then reference by id, instead of every section hardcoding the same options. See [`references/option-groups.md`](references/option-groups.md).
|
|
186
|
+
|
|
187
|
+
## What the builder writes back
|
|
188
|
+
|
|
189
|
+
The Themes panel edits `config/settings_schema.json` in place. Expect these and leave them alone:
|
|
190
|
+
|
|
191
|
+
| What | Written when |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| `generated_for: "<group or preset>"` on a setting | The builder created that setting — a font added via **Add font**. Only settings carrying this key can be removed through the panel; one you authored is never deleted, because the theme's own Liquid may read it. |
|
|
194
|
+
| New entries in every `font_picker`'s `options` | A merchant added a Google font or uploaded a font file. It becomes selectable on every font in the theme. |
|
|
195
|
+
| `font_family_ref` / `font_weight_ref` on a preset | A merchant repointed that preset. |
|
|
196
|
+
| `--<preset_id>_font_family` / `_font_weight` in `layouts/theme.liquid` | A preset was applied to rich-text content. See [`references/css-variables.md`](references/css-variables.md). |
|
|
197
|
+
|
|
198
|
+
Do not strip `generated_for` by hand: it is the only thing separating a setting the builder may remove from one it must not.
|
|
199
|
+
|
|
200
|
+
## Authoring checklist
|
|
201
|
+
|
|
202
|
+
- [ ] Groups are named objects; the preset group is named exactly `typography`
|
|
203
|
+
- [ ] No reliance on `header` settings for structure in the Themes panel
|
|
204
|
+
- [ ] Every setting has a unique, meaningful `id`, plus `type` and `label`
|
|
205
|
+
- [ ] Font ids carry the role (`font_family_heading`, not `brand_typeface`)
|
|
206
|
+
- [ ] At least one of `font_family_heading` / `font_family_body` exists
|
|
207
|
+
- [ ] Every preset sets `role` explicitly
|
|
208
|
+
- [ ] Every preset sets `unit`
|
|
209
|
+
- [ ] Every `*_ref` points at an id that exists
|
|
210
|
+
- [ ] No size preset's id or label contains "font weight" (it would stop being a preset — see the pitfalls reference)
|
|
211
|
+
- [ ] `layouts/theme.liquid` declares `--<id>: {{ settings.<id> }}` for every font, weight and size
|
|
212
|
+
- [ ] The theme's CSS consumes those variables rather than hardcoding fonts
|
|
213
|
+
|
|
214
|
+
## Audit
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Groups, in order — is there one named exactly "typography"?
|
|
218
|
+
jq -r '.[].name' config/settings_schema.json
|
|
219
|
+
|
|
220
|
+
# Every setting id, with its type — duplicates here are a bug
|
|
221
|
+
jq -r '.[].settings[]? | "\(.id // "-")\t\(.type)"' config/settings_schema.json | sort | uniq -d
|
|
222
|
+
|
|
223
|
+
# Presets and what they link to
|
|
224
|
+
jq -r '.[] | select(.name=="typography") | .settings[]?
|
|
225
|
+
| select(.font_family_ref or .font_weight_ref or (.id // "" | startswith("font_size")))
|
|
226
|
+
| "\(.id)\trole=\(.role // "-")\tfamily=\(.font_family_ref // "-")\tweight=\(.font_weight_ref // "-")\tunit=\(.unit // "px")"' \
|
|
227
|
+
config/settings_schema.json
|
|
228
|
+
|
|
229
|
+
# Refs that point at nothing — must print nothing
|
|
230
|
+
jq -r '[.[].settings[]?.id] as $ids
|
|
231
|
+
| .[].settings[]? | select(.font_family_ref and (.font_family_ref | IN($ids[]) | not))
|
|
232
|
+
| "DANGLING family_ref: \(.id) -> \(.font_family_ref)"' config/settings_schema.json
|
|
233
|
+
jq -r '[.[].settings[]?.id] as $ids
|
|
234
|
+
| .[].settings[]? | select(.font_weight_ref and (.font_weight_ref | IN($ids[]) | not))
|
|
235
|
+
| "DANGLING weight_ref: \(.id) -> \(.font_weight_ref)"' config/settings_schema.json
|
|
236
|
+
|
|
237
|
+
# Fonts, weights and presets the layout does not publish as a CSS variable.
|
|
238
|
+
# Anything listed here cannot be linked, so content applying it gets a baked
|
|
239
|
+
# literal instead of something that follows later edits.
|
|
240
|
+
#
|
|
241
|
+
# Two halves, and both matter:
|
|
242
|
+
#
|
|
243
|
+
# LINKED what the builder can actually see — a `--x: {{ settings.<id> }}`
|
|
244
|
+
# custom property *inside* a {% style %} or <style> block. A
|
|
245
|
+
# {{ settings.<id> }} in a meta tag, an inline style= attribute, or
|
|
246
|
+
# `font-family: {{ settings.x }}` is output, not a variable, and
|
|
247
|
+
# grepping the whole file for it reports a theme as wired when it
|
|
248
|
+
# is not.
|
|
249
|
+
#
|
|
250
|
+
# the jq selects by the classification rules rather than a list of types:
|
|
251
|
+
# a font may be typed font_picker/font_family/font, a weight is
|
|
252
|
+
# usually a plain `range` named for one, and a size may be typed
|
|
253
|
+
# anything at all.
|
|
254
|
+
LINKED=$(awk '
|
|
255
|
+
/<style|\{%[ \t]*style[ \t]*%\}/ { inblock = 1 }
|
|
256
|
+
inblock { print }
|
|
257
|
+
/<\/style>|\{%[ \t]*endstyle[ \t]*%\}/ { inblock = 0 }
|
|
258
|
+
' layouts/theme.liquid \
|
|
259
|
+
| grep -oE -- '--[A-Za-z0-9_-]+[ \t]*:[ \t]*\{\{[ \t]*settings\.[A-Za-z0-9_]+' \
|
|
260
|
+
| sed -E 's/.*settings\.//' | sort -u)
|
|
261
|
+
|
|
262
|
+
jq -r '
|
|
263
|
+
.[] | .name as $group | .settings[]?
|
|
264
|
+
| select(.id)
|
|
265
|
+
| select(
|
|
266
|
+
((.type // "") | test("^(font_picker|font_family|font|font_weight)$"))
|
|
267
|
+
or ((.role // "") | test("^(font_family|font_weight)$"))
|
|
268
|
+
or (.id | test("(^|[_-])font[_-]?weight($|[_-])"; "i"))
|
|
269
|
+
or ((.label // "") | test("font\\s*weight"; "i"))
|
|
270
|
+
or ($group == "typography")
|
|
271
|
+
)
|
|
272
|
+
| .id' config/settings_schema.json | while read -r id; do
|
|
273
|
+
printf '%s\n' "$LINKED" | grep -qx "$id" || echo "NO CSS VAR: $id"
|
|
274
|
+
done
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Then run the repo's own validator:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
fluid theme lint --json
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`settings_schema.json` is not Liquid, so the linter does not read it — these checks are the only ones that cover it.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# CSS variables — the contract between the schema and the page
|
|
2
|
+
|
|
3
|
+
> Part of the `themes-settings-schema` skill. See [`../SKILL.md`](../SKILL.md) for the authoring workflow.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- Why the layout is not optional
|
|
8
|
+
- Declaring a setting's variable
|
|
9
|
+
- Preset variables, and why they exist
|
|
10
|
+
- Consuming variables in theme CSS
|
|
11
|
+
- Custom fonts
|
|
12
|
+
- Pitfalls
|
|
13
|
+
|
|
14
|
+
## Why the layout is not optional
|
|
15
|
+
|
|
16
|
+
`config/settings_schema.json` describes *what a merchant may change*. `layouts/theme.liquid` is what makes a change reach the page. A theme with a perfect schema and no `:root` block gives a merchant a panel full of controls that do nothing.
|
|
17
|
+
|
|
18
|
+
```liquid
|
|
19
|
+
{% style %}
|
|
20
|
+
:root {
|
|
21
|
+
--font_family_heading: {{ settings.font_family_heading }};
|
|
22
|
+
--font_family_body: {{ settings.font_family_body }};
|
|
23
|
+
--font_weight_bold: {{ settings.font_weight_bold }};
|
|
24
|
+
--font_size_h1: {{ settings.font_size_h1 }}px;
|
|
25
|
+
}
|
|
26
|
+
{% endstyle %}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Either `{% style %}…{% endstyle %}` or `<style>…</style>` works.
|
|
30
|
+
|
|
31
|
+
## Declaring a setting's variable
|
|
32
|
+
|
|
33
|
+
The builder scans the layout for exactly this shape:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
--<any-name>: {{ settings.<setting_id> }}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
and builds a table of *setting id → variable name*. Consequences:
|
|
40
|
+
|
|
41
|
+
- **A setting with no such declaration cannot be linked.** It still edits, and the preview still re-renders, but a text preset that references it resolves to a literal value baked into content at the moment it was applied, rather than to something that follows later edits.
|
|
42
|
+
- **The variable name does not have to match the id** for reading. `--heading-font: {{ settings.font_family_heading }}` is found.
|
|
43
|
+
- **But match it anyway.** When the builder writes a variable for a setting it created, it writes `--<id>`, and when it removes one it removes only that exact form. A theme that names variables differently ends up with both conventions in one file.
|
|
44
|
+
- **Add the unit in the layout, not the schema.** `unit` in the schema is a display hint for the panel and the preset resolver; the variable is what the browser reads, so `{{ settings.font_size_h1 }}px` is what makes it a length.
|
|
45
|
+
|
|
46
|
+
## Preset variables, and why they exist
|
|
47
|
+
|
|
48
|
+
Beyond one variable per setting, the builder maintains **one variable per preset property**:
|
|
49
|
+
|
|
50
|
+
```css
|
|
51
|
+
--font_size_h1_font_family: var(--font_family_heading);
|
|
52
|
+
--font_size_h1_font_weight: var(--font_weight_bold);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
These are written automatically the first time a merchant applies that preset to rich-text content. You do not author them, but you need to know why they exist.
|
|
56
|
+
|
|
57
|
+
When a merchant applies "H1" to a paragraph, the content records the preset's variables — `font-family: var(--font_size_h1_font_family)` — **named after the preset, not after the font it currently uses**. Later, when the merchant repoints H1 from the heading font to the body font, the builder rewrites that one declaration and every paragraph ever styled as H1 follows.
|
|
58
|
+
|
|
59
|
+
Recording `var(--font_family_heading)` in the content directly would freeze the decision at the moment it was applied: repointing the preset would update the schema and change nothing already on the page.
|
|
60
|
+
|
|
61
|
+
A theme never needs to write these, and should not delete them — they are the only link between styled content and the preset definition.
|
|
62
|
+
|
|
63
|
+
## Consuming variables in theme CSS
|
|
64
|
+
|
|
65
|
+
```css
|
|
66
|
+
h1 {
|
|
67
|
+
font-family: var(--font_size_h1_font_family, var(--font_family_heading));
|
|
68
|
+
font-weight: var(--font_size_h1_font_weight, var(--font_weight_bold));
|
|
69
|
+
font-size: var(--font_size_h1);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Reading the **preset's** variable first, with the target's as a fallback, is the shape that keeps a theme's own elements in step with the preset. A theme that reads only `var(--font_family_heading)` will style its `<h1>` elements from the heading font no matter what the H1 preset has been repointed to — the panel will look correct and the page will disagree.
|
|
74
|
+
|
|
75
|
+
This is the single most common reason a merchant reports "I changed it and nothing happened".
|
|
76
|
+
|
|
77
|
+
## Custom fonts
|
|
78
|
+
|
|
79
|
+
When a merchant picks a Google font or uploads a font file, the builder:
|
|
80
|
+
|
|
81
|
+
1. appends the font to the `options` of **every** `font_picker` in the schema, so it is selectable everywhere;
|
|
82
|
+
2. injects the loader into the layout `<head>` — a `<link rel="stylesheet">` for Google, or a `<link rel="preload">` plus an `@font-face` block for an upload;
|
|
83
|
+
3. stores the **display name** as the setting's value.
|
|
84
|
+
|
|
85
|
+
So a custom font resolves **by name**: the display name a merchant types becomes the CSS `font-family` identifier, and the `@font-face` in the layout is what makes that name mean something. Two consequences worth knowing when debugging:
|
|
86
|
+
|
|
87
|
+
- Two fonts given the same display name collide — one `@font-face` wins.
|
|
88
|
+
- The name flows through every variable unchanged, so preset linkage works for uploaded fonts exactly as for built-in ones.
|
|
89
|
+
|
|
90
|
+
## Pitfalls
|
|
91
|
+
|
|
92
|
+
| Symptom | Cause | Fix |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| Panel edits change nothing anywhere | No `:root` block, or the layout reads `settings.*` without emitting variables | Add the `{% style %}` block. |
|
|
95
|
+
| Panel edits change the preview but not real elements | Theme CSS hardcodes fonts and sizes | Rewire the CSS to `var(…)`. |
|
|
96
|
+
| Repointing a preset moves rich-text content but not the theme's own headings | Theme CSS reads the target variable directly | Read the preset variable first, with the target as fallback. |
|
|
97
|
+
| Sizes render unstyled | `unit` set in the schema but no unit in the layout | Append the unit in the Liquid: `{{ settings.font_size_h1 }}px`. |
|
|
98
|
+
| A preset applies a literal instead of a variable | The target setting has no declaration in the layout | Declare `--<id>: {{ settings.<id> }}`. |
|
|
99
|
+
| Uploaded `.ttf` / `.otf` fonts do not render | The generated `@font-face` declares `format('woff2')` regardless of the file | Convert to `.woff2` before uploading. |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# `option_group` — feeding section dropdowns from theme settings
|
|
2
|
+
|
|
3
|
+
> Part of the `themes-settings-schema` skill. See [`../SKILL.md`](../SKILL.md) for the authoring workflow.
|
|
4
|
+
|
|
5
|
+
A section setting usually declares its own `options` array. When the choices are really *the theme's* — its palette, its size scale — that duplicates the same list into every section, and the lists drift.
|
|
6
|
+
|
|
7
|
+
`option_group` inverts it: a **theme** setting publishes itself into a named token list, and a **section** setting names that list instead of enumerating options.
|
|
8
|
+
|
|
9
|
+
## Theme side
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"type": "color_background",
|
|
14
|
+
"id": "color_primary",
|
|
15
|
+
"label": "Primary",
|
|
16
|
+
"default": "#49473e",
|
|
17
|
+
"option_group": {
|
|
18
|
+
"id": "background_colors",
|
|
19
|
+
"label": "Primary",
|
|
20
|
+
"value": "var(--clr-primary)"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| Key | Meaning |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `id` | The token list this setting joins. Any number of settings may share one. Must match `^[a-zA-Z0-9_-]+$`. |
|
|
28
|
+
| `label` | What a merchant sees in the section dropdown. |
|
|
29
|
+
| `value` | What the section setting stores when chosen — almost always a `var(…)`, so the choice follows later theme edits. |
|
|
30
|
+
|
|
31
|
+
`label` and `value` must both be non-empty strings, or the entry is skipped and a warning is logged once.
|
|
32
|
+
|
|
33
|
+
## Section side
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"type": "select",
|
|
38
|
+
"id": "background",
|
|
39
|
+
"label": "Background",
|
|
40
|
+
"options": "background_colors"
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`options` as a **string** is a token reference; as an **array** it is a literal list.
|
|
45
|
+
|
|
46
|
+
## Conventions
|
|
47
|
+
|
|
48
|
+
Keep one token list per visual axis, and let every setting on that axis join it:
|
|
49
|
+
|
|
50
|
+
| Token list | Joined by |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `background_colors` | every palette colour usable as a surface |
|
|
53
|
+
| `font_sizes` | every text preset |
|
|
54
|
+
| `font_weights` | every weight in the vocabulary |
|
|
55
|
+
| `spacing_sizes` | every step in the spacing scale |
|
|
56
|
+
| `border_radii` | every radius step |
|
|
57
|
+
|
|
58
|
+
Point `value` at the CSS variable, not the raw value. `"value": "var(--clr-primary)"` keeps a section following the theme; `"value": "#49473e"` freezes the colour at the moment it was chosen, and re-themeing leaves it behind.
|
|
59
|
+
|
|
60
|
+
## Pitfalls
|
|
61
|
+
|
|
62
|
+
| Symptom | Cause |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| Section dropdown is empty | No theme setting registers that `option_group.id`, or the id is misspelled on one side |
|
|
65
|
+
| One choice missing from a populated dropdown | That setting's `option_group` lacks `label` or `value` |
|
|
66
|
+
| Re-theming leaves stale colours in sections | `value` holds a literal instead of a `var(…)` |
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Text presets — what the builder counts, and how it resolves
|
|
2
|
+
|
|
3
|
+
> Part of the `themes-settings-schema` skill. See [`../SKILL.md`](../SKILL.md) for the authoring workflow.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- What becomes a text preset
|
|
8
|
+
- Roles
|
|
9
|
+
- Resolving a preset's font
|
|
10
|
+
- Resolving a preset's weight
|
|
11
|
+
- Worked examples
|
|
12
|
+
- Pitfalls
|
|
13
|
+
|
|
14
|
+
## What becomes a text preset
|
|
15
|
+
|
|
16
|
+
Every setting in the `typography` group is tested against three questions. A setting becomes a **preset card** only if all three say "not a font, not a weight".
|
|
17
|
+
|
|
18
|
+
A setting is a **font** if either holds:
|
|
19
|
+
|
|
20
|
+
- `role` is `"font_family"`
|
|
21
|
+
- `type` is `font_picker`, `font_family`, or `font`
|
|
22
|
+
|
|
23
|
+
A setting is a **weight** if any holds:
|
|
24
|
+
|
|
25
|
+
- `role` is `"font_weight"`
|
|
26
|
+
- `type` is `font_weight`
|
|
27
|
+
- its **id** matches `/(^|[_-])font[_-]?weight($|[_-])/i` — `font_weight_bold`, `heading-font-weight`, `fontweight_x`
|
|
28
|
+
- its **label** matches `/font\s*weight/i` — "Font weight", "Body font-weight"
|
|
29
|
+
|
|
30
|
+
Everything else in `typography` with a string `id`, `type` and `label` is a **preset**. The check is exclusion, not an allow-list, so a size may be typed `range`, `number`, `font_size`, or anything else — legacy themes that typed sizes inconsistently keep working.
|
|
31
|
+
|
|
32
|
+
The id and label checks exist because themes overwhelmingly author weights as `range`, not as a dedicated `font_weight` type. Without them, `font_weight_body` would become a preset card called "Weight" sitting between H1 and H2.
|
|
33
|
+
|
|
34
|
+
## Roles
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
role: "heading" | "body"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Explicit `role` wins. Without it, an id starting `font_size_h` reads as `heading`; everything else reads as `body`.
|
|
41
|
+
|
|
42
|
+
Role decides two things: the fallback font (`font_family_heading` vs `font_family_body`) and the fallback weight (`bold` vs `normal`). It does nothing when both refs are set — but set it anyway, so the preset still resolves sensibly if a ref is later removed.
|
|
43
|
+
|
|
44
|
+
## Resolving a preset's font
|
|
45
|
+
|
|
46
|
+
In order:
|
|
47
|
+
|
|
48
|
+
1. **`font_family_ref`** — the id of a font setting, resolved across the whole schema. **Authoritative:** if it resolves to nothing, the preset has no font. It does *not* fall through to the role.
|
|
49
|
+
2. **A literal `font_family`** on the preset — a value, not a reference. Used by builder-created custom presets; avoid hand-authoring it, since nothing in the panel can edit it.
|
|
50
|
+
3. **The role fallback** — `heading` → `font_family_heading`, `body` → `font_family_body`. If only one of the pair exists, both roles use it. Only settings that are actually fonts qualify.
|
|
51
|
+
|
|
52
|
+
## Resolving a preset's weight
|
|
53
|
+
|
|
54
|
+
In order:
|
|
55
|
+
|
|
56
|
+
1. **`font_weight_ref`** — id of a weight setting, resolved across the whole schema. Unlike the family, a ref that resolves to nothing **falls back to the role weight**.
|
|
57
|
+
2. **A literal `font_weight`** on the preset.
|
|
58
|
+
3. **The role weight** — `heading` → `bold`, `body` → `normal`.
|
|
59
|
+
|
|
60
|
+
The asymmetry is deliberate: a missing weight has a sane universal default, a missing typeface does not.
|
|
61
|
+
|
|
62
|
+
## Worked examples
|
|
63
|
+
|
|
64
|
+
### The shape to copy
|
|
65
|
+
|
|
66
|
+
Fonts in their own group, weights in theirs, presets referencing both by id.
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
[
|
|
70
|
+
{ "name": "theme_info", "theme_name": "My Theme", "theme_version": "1.0.0" },
|
|
71
|
+
{
|
|
72
|
+
"name": "fonts",
|
|
73
|
+
"settings": [
|
|
74
|
+
{ "type": "font_picker", "id": "font_family_heading", "label": "Heading font", "default": "Reckless Neue" },
|
|
75
|
+
{ "type": "font_picker", "id": "font_family_body", "label": "Body font", "default": "Basis Grotesque" }
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "font_weights",
|
|
80
|
+
"settings": [
|
|
81
|
+
{ "type": "range", "id": "font_weight_normal", "label": "Normal", "min": 100, "max": 900, "step": 100, "default": 400 },
|
|
82
|
+
{ "type": "range", "id": "font_weight_bold", "label": "Bold", "min": 100, "max": 900, "step": 100, "default": 700 }
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "typography",
|
|
87
|
+
"settings": [
|
|
88
|
+
{
|
|
89
|
+
"type": "range", "id": "font_size_h1", "label": "H1",
|
|
90
|
+
"min": 32, "max": 120, "step": 1, "unit": "px", "default": 64,
|
|
91
|
+
"role": "heading",
|
|
92
|
+
"font_family_ref": "font_family_heading",
|
|
93
|
+
"font_weight_ref": "font_weight_bold"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"type": "range", "id": "font_size_base", "label": "Base",
|
|
97
|
+
"min": 14, "max": 20, "step": 0.5, "unit": "px", "default": 16,
|
|
98
|
+
"role": "body",
|
|
99
|
+
"font_family_ref": "font_family_body",
|
|
100
|
+
"font_weight_ref": "font_weight_normal"
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Renders as: a **fonts** accordion with two pickers, a **font weights** accordion with two sliders, and a **typography** accordion with one card per preset — each card showing its size, a font dropdown reading "Heading" / "Body", and a weight dropdown reading "Normal" / "Bold".
|
|
108
|
+
|
|
109
|
+
### The minimum that still works
|
|
110
|
+
|
|
111
|
+
No refs at all. Presets resolve by role. Valid, and what most older themes look like.
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"name": "typography",
|
|
116
|
+
"settings": [
|
|
117
|
+
{ "type": "font_picker", "id": "font_family_heading", "label": "Heading font", "default": "Inter" },
|
|
118
|
+
{ "type": "font_picker", "id": "font_family_body", "label": "Body font", "default": "Inter" },
|
|
119
|
+
{ "type": "range", "id": "font_size_h1", "label": "H1", "unit": "px", "default": 48, "role": "heading" },
|
|
120
|
+
{ "type": "range", "id": "font_size_base", "label": "Base", "unit": "px", "default": 16, "role": "body" }
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
H1 resolves to the heading font at `bold`; Base to the body font at `normal`. Fonts and presets may share one group — the panel gathers every font into whichever group declares the first one.
|
|
126
|
+
|
|
127
|
+
## Pitfalls
|
|
128
|
+
|
|
129
|
+
| Symptom | Cause | Fix |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| A size setting never appears as a card | Its id or label matches the weight patterns — e.g. a size labelled "Heading font weight scale" | Rename it. Nothing in the schema can override the classification. |
|
|
132
|
+
| Typography renders as a flat list | The group is not named exactly `typography` | Rename the group. `type`, `text`, `Typography` all miss. |
|
|
133
|
+
| A card shows a size slider and nothing else | Its `font_family_ref` / `font_weight_ref` point at ids that do not exist | Fix the ids, or drop the refs and let `role` resolve them. |
|
|
134
|
+
| Two font settings are indistinguishable in a preset's dropdown | Both labelled "Font family", ids not role-named | Rename ids to `font_family_<role>`. The dropdown reads the id, not the label. |
|
|
135
|
+
| Headers vanish | The Themes panel discards settings without `id`/`label` | Use separate groups instead. |
|
|
136
|
+
| A font appears in a group the theme did not declare it in | The panel gathers every font into the first group that declares one, so a merchant finds them all in one place | Expected. Declare fonts in one group to keep schema and panel aligned. |
|
|
137
|
+
| Changing a preset's font moves nothing on the page | The theme's CSS reads `--font-heading` directly rather than the preset's own variable | See [`css-variables.md`](css-variables.md). |
|
|
138
|
+
| An id like `font_size_body` behaves oddly | Its derived names collide with the shared `font_family_body` | Avoid `font_size_<role>` ids; use `font_size_base`, `font_size_lg`. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluid-app/fluid-cli-theme-dev",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"description": "Fluid CLI plugin for theme developer workflows — dev server, push, pull, init",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"open": "^10.0.0",
|
|
28
28
|
"ora": "^8.0.0",
|
|
29
29
|
"prompts": "^2.4.2",
|
|
30
|
-
"@fluid-app/fluid-cli": "0.1.
|
|
30
|
+
"@fluid-app/fluid-cli": "0.1.17"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@swc/core": "^1.15.18",
|