sliderpro-agentic-skills-etch 0.2.0 → 0.2.2
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/bin/cli.js +23 -2
- package/package.json +1 -1
- package/skills-package/components/dwc-slider-pagination.md +38 -0
- package/skills-package/manifest.json +5 -1
- package/skills-package/slider-skills/slider-pro-skills-build.md +438 -0
- package/skills-package/slider-skills/slider-pro-skills-reference.md +8 -3
- package/skills-package/slider-skills/slider-pro-skills.md +236 -355
package/bin/cli.js
CHANGED
|
@@ -140,6 +140,24 @@ function copyBundle(target) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// When each skills file is meant to be read. The split exists so that only the
|
|
144
|
+
// entry file is read up front, so saying so here is part of the install.
|
|
145
|
+
const SKILL_ROLES = {
|
|
146
|
+
"slider-pro-skills.md": "read this first, every session",
|
|
147
|
+
"slider-pro-skills-build.md": "read only when building from scratch",
|
|
148
|
+
"slider-pro-skills-reference.md": "lookup only, never read whole",
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
function installedSkills(target) {
|
|
152
|
+
try {
|
|
153
|
+
return readdirSync(join(target, OUT_SKILLS_DIR))
|
|
154
|
+
.filter((f) => f.endsWith(".md"))
|
|
155
|
+
.sort();
|
|
156
|
+
} catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
143
161
|
function countComponents(target) {
|
|
144
162
|
try {
|
|
145
163
|
return readdirSync(join(target, OUT_COMPONENTS_DIR)).filter((f) => f.endsWith(".md")).length;
|
|
@@ -183,10 +201,13 @@ async function main() {
|
|
|
183
201
|
|
|
184
202
|
console.log(`Installed Slider Pro AI Connector skills to ${target}`);
|
|
185
203
|
console.log(` source: ${source}`);
|
|
186
|
-
|
|
187
|
-
|
|
204
|
+
for (const name of installedSkills(target)) {
|
|
205
|
+
const role = SKILL_ROLES[name];
|
|
206
|
+
console.log(` ${OUT_SKILLS_DIR}/${name}${role ? ` (${role})` : ""}`);
|
|
207
|
+
}
|
|
188
208
|
console.log(` ${OUT_COMPONENTS_DIR}/ (${countComponents(target)} component prop docs)`);
|
|
189
209
|
console.log(`\nPoint your AI coding agent at ${OUT_SKILLS_DIR}/slider-pro-skills.md to load it.`);
|
|
210
|
+
console.log("It reads the other two only when a task needs them.");
|
|
190
211
|
console.log('Then tell the agent: "npx @digital-gravy/etch-connector serve"');
|
|
191
212
|
}
|
|
192
213
|
|
package/package.json
CHANGED
|
@@ -58,3 +58,41 @@ Set **Custom Pagination Mode** to `Template` and design the *first* item as your
|
|
|
58
58
|
|
|
59
59
|
- **Numbers** count up starting from whatever number you type, so `1` produces `1, 2, 3, ...` while `2003` produces `2003, 2004, 2005, ...`. Leading zeros are preserved as the number grows (`007` → `007, 008, ..., 010, 011, ...`).
|
|
60
60
|
- **Letters and roman numerals** (`a`, `A`, `i`, `I`) always start at the beginning of their sequence (a/b/c…, i/ii/iii…). Only numbers support a custom starting point.
|
|
61
|
+
|
|
62
|
+
## Styling the active item
|
|
63
|
+
|
|
64
|
+
Slider Pro adds the class `is-active` to whichever pagination item matches the slide on screen. Use
|
|
65
|
+
it to style the current one:
|
|
66
|
+
|
|
67
|
+
```css
|
|
68
|
+
.my-dot { background: #e5e7eb; }
|
|
69
|
+
.my-dot.is-active { background: #45bf55; }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The class moves on its own as the slider changes, so you do not need any JavaScript. It works the
|
|
73
|
+
same in `Default` and `Template` mode.
|
|
74
|
+
|
|
75
|
+
***
|
|
76
|
+
|
|
77
|
+
## If your own layout does not apply
|
|
78
|
+
|
|
79
|
+
The pagination lays itself out as a flex row or column, using the **Direction** and **Gap**
|
|
80
|
+
settings above, and it is only as tall as its items.
|
|
81
|
+
|
|
82
|
+
If you write your own layout on it, for example a grid, and nothing seems to happen, this is why:
|
|
83
|
+
your rule and the plugin's rule are equally specific, so the plugin's wins. Put the plugin's class
|
|
84
|
+
in front of yours to make your rule stronger:
|
|
85
|
+
|
|
86
|
+
```css
|
|
87
|
+
/* has no effect on its own */
|
|
88
|
+
.my-pagination { display: grid; }
|
|
89
|
+
|
|
90
|
+
/* wins */
|
|
91
|
+
.dwc-slider-pagination-wrapper.my-pagination {
|
|
92
|
+
display: grid;
|
|
93
|
+
block-size: 100%;
|
|
94
|
+
gap: 0;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Remember to reset `gap` if you set your own spacing, because the flex gap still applies.
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-08-
|
|
2
|
+
"generated": "2026-08-29",
|
|
3
3
|
"skills": [
|
|
4
|
+
{
|
|
5
|
+
"repo": "ai-connector/slider-skills/slider-pro-skills-build.md",
|
|
6
|
+
"out": "slider-skills/slider-pro-skills-build.md"
|
|
7
|
+
},
|
|
4
8
|
{
|
|
5
9
|
"repo": "ai-connector/slider-skills/slider-pro-skills-reference.md",
|
|
6
10
|
"out": "slider-skills/slider-pro-skills-reference.md"
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
---
|
|
2
|
+
icon: hammer
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# AI Skills Reference: Building
|
|
6
|
+
|
|
7
|
+
Read this when you are building a slider, a card deck or a template **from scratch**. For changing
|
|
8
|
+
a prop, adjusting CSS or fixing an existing build, `slider-pro-skills.md` (same folder) is enough.
|
|
9
|
+
|
|
10
|
+
Everything in `slider-pro-skills.md` still applies here: the native-first gate, the one-extra-brace
|
|
11
|
+
rule, the class mechanisms, the CSS conventions and the gotchas. This file adds only what building
|
|
12
|
+
from nothing needs. Read it after that file, never instead of it.
|
|
13
|
+
|
|
14
|
+
***
|
|
15
|
+
|
|
16
|
+
## Block JSON
|
|
17
|
+
|
|
18
|
+
### Component instances are the unit of work
|
|
19
|
+
|
|
20
|
+
A Slider Pro build is a tree of `etch/component` blocks, each naming a component and carrying a
|
|
21
|
+
flat `attributes` map of prop values. **Two different shapes describe the same tree, and mixing
|
|
22
|
+
them up is the first thing that goes wrong in a session.**
|
|
23
|
+
|
|
24
|
+
**The live shape**, which `etch.blocks.getTree()` returns and `etch.blocks.create()` accepts:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"type": "etch/component",
|
|
29
|
+
"version": 1,
|
|
30
|
+
"context": {},
|
|
31
|
+
"children": [],
|
|
32
|
+
"componentId": 6455,
|
|
33
|
+
"attributes": { "wrapperHeight": "350px", "spaceBetweenSliders": "50px" }
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`version`, `context` and `children` are required. Block ids are strings (`"ot2wnef"`), not numbers.
|
|
38
|
+
|
|
39
|
+
**The export shape**, which you only ever see inside a premade template `.json` file, uses
|
|
40
|
+
`blockName`, nests under `attrs`, and calls the component id `ref`:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{ "blockName": "etch/component", "attrs": { "ref": 6455, "attributes": {} }, "innerBlocks": [] }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Never pass the export shape to `create()`. It is rejected with
|
|
47
|
+
`Invalid block JSON: expected string, received undefined` at `path: ["type"]`. To put a template on
|
|
48
|
+
the page, hand its whole `gutenbergBlock` to `etch.blocks.pasteAsync()` instead.
|
|
49
|
+
|
|
50
|
+
### Children go in a slot, never directly on the component
|
|
51
|
+
|
|
52
|
+
A component's children must be wrapped in an `etch/slot-content` node naming the slot they fill.
|
|
53
|
+
Putting a Slider straight inside a Wrapper's `children` renders nothing.
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{ "type": "etch/slot-content", "version": 1, "context": {}, "children": [], "slotName": "Slides" }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Component | Slots |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| DWC Slider Wrapper | `Sliders_and_Controls` |
|
|
62
|
+
| DWC Slider | `Top__Controls`, `Slides`, `Bottom__Controls` |
|
|
63
|
+
| DWC Slide | `Content` |
|
|
64
|
+
| DWC Slider Pagination | `PaginationButtons` |
|
|
65
|
+
| DWC Slider Nav Button | `Nav_Btn_Content` |
|
|
66
|
+
| DWC Slider Progress, DWC Slider Play-Pause | none |
|
|
67
|
+
|
|
68
|
+
Controls are siblings of the Slider inside `Sliders_and_Controls`, or children of the Slider's own
|
|
69
|
+
`Top__Controls` / `Bottom__Controls`. Both work, because controls find their own slider.
|
|
70
|
+
|
|
71
|
+
***
|
|
72
|
+
|
|
73
|
+
## Design recipes
|
|
74
|
+
|
|
75
|
+
Each of these is a shipped template reduced to the settings that produce it.
|
|
76
|
+
|
|
77
|
+
### Cover-flow carousel (Slider Flow): no script, no CSS
|
|
78
|
+
|
|
79
|
+
The whole effect is the `slides.*` group, which applies transforms to inactive slides and separate
|
|
80
|
+
`-Active` values to the current one.
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
setGroup(sliderId, 'layout', { slidesPerPage: '3 lg:1', gapBetweenSlides: '20px',
|
|
84
|
+
sliderEdgeOffset: 'lg:20% md:18%' });
|
|
85
|
+
setGroup(sliderId, 'motion', { focus: 'center', loop: '{true}', speed: '800',
|
|
86
|
+
updateOnMove: '{true}' });
|
|
87
|
+
setGroup(sliderId, 'slides', { perspective: '950px', opacity: '0.3', scale: '0.9',
|
|
88
|
+
borderRadius: '1rem',
|
|
89
|
+
transition: 'transform 0.5s ease, opacity 0.5s ease' });
|
|
90
|
+
setGroup(sliderId, 'dimensions', { sliderHeight: '350px md:300px sm:180px' });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`motion.focus: 'center'` is what puts the active slide in the middle. The neighbours are dimmed and
|
|
94
|
+
shrunk purely by `slides.opacity` and `slides.scale`, which apply to inactive slides only.
|
|
95
|
+
|
|
96
|
+
For an angled flip, add `slides.rotateY` and turn on `slides.flipNextRotateY` so the slide on the
|
|
97
|
+
other side mirrors the angle instead of repeating it. Same for `translateX` with
|
|
98
|
+
`flipNextTranslateX`.
|
|
99
|
+
|
|
100
|
+
### Logo marquee (Slider Marquee): no script
|
|
101
|
+
|
|
102
|
+
Two Sliders in one Wrapper, running in opposite directions.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
// both rows
|
|
106
|
+
setGroup(sliderId, 'dimensions', { slideAutoWidth: '{true}' }); // slides size to content
|
|
107
|
+
setGroup(sliderId, 'motion', { loop: '{true}' });
|
|
108
|
+
setGroup(sliderId, 'autoscroll', { infiniteScroll: '{true}', scrollSpeed: '1',
|
|
109
|
+
pauseOnHover: '{false}', pauseOnFocus: '{false}' });
|
|
110
|
+
setGroup(sliderId, 'layout', { gapBetweenSlides: '2.4rem' });
|
|
111
|
+
|
|
112
|
+
// second row only
|
|
113
|
+
setGroup(secondId, 'sliderSetup', { slldeDirection: 'rtl' }); // note the spelling
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Edge Fade goes on the **Wrapper** (`props.edgeFade`), not the sliders, so both rows fade as one
|
|
117
|
+
unit. `props.pauseSlidersOnHover` on the Wrapper pauses both rows together.
|
|
118
|
+
|
|
119
|
+
Infinite Scroll needs the Auto-Scroll extension enabled in admin settings, which is the default.
|
|
120
|
+
|
|
121
|
+
### Synced elements (Team, Stack)
|
|
122
|
+
|
|
123
|
+
Driving arbitrary elements on the page from the slider is one prop.
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
setGroup(sliderId, 'sliderSetup', { syncCustomElement: '.timeline-node',
|
|
127
|
+
syncCustomElementNav: '{true}' });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The plugin then moves `is-active`, `is-prev` and `is-next` across those elements as the slider
|
|
131
|
+
moves, and with nav on, clicking one jumps the slider to it. **You write the CSS for the three
|
|
132
|
+
states.** The elements do not need to be inside the slider and do not need to match the slide
|
|
133
|
+
count.
|
|
134
|
+
|
|
135
|
+
The selector takes a comma-separated list, so one slider can drive several groups at once. Team
|
|
136
|
+
uses `'.slider-team__sync, .slider-team__sync-heading'` to move a portrait and a heading together.
|
|
137
|
+
|
|
138
|
+
Grep `## 3. Sync Custom Element` in the reference for multiple selectors and the overlap caveat.
|
|
139
|
+
|
|
140
|
+
### Main and thumbnails in one wrapper (Zeon)
|
|
141
|
+
|
|
142
|
+
A full-bleed hero with a thumbnail strip needs no sync prop at all, just roles:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
setGroup(mainId, 'sliderSetup', { sliderRole: 'main', transitionType: 'Fade' });
|
|
146
|
+
setGroup(thumbId, 'sliderSetup', { sliderRole: 'thumbnails', transitionType: 'Loop' });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Both inside the same Wrapper and they pair automatically. A Wrapper can hold more than one
|
|
150
|
+
thumbnail slider, which is how Zeon runs a background layer and a strip off the same main.
|
|
151
|
+
|
|
152
|
+
### Animating slide content: no script
|
|
153
|
+
|
|
154
|
+
Splide puts `is-active` on the current `.splide__slide`, which is all you need to animate anything
|
|
155
|
+
inside it. Give the element its resting state, then reveal it from the active slide:
|
|
156
|
+
|
|
157
|
+
```css
|
|
158
|
+
/* in .slider-<name>__figure's own entry */
|
|
159
|
+
opacity: 0;
|
|
160
|
+
transform: translateY(30px);
|
|
161
|
+
transition: opacity 820ms cubic-bezier(0.22, 0.68, 0.24, 1),
|
|
162
|
+
transform 820ms cubic-bezier(0.22, 0.68, 0.24, 1);
|
|
163
|
+
|
|
164
|
+
.splide__slide.is-active & { opacity: 1; transform: translateY(0); }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Stagger a caption by putting the same pair on each line with a growing `transition-delay`
|
|
168
|
+
(thumbnail 140ms, status 200ms, title 260ms, body 320ms) so the block assembles rather than
|
|
169
|
+
appearing at once.
|
|
170
|
+
|
|
171
|
+
Two things that bite:
|
|
172
|
+
|
|
173
|
+
* **Keep layout transforms in the animated transform.** An element centred with
|
|
174
|
+
`translateX(-50%)` must animate as `translateX(-50%) translateY(30px)`, or it jumps sideways on
|
|
175
|
+
every transition. Where a breakpoint drops the centring, restate the transform there too.
|
|
176
|
+
* **A slow drift on the backdrop** (`scale(1.06)` to `scale(1)` over several seconds) makes a
|
|
177
|
+
change read as movement rather than a cut. Put it on the image, not the slide, so it does not
|
|
178
|
+
fight the transition.
|
|
179
|
+
|
|
180
|
+
This works with the Fade transition, which crossfades the slides underneath while the contents
|
|
181
|
+
move independently.
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
### Numbered steps beside a slider: no script
|
|
185
|
+
|
|
186
|
+
A vertical list of steps next to an image, where the number highlights as the image changes, uses
|
|
187
|
+
**two mechanisms on one slider**, split by whether the content repeats:
|
|
188
|
+
|
|
189
|
+
* **What repeats goes in the pagination.** The numbered markers are identical except for the digit,
|
|
190
|
+
which is exactly what `customPaginationMode: 'Template'` does: put one item in the
|
|
191
|
+
`PaginationButtons` slot with a literal `1` in it and the engine clones it per slide, substituting
|
|
192
|
+
the number. Add a slide later and the marker appears by itself. The active marker gets
|
|
193
|
+
`is-active`, and each is a real button, so clicking navigates.
|
|
194
|
+
* **What differs goes in a synced set.** Titles and descriptions are per step, and a clone cannot
|
|
195
|
+
carry them, so those are your own elements driven by
|
|
196
|
+
`sliderSetup.syncCustomElement` with `syncCustomElementNav` on.
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
setGroup(sliderId, 'sliderSetup', { transitionType: 'Fade',
|
|
200
|
+
syncCustomElement: '.step',
|
|
201
|
+
syncCustomElementNav: '{true}' });
|
|
202
|
+
etch.blocks.setAttribute(pagId, 'customPaginationMode', 'Template');
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Do not force one mechanism to do both. Template mode cannot give each clone its own text, and
|
|
206
|
+
faking it with a script to fill the clones is the kind of work the native features already cover.
|
|
207
|
+
|
|
208
|
+
**Give the template two levels.** The cloned root has to stretch to its row so it can carry the
|
|
209
|
+
connecting line, while the circle stays a fixed square at the top:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
.mark <- the cloned root; receives is-active, fills the row, draws the line
|
|
213
|
+
.mark-dot <- the circle and the number
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
State travels from the root to the dot through custom properties, so no descendant selector has to
|
|
217
|
+
fight the plugin for the same box:
|
|
218
|
+
|
|
219
|
+
```css
|
|
220
|
+
.mark { --dot-bg: #e5e7eb; --dot-fg: #4b5563; }
|
|
221
|
+
.mark.is-active { --dot-bg: #45bf55; --dot-fg: #fff; }
|
|
222
|
+
.mark-dot { background: var(--dot-bg); color: var(--dot-fg); }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Let the row count come from the slides, not the stylesheet.** Put both columns in one parent row
|
|
226
|
+
so they are the same height, then give each `grid-auto-rows`. Marker *n* then lines up with card
|
|
227
|
+
*n* whatever the copy length, and adding a slide needs no CSS change:
|
|
228
|
+
|
|
229
|
+
```css
|
|
230
|
+
.rail { display: grid; grid-template-columns: auto 1fr; gap: 0 to-rem(24px); }
|
|
231
|
+
.marks,
|
|
232
|
+
.steps { display: grid; grid-auto-rows: minmax(0, 1fr); }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Hang the connecting line off each marker, not the container.** A single line drawn on the
|
|
236
|
+
container has to be told where to stop, which means hardcoding the count. Per marker it is
|
|
237
|
+
self-terminating:
|
|
238
|
+
|
|
239
|
+
```css
|
|
240
|
+
.mark:not(:last-child)::after {
|
|
241
|
+
content: ""; position: absolute;
|
|
242
|
+
inset-inline-start: 50%; transform: translateX(-50%);
|
|
243
|
+
inset-block-start: calc(var(--dot-top) + var(--dot-size));
|
|
244
|
+
inset-block-end: 0;
|
|
245
|
+
inline-size: to-rem(2px); background: #e5e7eb;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The last marker gets no line for free, at any slide count.
|
|
250
|
+
|
|
251
|
+
Three traps, all silent:
|
|
252
|
+
|
|
253
|
+
* **Use `minmax(0, 1fr)`, not `1fr`.** `1fr` means `minmax(auto, 1fr)`, so a longer description
|
|
254
|
+
expands its own row and the two columns drift apart by a few pixels per row.
|
|
255
|
+
* **Match `gap` on both columns.** The pagination carries a flex `gap` of its own, which shortens
|
|
256
|
+
every row on that side only. Set it explicitly, including `0`.
|
|
257
|
+
* **The pagination will not accept your layout without a co-class.** It sets `display: flex` and
|
|
258
|
+
`block-size: fit-content` on `.dwc-slider-pagination-wrapper`, which ties with a single class of
|
|
259
|
+
yours and wins on order. Write the override **inside the entry its class prop already points at**,
|
|
260
|
+
so it stays findable from the element:
|
|
261
|
+
|
|
262
|
+
```css
|
|
263
|
+
/* the .marks entry itself */
|
|
264
|
+
position: relative;
|
|
265
|
+
|
|
266
|
+
&.dwc-slider-pagination-wrapper {
|
|
267
|
+
display: grid; grid-auto-rows: minmax(0, 1fr); block-size: 100%; gap: 0;
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
### Carousel on mobile, grid on desktop
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
setGroup(sliderId, 'layout', { layoutMode: 'static md:slider', gridColumns: '3 sm:1' });
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Above `md` it is a plain CSS grid; at `md` and below it is a real carousel. The slider is built and
|
|
279
|
+
torn down live as the viewport crosses the breakpoint. **Controls are hidden in static mode**,
|
|
280
|
+
because there is no live slider to drive.
|
|
281
|
+
|
|
282
|
+
### Card decks with no slider (Deck, Fall): the one place script is allowed
|
|
283
|
+
|
|
284
|
+
Structure: a Wrapper with `sliderlessSync.customElement` pointing at your cards, no Slider inside,
|
|
285
|
+
cards nested three deep as `card > lift > content` so the 3-D survives.
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
setGroup(wrapperId, 'sliderlessSync', {
|
|
289
|
+
customElement: '.deck-card', customElementNav: '{true}',
|
|
290
|
+
loop: '{true}', autoplay: '{true}', autoplayInterval: '4000'
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
CSS handles the front card and its two neighbours from `is-active` / `is-prev` / `is-next`. What
|
|
295
|
+
CSS cannot do is know that a given card is *three* back, so the rows behind, the z-index ladder and
|
|
296
|
+
the click targets need a script. That is case 1 of the native-first gate.
|
|
297
|
+
|
|
298
|
+
The contract between that script and the CSS is one attribute, **`data-pos`**, which the script
|
|
299
|
+
derives from `is-active` and writes on every card. It is a **signed** offset from the active card,
|
|
300
|
+
so the CSS can fan the left and right sides in opposite directions:
|
|
301
|
+
|
|
302
|
+
```css
|
|
303
|
+
.deck-stack-featured &:is([data-pos='2'], [data-pos='-2']) { /* ring 2, both sides */ }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The stack container carries **`data-tiers`**, how many rings are actually in play. It is computed,
|
|
307
|
+
not fixed: a looping deck needs spare cards to hide the wrap behind, so the script reduces the
|
|
308
|
+
tiers when there are too few cards.
|
|
309
|
+
|
|
310
|
+
Writing an unsigned rank, or naming the attribute anything else, produces a flat pile: the rules
|
|
311
|
+
above simply never match. The shipped script does this:
|
|
312
|
+
|
|
313
|
+
```js
|
|
314
|
+
// 1. make a custom property interpolable, so a blur can animate (case 2)
|
|
315
|
+
CSS.registerProperty({ name: '--stack-focus', syntax: '<number>',
|
|
316
|
+
inherits: true, initialValue: '0' });
|
|
317
|
+
|
|
318
|
+
// 2. how many rings this deck can afford, given looping and the card count
|
|
319
|
+
var mayLoop = wrap.dataset.loop === 'true' && total >= MIN_LOOP;
|
|
320
|
+
stack.dataset.tiers = mayLoop ? Math.min(MAX_TIERS, Math.floor((total - 3) / 2)) : MAX_TIERS;
|
|
321
|
+
|
|
322
|
+
// 3. per card: SIGNED offset from the active card. On a looping deck the offset
|
|
323
|
+
// wraps, so the far end of the list reads as the near side, not as distance 8.
|
|
324
|
+
var d = n - active;
|
|
325
|
+
if (mayLoop) {
|
|
326
|
+
if (d > total / 2) d -= total;
|
|
327
|
+
if (d < -total / 2) d += total;
|
|
328
|
+
}
|
|
329
|
+
card.dataset.pos = d;
|
|
330
|
+
|
|
331
|
+
// 4. hit-testing: stack order by distance, and clicks off beyond the clickable rings
|
|
332
|
+
var rank = Math.abs(d);
|
|
333
|
+
card.style.zIndex = String(total - rank);
|
|
334
|
+
lift.style.pointerEvents = rank <= NAV_RINGS ? 'auto' : 'none';
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Do not clamp `data-pos` to the tier count. Cards past the deepest ring should match no rule and
|
|
338
|
+
stay stacked at the back; clamping piles them onto the last visible ring instead.
|
|
339
|
+
|
|
340
|
+
Both decks also set `--i` on each line of card content so the CSS can stagger it:
|
|
341
|
+
|
|
342
|
+
```js
|
|
343
|
+
card.querySelectorAll('.deck-card-featured__content > *')
|
|
344
|
+
.forEach(function (line, j) { line.style.setProperty('--i', j); });
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Step 4 is Deck only. **Fall's script is much smaller**: it sets `--i` and a signed `data-pos`, and
|
|
348
|
+
nothing else. Fall never loops, so there is no wrap to fold and no z-index ladder to maintain,
|
|
349
|
+
because the cards fall past each other rather than fanning around a front card.
|
|
350
|
+
|
|
351
|
+
Recompute on every change: watch the stack with a `MutationObserver` filtered to `class`, since
|
|
352
|
+
`is-active` moving is the only signal you get.
|
|
353
|
+
|
|
354
|
+
### Where a script goes
|
|
355
|
+
|
|
356
|
+
**Not in a `<script>` element.** Etch silently drops one from the render, so the page looks right
|
|
357
|
+
in the builder and ships with no behaviour at all.
|
|
358
|
+
|
|
359
|
+
Every block has an optional `script` field (`EtchBlockScript`), which is Etch's JavaScript code
|
|
360
|
+
block. Put the code on the **component instance that owns the markup**, which for a card stack is
|
|
361
|
+
the Wrapper:
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
const wrapperId = etch.blocks.create({
|
|
365
|
+
type: 'etch/component', version: 1, context: {}, children: [ /* ... */ ],
|
|
366
|
+
componentId: C['DWC Slider Wrapper'], attributes: {},
|
|
367
|
+
script: { code: "(function () { /* ... */ })();" }
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
`code` is **plain source** over the API. The base64 you see in a premade template `.json` is only
|
|
372
|
+
how the export serialises it; do not encode it yourself.
|
|
373
|
+
|
|
374
|
+
Etch renders it as `<script type="module" defer>`, so it runs after parsing and top-level `await`
|
|
375
|
+
is available. Read it back with `etch.blocks.getJson(id).script`.
|
|
376
|
+
|
|
377
|
+
Component scripts are also the reason a card deck keeps working: the bridge leaves Sync Without
|
|
378
|
+
Slider wrappers unreconstructed precisely so an author's script keeps observing the original nodes.
|
|
379
|
+
|
|
380
|
+
Card counts for a looping deck: three are always visible (front plus two), and a loop needs a
|
|
381
|
+
hidden slot at each end, so **five is the minimum** and nine gives the full three-row fan. Fall
|
|
382
|
+
never loops, so it has no minimum.
|
|
383
|
+
|
|
384
|
+
***
|
|
385
|
+
|
|
386
|
+
## Block builders
|
|
387
|
+
|
|
388
|
+
These extend the four helpers in `slider-pro-skills.md`. Include both sets.
|
|
389
|
+
|
|
390
|
+
```js
|
|
391
|
+
function node(extra, children) {
|
|
392
|
+
return Object.assign({ version: 1, context: {}, children: children || [] }, extra);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function component(componentId, attributes, children) {
|
|
396
|
+
return node({ type: 'etch/component', componentId: componentId,
|
|
397
|
+
attributes: attributes || {} }, children);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function slot(slotName, children) {
|
|
401
|
+
return node({ type: 'etch/slot-content', slotName: slotName }, children);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function el(tag, attributes, children) {
|
|
405
|
+
return node({ type: 'etch/element', tag: tag, attributes: attributes || {} }, children);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function text(t) { return node({ type: 'etch/text', text: t }); }
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Build a wrapper with N slides
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
const C = comps();
|
|
415
|
+
const slides = [];
|
|
416
|
+
for (let i = 0; i < 5; i++) {
|
|
417
|
+
slides.push(component(C['DWC Slide'], {}, [
|
|
418
|
+
slot('Content', [ el('div', {}, [ text('Slide ' + (i + 1)) ]) ])
|
|
419
|
+
]));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const id = etch.blocks.create(
|
|
423
|
+
component(C['DWC Slider Wrapper'], {}, [
|
|
424
|
+
slot('Sliders_and_Controls', [
|
|
425
|
+
component(C['DWC Slider'], {}, [ slot('Slides', slides) ]),
|
|
426
|
+
component(C['DWC Slider Nav Button'], {}),
|
|
427
|
+
component(C['DWC Slider Pagination'], {})
|
|
428
|
+
])
|
|
429
|
+
])
|
|
430
|
+
);
|
|
431
|
+
return id;
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
An empty Slide has no content and so no height. Give slides something to size before wondering why
|
|
435
|
+
the slider is a flat line.
|
|
436
|
+
|
|
437
|
+
Build **one** first, screenshot it, then scale to the full count. Do not generate twenty slides
|
|
438
|
+
before you have seen one render.
|
|
@@ -189,6 +189,7 @@ matches the components installed on the site. Do not hand-edit this section.
|
|
|
189
189
|
| Background Blur | `props.style.backgroundBlur` | `style` | `8px` | Shown when `props.progressType === "counter" \|\| (props.progressType === "circular" && props.circularCounter)` |
|
|
190
190
|
| Progress Fill Color | `props.style.progressFillColor` | `style` | `#ff4d6a` | Shown when `props.progressType === "bar" \|\| props.progressType === "circular"` |
|
|
191
191
|
| Progress Fill Color 2 | `props.style.progressFillColor2` | `style` | `#ff8fa3` | Shown when `props.progressType === "bar"` |
|
|
192
|
+
| Progress Height | `props.style.progressHeight` | `style` | `3px` | Shown when `props.progressType === "bar"` |
|
|
192
193
|
| Progress Track Color | `props.style.progressTrackColor` | `style` | `rgba(255, 255, 255, 0.1)` | Shown when `props.progressType === "bar" \|\| props.progressType === "circular"` |
|
|
193
194
|
| Background Color | `props.style.backgroundColor` | `style` | `rgba(0, 0, 0, 0.4)` | Shown when `props.progressType === "counter"` |
|
|
194
195
|
| Border Radius | `props.style.borderRadius` | `style` | `20px` | Shown when `props.progressType === "counter"` |
|
|
@@ -280,9 +281,13 @@ Arrows, dots, pagination, progress, play/pause and the lightbox are styled with
|
|
|
280
281
|
properties, not props. The template class is **`.slider-navigation-vars`**, holding 94 variables.
|
|
281
282
|
|
|
282
283
|
**Per-slider styling.** Each Slider's **Slider Class** prop (`props.sliderClass`) names the class
|
|
283
|
-
its variables are read from, so two sliders on one page style independently. The plugin
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
its variables are read from, so two sliders on one page style independently. The plugin gives each
|
|
285
|
+
slider its own minted `.dwc-slider-vars-*` copy of that class, and mints a fresh one whenever a
|
|
286
|
+
slider is duplicated. So never style directly on `.slider-navigation-vars`, and never write that
|
|
287
|
+
shared class into the prop: doing so stops the minting and makes every duplicate share one set of
|
|
288
|
+
variables. Add your own class (e.g. `.my-slider`) alongside the minted one and style that. Classes
|
|
289
|
+
you add yourself are never touched. See "`sliderClass` carries a minted per-instance class" in
|
|
290
|
+
[slider-pro-skills.md](slider-pro-skills.md).
|
|
286
291
|
|
|
287
292
|
### Arrows
|
|
288
293
|
|
|
@@ -4,9 +4,13 @@ icon: robot
|
|
|
4
4
|
|
|
5
5
|
# AI Skills Reference
|
|
6
6
|
|
|
7
|
-
Everything an agent needs to
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Everything an agent needs to configure **Slider Pro for Etch** through the Etch AI Connector. Read
|
|
8
|
+
this file in full at session start. It has two companions in the same folder, and neither is read
|
|
9
|
+
up front:
|
|
10
|
+
|
|
11
|
+
* **`slider-pro-skills-build.md`** is the building guide: block JSON, slots, the design recipes
|
|
12
|
+
and the block builders. Read it when you are creating something from scratch, and not otherwise.
|
|
13
|
+
* **`slider-pro-skills-reference.md`** is lookup-only. Grep into it, never read it whole.
|
|
10
14
|
|
|
11
15
|
### When to consult the reference file
|
|
12
16
|
|
|
@@ -106,8 +110,15 @@ npx @digital-gravy/etch-connector eval "return etch.blocks.getTree().length" -t
|
|
|
106
110
|
npx @digital-gravy/etch-connector eval -f ./script.js -t "<tab>" --timeout 30000
|
|
107
111
|
```
|
|
108
112
|
|
|
109
|
-
Anything longer than one expression goes in a file with `-f
|
|
110
|
-
|
|
113
|
+
Anything longer than one expression goes in a file with `-f`, and use an **absolute path**: a bare
|
|
114
|
+
`./script.js` resolves against whatever directory happens to be current.
|
|
115
|
+
|
|
116
|
+
**Write that file with your file-writing tool, never a shell heredoc.** Script bodies are full of
|
|
117
|
+
quotes, backticks, `$` and regex, all of which the shell tries to interpret first. `cat > f <<'EOF'`
|
|
118
|
+
fails to parse and writes nothing, so you lose the call and learn nothing. Write the file directly
|
|
119
|
+
and the shell never sees the contents.
|
|
120
|
+
|
|
121
|
+
Keep every temp script in a working subdirectory you made for the task, and delete it when done.
|
|
111
122
|
|
|
112
123
|
### Safe mode
|
|
113
124
|
|
|
@@ -143,64 +154,13 @@ etch.components.list() // [{ id, name }, ...]
|
|
|
143
154
|
etch.components.getJson(id) // .properties and .blocks
|
|
144
155
|
etch.styles.list() // [{ id, selector, type, collection, css }]
|
|
145
156
|
etch.styles.create(selector, css) // returns a STYLE ID; pass it to a class prop
|
|
157
|
+
etch.styles.update(id, { selector?, css? }) // NOTE the object. See the warning below
|
|
158
|
+
etch.styles.delete(id)
|
|
146
159
|
etch.blocks.addClass(blockId, name) // plain elements only; throws on a component
|
|
147
160
|
etch.blocks.getJson(id).script // { code }, the JavaScript code block
|
|
148
161
|
await etch.saveAsync()
|
|
149
162
|
```
|
|
150
163
|
|
|
151
|
-
### Component instances are the unit of work
|
|
152
|
-
|
|
153
|
-
A Slider Pro build is a tree of `etch/component` blocks, each naming a component and carrying a
|
|
154
|
-
flat `attributes` map of prop values. **Two different shapes describe the same tree, and mixing
|
|
155
|
-
them up is the first thing that goes wrong in a session.**
|
|
156
|
-
|
|
157
|
-
**The live shape**, which `etch.blocks.getTree()` returns and `etch.blocks.create()` accepts:
|
|
158
|
-
|
|
159
|
-
```json
|
|
160
|
-
{
|
|
161
|
-
"type": "etch/component",
|
|
162
|
-
"version": 1,
|
|
163
|
-
"context": {},
|
|
164
|
-
"children": [],
|
|
165
|
-
"componentId": 6455,
|
|
166
|
-
"attributes": { "wrapperHeight": "350px", "spaceBetweenSliders": "50px" }
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
`version`, `context` and `children` are required. Block ids are strings (`"ot2wnef"`), not numbers.
|
|
171
|
-
|
|
172
|
-
**The export shape**, which you only ever see inside a premade template `.json` file, uses
|
|
173
|
-
`blockName`, nests under `attrs`, and calls the component id `ref`:
|
|
174
|
-
|
|
175
|
-
```json
|
|
176
|
-
{ "blockName": "etch/component", "attrs": { "ref": 6455, "attributes": {} }, "innerBlocks": [] }
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Never pass the export shape to `create()`. It is rejected with
|
|
180
|
-
`Invalid block JSON: expected string, received undefined` at `path: ["type"]`. To put a template on
|
|
181
|
-
the page, hand its whole `gutenbergBlock` to `etch.blocks.pasteAsync()` instead.
|
|
182
|
-
|
|
183
|
-
### Children go in a slot, never directly on the component
|
|
184
|
-
|
|
185
|
-
A component's children must be wrapped in an `etch/slot-content` node naming the slot they fill.
|
|
186
|
-
Putting a Slider straight inside a Wrapper's `children` renders nothing.
|
|
187
|
-
|
|
188
|
-
```json
|
|
189
|
-
{ "type": "etch/slot-content", "version": 1, "context": {}, "children": [], "slotName": "Slides" }
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
| Component | Slots |
|
|
193
|
-
| --- | --- |
|
|
194
|
-
| DWC Slider Wrapper | `Sliders_and_Controls` |
|
|
195
|
-
| DWC Slider | `Top__Controls`, `Slides`, `Bottom__Controls` |
|
|
196
|
-
| DWC Slide | `Content` |
|
|
197
|
-
| DWC Slider Pagination | `PaginationButtons` |
|
|
198
|
-
| DWC Slider Nav Button | `Nav_Btn_Content` |
|
|
199
|
-
| DWC Slider Progress, DWC Slider Play-Pause | none |
|
|
200
|
-
|
|
201
|
-
Controls are siblings of the Slider inside `Sliders_and_Controls`, or children of the Slider's own
|
|
202
|
-
`Top__Controls` / `Bottom__Controls`. Both work, because controls find their own slider.
|
|
203
|
-
|
|
204
164
|
### Classes: two different mechanisms
|
|
205
165
|
|
|
206
166
|
Adding a class to a plain element and adding one to a component are **not the same operation**, and
|
|
@@ -230,8 +190,7 @@ The class-typed props, one per component:
|
|
|
230
190
|
**On a plain element**, use `addClass` with the bare class name, no dot and no id:
|
|
231
191
|
|
|
232
192
|
```js
|
|
233
|
-
|
|
234
|
-
etch.blocks.addClass(id, 'fall-card');
|
|
193
|
+
etch.blocks.addClass(plainElementId, 'fall-card'); // no dot, no style id
|
|
235
194
|
```
|
|
236
195
|
|
|
237
196
|
Passing a style id here does not resolve. It is treated as a literal name and sanitised, so
|
|
@@ -275,6 +234,35 @@ Inside an element's **own** entry, nesting is preferred for media queries, pseud
|
|
|
275
234
|
(`.splide__slide.is-active &`) and child tag selectors (`& img`, `& svg`). Do **not** nest another
|
|
276
235
|
class's rules (`&__title`) inside a different class's entry: that class gets its own entry.
|
|
277
236
|
|
|
237
|
+
**Wrap px values in `to-rem()`.** Etch preprocesses style-entry CSS and converts them, so write
|
|
238
|
+
`to-rem(80px)`, never a raw `80px` and never a hand-converted `5rem`. It works inside media queries
|
|
239
|
+
too. Leave unitless values alone (`z-index`, `opacity`, `line-height`, aspect ratios).
|
|
240
|
+
|
|
241
|
+
```css
|
|
242
|
+
--offset: to-rem(80px);
|
|
243
|
+
@media (width <= to-rem(767px)) { --offset: to-rem(40px); }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Every rule for an element belongs in the entry its class prop already points at.** It is tempting
|
|
247
|
+
to answer a specificity problem with a second, more specific entry, for example
|
|
248
|
+
`.dwc-slider-pagination-wrapper.my-class`. Do not. That entry is attached to no block, so selecting
|
|
249
|
+
the element in Etch does not reveal it and the author cannot find the rule to edit. Nest the
|
|
250
|
+
stronger selector inside the element's own entry instead, which gives the same specificity:
|
|
251
|
+
|
|
252
|
+
```css
|
|
253
|
+
/* inside the .my-class entry */
|
|
254
|
+
&.dwc-slider-pagination-wrapper { display: grid; }
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
A style entry that no block references also survives deleting the element it was written for.
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
**`etch.styles.update` takes an object, and a string fails silently.** The signature is
|
|
261
|
+
`update(id, { selector, css })` and it reads `arg.css`. Hand it a bare CSS string and `arg.css` is
|
|
262
|
+
`undefined`, so it falls back to the entry's existing CSS, writes that back unchanged, and **throws
|
|
263
|
+
nothing**. The call looks like it worked. Read the entry back from `etch.styles.list()` and compare
|
|
264
|
+
the CSS before believing an update landed.
|
|
265
|
+
|
|
278
266
|
**Renaming is not one operation.** A style entry's selector and a block's `class` attribute are
|
|
279
267
|
independent. Renaming the selector rewrites the rendered class only where the class came from a
|
|
280
268
|
**component class prop**, because that prop stores the style id and resolves it at render. On a
|
|
@@ -314,16 +302,50 @@ the prop reference.
|
|
|
314
302
|
|
|
315
303
|
### Visual verification
|
|
316
304
|
|
|
317
|
-
You cannot see the page from inside safe mode
|
|
305
|
+
You cannot see the page from inside safe mode, and a read-back is not a substitute. A
|
|
306
|
+
`getAttribute` read proves a value **persisted**; only looking at output proves the page is
|
|
307
|
+
**right**. Say which of the two you reached.
|
|
308
|
+
|
|
309
|
+
**You can reach rendered verification on your own.** The Etch builder sits behind the user's
|
|
310
|
+
WordPress session, but the published page does not. Save first, or you will verify the old page.
|
|
311
|
+
|
|
312
|
+
**1. Fetch the published page.** Cheapest, and often enough. Proves classes, attributes and text
|
|
313
|
+
reached the markup.
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
curl -s "https://the-site.com/the-page/" -o page.html
|
|
317
|
+
grep -o 'data-focus="[^"]*"' page.html # did the prop reach the engine?
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Count matches with `grep -o ... | wc -l`, not `grep -c`, which counts lines: rendered pages are
|
|
321
|
+
minified onto very few lines and `grep -c` will report 1 for everything. Remember the page also
|
|
322
|
+
inlines your style entries, so a class name appears once more than it does in the markup.
|
|
323
|
+
|
|
324
|
+
**2. Screenshot it with your own headless browser.** Launch a fresh instance on a spare port with
|
|
325
|
+
its own throwaway profile, then kill it and delete the profile when done. The page is public, so it
|
|
326
|
+
needs no session.
|
|
318
327
|
|
|
319
328
|
```bash
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
npx @digital-gravy/etch-connector computed ".my-slider .splide__slide" -t "<tab>" --props transform,opacity
|
|
329
|
+
chrome.exe --headless=new --remote-debugging-port=9555 \
|
|
330
|
+
--user-data-dir="<temp dir>/cdp-profile" --no-first-run about:blank &
|
|
323
331
|
```
|
|
324
332
|
|
|
325
|
-
Then
|
|
326
|
-
|
|
333
|
+
Then drive it over CDP: `Page.navigate`, wait for `Page.loadEventFired`, wait again for Splide to
|
|
334
|
+
mount and images to decode, then `Page.captureScreenshot`. `Runtime.evaluate` gives you
|
|
335
|
+
`getComputedStyle` and `getBoundingClientRect`, which is how you turn "the dots look small" into a
|
|
336
|
+
measurement. **Then actually look at the screenshot.** A slider that mounted is not a slider that
|
|
337
|
+
looks right.
|
|
338
|
+
|
|
339
|
+
Write attribute probes as `[data-x="false"]`, not `[data-x]`: the bare attribute selector matches
|
|
340
|
+
whatever the value is, so a switched-off control still counts and you will report it as still there.
|
|
341
|
+
|
|
342
|
+
> **Do not use the connector's `shot`, `html` or `computed`, and never ask the user to relaunch
|
|
343
|
+
> their browser with `--remote-debugging-port=9222`.** Those attach to the user's own Chrome, so
|
|
344
|
+
> they cost the logged-in session being tested and fail outright when that port is not already
|
|
345
|
+
> open. The route above needs neither.
|
|
346
|
+
|
|
347
|
+
**What this cannot show you**, and has to be handed to the user: unpublished pages, logged-in-only
|
|
348
|
+
content, and anything you have not saved yet.
|
|
327
349
|
|
|
328
350
|
### Saving
|
|
329
351
|
|
|
@@ -366,7 +388,7 @@ Four rules follow from it:
|
|
|
366
388
|
|
|
367
389
|
## Decision tree
|
|
368
390
|
|
|
369
|
-
**Build from scratch:**
|
|
391
|
+
**Build from scratch:** read `slider-pro-skills-build.md` now. Components and props first, then CSS.
|
|
370
392
|
|
|
371
393
|
Reach for a premade template only when the user names one ("use Deck Featured"). Then paste its
|
|
372
394
|
JSON and customise, and read `https://design-with-cracka.gitbook.io/etchsliderpro/card-stack-templates` or `https://design-with-cracka.gitbook.io/etchsliderpro/premade-templates` for
|
|
@@ -378,231 +400,6 @@ configure it the way its documentation describes instead of inventing new CSS ov
|
|
|
378
400
|
|
|
379
401
|
***
|
|
380
402
|
|
|
381
|
-
## Design recipes
|
|
382
|
-
|
|
383
|
-
Each of these is a shipped template reduced to the settings that produce it.
|
|
384
|
-
|
|
385
|
-
### Cover-flow carousel (Slider Flow): no script, no CSS
|
|
386
|
-
|
|
387
|
-
The whole effect is the `slides.*` group, which applies transforms to inactive slides and separate
|
|
388
|
-
`-Active` values to the current one.
|
|
389
|
-
|
|
390
|
-
```js
|
|
391
|
-
setGroup(sliderId, 'layout', { slidesPerPage: '3 lg:1', gapBetweenSlides: '20px',
|
|
392
|
-
sliderEdgeOffset: 'lg:20% md:18%' });
|
|
393
|
-
setGroup(sliderId, 'motion', { focus: 'center', loop: '{true}', speed: '800',
|
|
394
|
-
updateOnMove: '{true}' });
|
|
395
|
-
setGroup(sliderId, 'slides', { perspective: '950px', opacity: '0.3', scale: '0.9',
|
|
396
|
-
borderRadius: '1rem',
|
|
397
|
-
transition: 'transform 0.5s ease, opacity 0.5s ease' });
|
|
398
|
-
setGroup(sliderId, 'dimensions', { sliderHeight: '350px md:300px sm:180px' });
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
`motion.focus: 'center'` is what puts the active slide in the middle. The neighbours are dimmed and
|
|
402
|
-
shrunk purely by `slides.opacity` and `slides.scale`, which apply to inactive slides only.
|
|
403
|
-
|
|
404
|
-
For an angled flip, add `slides.rotateY` and turn on `slides.flipNextRotateY` so the slide on the
|
|
405
|
-
other side mirrors the angle instead of repeating it. Same for `translateX` with
|
|
406
|
-
`flipNextTranslateX`.
|
|
407
|
-
|
|
408
|
-
### Logo marquee (Slider Marquee): no script
|
|
409
|
-
|
|
410
|
-
Two Sliders in one Wrapper, running in opposite directions.
|
|
411
|
-
|
|
412
|
-
```js
|
|
413
|
-
// both rows
|
|
414
|
-
setGroup(sliderId, 'dimensions', { slideAutoWidth: '{true}' }); // slides size to content
|
|
415
|
-
setGroup(sliderId, 'motion', { loop: '{true}' });
|
|
416
|
-
setGroup(sliderId, 'autoscroll', { infiniteScroll: '{true}', scrollSpeed: '1',
|
|
417
|
-
pauseOnHover: '{false}', pauseOnFocus: '{false}' });
|
|
418
|
-
setGroup(sliderId, 'layout', { gapBetweenSlides: '2.4rem' });
|
|
419
|
-
|
|
420
|
-
// second row only
|
|
421
|
-
setGroup(secondId, 'sliderSetup', { slldeDirection: 'rtl' }); // note the spelling
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
Edge Fade goes on the **Wrapper** (`props.edgeFade`), not the sliders, so both rows fade as one
|
|
425
|
-
unit. `props.pauseSlidersOnHover` on the Wrapper pauses both rows together.
|
|
426
|
-
|
|
427
|
-
Infinite Scroll needs the Auto-Scroll extension enabled in admin settings, which is the default.
|
|
428
|
-
|
|
429
|
-
### Synced elements (Team, Stack)
|
|
430
|
-
|
|
431
|
-
Driving arbitrary elements on the page from the slider is one prop.
|
|
432
|
-
|
|
433
|
-
```js
|
|
434
|
-
setGroup(sliderId, 'sliderSetup', { syncCustomElement: '.timeline-node',
|
|
435
|
-
syncCustomElementNav: '{true}' });
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
The plugin then moves `is-active`, `is-prev` and `is-next` across those elements as the slider
|
|
439
|
-
moves, and with nav on, clicking one jumps the slider to it. **You write the CSS for the three
|
|
440
|
-
states.** The elements do not need to be inside the slider and do not need to match the slide
|
|
441
|
-
count.
|
|
442
|
-
|
|
443
|
-
The selector takes a comma-separated list, so one slider can drive several groups at once. Team
|
|
444
|
-
uses `'.slider-team__sync, .slider-team__sync-heading'` to move a portrait and a heading together.
|
|
445
|
-
|
|
446
|
-
Grep `## 3. Sync Custom Element` in the reference for multiple selectors and the overlap caveat.
|
|
447
|
-
|
|
448
|
-
### Main and thumbnails in one wrapper (Zeon)
|
|
449
|
-
|
|
450
|
-
A full-bleed hero with a thumbnail strip needs no sync prop at all, just roles:
|
|
451
|
-
|
|
452
|
-
```js
|
|
453
|
-
setGroup(mainId, 'sliderSetup', { sliderRole: 'main', transitionType: 'Fade' });
|
|
454
|
-
setGroup(thumbId, 'sliderSetup', { sliderRole: 'thumbnails', transitionType: 'Loop' });
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
Both inside the same Wrapper and they pair automatically. A Wrapper can hold more than one
|
|
458
|
-
thumbnail slider, which is how Zeon runs a background layer and a strip off the same main.
|
|
459
|
-
|
|
460
|
-
### Animating slide content: no script
|
|
461
|
-
|
|
462
|
-
Splide puts `is-active` on the current `.splide__slide`, which is all you need to animate anything
|
|
463
|
-
inside it. Give the element its resting state, then reveal it from the active slide:
|
|
464
|
-
|
|
465
|
-
```css
|
|
466
|
-
/* in .slider-<name>__figure's own entry */
|
|
467
|
-
opacity: 0;
|
|
468
|
-
transform: translateY(30px);
|
|
469
|
-
transition: opacity 820ms cubic-bezier(0.22, 0.68, 0.24, 1),
|
|
470
|
-
transform 820ms cubic-bezier(0.22, 0.68, 0.24, 1);
|
|
471
|
-
|
|
472
|
-
.splide__slide.is-active & { opacity: 1; transform: translateY(0); }
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
Stagger a caption by putting the same pair on each line with a growing `transition-delay`
|
|
476
|
-
(thumbnail 140ms, status 200ms, title 260ms, body 320ms) so the block assembles rather than
|
|
477
|
-
appearing at once.
|
|
478
|
-
|
|
479
|
-
Two things that bite:
|
|
480
|
-
|
|
481
|
-
* **Keep layout transforms in the animated transform.** An element centred with
|
|
482
|
-
`translateX(-50%)` must animate as `translateX(-50%) translateY(30px)`, or it jumps sideways on
|
|
483
|
-
every transition. Where a breakpoint drops the centring, restate the transform there too.
|
|
484
|
-
* **A slow drift on the backdrop** (`scale(1.06)` to `scale(1)` over several seconds) makes a
|
|
485
|
-
change read as movement rather than a cut. Put it on the image, not the slide, so it does not
|
|
486
|
-
fight the transition.
|
|
487
|
-
|
|
488
|
-
This works with the Fade transition, which crossfades the slides underneath while the contents
|
|
489
|
-
move independently.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
### Carousel on mobile, grid on desktop
|
|
493
|
-
|
|
494
|
-
```js
|
|
495
|
-
setGroup(sliderId, 'layout', { layoutMode: 'static md:slider', gridColumns: '3 sm:1' });
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
Above `md` it is a plain CSS grid; at `md` and below it is a real carousel. The slider is built and
|
|
499
|
-
torn down live as the viewport crosses the breakpoint. **Controls are hidden in static mode**,
|
|
500
|
-
because there is no live slider to drive.
|
|
501
|
-
|
|
502
|
-
### Card decks with no slider (Deck, Fall): the one place script is allowed
|
|
503
|
-
|
|
504
|
-
Structure: a Wrapper with `sliderlessSync.customElement` pointing at your cards, no Slider inside,
|
|
505
|
-
cards nested three deep as `card > lift > content` so the 3-D survives.
|
|
506
|
-
|
|
507
|
-
```js
|
|
508
|
-
setGroup(wrapperId, 'sliderlessSync', {
|
|
509
|
-
customElement: '.deck-card', customElementNav: '{true}',
|
|
510
|
-
loop: '{true}', autoplay: '{true}', autoplayInterval: '4000'
|
|
511
|
-
});
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
CSS handles the front card and its two neighbours from `is-active` / `is-prev` / `is-next`. What
|
|
515
|
-
CSS cannot do is know that a given card is *three* back, so the rows behind, the z-index ladder and
|
|
516
|
-
the click targets need a script. That is case 1 of the native-first gate.
|
|
517
|
-
|
|
518
|
-
The contract between that script and the CSS is one attribute, **`data-pos`**, which the script
|
|
519
|
-
derives from `is-active` and writes on every card. It is a **signed** offset from the active card,
|
|
520
|
-
so the CSS can fan the left and right sides in opposite directions:
|
|
521
|
-
|
|
522
|
-
```css
|
|
523
|
-
.deck-stack-featured &:is([data-pos='2'], [data-pos='-2']) { /* ring 2, both sides */ }
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
The stack container carries **`data-tiers`**, how many rings are actually in play. It is computed,
|
|
527
|
-
not fixed: a looping deck needs spare cards to hide the wrap behind, so the script reduces the
|
|
528
|
-
tiers when there are too few cards.
|
|
529
|
-
|
|
530
|
-
Writing an unsigned rank, or naming the attribute anything else, produces a flat pile: the rules
|
|
531
|
-
above simply never match. The shipped script does this:
|
|
532
|
-
|
|
533
|
-
```js
|
|
534
|
-
// 1. make a custom property interpolable, so a blur can animate (case 2)
|
|
535
|
-
CSS.registerProperty({ name: '--stack-focus', syntax: '<number>',
|
|
536
|
-
inherits: true, initialValue: '0' });
|
|
537
|
-
|
|
538
|
-
// 2. how many rings this deck can afford, given looping and the card count
|
|
539
|
-
var mayLoop = wrap.dataset.loop === 'true' && total >= MIN_LOOP;
|
|
540
|
-
stack.dataset.tiers = mayLoop ? Math.min(MAX_TIERS, Math.floor((total - 3) / 2)) : MAX_TIERS;
|
|
541
|
-
|
|
542
|
-
// 3. per card: SIGNED offset from the active card. On a looping deck the offset
|
|
543
|
-
// wraps, so the far end of the list reads as the near side, not as distance 8.
|
|
544
|
-
var d = n - active;
|
|
545
|
-
if (mayLoop) {
|
|
546
|
-
if (d > total / 2) d -= total;
|
|
547
|
-
if (d < -total / 2) d += total;
|
|
548
|
-
}
|
|
549
|
-
card.dataset.pos = d;
|
|
550
|
-
|
|
551
|
-
// 4. hit-testing: stack order by distance, and clicks off beyond the clickable rings
|
|
552
|
-
var rank = Math.abs(d);
|
|
553
|
-
card.style.zIndex = String(total - rank);
|
|
554
|
-
lift.style.pointerEvents = rank <= NAV_RINGS ? 'auto' : 'none';
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
Do not clamp `data-pos` to the tier count. Cards past the deepest ring should match no rule and
|
|
558
|
-
stay stacked at the back; clamping piles them onto the last visible ring instead.
|
|
559
|
-
|
|
560
|
-
Both decks also set `--i` on each line of card content so the CSS can stagger it:
|
|
561
|
-
|
|
562
|
-
```js
|
|
563
|
-
card.querySelectorAll('.deck-card-featured__content > *')
|
|
564
|
-
.forEach(function (line, j) { line.style.setProperty('--i', j); });
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
Step 4 is Deck only. **Fall's script is much smaller**: it sets `--i` and a signed `data-pos`, and
|
|
568
|
-
nothing else. Fall never loops, so there is no wrap to fold and no z-index ladder to maintain,
|
|
569
|
-
because the cards fall past each other rather than fanning around a front card.
|
|
570
|
-
|
|
571
|
-
Recompute on every change: watch the stack with a `MutationObserver` filtered to `class`, since
|
|
572
|
-
`is-active` moving is the only signal you get.
|
|
573
|
-
|
|
574
|
-
### Where a script goes
|
|
575
|
-
|
|
576
|
-
**Not in a `<script>` element.** Etch silently drops one from the render, so the page looks right
|
|
577
|
-
in the builder and ships with no behaviour at all.
|
|
578
|
-
|
|
579
|
-
Every block has an optional `script` field (`EtchBlockScript`), which is Etch's JavaScript code
|
|
580
|
-
block. Put the code on the **component instance that owns the markup**, which for a card stack is
|
|
581
|
-
the Wrapper:
|
|
582
|
-
|
|
583
|
-
```js
|
|
584
|
-
const wrapperId = etch.blocks.create({
|
|
585
|
-
type: 'etch/component', version: 1, context: {}, children: [ /* ... */ ],
|
|
586
|
-
componentId: C['DWC Slider Wrapper'], attributes: {},
|
|
587
|
-
script: { code: "(function () { /* ... */ })();" }
|
|
588
|
-
});
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
`code` is **plain source** over the API. The base64 you see in a premade template `.json` is only
|
|
592
|
-
how the export serialises it; do not encode it yourself.
|
|
593
|
-
|
|
594
|
-
Etch renders it as `<script type="module" defer>`, so it runs after parsing and top-level `await`
|
|
595
|
-
is available. Read it back with `etch.blocks.getJson(id).script`.
|
|
596
|
-
|
|
597
|
-
Component scripts are also the reason a card deck keeps working: the bridge leaves Sync Without
|
|
598
|
-
Slider wrappers unreconstructed precisely so an author's script keeps observing the original nodes.
|
|
599
|
-
|
|
600
|
-
Card counts for a looping deck: three are always visible (front plus two), and a loop needs a
|
|
601
|
-
hidden slot at each end, so **five is the minimum** and nine gives the full three-row fan. Fall
|
|
602
|
-
never loops, so it has no minimum.
|
|
603
|
-
|
|
604
|
-
***
|
|
605
|
-
|
|
606
403
|
## Script library
|
|
607
404
|
|
|
608
405
|
Include these at the top of any script.
|
|
@@ -624,25 +421,6 @@ function setGroup(id, key, patch) {
|
|
|
624
421
|
etch.blocks.setAttribute(id, key, '{' + JSON.stringify(next) + '}');
|
|
625
422
|
}
|
|
626
423
|
|
|
627
|
-
function node(extra, children) {
|
|
628
|
-
return Object.assign({ version: 1, context: {}, children: children || [] }, extra);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function component(componentId, attributes, children) {
|
|
632
|
-
return node({ type: 'etch/component', componentId: componentId,
|
|
633
|
-
attributes: attributes || {} }, children);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function slot(slotName, children) {
|
|
637
|
-
return node({ type: 'etch/slot-content', slotName: slotName }, children);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
function el(tag, attributes, children) {
|
|
641
|
-
return node({ type: 'etch/element', tag: tag, attributes: attributes || {} }, children);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function text(t) { return node({ type: 'etch/text', text: t }); }
|
|
645
|
-
|
|
646
424
|
function findByRef(nodes, componentId, out) {
|
|
647
425
|
out = out || [];
|
|
648
426
|
for (const n of nodes || []) {
|
|
@@ -653,35 +431,6 @@ function findByRef(nodes, componentId, out) {
|
|
|
653
431
|
}
|
|
654
432
|
```
|
|
655
433
|
|
|
656
|
-
### Build a wrapper with N slides
|
|
657
|
-
|
|
658
|
-
```js
|
|
659
|
-
const C = comps();
|
|
660
|
-
const slides = [];
|
|
661
|
-
for (let i = 0; i < 5; i++) {
|
|
662
|
-
slides.push(component(C['DWC Slide'], {}, [
|
|
663
|
-
slot('Content', [ el('div', {}, [ text('Slide ' + (i + 1)) ]) ])
|
|
664
|
-
]));
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const id = etch.blocks.create(
|
|
668
|
-
component(C['DWC Slider Wrapper'], {}, [
|
|
669
|
-
slot('Sliders_and_Controls', [
|
|
670
|
-
component(C['DWC Slider'], {}, [ slot('Slides', slides) ]),
|
|
671
|
-
component(C['DWC Slider Nav Button'], {}),
|
|
672
|
-
component(C['DWC Slider Pagination'], {})
|
|
673
|
-
])
|
|
674
|
-
])
|
|
675
|
-
);
|
|
676
|
-
return id;
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
An empty Slide has no content and so no height. Give slides something to size before wondering why
|
|
680
|
-
the slider is a flat line.
|
|
681
|
-
|
|
682
|
-
Build **one** first, screenshot it, then scale to the full count. Do not generate twenty slides
|
|
683
|
-
before you have seen one render.
|
|
684
|
-
|
|
685
434
|
### Read the current state of a slider
|
|
686
435
|
|
|
687
436
|
```js
|
|
@@ -710,6 +459,28 @@ mistake, and `ltr sm:ttb` does the opposite of what it looks like.
|
|
|
710
459
|
|
|
711
460
|
**Never set a prop to its default.** It adds noise to the markup and hides real intent.
|
|
712
461
|
|
|
462
|
+
**Adding a control component does not remove the built-in one. Switch the built-in off yourself.**
|
|
463
|
+
Three of them ship **on**, and each renders a second set of controls underneath your design:
|
|
464
|
+
|
|
465
|
+
| Built-in | Prop | Default |
|
|
466
|
+
| --- | --- | --- |
|
|
467
|
+
| Arrows | `props.navigation.navigationArrows` | `true` |
|
|
468
|
+
| Pagination dots | `props.navigation.paginationDots` | `true` |
|
|
469
|
+
| Play/pause toggle | `props.autoplay.playPauseButton` | `true` |
|
|
470
|
+
|
|
471
|
+
Dropping in a DWC Slider Pagination while `paginationDots` is still `true` gives you two sets of
|
|
472
|
+
dots, and `playPauseButton` puts a circular toggle at the slider's bottom-right of a design that
|
|
473
|
+
never asked for one. This is the one place the "never set a prop to its default" rule reads
|
|
474
|
+
backwards: you are not setting a default, you are turning an unwanted default off.
|
|
475
|
+
|
|
476
|
+
**Pagination dot size is derived from the dot font size. Change the font size, not the size.**
|
|
477
|
+
`props.dot.size` is declared as `calc(var(--font-size) * 2)`, so it tracks `props.dot.fontSize`
|
|
478
|
+
by design. Writing a fixed `size` severs that and the dots stop scaling with the type. Leave
|
|
479
|
+
`size` alone and set `fontSize` to half the dot you want: `0.32rem` gives a 10px dot.
|
|
480
|
+
|
|
481
|
+
Default mode also puts the slide **number** inside each dot. For plain dots set
|
|
482
|
+
`props.dot.textColor` and `props.dot.activeTextColor` to `transparent`.
|
|
483
|
+
|
|
713
484
|
**A nested inner slider must be Slide or Fade, never Loop or Infinite Scroll.** A looping slider
|
|
714
485
|
makes hidden copies of its slides, and copying a slider that contains a slider breaks both. To wrap
|
|
715
486
|
around, use Fade, or Slide with Rewind.
|
|
@@ -733,27 +504,124 @@ rather than debugging a slider that will never start.
|
|
|
733
504
|
|
|
734
505
|
**Do not touch the plugin's own stylesheet.** Style through the Slider Class and CSS variables.
|
|
735
506
|
|
|
736
|
-
**A
|
|
737
|
-
the
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
507
|
+
**A component's declared default is never written onto an instance you create.** Defaults populate
|
|
508
|
+
the Etch settings panel. They are not present in the attributes map of a block you create through
|
|
509
|
+
the API, so `getAttribute` returns nothing for them. This is the one exception to "never set a prop
|
|
510
|
+
to its default", and it fails silently in two different ways.
|
|
511
|
+
|
|
512
|
+
**Symptom one, a condition that never matches.** When a condition inside the component reads a flag
|
|
513
|
+
prop and the key is absent, the expression does not resolve and the condition returns false
|
|
514
|
+
**whichever operator it uses**, so both branches vanish at once. A DWC Slider Nav Button created
|
|
515
|
+
with `navigationType` alone renders `<button>` with no icon: the default arrow sits behind
|
|
516
|
+
`useCustomArrow isFalsy` and the custom SVG behind `useCustomArrow isTruthy`, and neither appears.
|
|
517
|
+
Writing `useCustomArrow: '{false}'`, already the default, makes the arrow render. If a component
|
|
518
|
+
renders structurally but its inner content is missing, check this first.
|
|
519
|
+
|
|
520
|
+
**Symptom two, a class prop that silently replaces the default.** Class props are arrays of
|
|
521
|
+
**space-separated style ids**. Writing one id does not add to the default, it becomes the whole
|
|
522
|
+
value, and the default is gone with no error. Setting `sliderClass` to your own class alone drops
|
|
523
|
+
the slider's navigation variables, which carry the arrow, pagination, play/pause and progress
|
|
524
|
+
styling, and nothing looks wrong until one of those controls misbehaves. Always read, append,
|
|
525
|
+
write back, never overwrite.
|
|
526
|
+
|
|
527
|
+
## `sliderClass` carries a minted per-instance class
|
|
528
|
+
|
|
529
|
+
Every slider needs its **own copy** of the navigation variables so two sliders on one page can be
|
|
530
|
+
styled independently. The plugin mints that copy for you as a class named `.dwc-slider-vars-XXXXXX`
|
|
531
|
+
whose CSS is copied from `.slider-navigation-vars`. A correctly built slider holds the minted class
|
|
532
|
+
plus any design class of your own, which is what every premade template does:
|
|
533
|
+
|
|
534
|
+
```
|
|
535
|
+
.dwc-slider-vars-vsj76d .slider-chronos
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**Never put `.slider-navigation-vars` into the slot yourself.** Resolving it by selector and writing
|
|
539
|
+
that shared id looks right and renders right, so nothing warns you. It silently breaks duplication:
|
|
540
|
+
the plugin only manages classes matching `.dwc-slider-vars-*`, so a slider carrying the shared
|
|
541
|
+
default gets no fresh class when it is duplicated, and every copy then reads one set of variables.
|
|
542
|
+
Recolouring one slider's arrows changes them all. This is the single easiest way to ship a slider
|
|
543
|
+
that looks finished and is not.
|
|
544
|
+
|
|
545
|
+
**Create first, append second.** Leave `sliderClass` alone when you create the slider. The plugin
|
|
546
|
+
polls roughly every two seconds and mints a class for any instance that appeared after the builder
|
|
547
|
+
loaded while the prop is still unset. Once it has, read the value back and append your design class
|
|
548
|
+
to what it wrote:
|
|
549
|
+
|
|
550
|
+
```js
|
|
551
|
+
const ids = (etch.blocks.getAttribute(sliderId, 'sliderClass') || '').split(/\s+/).filter(Boolean);
|
|
552
|
+
// ids now starts with a minted .dwc-slider-vars-* id. Append to it, never replace it.
|
|
553
|
+
ids.push(myStyleId);
|
|
554
|
+
etch.blocks.setAttribute(sliderId, 'sliderClass', ids.join(' '));
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
If the prop is still empty after a few seconds, mint one yourself. Copy the CSS from
|
|
558
|
+
`.slider-navigation-vars` and keep the required name shape, because a class named anything else is
|
|
559
|
+
not managed:
|
|
560
|
+
|
|
561
|
+
```js
|
|
562
|
+
const src = etch.styles.list().find(s => s.selector === '.slider-navigation-vars');
|
|
563
|
+
const taken = new Set(etch.styles.list().map(s => s.selector));
|
|
564
|
+
let selector;
|
|
565
|
+
do {
|
|
566
|
+
selector = '.dwc-slider-vars-' + Math.random().toString(36).slice(2, 8);
|
|
567
|
+
} while (taken.has(selector));
|
|
568
|
+
const varsId = etch.styles.create(selector, src.css);
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**Resolve by selector, not by the documented id.** The prop reference records `sliderClass`'s
|
|
572
|
+
default as a style id, but that id differs per install, so look the style up by
|
|
573
|
+
`.slider-navigation-vars` when you need its CSS to copy.
|
|
742
574
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
already the default, makes the arrow render. If a component renders structurally but its inner
|
|
747
|
-
content is missing, this is the first thing to check.
|
|
575
|
+
**Verify the shape before you call the slider done.** Read `sliderClass` back and confirm it holds
|
|
576
|
+
exactly one `.dwc-slider-vars-*` id, plus your design class if you added one. A slider holding
|
|
577
|
+
`.slider-navigation-vars`, or holding no vars class at all, is a defect you introduced.
|
|
748
578
|
|
|
749
|
-
**
|
|
750
|
-
|
|
751
|
-
|
|
579
|
+
**Anything whose resting state waits on `is-active` breaks in edit mode.** The slider runs in
|
|
580
|
+
Preview but not while the user is editing, so in edit mode no slide carries `is-active` and none of
|
|
581
|
+
your active-state rules apply. Height that comes from the active slide collapses; an element
|
|
582
|
+
resting at `opacity: 0` stays invisible. Guard on the condition "nothing is active yet", which is
|
|
583
|
+
true only in edit mode and switches itself off the moment the slider runs:
|
|
752
584
|
|
|
753
585
|
```css
|
|
754
|
-
|
|
586
|
+
.figure { opacity: 0; }
|
|
587
|
+
.splide__slide.is-active .figure { opacity: 1; }
|
|
588
|
+
.splide:not(:has(.splide__slide.is-active)) .figure { opacity: 1; }
|
|
755
589
|
```
|
|
756
590
|
|
|
591
|
+
**Where the check goes depends on which element receives the class.** Above, the slide becomes
|
|
592
|
+
active and the figure is inside it, so the test sits on a shared ancestor. With Sync Custom Element
|
|
593
|
+
your own elements receive `is-active` themselves, so test their container instead:
|
|
594
|
+
`.steps:not(:has(.step.is-active)) .step`. A single canned selector does not fit every case; work
|
|
595
|
+
out which element actually gets the class first.
|
|
596
|
+
|
|
597
|
+
`.etch-builder-block` alone is not enough. It says "in the builder", not "not running", so a rule
|
|
598
|
+
gated only on it can also suppress the effect in Preview.
|
|
599
|
+
|
|
600
|
+
**The same gate is needed for anything positioned against the slide, not just for `is-active`
|
|
601
|
+
states.** A slide's height arrives with Splide. Until it mounts the slide has none, so absolutely
|
|
602
|
+
positioned children have nothing to sit in, and the whole design collapses into a pile in the
|
|
603
|
+
builder. That covers an overlay caption, a corner button and an `inset: 0` image alike. Give the slide a height under
|
|
604
|
+
the same "nothing is active yet" condition, so it applies in the builder and stops the moment the
|
|
605
|
+
slider runs:
|
|
606
|
+
|
|
607
|
+
```css
|
|
608
|
+
/* inside the slide class's own entry */
|
|
609
|
+
position: relative;
|
|
610
|
+
|
|
611
|
+
.splide:not(:has(.splide__slide.is-active)) & {
|
|
612
|
+
min-block-size: to-rem(480px);
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Build the design so the builder shows something honest. An author who cannot see the slide cannot
|
|
617
|
+
edit its content.
|
|
618
|
+
|
|
619
|
+
**Read computed style, not source, to decide what the plugin already does.** `getComputedStyle(el)`
|
|
620
|
+
answers directly and in one call. Do not conclude from page source that a rule is absent: component
|
|
621
|
+
styles are inlined into the page but the engine's stylesheet is a separate file, so a rule can be
|
|
622
|
+
in force while being nowhere in the HTML. Presence in the DOM is not visibility either, since a
|
|
623
|
+
hidden element is still there.
|
|
624
|
+
|
|
757
625
|
**Do not anchor against a neighbour that stops shrinking.** A `vw` offset tuned at one width
|
|
758
626
|
silently collides at another, because `min()` and `clamp()` neighbours stop shrinking while the
|
|
759
627
|
`vw` keeps going. Derive the clearance from the neighbour's real footprint instead:
|
|
@@ -768,6 +636,19 @@ right: calc(4vw + min(15vw, 195px) + 2.5vw);
|
|
|
768
636
|
Check the mid range explicitly. Between the widest layout and the first breakpoint is where
|
|
769
637
|
side-by-side compositions fail, and it is the range nobody screenshots.
|
|
770
638
|
|
|
639
|
+
**Missing Preview and Grid buttons on a slider you just built mean the install is older than
|
|
640
|
+
1.2.1, not that the build is broken.** The builder watches the canvas and attaches those controls
|
|
641
|
+
to sliders as they appear, but before 1.2.1 that watcher only started if a slider was **already**
|
|
642
|
+
on the page when the builder loaded. Build onto an empty page there and nothing is watching, so the
|
|
643
|
+
controls never appear until the tab is reloaded, however the blocks were added, by script or by
|
|
644
|
+
hand.
|
|
645
|
+
|
|
646
|
+
You cannot fix it from a script on those versions: the refresh is an iframe reload, and safe mode
|
|
647
|
+
has no `window` or `document` to reach it with. Do not paper over it by sending the user to reload
|
|
648
|
+
every time. Confirm the blocks and the published page are right, tell them it is fixed in 1.2.1,
|
|
649
|
+
and mention that a reload brings the controls back meanwhile. Preview is how they watch the slider
|
|
650
|
+
actually run, so it is worth naming rather than leaving them to find it.
|
|
651
|
+
|
|
771
652
|
**A save can lag the front end.** `etch.saveAsync()` resolves before the change is necessarily
|
|
772
653
|
readable on the published page, so a fetch straight afterwards can return the previous value and
|
|
773
654
|
look like a failed write. Re-read through `etch.blocks.getAttribute` to confirm intent, and treat a
|