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.
@@ -4,9 +4,13 @@ icon: robot
4
4
 
5
5
  # AI Skills Reference
6
6
 
7
- Everything an agent needs to build and configure **Slider Pro for Etch** through the Etch AI
8
- Connector. Read this file in full at session start. Its companion,
9
- `slider-pro-skills-reference.md`, is lookup-only: Grep into it, never read it whole.
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`. Inline multi-line quoting breaks in
110
- every shell eventually.
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
- const id = etch.blocks.create(el('article', {}, []));
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
@@ -242,6 +201,75 @@ Either way the CSS rule itself is `etch.styles.create('.fall-card', '...')`, whi
242
201
  selector has to match.
243
202
 
244
203
 
204
+ ### CSS conventions
205
+
206
+ Match the shipped templates. An install that already has premade sliders on it has these
207
+ conventions everywhere, and a build that ignores them reads as foreign.
208
+
209
+ **Naming.** The block is the slider, named after the design:
210
+
211
+ | Thing | Pattern | Example |
212
+ | --- | --- | --- |
213
+ | Wrapper class, slider designs | `.slider-<name>-wrapper` | `.slider-flow-wrapper` |
214
+ | Wrapper class, card stacks | `.slider-wrapper-<name>` | `.slider-wrapper-fall` |
215
+ | Slider class | `.slider-<name>` | `.slider-chronos` |
216
+ | Elements | `.slider-<name>__<element>` | `.slider-team__sync` |
217
+ | Modifiers | `.slider-<name>__<element>--<variant>` | `.slider-afterform__word--form` |
218
+
219
+ The wrapper class carries the word `wrapper` and the slider class carries the word `slider`. Do not
220
+ invent a short prefix of your own.
221
+
222
+ **One style entry per element class.** Never one nested parent entry holding the whole component.
223
+ `etch.styles.create()` per class, each with its own CSS:
224
+
225
+ ```js
226
+ const S = {};
227
+ S.wrapper = etch.styles.create('.slider-afterform-wrapper', '/* ... */');
228
+ S.caption = etch.styles.create('.slider-afterform__caption', '/* ... */');
229
+ S.title = etch.styles.create('.slider-afterform__caption-title', '/* ... */');
230
+ ```
231
+
232
+ Inside an element's **own** entry, nesting is preferred for media queries, pseudo-elements
233
+ (`&::after`), states (`&:hover`, `&.is-active`), ancestor states
234
+ (`.splide__slide.is-active &`) and child tag selectors (`& img`, `& svg`). Do **not** nest another
235
+ class's rules (`&__title`) inside a different class's entry: that class gets its own entry.
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
+
266
+ **Renaming is not one operation.** A style entry's selector and a block's `class` attribute are
267
+ independent. Renaming the selector rewrites the rendered class only where the class came from a
268
+ **component class prop**, because that prop stores the style id and resolves it at render. On a
269
+ plain element the `class` attribute is literal, so a selector rename leaves it pointing at a
270
+ selector that no longer exists. Rename both, or the element silently loses its CSS.
271
+
272
+
245
273
  ### Group props and the one-extra-brace rule
246
274
 
247
275
  Most Slider props live in groups (`layout`, `motion`, `slides`, `autoplay`, ...). A group is stored
@@ -274,16 +302,50 @@ the prop reference.
274
302
 
275
303
  ### Visual verification
276
304
 
277
- You cannot see the page from inside safe mode. After any visual change:
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.
278
314
 
279
315
  ```bash
