sliderpro-agentic-skills-etch 0.1.1 → 0.2.0
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 +130 -41
- package/lib/skills-source.js +65 -0
- package/package.json +2 -1
- package/skills-package/manifest.json +43 -0
- package/skills-package/slider-skills/slider-pro-skills.md +207 -14
package/bin/cli.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Installs the Slider Pro AI Connector skills into a project.
|
|
4
|
+
//
|
|
5
|
+
// The skills live in the docs repo and change more often than this package is
|
|
6
|
+
// published, so the current files are fetched at install time and the bundled
|
|
7
|
+
// copy is only a fallback for when the network is unavailable. Fetching is
|
|
8
|
+
// all-or-nothing: a partial fetch would mix a new skills file with stale
|
|
9
|
+
// component docs, which is worse than either source on its own.
|
|
10
|
+
|
|
11
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
4
12
|
import { dirname, join, resolve } from "node:path";
|
|
5
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
+
import {
|
|
15
|
+
applyRewrites,
|
|
16
|
+
DEFAULT_REF,
|
|
17
|
+
looksLikeMarkdown,
|
|
18
|
+
OUT_COMPONENTS_DIR,
|
|
19
|
+
OUT_SKILLS_DIR,
|
|
20
|
+
rawUrl,
|
|
21
|
+
REPO,
|
|
22
|
+
REPO_MANIFEST,
|
|
23
|
+
} from "../lib/skills-source.js";
|
|
6
24
|
|
|
7
25
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
26
|
const packageRoot = join(__dirname, "..");
|
|
9
|
-
const
|
|
27
|
+
const bundleRoot = join(packageRoot, "skills-package");
|
|
10
28
|
|
|
11
|
-
const DIRS = [
|
|
29
|
+
const DIRS = [OUT_SKILLS_DIR, OUT_COMPONENTS_DIR];
|
|
30
|
+
const TIMEOUT_MS = 10000;
|
|
12
31
|
|
|
13
32
|
function printHelp() {
|
|
14
33
|
console.log(`
|
|
@@ -18,27 +37,37 @@ Usage:
|
|
|
18
37
|
npx sliderpro-agentic-skills-etch [directory] [options]
|
|
19
38
|
|
|
20
39
|
Options:
|
|
21
|
-
--force, -f
|
|
22
|
-
--
|
|
40
|
+
--force, -f Overwrite existing slider-skills/ or components/
|
|
41
|
+
--offline Skip the network and install the copy bundled with this package
|
|
42
|
+
--ref <ref> Install from a branch or tag (default: ${DEFAULT_REF})
|
|
43
|
+
--help, -h Show this help message
|
|
44
|
+
|
|
45
|
+
By default the current skills are fetched from ${REPO} so they are never
|
|
46
|
+
older than this package. Without a network the bundled copy is used instead.
|
|
23
47
|
|
|
24
48
|
Examples:
|
|
25
49
|
npx sliderpro-agentic-skills-etch
|
|
26
50
|
npx sliderpro-agentic-skills-etch ./my-site-project
|
|
27
|
-
npx sliderpro-agentic-skills-etch --force
|
|
51
|
+
npx sliderpro-agentic-skills-etch --offline --force
|
|
28
52
|
`);
|
|
29
53
|
}
|
|
30
54
|
|
|
31
55
|
function parseArgs(argv) {
|
|
32
|
-
const options = { force: false, help: false, target: process.cwd() };
|
|
33
|
-
|
|
34
|
-
for (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
options.
|
|
41
|
-
|
|
56
|
+
const options = { force: false, help: false, offline: false, ref: DEFAULT_REF, target: process.cwd() };
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < argv.length; i++) {
|
|
59
|
+
const arg = argv[i];
|
|
60
|
+
if (arg === "--help" || arg === "-h") options.help = true;
|
|
61
|
+
else if (arg === "--force" || arg === "-f") options.force = true;
|
|
62
|
+
else if (arg === "--offline") options.offline = true;
|
|
63
|
+
else if (arg === "--ref") {
|
|
64
|
+
options.ref = argv[++i];
|
|
65
|
+
if (!options.ref) {
|
|
66
|
+
console.error("--ref needs a branch or tag name");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
} else if (!arg.startsWith("-")) options.target = resolve(arg);
|
|
70
|
+
else {
|
|
42
71
|
console.error(`Unknown option: ${arg}`);
|
|
43
72
|
printHelp();
|
|
44
73
|
process.exit(1);
|
|
@@ -48,60 +77,120 @@ function parseArgs(argv) {
|
|
|
48
77
|
return options;
|
|
49
78
|
}
|
|
50
79
|
|
|
51
|
-
function
|
|
52
|
-
|
|
80
|
+
async function get(url) {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch(url, { signal: controller.signal, headers: { "user-agent": "sliderpro-agentic-skills-etch" } });
|
|
85
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
86
|
+
return await res.text();
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
}
|
|
53
90
|
}
|
|
54
91
|
|
|
55
|
-
function
|
|
56
|
-
|
|
92
|
+
function bundledManifest() {
|
|
93
|
+
return JSON.parse(readFileSync(join(bundleRoot, "manifest.json"), "utf8"));
|
|
94
|
+
}
|
|
57
95
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
96
|
+
/** Returns { files: [{out, content}] } or null if anything at all went wrong. */
|
|
97
|
+
async function fetchLive(ref) {
|
|
98
|
+
let manifest;
|
|
99
|
+
try {
|
|
100
|
+
manifest = JSON.parse(await get(rawUrl(ref, REPO_MANIFEST)));
|
|
101
|
+
} catch {
|
|
102
|
+
// The manifest may predate this feature on an older ref; the bundled list
|
|
103
|
+
// is still a reasonable guess at what to fetch.
|
|
104
|
+
try {
|
|
105
|
+
manifest = bundledManifest();
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const entries = [...(manifest.skills || []), ...(manifest.components || [])];
|
|
112
|
+
if (entries.length === 0) return null;
|
|
61
113
|
|
|
62
|
-
|
|
63
|
-
|
|
114
|
+
const files = [];
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
let text;
|
|
117
|
+
try {
|
|
118
|
+
text = await get(rawUrl(ref, entry.repo));
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
64
121
|
}
|
|
122
|
+
if (!looksLikeMarkdown(text)) return null;
|
|
123
|
+
files.push({ out: entry.out, content: applyRewrites(text) });
|
|
124
|
+
}
|
|
125
|
+
return files;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeFiles(target, files) {
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const dest = join(target, file.out);
|
|
131
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
132
|
+
writeFileSync(dest, file.content);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
65
135
|
|
|
66
|
-
|
|
136
|
+
function copyBundle(target) {
|
|
137
|
+
for (const dir of DIRS) {
|
|
138
|
+
const source = join(bundleRoot, dir);
|
|
139
|
+
if (existsSync(source)) cpSync(source, join(target, dir), { recursive: true });
|
|
67
140
|
}
|
|
68
141
|
}
|
|
69
142
|
|
|
70
|
-
function countComponents() {
|
|
143
|
+
function countComponents(target) {
|
|
71
144
|
try {
|
|
72
|
-
return readdirSync(join(
|
|
145
|
+
return readdirSync(join(target, OUT_COMPONENTS_DIR)).filter((f) => f.endsWith(".md")).length;
|
|
73
146
|
} catch {
|
|
74
147
|
return 0;
|
|
75
148
|
}
|
|
76
149
|
}
|
|
77
150
|
|
|
78
|
-
function main() {
|
|
79
|
-
const { force, help, target } = parseArgs(process.argv.slice(2));
|
|
151
|
+
async function main() {
|
|
152
|
+
const { force, help, offline, ref, target } = parseArgs(process.argv.slice(2));
|
|
80
153
|
|
|
81
154
|
if (help) {
|
|
82
155
|
printHelp();
|
|
83
156
|
return;
|
|
84
157
|
}
|
|
85
158
|
|
|
86
|
-
const conflicts =
|
|
87
|
-
|
|
159
|
+
const conflicts = DIRS.filter((dir) => existsSync(join(target, dir))).map((dir) => `${dir}/`);
|
|
88
160
|
if (conflicts.length > 0 && !force) {
|
|
89
161
|
console.error("Installation blocked, the following already exist:");
|
|
90
|
-
for (const conflict of conflicts) {
|
|
91
|
-
console.error(` ${conflict}`);
|
|
92
|
-
}
|
|
162
|
+
for (const conflict of conflicts) console.error(` ${conflict}`);
|
|
93
163
|
console.error("\nRe-run with --force to overwrite.");
|
|
94
164
|
process.exit(1);
|
|
95
165
|
}
|
|
96
166
|
|
|
97
|
-
|
|
167
|
+
mkdirSync(target, { recursive: true });
|
|
168
|
+
|
|
169
|
+
let source;
|
|
170
|
+
if (offline) {
|
|
171
|
+
copyBundle(target);
|
|
172
|
+
source = "the copy bundled with this package (--offline)";
|
|
173
|
+
} else {
|
|
174
|
+
const files = await fetchLive(ref);
|
|
175
|
+
if (files) {
|
|
176
|
+
writeFiles(target, files);
|
|
177
|
+
source = `${REPO} (${ref})`;
|
|
178
|
+
} else {
|
|
179
|
+
copyBundle(target);
|
|
180
|
+
source = "the copy bundled with this package (network unavailable)";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
98
183
|
|
|
99
184
|
console.log(`Installed Slider Pro AI Connector skills to ${target}`);
|
|
100
|
-
console.log(`
|
|
101
|
-
console.log(`
|
|
102
|
-
console.log(`
|
|
103
|
-
console.log(
|
|
185
|
+
console.log(` source: ${source}`);
|
|
186
|
+
console.log(` ${OUT_SKILLS_DIR}/slider-pro-skills.md`);
|
|
187
|
+
console.log(` ${OUT_SKILLS_DIR}/slider-pro-skills-reference.md`);
|
|
188
|
+
console.log(` ${OUT_COMPONENTS_DIR}/ (${countComponents(target)} component prop docs)`);
|
|
189
|
+
console.log(`\nPoint your AI coding agent at ${OUT_SKILLS_DIR}/slider-pro-skills.md to load it.`);
|
|
104
190
|
console.log('Then tell the agent: "npx @digital-gravy/etch-connector serve"');
|
|
105
191
|
}
|
|
106
192
|
|
|
107
|
-
main()
|
|
193
|
+
main().catch((err) => {
|
|
194
|
+
console.error(err.message || err);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Single source of truth for where the skills come from and how their links are
|
|
2
|
+
// rewritten for an installed project. Both scripts/build.js (which bundles a
|
|
3
|
+
// copy at publish time) and bin/cli.js (which prefers a live copy at install
|
|
4
|
+
// time) import this, so the two paths cannot drift in how they rewrite links.
|
|
5
|
+
|
|
6
|
+
export const REPO = "udoro/SliderPro-Etch-Docs";
|
|
7
|
+
export const DEFAULT_REF = "master";
|
|
8
|
+
export const DOCS_URL = "https://design-with-cracka.gitbook.io/etchsliderpro";
|
|
9
|
+
|
|
10
|
+
// Where things live in the docs repo.
|
|
11
|
+
export const REPO_SKILLS_DIR = "ai-connector/slider-skills";
|
|
12
|
+
export const REPO_COMPONENTS_DIR = "components";
|
|
13
|
+
export const REPO_MANIFEST = "ai-connector/skills-manifest.json";
|
|
14
|
+
|
|
15
|
+
// Where they land in a user's project.
|
|
16
|
+
export const OUT_SKILLS_DIR = "slider-skills";
|
|
17
|
+
export const OUT_COMPONENTS_DIR = "components";
|
|
18
|
+
|
|
19
|
+
// The docs repo nests slider-skills/ two levels below the root
|
|
20
|
+
// (ai-connector/slider-skills/), so it links components/ as ../../components/.
|
|
21
|
+
// Installed, slider-skills/ sits directly under the project root, so that
|
|
22
|
+
// becomes ../components/.
|
|
23
|
+
//
|
|
24
|
+
// The sibling doc pages the skills link to do not ship in the package at all,
|
|
25
|
+
// so point those at the public site rather than leave a link resolving to
|
|
26
|
+
// nothing.
|
|
27
|
+
export const PATH_REWRITE = [
|
|
28
|
+
[/\.\.\/\.\.\/components\//g, "../components/"],
|
|
29
|
+
[/\.\.\/\.\.\/card-stack-templates\.md/g, `${DOCS_URL}/card-stack-templates`],
|
|
30
|
+
[/\.\.\/\.\.\/premade-templates\.md/g, `${DOCS_URL}/premade-templates`],
|
|
31
|
+
[/\.\.\/\.\.\/javascript-api\.md/g, `${DOCS_URL}/javascript-api`],
|
|
32
|
+
[/\.\.\/\.\.\/styling-and-responsive\.md/g, `${DOCS_URL}/styling-and-responsive`],
|
|
33
|
+
[/\.\.\/\.\.\/admin-settings\.md/g, `${DOCS_URL}/admin-settings`],
|
|
34
|
+
[/\.\.\/\.\.\/troubleshooting\.md/g, `${DOCS_URL}/troubleshooting`],
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function applyRewrites(content) {
|
|
38
|
+
let out = content;
|
|
39
|
+
for (const [pattern, replacement] of PATH_REWRITE) out = out.replace(pattern, replacement);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Only a markdown LINK TARGET pointing outside the package is a defect: it would
|
|
45
|
+
* 404 for every installed user. A bare ../../ path in prose is not, because the
|
|
46
|
+
* skills files deliberately reference optional local files and always say what
|
|
47
|
+
* to do when they are absent.
|
|
48
|
+
*/
|
|
49
|
+
export function findStaleLinks(content) {
|
|
50
|
+
return [...content.matchAll(/\]\((\.\.\/\.\.\/[^)]+)\)/g)].map((m) => m[1]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function rawUrl(ref, repoPath) {
|
|
54
|
+
return `https://raw.githubusercontent.com/${REPO}/${ref}/${repoPath}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A fetched file only counts if it is non-empty markdown. A proxy or an error
|
|
59
|
+
* page returning 200 with HTML would otherwise be written out as a skills file.
|
|
60
|
+
*/
|
|
61
|
+
export function looksLikeMarkdown(text) {
|
|
62
|
+
if (!text || text.trim().length < 200) return false;
|
|
63
|
+
const head = text.slice(0, 400).toLowerCase();
|
|
64
|
+
return !head.includes("<!doctype html") && !head.includes("<html");
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sliderpro-agentic-skills-etch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Installs the Slider Pro for Etch AI Connector skills files and component docs into any project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
|
+
"lib",
|
|
11
12
|
"skills-package"
|
|
12
13
|
],
|
|
13
14
|
"scripts": {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generated": "2026-08-22",
|
|
3
|
+
"skills": [
|
|
4
|
+
{
|
|
5
|
+
"repo": "ai-connector/slider-skills/slider-pro-skills-reference.md",
|
|
6
|
+
"out": "slider-skills/slider-pro-skills-reference.md"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"repo": "ai-connector/slider-skills/slider-pro-skills.md",
|
|
10
|
+
"out": "slider-skills/slider-pro-skills.md"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"components": [
|
|
14
|
+
{
|
|
15
|
+
"repo": "components/dwc-slide.md",
|
|
16
|
+
"out": "components/dwc-slide.md"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"repo": "components/dwc-slider-nav-button.md",
|
|
20
|
+
"out": "components/dwc-slider-nav-button.md"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"repo": "components/dwc-slider-pagination.md",
|
|
24
|
+
"out": "components/dwc-slider-pagination.md"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"repo": "components/dwc-slider-play-pause.md",
|
|
28
|
+
"out": "components/dwc-slider-play-pause.md"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"repo": "components/dwc-slider-progress.md",
|
|
32
|
+
"out": "components/dwc-slider-progress.md"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"repo": "components/dwc-slider-wrapper.md",
|
|
36
|
+
"out": "components/dwc-slider-wrapper.md"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"repo": "components/dwc-slider.md",
|
|
40
|
+
"out": "components/dwc-slider.md"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -142,6 +142,9 @@ etch.blocks.create(json, parentId?, index?) // returns new id
|
|
|
142
142
|
etch.components.list() // [{ id, name }, ...]
|
|
143
143
|
etch.components.getJson(id) // .properties and .blocks
|
|
144
144
|
etch.styles.list() // [{ id, selector, type, collection, css }]
|
|
145
|
+
etch.styles.create(selector, css) // returns a STYLE ID; pass it to a class prop
|
|
146
|
+
etch.blocks.addClass(blockId, name) // plain elements only; throws on a component
|
|
147
|
+
etch.blocks.getJson(id).script // { code }, the JavaScript code block
|
|
145
148
|
await etch.saveAsync()
|
|
146
149
|
```
|
|
147
150
|
|
|
@@ -198,6 +201,87 @@ Putting a Slider straight inside a Wrapper's `children` renders nothing.
|
|
|
198
201
|
Controls are siblings of the Slider inside `Sliders_and_Controls`, or children of the Slider's own
|
|
199
202
|
`Top__Controls` / `Bottom__Controls`. Both work, because controls find their own slider.
|
|
200
203
|
|
|
204
|
+
### Classes: two different mechanisms
|
|
205
|
+
|
|
206
|
+
Adding a class to a plain element and adding one to a component are **not the same operation**, and
|
|
207
|
+
the component path is the one that matters here, because everything Slider Pro ships is a component.
|
|
208
|
+
|
|
209
|
+
**On a component**, `addClass` throws `Block "<id>" is not an HTML block.` Classes go through the
|
|
210
|
+
component's own class-typed prop, and the value is the **style id**, not the class name:
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
const styleId = etch.styles.create('.flow-demo', 'border-radius: 1rem;'); // returns an id
|
|
214
|
+
etch.blocks.setAttribute(wrapperId, 'customClass', styleId);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Etch resolves the id to the selector name when it renders, so the page gets
|
|
218
|
+
`class="dwc-slider-wrapper flow-demo"`. The id never appears in the output. Store the id, not the
|
|
219
|
+
name, so the class survives being renamed in the Etch UI.
|
|
220
|
+
|
|
221
|
+
The class-typed props, one per component:
|
|
222
|
+
|
|
223
|
+
| Component | Prop |
|
|
224
|
+
| --- | --- |
|
|
225
|
+
| DWC Slider Wrapper | `customClass` |
|
|
226
|
+
| DWC Slider | `sliderClass` |
|
|
227
|
+
| DWC Slide, Progress, Play-Pause, Pagination | `class` |
|
|
228
|
+
| DWC Slider Nav Button | `buttonClass`, `buttonWrapperClass` |
|
|
229
|
+
|
|
230
|
+
**On a plain element**, use `addClass` with the bare class name, no dot and no id:
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
const id = etch.blocks.create(el('article', {}, []));
|
|
234
|
+
etch.blocks.addClass(id, 'fall-card');
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Passing a style id here does not resolve. It is treated as a literal name and sanitised, so
|
|
238
|
+
`30xsolr` silently becomes the class `xsolr`. Creating the element with `attributes: { class: '...' }`
|
|
239
|
+
works too and is fine when you are building a subtree in one call.
|
|
240
|
+
|
|
241
|
+
Either way the CSS rule itself is `etch.styles.create('.fall-card', '...')`, which is what the
|
|
242
|
+
selector has to match.
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
### CSS conventions
|
|
246
|
+
|
|
247
|
+
Match the shipped templates. An install that already has premade sliders on it has these
|
|
248
|
+
conventions everywhere, and a build that ignores them reads as foreign.
|
|
249
|
+
|
|
250
|
+
**Naming.** The block is the slider, named after the design:
|
|
251
|
+
|
|
252
|
+
| Thing | Pattern | Example |
|
|
253
|
+
| --- | --- | --- |
|
|
254
|
+
| Wrapper class, slider designs | `.slider-<name>-wrapper` | `.slider-flow-wrapper` |
|
|
255
|
+
| Wrapper class, card stacks | `.slider-wrapper-<name>` | `.slider-wrapper-fall` |
|
|
256
|
+
| Slider class | `.slider-<name>` | `.slider-chronos` |
|
|
257
|
+
| Elements | `.slider-<name>__<element>` | `.slider-team__sync` |
|
|
258
|
+
| Modifiers | `.slider-<name>__<element>--<variant>` | `.slider-afterform__word--form` |
|
|
259
|
+
|
|
260
|
+
The wrapper class carries the word `wrapper` and the slider class carries the word `slider`. Do not
|
|
261
|
+
invent a short prefix of your own.
|
|
262
|
+
|
|
263
|
+
**One style entry per element class.** Never one nested parent entry holding the whole component.
|
|
264
|
+
`etch.styles.create()` per class, each with its own CSS:
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
const S = {};
|
|
268
|
+
S.wrapper = etch.styles.create('.slider-afterform-wrapper', '/* ... */');
|
|
269
|
+
S.caption = etch.styles.create('.slider-afterform__caption', '/* ... */');
|
|
270
|
+
S.title = etch.styles.create('.slider-afterform__caption-title', '/* ... */');
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Inside an element's **own** entry, nesting is preferred for media queries, pseudo-elements
|
|
274
|
+
(`&::after`), states (`&:hover`, `&.is-active`), ancestor states
|
|
275
|
+
(`.splide__slide.is-active &`) and child tag selectors (`& img`, `& svg`). Do **not** nest another
|
|
276
|
+
class's rules (`&__title`) inside a different class's entry: that class gets its own entry.
|
|
277
|
+
|
|
278
|
+
**Renaming is not one operation.** A style entry's selector and a block's `class` attribute are
|
|
279
|
+
independent. Renaming the selector rewrites the rendered class only where the class came from a
|
|
280
|
+
**component class prop**, because that prop stores the style id and resolves it at render. On a
|
|
281
|
+
plain element the `class` attribute is literal, so a selector rename leaves it pointing at a
|
|
282
|
+
selector that no longer exists. Rename both, or the element silently loses its CSS.
|
|
283
|
+
|
|
284
|
+
|
|
201
285
|
### Group props and the one-extra-brace rule
|
|
202
286
|
|
|
203
287
|
Most Slider props live in groups (`layout`, `motion`, `slides`, `autoplay`, ...). A group is stored
|
|
@@ -373,6 +457,38 @@ setGroup(thumbId, 'sliderSetup', { sliderRole: 'thumbnails', transitionType: 'Lo
|
|
|
373
457
|
Both inside the same Wrapper and they pair automatically. A Wrapper can hold more than one
|
|
374
458
|
thumbnail slider, which is how Zeon runs a background layer and a strip off the same main.
|
|
375
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
|
+
|
|
376
492
|
### Carousel on mobile, grid on desktop
|
|
377
493
|
|
|
378
494
|
```js
|
|
@@ -407,43 +523,79 @@ so the CSS can fan the left and right sides in opposite directions:
|
|
|
407
523
|
.deck-stack-featured &:is([data-pos='2'], [data-pos='-2']) { /* ring 2, both sides */ }
|
|
408
524
|
```
|
|
409
525
|
|
|
410
|
-
The stack container carries **`data-tiers`**,
|
|
411
|
-
|
|
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.
|
|
412
529
|
|
|
413
530
|
Writing an unsigned rank, or naming the attribute anything else, produces a flat pile: the rules
|
|
414
|
-
above simply never match. The script does
|
|
531
|
+
above simply never match. The shipped script does this:
|
|
415
532
|
|
|
416
533
|
```js
|
|
417
534
|
// 1. make a custom property interpolable, so a blur can animate (case 2)
|
|
418
535
|
CSS.registerProperty({ name: '--stack-focus', syntax: '<number>',
|
|
419
536
|
inherits: true, initialValue: '0' });
|
|
420
537
|
|
|
421
|
-
// 2.
|
|
422
|
-
var
|
|
423
|
-
|
|
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;
|
|
424
541
|
|
|
425
|
-
// 3.
|
|
426
|
-
|
|
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);
|
|
427
553
|
card.style.zIndex = String(total - rank);
|
|
428
554
|
lift.style.pointerEvents = rank <= NAV_RINGS ? 'auto' : 'none';
|
|
429
555
|
```
|
|
430
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
|
+
|
|
431
571
|
Recompute on every change: watch the stack with a `MutationObserver` filtered to `class`, since
|
|
432
572
|
`is-active` moving is the only signal you get.
|
|
433
573
|
|
|
434
574
|
### Where a script goes
|
|
435
575
|
|
|
436
|
-
|
|
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:
|
|
437
582
|
|
|
438
583
|
```js
|
|
439
|
-
|
|
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
|
+
});
|
|
440
589
|
```
|
|
441
590
|
|
|
442
|
-
|
|
443
|
-
|
|
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.
|
|
444
593
|
|
|
445
|
-
|
|
446
|
-
|
|
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.
|
|
447
599
|
|
|
448
600
|
Card counts for a looping deck: three are always visible (front plus two), and a loop needs a
|
|
449
601
|
hidden slot at each end, so **five is the minimum** and nine gives the full three-row fan. Fall
|
|
@@ -581,6 +733,47 @@ rather than debugging a slider that will never start.
|
|
|
581
733
|
|
|
582
734
|
**Do not touch the plugin's own stylesheet.** Style through the Slider Class and CSS variables.
|
|
583
735
|
|
|
736
|
+
**A prop that gates a component's internals must be set explicitly, even to its default.** This is
|
|
737
|
+
the one exception to "never set a prop to its default", and it is invisible when you hit it. A
|
|
738
|
+
component's declared default populates the Etch settings panel; it is **not** written onto an
|
|
739
|
+
instance you create programmatically. When a condition inside the component reads a
|
|
740
|
+
flag prop and the key is absent, the expression does not resolve and the condition returns
|
|
741
|
+
false **whichever operator it uses**. Both branches vanish at once.
|
|
742
|
+
|
|
743
|
+
The worked case: a DWC Slider Nav Button created with `navigationType` alone renders
|
|
744
|
+
`<button>` with no icon. The default arrow sits behind `useCustomArrow isFalsy` and the custom SVG
|
|
745
|
+
behind `useCustomArrow isTruthy`, and neither appears. Writing `useCustomArrow: '{false}'`, which is
|
|
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.
|
|
748
|
+
|
|
749
|
+
**Give the slider a builder height when slide content is absolutely positioned.** A slide whose
|
|
750
|
+
children are all `position: absolute` has no intrinsic height, so before the slider initialises it
|
|
751
|
+
collapses to nothing and the layout is unusable in the builder. Guard it:
|
|
752
|
+
|
|
753
|
+
```css
|
|
754
|
+
&.etch-builder-block { min-block-size: 640px; }
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**Do not anchor against a neighbour that stops shrinking.** A `vw` offset tuned at one width
|
|
758
|
+
silently collides at another, because `min()` and `clamp()` neighbours stop shrinking while the
|
|
759
|
+
`vw` keeps going. Derive the clearance from the neighbour's real footprint instead:
|
|
760
|
+
|
|
761
|
+
```css
|
|
762
|
+
/* wrong: fits at 1771, overlaps the column from 1024 to 1430 */
|
|
763
|
+
right: 17.6vw;
|
|
764
|
+
/* right: reserve exactly what the column occupies */
|
|
765
|
+
right: calc(4vw + min(15vw, 195px) + 2.5vw);
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
Check the mid range explicitly. Between the widest layout and the first breakpoint is where
|
|
769
|
+
side-by-side compositions fail, and it is the range nobody screenshots.
|
|
770
|
+
|
|
771
|
+
**A save can lag the front end.** `etch.saveAsync()` resolves before the change is necessarily
|
|
772
|
+
readable on the published page, so a fetch straight afterwards can return the previous value and
|
|
773
|
+
look like a failed write. Re-read through `etch.blocks.getAttribute` to confirm intent, and treat a
|
|
774
|
+
stale page as latency rather than loss. Reloading the builder tab flushes anything pending.
|
|
775
|
+
|
|
776
|
+
|
|
584
777
|
### Do not
|
|
585
778
|
|
|
586
779
|
* Hardcode component IDs.
|