@sleekcms/sync 2.12.0 → 2.15.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.
Files changed (3) hide show
  1. package/README.md +6 -1
  2. package/dist/AGENT.md +216 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -326,6 +326,7 @@ Models describe the shape of your content. They're JSON-like files with unquoted
326
326
  |---|---|
327
327
  | `render(val)` | Render a block or entry through its template |
328
328
  | `marked(md)` | Convert markdown to HTML |
329
+ | `remark(md, opts?)` | Convert markdown to HTML with syntax-highlighted code blocks, math (`$…$`, `$$…$$`) and Mermaid diagrams, adding only the CSS/JS a page needs. `opts.codeTheme` picks a highlight.js theme (default `github-dark`) |
329
330
 
330
331
  **Images**
331
332
 
@@ -406,7 +407,11 @@ Models describe the shape of your content. They're JSON-like files with unquoted
406
407
  </form>
407
408
  ```
408
409
 
409
- Any form with `data-sleekcms="<name>"` captures and stores submissions in the CMS dashboard automatically.
410
+ Any form with `data-sleekcms="<name>"` captures and stores submissions in the CMS dashboard automatically. File uploads work out of the box too — add an `<input type="file">` and visitors can attach documents (PDF, Word, Excel, CSV, text) and images:
411
+
412
+ ```html
413
+ <input name="resume" type="file" accept=".pdf">
414
+ ```
410
415
 
411
416
  ---
412
417
 
package/dist/AGENT.md CHANGED
@@ -203,7 +203,7 @@ Like JSON but with **no quotes**; each value is a field type name:
203
203
  |---|---|
204
204
  | `text`, `paragraph`, `code`, `color`, `link` | String |
205
205
  | `richtext` | String of **HTML** — output with `<%- %>` |
206
- | `markdown` | String of **raw markdown** — convert with `<%- marked(item.x) %>` |
206
+ | `markdown` | String of **raw markdown** — convert with `<%- marked(item.x) %>`, or `<%- remark(item.x) %>` when it holds code blocks, math or diagrams |
207
207
  | `number` | Number |
208
208
  | `boolean` | `true` / `false` |
209
209
  | `emoji` | One emoji character string, e.g. `"😀"` (not free text) |
@@ -350,7 +350,7 @@ Frontmatter may be JSON (canonical — prefer it when generating), JSON5, or YAM
350
350
  | Tag | Purpose |
351
351
  |---|---|
352
352
  | `<%= expr %>` | Output, HTML-escaped (plain text) |
353
- | `<%- expr %>` | Output raw HTML (`render()`, `img()`, `marked()`, richtext) |
353
+ | `<%- expr %>` | Output raw HTML (`render()`, `img()`, `marked()`, `remark()`, richtext) |
354
354
  | `<% code %>` | Run JS (loops, conditionals) |
355
355
 
356
356
  ### Context variables (every view)
@@ -404,6 +404,18 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
404
404
  |---|---|
405
405
  | `render(val, separator?)` | Render a block, entry, or array of them through its `blocks/<key>.ejs` / `entries/<key>.ejs` view. An `entries/<key>.ejs` view is only needed if you call `render()` on that entry — otherwise just use its data directly |
406
406
  | `marked(md)` | Markdown string → HTML string |
407
+ | `remark(md, opts?)` | Markdown string → HTML string, plus highlighted code blocks, math and Mermaid diagrams (below) |
408
+
409
+ Use `remark()` instead of `marked()` for docs, tutorials and technical posts. On top of `marked()` it renders fenced code blocks (```` ```js ````) with syntax highlighting, math written as `$e^{i\pi} + 1 = 0$` inline, `$$ … $$` on its own lines or a ```` ```math ```` fence (KaTeX), and ```` ```mermaid ```` fences as diagrams drawn in the visitor's browser. It injects only the CSS/JS the rendered markdown needs (KaTeX CSS, a code theme, the Mermaid runtime), so never `link()`/`script()` those libraries yourself.
410
+
411
+ - `opts.codeTheme` — a highlight.js theme name: default `github-dark`, or e.g. `github`, `atom-one-dark`, `base16/dracula`. `github-dark` suits the dark `pre` that `prose` draws; use a light theme only when the site gives `pre` a light background. One theme per page.
412
+ - A single-`$` pair is math only when it hugs its content (`$x^2$`), so prices like "$5 to $10" stay text. Escape a dollar that must stay literal as `\$`.
413
+ - Mermaid uses its light default theme. On a dark design, start each diagram with frontmatter: `---`, `config:`, ` theme: dark`, `---` on their own lines above the diagram.
414
+
415
+ ```ejs
416
+ <div class="prose prose-lg max-w-none"><%- remark(item.body) %></div>
417
+ <%- remark(item.body, { codeTheme: 'github' }) %> <!-- light code theme -->
418
+ ```
407
419
 