280
- npx @digital-gravy/etch-connector shot -t "<tab>" -s ".my-slider" -o ./out.png
281
- npx @digital-gravy/etch-connector html ".my-slider" -t "<tab>"
282
- npx @digital-gravy/etch-connector computed ".my-slider .splide__slide" -t "<tab>" --props transform,opacity
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?
283
318
  ```
284
319
 
285
- Then actually look at the screenshot. A slider that mounted is not the same as a slider that looks
286
- right.
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.
327
+
328
+ ```bash
329
+ chrome.exe --headless=new --remote-debugging-port=9555 \
330
+ --user-data-dir="<temp dir>/cdp-profile" --no-first-run about:blank &
331
+ ```
332
+
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.
287
349
 
288
350
  ### Saving
289
351
 
@@ -326,7 +388,7 @@ Four rules follow from it:
326
388
 
327
389
  ## Decision tree
328
390
 
329
- **Build from scratch:** components and props first, then CSS.
391
+ **Build from scratch:** read `slider-pro-skills-build.md` now. Components and props first, then CSS.
330
392
 
331
393
  Reach for a premade template only when the user names one ("use Deck Featured"). Then paste its
332
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
@@ -338,199 +400,6 @@ configure it the way its documentation describes instead of inventing new CSS ov
338
400
 
339
401
  ***
340
402
 
341
- ## Design recipes
342
-
343
- Each of these is a shipped template reduced to the settings that produce it.
344
-
345
- ### Cover-flow carousel (Slider Flow): no script, no CSS
346
-
347
- The whole effect is the `slides.*` group, which applies transforms to inactive slides and separate
348
- `-Active` values to the current one.
349
-
350
- ```js
351
- setGroup(sliderId, 'layout', { slidesPerPage: '3 lg:1', gapBetweenSlides: '20px',
352
- sliderEdgeOffset: 'lg:20% md:18%' });
353
- setGroup(sliderId, 'motion', { focus: 'center', loop: '{true}', speed: '800',
354
- updateOnMove: '{true}' });
355
- setGroup(sliderId, 'slides', { perspective: '950px', opacity: '0.3', scale: '0.9',
356
- borderRadius: '1rem',
357
- transition: 'transform 0.5s ease, opacity 0.5s ease' });
358
- setGroup(sliderId, 'dimensions', { sliderHeight: '350px md:300px sm:180px' });
359
- ```
360
-
361
- `motion.focus: 'center'` is what puts the active slide in the middle. The neighbours are dimmed and
362
- shrunk purely by `slides.opacity` and `slides.scale`, which apply to inactive slides only.
363
-
364
- For an angled flip, add `slides.rotateY` and turn on `slides.flipNextRotateY` so the slide on the
365
- other side mirrors the angle instead of repeating it. Same for `translateX` with
366
- `flipNextTranslateX`.
367
-
368
- ### Logo marquee (Slider Marquee): no script
369
-
370
- Two Sliders in one Wrapper, running in opposite directions.
371
-
372
- ```js
373
- // both rows
374
- setGroup(sliderId, 'dimensions', { slideAutoWidth: '{true}' }); // slides size to content
375
- setGroup(sliderId, 'motion', { loop: '{true}' });
376
- setGroup(sliderId, 'autoscroll', { infiniteScroll: '{true}', scrollSpeed: '1',
377
- pauseOnHover: '{false}', pauseOnFocus: '{false}' });
378
- setGroup(sliderId, 'layout', { gapBetweenSlides: '2.4rem' });
379
-
380
- // second row only
381
- setGroup(secondId, 'sliderSetup', { slldeDirection: 'rtl' }); // note the spelling
382
- ```
383
-
384
- Edge Fade goes on the **Wrapper** (`props.edgeFade`), not the sliders, so both rows fade as one
385
- unit. `props.pauseSlidersOnHover` on the Wrapper pauses both rows together.
386
-
387
- Infinite Scroll needs the Auto-Scroll extension enabled in admin settings, which is the default.
388
-
389
- ### Synced elements (Team, Stack)
390
-
391
- Driving arbitrary elements on the page from the slider is one prop.
392
-
393
- ```js
394
- setGroup(sliderId, 'sliderSetup', { syncCustomElement: '.timeline-node',
395
- syncCustomElementNav: '{true}' });
396
- ```
397
-
398
- The plugin then moves `is-active`, `is-prev` and `is-next` across those elements as the slider
399
- moves, and with nav on, clicking one jumps the slider to it. **You write the CSS for the three
400
- states.** The elements do not need to be inside the slider and do not need to match the slide
401
- count.
402
-
403
- The selector takes a comma-separated list, so one slider can drive several groups at once. Team
404
- uses `'.slider-team__sync, .slider-team__sync-heading'` to move a portrait and a heading together.
405
-
406
- Grep `## 3. Sync Custom Element` in the reference for multiple selectors and the overlap caveat.
407
-
408
- ### Main and thumbnails in one wrapper (Zeon)
409
-
410
- A full-bleed hero with a thumbnail strip needs no sync prop at all, just roles:
411
-
412
- ```js
413
- setGroup(mainId, 'sliderSetup', { sliderRole: 'main', transitionType: 'Fade' });
414
- setGroup(thumbId, 'sliderSetup', { sliderRole: 'thumbnails', transitionType: 'Loop' });
415
- ```
416
-
417
- Both inside the same Wrapper and they pair automatically. A Wrapper can hold more than one
418
- thumbnail slider, which is how Zeon runs a background layer and a strip off the same main.
419
-
420
- ### Carousel on mobile, grid on desktop
421
-
422
- ```js
423
- setGroup(sliderId, 'layout', { layoutMode: 'static md:slider', gridColumns: '3 sm:1' });
424
- ```
425
-
426
- Above `md` it is a plain CSS grid; at `md` and below it is a real carousel. The slider is built and
427
- torn down live as the viewport crosses the breakpoint. **Controls are hidden in static mode**,
428
- because there is no live slider to drive.
429
-
430
- ### Card decks with no slider (Deck, Fall): the one place script is allowed
431
-
432
- Structure: a Wrapper with `sliderlessSync.customElement` pointing at your cards, no Slider inside,
433
- cards nested three deep as `card > lift > content` so the 3-D survives.
434
-
435
- ```js
436
- setGroup(wrapperId, 'sliderlessSync', {
437
- customElement: '.deck-card', customElementNav: '{true}',
438
- loop: '{true}', autoplay: '{true}', autoplayInterval: '4000'
439
- });
440
- ```
441
-
442
- CSS handles the front card and its two neighbours from `is-active` / `is-prev` / `is-next`. What
443
- CSS cannot do is know that a given card is *three* back, so the rows behind, the z-index ladder and
444
- the click targets need a script. That is case 1 of the native-first gate.
445
-
446
- The contract between that script and the CSS is one attribute, **`data-pos`**, which the script
447
- derives from `is-active` and writes on every card. It is a **signed** offset from the active card,
448
- so the CSS can fan the left and right sides in opposite directions:
449
-
450
- ```css
451
- .deck-stack-featured &:is([data-pos='2'], [data-pos='-2']) { /* ring 2, both sides */ }
452
- ```
453
-
454
- The stack container carries **`data-tiers`**, how many rings are actually in play. It is computed,
455
- not fixed: a looping deck needs spare cards to hide the wrap behind, so the script reduces the
456
- tiers when there are too few cards.
457
-
458
- Writing an unsigned rank, or naming the attribute anything else, produces a flat pile: the rules
459
- above simply never match. The shipped script does this:
460
-
461
- ```js
462
- // 1. make a custom property interpolable, so a blur can animate (case 2)
463
- CSS.registerProperty({ name: '--stack-focus', syntax: '<number>',
464
- inherits: true, initialValue: '0' });
465
-
466
- // 2. how many rings this deck can afford, given looping and the card count
467
- var mayLoop = wrap.dataset.loop === 'true' && total >= MIN_LOOP;
468
- stack.dataset.tiers = mayLoop ? Math.min(MAX_TIERS, Math.floor((total - 3) / 2)) : MAX_TIERS;
469
-
470
- // 3. per card: SIGNED offset from the active card. On a looping deck the offset
471
- // wraps, so the far end of the list reads as the near side, not as distance 8.
472
- var d = n - active;
473
- if (mayLoop) {
474
- if (d > total / 2) d -= total;
475
- if (d < -total / 2) d += total;
476
- }
477
- card.dataset.pos = d;
478
-
479
- // 4. hit-testing: stack order by distance, and clicks off beyond the clickable rings
480
- var rank = Math.abs(d);
481
- card.style.zIndex = String(total - rank);
482
- lift.style.pointerEvents = rank <= NAV_RINGS ? 'auto' : 'none';
483
- ```
484
-
485
- Do not clamp `data-pos` to the tier count. Cards past the deepest ring should match no rule and
486
- stay stacked at the back; clamping piles them onto the last visible ring instead.
487
-
488
- Both decks also set `--i` on each line of card content so the CSS can stagger it:
489
-
490
- ```js
491
- card.querySelectorAll('.deck-card-featured__content > *')
492
- .forEach(function (line, j) { line.style.setProperty('--i', j); });
493
- ```
494
-
495
- Step 4 is Deck only. **Fall's script is much smaller**: it sets `--i` and a signed `data-pos`, and
496
- nothing else. Fall never loops, so there is no wrap to fold and no z-index ladder to maintain,
497
- because the cards fall past each other rather than fanning around a front card.
498
-
499
- Recompute on every change: watch the stack with a `MutationObserver` filtered to `class`, since
500
- `is-active` moving is the only signal you get.
501
-
502
- ### Where a script goes
503
-
504
- **Not in a `<script>` element.** Etch silently drops one from the render, so the page looks right
505
- in the builder and ships with no behaviour at all.
506
-
507
- Every block has an optional `script` field (`EtchBlockScript`), which is Etch's JavaScript code
508
- block. Put the code on the **component instance that owns the markup**, which for a card stack is
509
- the Wrapper:
510
-
511
- ```js
512
- const wrapperId = etch.blocks.create({
513
- type: 'etch/component', version: 1, context: {}, children: [ /* ... */ ],
514
- componentId: C['DWC Slider Wrapper'], attributes: {},
515
- script: { code: "(function () { /* ... */ })();" }
516
- });
517
- ```
518
-
519
- `code` is **plain source** over the API. The base64 you see in a premade template `.json` is only
520
- how the export serialises it; do not encode it yourself.
521
-
522
- Etch renders it as `<script type="module" defer>`, so it runs after parsing and top-level `await`
523
- is available. Read it back with `etch.blocks.getJson(id).script`.
524
-
525
- Component scripts are also the reason a card deck keeps working: the bridge leaves Sync Without
526
- Slider wrappers unreconstructed precisely so an author's script keeps observing the original nodes.
527
-
528
- Card counts for a looping deck: three are always visible (front plus two), and a loop needs a
529
- hidden slot at each end, so **five is the minimum** and nine gives the full three-row fan. Fall
530
- never loops, so it has no minimum.
531
-
532
- ***
533
-
534
403
  ## Script library
