sliderpro-agentic-skills-etch 0.1.2 → 0.2.1

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.
@@ -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 renames
284
- the auto-generated class on save, so never style directly on `.slider-navigation-vars`: add your
285
- own class (e.g. `.my-slider`) and style that. Classes you add yourself are never renamed.
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