408
420
  ### Helpers — images & maps
409
421
 
@@ -438,16 +450,16 @@ Callable from **any** view (page, block, entry, layout); duplicates are removed
438
450
  | `style(css, order?)` | Add `<style>` |
439
451
  | `script(value, order?)` | Add `<script>` — `.js` URL → external, otherwise inline |
440
452
 
441
- `script()`/`link()` also accept a predefined library name — `gsap` (includes ScrollTrigger, pre-registered), `aos` (auto-`AOS.init()`, JS+CSS), `swiper`, `animejs`, `chartjs`, `threejs` (import map — pair with a `type: 'module'` script), `lottie`, `typedjs`, `splide`, `htmx`, `vanilla-tilt` — either call injects all of the library's CDN assets, deduped. Prefer these names over hand-picked CDN URLs, and only add a library the site actually uses.
453
+ `script()`/`link()` also accept a predefined library name — `motion` (Motion, formerly Framer Motion: the `Motion` global, plus `import … from 'motion'` in a `type: 'module'` script), `gsap` (includes ScrollTrigger, pre-registered), `swiper`, `animejs`, `chartjs`, `threejs` (import map — pair with a `type: 'module'` script), `lottie`, `typedjs`, `splide`, `htmx`, `vanilla-tilt`, and legacy `aos` — either call injects all of the library's CDN assets, deduped. Prefer these names over hand-picked CDN URLs, and only add a library the site actually uses.
442
454
 
443
455
  `script()` also takes an attributes object — `script({ src: '/js/app.js', defer: '' })`, `script({ content: '...', type: 'module' })` — so a tag that needs attributes still never needs a raw `<script>`. That is how a third-party script is put behind cookie consent: `script({ src: 'https://…', 'data-sleekcms-consent': 'marketing' })` (§14).
444
456
 
457
+ Animation needs no `script()` call at all — `data-motion` attributes are built in (§15):
458
+
445
459
  ```ejs
446
- <% script('aos') %> <!-- injects aos.js + aos.css + AOS.init() -->
447
- <section data-aos="fade-up">
460
+ <section data-motion="fade-up">
448
461
  <h2><%= item.title %></h2>
449
462
  </section>
450
- <section data-aos="fade-up" data-aos-delay="150">...</section>
451
463
  ```
452
464
 
453
465
  **CSS/JS rules:** include assets only via `link()` / `script()` — never raw `<link>`/`<script>` tags in views. Exception: creating `css/tailwind.css` enables Tailwind v4 — the file is the Tailwind *input CSS*, compiled against the rendered HTML and injected automatically; never `link()` it. Whenever you write it, keep `@import "tailwindcss";` as its first statement (plus the two `@plugin` lines: `@plugin "@tailwindcss/typography";` and `@plugin "@tailwindcss/forms";`) — dropping the import silently removes all Tailwind styling. Define the site's fonts/colors/radii as `@theme` tokens there (giving utilities like `font-display` / `bg-brand-600`) with a `/* DESIGN */` header comment recording the design direction. v4, not v3: custom utilities go in `@utility name { … }`, never `@layer utilities { .name { … } }` (a v3 pattern that yields "Cannot apply unknown utility class"); config is CSS-first — there is no `tailwind.config.js`. Default to a polished Tailwind design with a deliberate design direction.