535
404
 
536
405
  Include these at the top of any script.
@@ -552,25 +421,6 @@ function setGroup(id, key, patch) {
552
421
  etch.blocks.setAttribute(id, key, '{' + JSON.stringify(next) + '}');
553
422
  }
554
423
 
555
- function node(extra, children) {
556
- return Object.assign({ version: 1, context: {}, children: children || [] }, extra);
557
- }
558
-
559
- function component(componentId, attributes, children) {
560
- return node({ type: 'etch/component', componentId: componentId,
561
- attributes: attributes || {} }, children);
562
- }
563
-
564
- function slot(slotName, children) {
565
- return node({ type: 'etch/slot-content', slotName: slotName }, children);
566
- }
567
-
568
- function el(tag, attributes, children) {
569
- return node({ type: 'etch/element', tag: tag, attributes: attributes || {} }, children);
570
- }
571
-
572
- function text(t) { return node({ type: 'etch/text', text: t }); }
573
-
574
424
  function findByRef(nodes, componentId, out) {
575
425
  out = out || [];
576
426
  for (const n of nodes || []) {
@@ -581,35 +431,6 @@ function findByRef(nodes, componentId, out) {
581
431
  }
582
432
  ```
583
433
 
584
- ### Build a wrapper with N slides
585
-
586
- ```js
587
- const C = comps();
588
- const slides = [];
589
- for (let i = 0; i < 5; i++) {
590
- slides.push(component(C['DWC Slide'], {}, [
591
- slot('Content', [ el('div', {}, [ text('Slide ' + (i + 1)) ]) ])
592
- ]));
593
- }
594
-
595
- const id = etch.blocks.create(
596
- component(C['DWC Slider Wrapper'], {}, [
597
- slot('Sliders_and_Controls', [
598
- component(C['DWC Slider'], {}, [ slot('Slides', slides) ]),
599
- component(C['DWC Slider Nav Button'], {}),
600
- component(C['DWC Slider Pagination'], {})
601
- ])
602
- ])
603
- );
604
- return id;
605
- ```
606
-
607
- An empty Slide has no content and so no height. Give slides something to size before wondering why
608
- the slider is a flat line.
609
-
610
- Build **one** first, screenshot it, then scale to the full count. Do not generate twenty slides
611
- before you have seen one render.
612
-
613
434
  ### Read the current state of a slider
614
435
 
615
436
  ```js
@@ -638,6 +459,28 @@ mistake, and `ltr sm:ttb` does the opposite of what it looks like.
638
459
 
639
460
  **Never set a prop to its default.** It adds noise to the markup and hides real intent.
640
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
+
641
484
  **A nested inner slider must be Slide or Fade, never Loop or Infinite Scroll.** A looping slider
642
485
  makes hidden copies of its slides, and copying a slider that contains a slider breaks both. To wrap
643
486
  around, use Fade, or Slide with Rewind.
@@ -661,6 +504,157 @@ rather than debugging a slider that will never start.
661
504
 
662
505
  **Do not touch the plugin's own stylesheet.** Style through the Slider Class and CSS variables.
663
506
 
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.
574
+
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.
578
+
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:
584
+
585
+ ```css
586
+ .figure { opacity: 0; }
587
+ .splide__slide.is-active .figure { opacity: 1; }
588
+ .splide:not(:has(.splide__slide.is-active)) .figure { opacity: 1; }
589
+ ```
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
+
625
+ **Do not anchor against a neighbour that stops shrinking.** A `vw` offset tuned at one width
626
+ silently collides at another, because `min()` and `clamp()` neighbours stop shrinking while the
627
+ `vw` keeps going. Derive the clearance from the neighbour's real footprint instead:
628
+
629
+ ```css
630
+ /* wrong: fits at 1771, overlaps the column from 1024 to 1430 */
631
+ right: 17.6vw;
632
+ /* right: reserve exactly what the column occupies */
633
+ right: calc(4vw + min(15vw, 195px) + 2.5vw);
634
+ ```
635
+
636
+ Check the mid range explicitly. Between the widest layout and the first breakpoint is where
637
+ side-by-side compositions fail, and it is the range nobody screenshots.
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
+
652
+ **A save can lag the front end.** `etch.saveAsync()` resolves before the change is necessarily
653
+ readable on the published page, so a fetch straight afterwards can return the previous value and
654
+ look like a failed write. Re-read through `etch.blocks.getAttribute` to confirm intent, and treat a
655
+ stale page as latency rather than loss. Reloading the builder tab flushes anything pending.
656
+
657
+
664
658
  ### Do not
665
659
 
666
660
  * Hardcode component IDs.