sliderpro-agentic-skills-etch 0.1.2 → 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 CHANGED
@@ -1,14 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
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 sourceRoot = join(packageRoot, "skills-package");
27
+ const bundleRoot = join(packageRoot, "skills-package");
10
28
 
11
- const DIRS = ["slider-skills", "components"];
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 Overwrite existing slider-skills/ or components/
22
- --help, -h Show this help message
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 (const arg of argv) {
35
- if (arg === "--help" || arg === "-h") {
36
- options.help = true;
37
- } else if (arg === "--force" || arg === "-f") {
38
- options.force = true;
39
- } else if (!arg.startsWith("-")) {
40
- options.target = resolve(arg);
41
- } else {
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 listConflicts(target) {
52
- return DIRS.filter((dir) => existsSync(join(target, dir))).map((dir) => `${dir}/`);
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 copyPackageFiles(target, force) {
56
- mkdirSync(target, { recursive: true });
92
+ function bundledManifest() {
93
+ return JSON.parse(readFileSync(join(bundleRoot, "manifest.json"), "utf8"));
94
+ }
57
95
 
58
- for (const dir of DIRS) {
59
- const source = join(sourceRoot, dir);
60
- const dest = join(target, dir);
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
- if (existsSync(dest) && !force) {
63
- continue;
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
- cpSync(source, dest, { recursive: true });
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(sourceRoot, "components")).filter((f) => f.endsWith(".md")).length;
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 = listConflicts(target);
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
- copyPackageFiles(target, force);
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(` slider-skills/slider-pro-skills.md`);
101
- console.log(` slider-skills/slider-pro-skills-reference.md`);
102
- console.log(` components/ (${countComponents()} component prop docs)`);
103
- console.log("\nPoint your AI coding agent at slider-skills/slider-pro-skills.md to load it.");
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.1.2",
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
+ }
@@ -242,6 +242,46 @@ Either way the CSS rule itself is `etch.styles.create('.fall-card', '...')`, whi
242
242
  selector has to match.
243
243
 
244
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
+
245
285
  ### Group props and the one-extra-brace rule
246
286
 
247
287
  Most Slider props live in groups (`layout`, `motion`, `slides`, `autoplay`, ...). A group is stored
@@ -417,6 +457,38 @@ setGroup(thumbId, 'sliderSetup', { sliderRole: 'thumbnails', transitionType: 'Lo
417
457
  Both inside the same Wrapper and they pair automatically. A Wrapper can hold more than one
418
458
  thumbnail slider, which is how Zeon runs a background layer and a strip off the same main.
419
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
+
420
492
  ### Carousel on mobile, grid on desktop
421
493
 
422
494
  ```js
@@ -661,6 +733,47 @@ rather than debugging a slider that will never start.
661
733
 
662
734
  **Do not touch the plugin's own stylesheet.** Style through the Slider Class and CSS variables.
663
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
+
664
777
  ### Do not
665
778
 
666
779
  * Hardcode component IDs.