@@ -491,6 +503,12 @@ Any `<form>` with `data-sleekcms="<hardcoded name>"` works automatically — sub
491
503
 
492
504
  The `data-sleekcms` value groups submissions by form; each input's `name` is stored as-is. Add an `<input type="datetime-local">` and the form becomes a **booking form** — see §11.
493
505
 
506
+ **File uploads** are supported out of the box — add an `<input type="file">` and visitors can attach documents (PDF, Word, Excel, CSV, text) and images (add `multiple` for several):
507
+
508
+ ```html
509
+ <input name="resume" type="file" accept=".pdf,.doc,.docx">
510
+ ```
511
+
494
512
  ## 11. Bookings
495
513
 
496
514
  Use this whenever visitors should **book or schedule** something — an appointment, a call, a demo, a consultation, a class, or a reservation ("pick a time / reserve a slot"). It fits appointment-based businesses: salon, spa, barber, dental/medical clinic, therapist, tutor, photographer, consultant, repair shop, restaurant reservations. **Only add a booking form when the business genuinely takes appointments and the user asks for it (or it is clearly core to the business) — never add one speculatively to an arbitrary site.**
@@ -648,14 +666,203 @@ No config element → all defaults. One per site: a second one is ignored with a
648
666
 
649
667
  **Notes** — gating happens at build time: tagged scripts ship as `type="text/plain"` with their `src` stashed, and gated iframes ship without `src`, so nothing can fire before consent even on slow connections. The banner is a fixed overlay (or modal), so no layout shift. `<noscript>` content cannot be gated, and `document.write` inside a gated script is unsupported. SleekCMS provides the consent *mechanism*; which script belongs to which category, and the privacy policy itself, are the site owner's call — ask when unsure, and link the policy page via `data-policy-url`.
650
668
 
651
- ## 15. Critical Rules Checklist
669
+ ## 15. Motion & Animation
670
+
671
+ Motion directs attention and makes the site feel responsive; every animation has to buy back the moment it costs the visitor. A well-animated page usually has **one** composed entrance above the fold, scroll reveals with **2-3** related treatments (not one fade-up on everything), tactile hover/focus feedback on every interactive element, **at most one** showpiece scroll moment (a parallax hero, a pinned zoom, a horizontal gallery) and **at most one** ambient loop. A calm brochure site may need only the hover feedback.
672
+
673
+ **Motion that hurts:** animating everything; entrances that hold content hostage (above the fold finishes in ~800ms); re-animating on every scroll pass; animating `width`/`height`/`top`/`left`/`margin` instead of `transform`/`opacity`; scroll-hijacking that takes the scroll wheel away from the visitor (parallax driven by their own scrolling is fine); and any failure that leaves content stuck at `opacity: 0`.
674
+
675
+ **Pick the smallest tool:**
676
+
677
+ | Need | Use |
678
+ |---|---|
679
+ | Hover, focus, active feedback; a looping ambient effect | **Tailwind alone** — `transition`, `hover:`, `active:`, `animate-*` |
680
+ | Reveals, staggered grids, split headlines, counters, parallax, scroll-scrubbed zoom/slide/fade, a pinned horizontal gallery, a marquee, tilt or magnetic hover | **`data-motion` attributes** — built in; add the attribute and the build loads everything. No `script()` call. |
681
+ | An effect the attributes can't express — a click-driven sequence, a scroll callback, a cursor follower | **`script('motion')`** — Motion (formerly Framer Motion) for your own script |
682
+ | A swipeable carousel · typewriter · Lottie | `swiper` / `splide` · `typedjs` · `lottie` (§8) |
683
+
684
+ Never hand-write an `IntersectionObserver` reveal, a parallax scroll handler, or a count-up — the attributes are exactly that. Never load two motion libraries: `data-motion` and `script('motion')` are one (Motion); adding `aos` or `gsap` to the same page makes two.
685
+
686
+ ### `data-motion` attributes
687
+
688
+ Any page whose markup uses a `data-motion*` attribute gets Motion and the SleekCMS motion runtime injected at build time. Each element reveals **once**, when its top passes 90% of the screen height. `prefers-reduced-motion` is handled for you (nothing moves; only scroll-linked fades remain), content is never left hidden (if anything fails to load, the page shows normally within 3 seconds), and elements added later are picked up too.
689
+
690
+ | Attribute | What it does |
691
+ |---|---|
692
+ | `data-motion="fade-up"` | Reveal on scroll. Presets: `fade` `fade-up` `fade-down` `fade-left` `fade-right` · `zoom-in` `zoom-out` · `blur-in` · `pop` (springy scale) · `flip-up` `flip-down` `flip-left` `flip-right` · `reveal-up` `reveal-down` `reveal-left` `reveal-right` (clip-path wipe) · `draw` (stroked SVG draws itself) |
693
+ | `data-motion="y: 60; rotate: -3; opacity: 0"` | A custom start state instead of a preset — keys `opacity` `x` `y` `scale` `rotate` `rotateX` `rotateY` `blur` `clip-path`; numbers are px / deg |
694
+ | `data-motion-delay="150"` `data-motion-duration="800"` `data-motion-easing="ease-out"` `data-motion-distance="40"` | Tuning, in ms like AOS (`"0.4s"` also works). Easing also takes `linear` `ease-in-out` `ease-out-cubic` `ease-out-back` `spring` or `[0.2,0.8,0.2,1]`. Defaults: 600ms, a soft ease-out, 24px. |
695
+ | `data-motion-stagger="90"` | On a container: its `data-motion` children that come into view together play in order, 90ms apart — no index math in loops. |
696
+ | `data-motion-text="words"` / `"chars"` | Splits the text and reveals it piece by piece with the element's own `data-motion` preset (default `fade-up`); `data-motion-stagger` sets the step. Headings and short lines. |
697
+ | `data-motion-counter` | Counts up to the number written inside it — `1,250+`, `$4.9M`, `98%` keep their formatting. |
698
+ | `data-motion-parallax="0.2"` | Drifts against the scroll — larger is slower and feels further away (0.1–0.3 is plenty); negative feels closer. |
699
+ | `data-motion-scroll="scale: .85 1; opacity: .4 1"` | Scrubs values with the scrollbar. Keys as above plus `scaleX` `scaleY` and any CSS property; more than two values spread evenly across the range. |
700
+ | `data-motion-scroll-range="enter"` | `enter` (default — while it scrolls into view) · `exit` · `cover` (its whole trip across the screen) · `pin` (while a tall section's sticky child is pinned) · `page` (the whole page) |
701
+ | `data-motion-scroll-target="section"` | Measure another element — the nearest ancestor matching the selector (or the first match on the page). |
702
+ | `data-motion-horizontal` + `data-motion-track` | Pinned horizontal gallery: vertical scrolling slides the track sideways. |
703
+ | `data-motion-hover="tilt"` / `"magnetic"` | Spring-physics tilt toward the pointer, or a pull toward it (`"tilt 12"`, `"magnetic 0.4"` set the strength). Mouse and trackpad only. |
704
+ | `data-motion-marquee="40"` | Endless ticker on the row it sits on, px per second (negative runs right); slows on hover, pauses off screen. |
705
+
706
+ **One job per element:** an element with `data-motion-parallax`, `-scroll` or `-hover` owns its `transform` — wrap it rather than stacking another motion attribute or Tailwind `translate-*`/`scale-*`/`rotate-*` on it. **Sticky needs unclipped ancestors:** `pin` and `horizontal` rely on `position: sticky`, which an `overflow-hidden` ancestor silently breaks — use `overflow-x-clip`.
707
+
708
+ ### Recipes
709
+
710
+ ```ejs
711
+ <%# Hero entrance — one composed moment, in order %>
712
+ <div data-motion-stagger="120" class="mx-auto max-w-4xl px-6 pt-36 pb-24 text-center">
713
+ <p data-motion="fade-up" class="text-sm font-semibold uppercase tracking-[0.2em] text-brand-600"><%= item.eyebrow %></p>
714
+ <h1 data-motion="fade-up" class="mt-5 font-display text-6xl tracking-tight md:text-8xl"><%= item.title %></h1>
715
+ <p data-motion="fade-up" class="mx-auto mt-6 max-w-[46ch] text-lg text-muted"><%= item.lede %></p>
716
+ <a data-motion="pop" href="/book" class="mt-10 inline-flex rounded-full bg-brand-600 px-8 py-4 text-white">Book a table</a>
717
+ </div>
718
+
719
+ <%# Staggered cards from a loop %>
720
+ <div data-motion-stagger="90" class="grid gap-6 md:grid-cols-3">
721
+ <% item.services.forEach(function (s) { %><article data-motion="fade-up" class="rounded-card bg-surface p-8">…</article><% }) %>
722
+ </div>
723
+
724
+ <%# Split headline — words rise out of a mask; chars + fade reads like typing %>
725
+ <h2 data-motion="reveal-up" data-motion-text="words" class="font-display text-6xl md:text-8xl">Bread worth waking up for.</h2>
726
+ <p data-motion="fade" data-motion-text="chars" data-motion-stagger="30" class="font-mono text-lg">Baked fresh at 5am.</p>
727
+
728
+ <%# Image wipe %>
729
+ <figure data-motion="reveal-left" data-motion-duration="1100" class="overflow-hidden rounded-card"><%- img(item.photo, { w: 1200, h: 800, class: 'w-full object-cover' }) %></figure>
730
+
731
+ <%# Parallax background — the layer drifts at most speed × half the screen height,
732
+ so extend it past its frame by at least that: -inset-y-[12vh] covers 0.2 %>
733
+ <section class="relative isolate h-[80vh] overflow-hidden">
734
+ <div data-motion-parallax="0.2" class="absolute inset-x-0 -inset-y-[12vh] -z-10"><%- img(item.backdrop, { w: 1920, h: 1400, class: 'h-full w-full object-cover' }) %></div>
735
+ <div class="mx-auto max-w-5xl px-6 pt-48 text-white">…</div>
736
+ </section>
737
+
738
+ <%# Layered depth — further back = larger speed; negative comes forward %>
739
+ <section class="relative isolate h-screen overflow-hidden">
740
+ <div data-motion-parallax="0.3" class="absolute inset-x-0 -inset-y-[16vh] -z-20"><%- img(item.sky, { class: 'h-full w-full object-cover' }) %></div>
741
+ <div data-motion-parallax="0.12" class="absolute inset-x-0 bottom-0 -z-10"><%- img(item.hills, { class: 'w-full' }) %></div>
742
+ <h1 data-motion-parallax="-0.06" class="relative pt-[30vh] text-center font-display text-8xl text-white"><%= item.title %></h1>
743
+ </section>
744
+
745
+ <%# Reading progress bar — in layouts/common.ejs %>
746
+ <div data-motion-scroll="scaleX: 0 1" data-motion-scroll-range="page" class="fixed inset-x-0 top-0 z-50 h-1 origin-left bg-brand-600 motion-reduce:hidden"></div>
747
+
748
+ <%# Zoom into view %>
749
+ <figure data-motion-scroll="scale: .85 1; opacity: .5 1" class="mx-auto max-w-6xl overflow-hidden rounded-[2rem]"><%- img(item.showcase, { w: 1600, h: 1000, class: 'w-full' }) %></figure>
750
+
751
+ <%# Sticky media with scrolling steps — CSS sticky does the pinning %>
752
+ <section class="mx-auto grid max-w-6xl gap-16 px-6 py-32 md:grid-cols-2">
753
+ <div class="md:sticky md:top-24 md:h-[70vh]"><%- img(item.media, { class: 'h-full w-full rounded-card object-cover' }) %></div>
754
+ <div class="space-y-[30vh] md:py-[20vh]">
755
+ <% item.steps.forEach(function (step) { %><article data-motion="fade-up"><h3 class="font-display text-3xl"><%= step.title %></h3><p class="mt-3 text-muted"><%= step.body %></p></article><% }) %>
756
+ </div>
757
+ </section>
758
+
759
+ <%# Pinned scrollytelling — section height sets how long it pins; each caption holds for its third (two "1" stops) and cross-fades %>
760
+ <section class="relative h-[300vh]">
761
+ <div class="sticky top-0 grid h-screen place-items-center overflow-hidden">
762
+ <div data-motion-scroll="scale: .6 1; opacity: .3 1" data-motion-scroll-range="pin" data-motion-scroll-target="section" class="w-[min(90vw,1100px)] overflow-hidden rounded-[2rem]"><%- img(item.feature, { w: 1600, h: 1000, class: 'w-full' }) %></div>
763
+ <p data-motion-scroll="opacity: 1 1 0 0 0 0" data-motion-scroll-range="pin" data-motion-scroll-target="section" class="absolute bottom-16 text-2xl">Sourced locally.</p>
764
+ <p data-motion-scroll="opacity: 0 0 1 1 0 0" data-motion-scroll-range="pin" data-motion-scroll-target="section" class="absolute bottom-16 text-2xl">Milled by hand.</p>
765
+ <p data-motion-scroll="opacity: 0 0 0 0 1 1" data-motion-scroll-range="pin" data-motion-scroll-target="section" class="absolute bottom-16 text-2xl">Baked every morning.</p>
766
+ </div>
767
+ </section>
768
+
769
+ <%# Horizontal gallery — sized by the runtime; a plain swipeable row without JS or with reduced motion %>
770
+ <section data-motion-horizontal class="relative">
771
+ <div class="sticky top-0 flex h-screen items-center overflow-x-auto">
772
+ <div data-motion-track class="flex gap-6 px-[8vw]">
773
+ <% item.projects.forEach(function (p) { %><article class="w-[80vw] shrink-0 md:w-[42vw]"><%- img(p.image, { w: 900, h: 1100, class: 'rounded-card' }) %></article><% }) %>
774
+ </div>
775
+ </div>
776
+ </section>
777
+
778
+ <%# Oversized type sliding sideways — x in % of its own width %>
779
+ <div class="overflow-x-clip py-16"><p data-motion-scroll="x: 10% -35%" data-motion-scroll-range="cover" class="whitespace-nowrap font-display text-[18vw] leading-none">Fresh · Local · Daily ·</p></div>
780
+
781
+ <%# Stat counters — write the real figure: it's what search engines and reduced-motion visitors read %>
782
+ <dl data-motion-stagger="100" class="grid grid-cols-2 gap-8 md:grid-cols-4">
783
+ <% item.stats.forEach(function (s) { %><div data-motion="fade-up" class="flex flex-col-reverse"><dt class="mt-2 text-sm text-muted"><%= s.label %></dt><dd data-motion-counter class="font-display text-5xl tabular-nums"><%= s.value %></dd></div><% }) %>
784
+ </dl>
785
+
786
+ <%# Logo marquee — the runtime clones the row for a seamless loop %>
787
+ <div class="overflow-hidden py-10 [mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]">
788
+ <div data-motion-marquee="40" class="flex items-center gap-16"><% item.logos.forEach(function (logo) { %><%- img(logo, { h: 40, class: 'h-10 w-auto opacity-70' }) %><% }) %></div>
789
+ </div>
790
+
791
+ <%# Tilt card, magnetic button %>
792
+ <a href="/work/harbor" data-motion-hover="tilt" class="block rounded-card bg-surface p-8 shadow-xl">…</a>
793
+ <a href="/contact" data-motion-hover="magnetic" class="inline-flex rounded-full bg-brand-600 px-8 py-4 text-white">Start a project</a>
794
+
795
+ <%# Floating shape — an ambient loop is Tailwind: an @theme --animate-* keyframe token in css/tailwind.css %>
796
+ <div class="absolute -right-24 top-10 -z-10 size-72 rounded-full bg-brand-500/20 blur-3xl animate-drift motion-reduce:animate-none"></div>
797
+
798
+ <%# SVG line draw — stroked outlines only; data-motion-stagger on the <svg> draws shapes in turn %>
799
+ <svg data-motion="draw" data-motion-duration="1400" viewBox="0 0 400 120" fill="none" stroke="currentColor" stroke-width="3" class="w-full text-brand-600"><path d="M5 100 C 120 10, 260 10, 395 60" /></svg>
800
+ ```
801
+
802
+ **Page transitions** — a cross-fade between pages with no JS, in `css/tailwind.css` (browsers without support just navigate):
803
+
804
+ ```css
805
+ @media (prefers-reduced-motion: no-preference) {
806
+ @view-transition { navigation: auto; }
807
+ }
808
+ ```
809
+
810
+ ### Custom effects with the Motion API
811
+
812
+ When the attributes can't express it, load Motion in the layout and write your own script **as a module**, importing Motion exactly as its documentation does:
813
+
814
+ ```ejs
815
+ <%# layouts/common.ejs %>
816
+ <% script('motion') %>
817
+ <% script({ type: 'module', src: '/js/site.js' }) %>
818
+ ```
819
+
820
+ ```js
821
+ // js/site.js
822
+ import { animate, scroll, stagger } from 'motion';
823
+
824
+ if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
825
+ // Header hides on the way down, returns on the way up
826
+ const header = document.querySelector('.js-header');
827
+ let last = 0;
828
+ scroll((_, info) => {
829
+ const y = info.y.current;
830
+ header.classList.toggle('-translate-y-full', y > last && y > 120);
831
+ last = y;
832
+ });
833
+
834
+ // Menu opens as a sequence: panel first, then its links in turn
835
+ document.querySelector('.js-menu-open')?.addEventListener('click', () => {
836
+ animate([
837
+ ['.js-menu', { opacity: [0, 1], transform: ['translateY(-12px)', 'none'] }, { duration: 0.25 }],
838
+ ['.js-menu a', { opacity: [0, 1], transform: ['translateX(-8px)', 'none'] }, { delay: stagger(0.05), at: '-0.1' }],
839
+ ]);
840
+ });
841
+
842
+ // A dot that springs after the pointer
843
+ const dot = document.querySelector('.js-cursor');
844
+ if (dot && matchMedia('(pointer: fine)').matches) {
845
+ addEventListener('pointermove', (e) => animate(dot, { x: e.clientX, y: e.clientY }, { type: 'spring', stiffness: 400, damping: 30 }));
846
+ }
847
+ }
848
+ ```
849
+
850
+ - The API: `animate` (elements, values, or a sequence of steps), `scroll`, `inView`, `hover`, `press`, `stagger`, `spring`. A classic script works too: `script('/js/site.js')` with `const { animate, scroll } = Motion;`.
851
+ - **Always `script('motion')` first.** Never import Motion from a CDN URL, and never rely on the copy the build adds for `data-motion` — it has no `'motion'` import and loads after your scripts.
852
+ - Entrances belong to `data-motion`: it hides content before the first paint, while your own script runs after the page has already shown.
853
+
854
+ **Other libraries** (§8): `gsap` only for what Motion can't do, with one `matchMedia('(prefers-reduced-motion: reduce)')` guard around the whole file; `aos` is legacy — on a page you rework, swap `data-aos*` for the matching `data-motion*` attributes and drop `script('aos')`; never run two motion libraries on one page.
855
+
856
+ **Reduced motion & performance:** `data-motion` handles reduced motion itself; Tailwind motion takes `motion-reduce:` variants; any hand-written animation sits behind a `matchMedia` guard. With reduced motion on, every element must be visible, in place, immediately. A `data-motion` element waits for Motion (about 50 KB) before it appears, so never put an entrance on the hero's largest image — animate the words around it instead.
857
+
858
+ ## 16. Critical Rules Checklist
652
859
 
653
860
  1. **`+` is part of the key.** A collection's `+` appears on the model, the view, and the content file/folder: `models/pages/blog+.model`, `pages/blog+.ejs`, `content/pages/blog+/<slug>.json`. A file without the `+` is a *different*, single-type key.
654
861
  2. Models first: a content field that isn't in the `.model` won't sync. Update the model, then the content.
655
862
  3. Blocks never have content files. Block values live inline in the page/entry JSON that declares them.
656
863
  4. `file` field values are plain file names ending in a supported extension (`"brochure.pdf"`) — the CMS attaches or placeholder-creates the file server-side; never write objects or URLs.
657
864
  5. Never invent `video_id`/`embed_url` objects — render videos with `embed()` only. Videos are set via refs: `content/videos.json` handles for shared videos, and `cms:<handle>` / provider refs in `video` fields (§7); values that read back as embed URLs or objects are user uploads — leave them untouched.
658
- 6. `richtext` → `<%- item.x %>`. `markdown` → `<%- marked(item.x) %>`.
865
+ 6. `richtext` → `<%- item.x %>`. `markdown` → `<%- marked(item.x) %>`, or `<%- remark(item.x) %>` when it holds code, math or diagrams.
659
866
  7. CSS/JS via `link()`/`script()` only; `css/tailwind.css` is auto-injected — never `link()` it. When writing tailwind.css, `@import "tailwindcss";` must stay as its first statement — dropping it silently removes all Tailwind styling.
660
867
  8. Editable copy belongs in `content/`, not hard-coded in `.ejs`. Fixed micro-labels may stay in views.
661
868
  9. In page keys, `-` separates words and `_` means `/`. `_index` is the home page only; a section index page uses the plain section key.
@@ -666,3 +873,4 @@ No config element → all defaults. One per site: a second one is ignored with a
666
873
  14. Always create an RSS feed for blogs (`rss.xml` key) and make every site strongly SEO- and sharing-friendly (reusable `seo` block rendered once from the layout head).
667
874
  15. Site-wide setup — default page title, Tailwind on/off, sitemap on/off, cookie consent on/off, analytics tag ids — lives in `setup.json` (§4). Change values there rather than hard-coding a site title into the layout, and never delete the file. Sitemap page exclusions are UI-only.
668
875
  16. Cookie consent (§14) is a `setup.json` switch plus `data-sleekcms-consent` attributes — tag third-party scripts through `script({ …, 'data-sleekcms-consent': 'category' })`, never a raw tag, and add a footer "Cookie settings" link. Only when the user asks for a banner.
876
+ 17. Motion (§15): `data-motion` attributes are built in (no `script()` call) — reveals, stagger, split text, counters, parallax, scroll effects, marquees; `script('motion')` only for your own Motion code; never two motion libraries on a page; reduced motion is handled for you.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleekcms/sync",
3
- "version": "2.12.0",
3
+ "version": "2.15.0",
4
4
  "description": "Edit SleekCMS sites locally — models, content, templates, images — with live two-way sync and AI agent support.",
5
5
  "keywords": [
6
6
  "sleekcms",