@sleekcms/sync 4.3.0 → 4.5.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/README.md CHANGED
@@ -398,7 +398,7 @@ Models describe the shape of your content. They're JSON-like files with unquoted
398
398
  **Forms** (no backend needed)
399
399
 
400
400
  ```html
401
- <form data-sleekcms="contact">
401
+ <form data-sleeksite-form="Contact">
402
402
  <input name="name" type="text" required>
403
403
  <input name="email" type="email" required>
404
404
  <textarea name="message"></textarea>
@@ -406,7 +406,7 @@ Models describe the shape of your content. They're JSON-like files with unquoted
406
406
  </form>
407
407
  ```
408
408
 
409
- 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:
409
+ Any form with `data-sleeksite-form="<name>"` captures and stores submissions in the CMS dashboard automatically (`data-sleeksite-book` makes it a booking form with a slot picker). On sync the form is created if it doesn't exist and the CMS stores the view with the name replaced by the form's key, so the published form stays linked when it's renamed in the app; your local file keeps the name as you wrote it. 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:
410
410
 
411
411
  ```html
412
412
  <input name="resume" type="file" accept=".pdf">
@@ -420,7 +420,7 @@ Content lives in `src/content/` as plain JSON. Edit any content file and save
420
420
 
421
421
  ## Site setup (`src/setup.json`)
422
422
 
423
- Site-wide decisions made once — the default page title, whether Tailwind is compiled, whether a sitemap is published, the cookie consent switch, the chat bot switch, and analytics tag ids — live in `src/setup.json`, mirrored from **Settings → Website** in the CMS. The file is JSON with a comment above each key explaining it:
423
+ Site-wide decisions made once — the default page title, whether Tailwind is compiled, whether a sitemap is published, the cookie consent switch, the chat bot switch and how the chat opens, and analytics tag ids — live in `src/setup.json`, mirrored from **Settings → Website** in the CMS. The file is JSON with a comment above each key explaining it:
424
424
 
425
425
  ```json5
426
426
  {
@@ -429,6 +429,7 @@ Site-wide decisions made once — the default page title, whether Tailwind is co
429
429
  sitemap_enabled: false, // publish sitemap.xml + robots.txt on every build
430
430
  cookie_consent: false, // consent banner; holds analytics + data-sleekcms-consent scripts until accepted
431
431
  chat_enabled: false, // chat bot on every page, answering from the site's published pages
432
+ chat_mode: "POPUP", // how the chat opens: "POPUP", "SIDEBAR" or "SPOTLIGHT" (any case accepted)
432
433
  analytics: { // tag ids injected into every page; include only the ones you use
433
434
  ga4: "G-XXXXXXXXXX",
434
435
  },
package/dist/AGENTS.md CHANGED
@@ -102,7 +102,7 @@ content/entries/<key>.json Content for a single entry (object)
102
102
  content/entries/<key>+.json Collection entry — ONE file holding a JSON array of all items
103
103
  content/images.json Site-level reusable images: handle → shortcut (§7)
104
104
  content/videos.json Site-level videos: handle → pexels:/pixabay: ref (user uploads show as embed_url) (§7)
105
- content/files.json The site's files (Media > Files), read-only: handle → { file, name, url } (§7)
105
+ content/files.json The site's files (Media > Files), read-only: file name → { file, url, size } (§7)
106
106
 
107
107
  setup.json Site setup (JSON, comments allowed): default page title, Tailwind on/off, sitemap on/off, cookie consent on/off, analytics tag ids — edit values in place, never delete
108
108
  ```
@@ -125,6 +125,7 @@ setup.json Site setup (JSON, comments allowed): default pag
125
125
  | `sitemap_enabled` | boolean | `true` publishes `sitemap.xml` + `robots.txt` on every build. Turn it on for public sites and SEO work. Which pages are *excluded* is a CMS UI setting (Manage Site > Search Visibility), never this file. |
126
126
  | `cookie_consent` | boolean | `true` injects a consent banner into every page and holds the analytics tags, plus any script or iframe tagged `data-sleekcms-consent`, until the visitor accepts its category. A toggle only — banner text, position and styling are authored in HTML (§14). |
127
127
  | `chat_enabled` | boolean | `true` adds the chat bot to every published page; it answers visitor questions from the site's published pages (re-read on every publish). A toggle only — the welcome message, knowledge files and instructions are managed in the CMS. `false` removes it from the next publish and stops open chats right away. |
128
+ | `chat_mode` | string | How the chat opens when a visitor clicks its launcher: `"POPUP"` (default, a small panel in the corner), `"SIDEBAR"` (a full-height panel docked right, over the page) or `"SPOTLIGHT"` (a centred search-style dialog). Any case is accepted and saved uppercase. On phones sidebar and spotlight open full screen. Changes with the next publish. A page can override it: `data-sleek-chat="sidebar"` (or `"spotlight"`, `"popup"`) on any button or link opens chat in that mode, and `--sleek-chat-mode` in the site's CSS sets it per page or breakpoint. |
128
129
  | `analytics` | object | Marketing/analytics tag ids injected into every published page. |
129
130
 
130
131
  `analytics` is a flat map — include only the keys the site uses:
@@ -249,7 +250,7 @@ Like JSON but with **no quotes**; each value is a field type name:
249
250
  | `image` | Shortcut string (preferred when writing) or resolved `{ "url", "alt" }` object — see §7. Shortcuts only resolve in `image`/`icon` fields — in a `text` field the string stays literal text |
250
251
  | `icon` | Svg icon. Use for icons the user is expected to edit/swap in the CMS (e.g. features/services). Fill with placeholder `"iconify:<prefix:name or search>"` (or `"cms:<handle>"` pointing at an svg); render with `svg(item.icon)`. Fixed UI icons don't need fields — call `svg('iconify:...')` directly in templates |
251
252
  | `video` | Ref string (preferred when writing) or the resolved object you read back. Write `"cms:<handle>"` to reuse a video from `content/videos.json` (or a user upload by its handle), or `"pexels:<search>\|<title>"` / `"pixabay:<search>\|<title>"` for a one-off — resolved server-side (see §7). **Never invent** `video_id`/`embed_url` objects; render with `embed()` only |
252
- | `file` | Write a plain file name, e.g. `"brochure.pdf"` (pdf, docx, xlsx, pptx, csv, tsv, txt, md — no path, no URL, never an object). A listed file's handle from `content/files.json` works too, and wins over a file with that name. On save the CMS attaches that file, or (for a file name nothing matches) creates a small placeholder file the user replaces in the CMS UI; reads back as the file name. Render a link with `item.x.url` / `item.x.name` |
253
+ | `file` | Write a plain file name, e.g. `"brochure.pdf"` (pdf, docx, xlsx, pptx, csv, tsv, txt, md — no path, no URL, never an object). A file listed in `content/files.json` is written by its listed name (`"forms/booking.docx"` for one in a folder). On save the CMS attaches that file, or (for a file name nothing matches) creates a small placeholder file the user replaces in the CMS UI; reads back as the file name. Render a link with `item.x.url` / `item.x.name` |
253
254
  | `json` | Any JSON |
254
255
  | `sheet` | Array of arrays |
255
256
  | `location` | `{ "markers": [{ "lat", "lng" }], "img": "<google static map url>" }` — `img` supports Google Static Maps params; markers are already part of the URL |
@@ -362,18 +363,18 @@ Content `video` fields accept the same refs: `"cms:<handle>"` reuses a declared
362
363
 
363
364
  ### Files
364
365
 
365
- `content/files.json` lists the site's files (Media > Files) as `handle → { file, name, url }`: `file` is the file name, `name` its display name, `url` its public download link.
366
+ `content/files.json` lists the site's files (Media > Files) as `file name → { file, url, size }`: `file` is the value a `file` field takes, `url` the path the site serves it at, `size` its bytes.
366
367
 
367
368
  ```json
368
- { "lunch-menu": { "file": "lunch-menu.pdf", "name": "Lunch Menu", "url": "https://files.sleekcms.com/…/lunch-menu.pdf" } }
369
+ { "lunch-menu.pdf": { "file": "lunch-menu.pdf", "url": "/_files/lunch-menu.pdf", "size": 48213 } }
369
370
  ```
370
371
 
371
372
  It's read-only: edits to it are never saved, and every sync replaces it with the site's current list. Check it before writing a new file name.
372
373
 
373
- A `file` field takes a listed handle (`"lunch-menu"`) or file name (`"lunch-menu.pdf"`). Render a file no field points at with `getFile(handle)`, which returns the same `{ name, url, size, type, ext }` as a `file` field (or `undefined` for an unknown handle):
374
+ A `file` field takes a listed file name (`"lunch-menu.pdf"`). Render a file no field points at with `getFile(name)`, which returns the same `{ name, url, size, type, ext }` as a `file` field (or `undefined` for an unknown name):
374
375
 
375
376
  ```ejs
376
- <% const menu = getFile('lunch-menu') %>
377
+ <% const menu = getFile('lunch-menu.pdf') %>
377
378
  <% if (menu) { %><a href="<%= menu.url %>" download><%= menu.name %></a><% } %>
378
379
  ```
379
380
 
@@ -391,7 +392,7 @@ node upload.mjs menu-v2.pdf --name lunch-menu.pdf # store it under another fi
391
392
  ```
392
393
 
393
394
  - **Images** (png, jpg, jpeg, webp, gif, svg, ico, up to 8 MB) become site images under a handle. The handle is `--handle`, or by default the file name lowercased and dashed (`Team Photo.jpg` → `team-photo`). The script adds the handle to `content/images.json`. Reference the image as `"cms:<handle>"` in any `image` field, or with `img('cms:<handle>')` / `src('cms:<handle>')` in a view. Uploading to an existing handle replaces that image everywhere it's used.
394
- - **Documents** (pdf, docx, xlsx, pptx, csv, tsv, txt, md, up to 10 MB) are added to Media > Files under their file name, and `content/files.json` is rewritten to list them. Put the printed file name (or its handle) in a `file` field, or render it with `getFile('<handle>')`. Uploading the same file name again replaces that file.
395
+ - **Documents** (pdf, docx, xlsx, pptx, csv, tsv, txt, md, up to 10 MB) are added to Media > Files under their file name, and `content/files.json` is rewritten to list them. Put the printed file name in a `file` field, or render it with `getFile('<file name>')`. Uploading the same file name again replaces that file.
395
396
  - The script prints the ref to use for each file. Copy it from the output rather than guessing it, because the server cleans up file names. Add `--json` for one JSON result per line. Exit code `0` means every file uploaded, `1` means some failed (the reason is printed), and `2` means it couldn't run.
396
397
  - Uploading needs an EDITOR or ADMIN on the site.
397
398
  - Never copy images or documents into `src/`. Binary files there are not synced; the sync log just points you back to `upload.mjs`.
@@ -472,7 +473,7 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
472
473
  | `getSlugs(path)` | string[] | Slugs under a collection path |
473
474
  | `getImage(handle)` | Object \| undefined | Resolved image from `images.json` (bare handle, no `cms:` prefix) |
474
475
  | `getVideo(handle)` | Object \| undefined | Video from `videos.json` as `{ title, source, video_id, embed_url }` — render with `embed()` |
475
- | `getFile(handle)` | Object \| undefined | File from `files.json` as `{ name, url, size, type, ext }`, like a `file` field |
476
+ | `getFile(name)` | Object \| undefined | File from `files.json` by its name, as `{ name, url, size, type, ext }`, like a `file` field |
476
477
  | `getContent(query?)` | Any | Full content payload, optionally filtered with JMESPath |
477
478
  | `path(page)` | String | URL path of a page object |
478
479
  | `url(pathOrPage?)` | String | Site origin; pass a path or page object for a full absolute URL |
@@ -572,10 +573,10 @@ Three site-level SEO settings live in `setup.json` (§4), not in views: `sitemap
572
573
 
573
574
  ## 10. Forms
574
575
 
575
- Any `<form>` with `data-sleekcms="<hardcoded name>"` works automatically — submissions are captured and shown in the CMS dashboard. No backend, no action URL, no JS.
576
+ Any `<form>` with `data-sleeksite-form="<name>"` works automatically — submissions are captured and shown in the CMS dashboard. No backend, no action URL, no JS.
576
577
 
577
578
  ```html
578
- <form data-sleekcms="contact">
579
+ <form data-sleeksite-form="Contact">
579
580
  <input name="name" type="text" required>
580
581
  <input name="email" type="email" required>
581
582
  <textarea name="message"></textarea>
@@ -583,7 +584,7 @@ Any `<form>` with `data-sleekcms="<hardcoded name>"` works automatically — sub
583
584
  </form>
584
585
  ```
585
586
 
586
- 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.
587
+ Write the form's **name** (e.g. `Contact`, `Newsletter`, `Quote request`), literally — never a template expression. On sync the form is created if it doesn't exist, and the CMS stores the view with the name replaced by the form's key (e.g. `data-sleeksite-form="mh3k2x9a"`), so the published form stays linked when the user renames it in the app. Your local file keeps the name as you wrote it. A key you find in a view is the form's id: keep it as it is. Each input's `name` is stored as-is.
587
588
 
588
589
  **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):
589
590
 
@@ -591,25 +592,27 @@ The `data-sleekcms` value groups submissions by form; each input's `name` is sto
591
592
  <input name="resume" type="file" accept=".pdf,.doc,.docx">
592
593
  ```
593
594
 
595
+ For visitors to **book a time**, use `data-sleeksite-book` instead — see §11. A walk-in queue's "Schedule a time" form uses `data-sleeksite-queue="<queue name>"` (fields `queue_date`, an empty `<select name="queue_time">`, `name`, `email`) and goes to the queue, not the inbox; the queue is created on sync, with the same name-to-key rule as forms.
596
+
594
597
  ## 11. Bookings
595
598
 
596
599
  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.**
597
600
 
598
- **A booking is just a form.** Give a normal `data-sleekcms` form an `<input type="datetime-local">` — at publish time that input is replaced by an availability slot picker, and submissions become appointments with email confirmation, calendar invites, and no-login cancel/reschedule. Every other field is yours: bookings store arbitrary fields, exactly like form submissions.
601
+ **A booking is just a form** with `data-sleeksite-book="<name>"` instead of `data-sleeksite-form` — on the page it gets an availability slot picker, and submissions become appointments with email confirmation, calendar invites, and no-login cancel/reschedule. Every other field is yours: bookings store arbitrary fields, exactly like form submissions.
599
602
 
600
603
  ```html
601
- <form data-sleekcms="consultation">
604
+ <form data-sleeksite-book="Consultation">
602
605
  <input name="name" type="text" required placeholder="Your name">
603
606
  <input name="email" type="email" required placeholder="Email">
604
- <input name="slot" type="datetime-local" required>
607
+ <div data-sleeksite-slot></div>
605
608
  <textarea name="notes" placeholder="Anything we should know?"></textarea>
606
609
  <button type="submit">Book appointment</button>
607
610
  </form>
608
611
  ```
609
612
 
610
- Rules: **include an email field** (`type="email"` or `name="email"`) — confirmation and manage links go there; the datetime input's own `name` doesn't matter (it is replaced by the picker, and the chosen time is stored as the booking's slot); style the form like any other — the picker inherits the page's `--accent` CSS variable (or set `data-accent` on the form).
613
+ Rules: **include an email field** (`type="email"` or `name="email"`) — confirmation and manage links go there; the picker goes where an element with `data-sleeksite-slot` (or an `<input type="datetime-local">`) is, and replaces it — without one it goes just before the submit button, and the chosen time is stored as the booking's slot; style the form like any other — the picker inherits the page's `--accent` CSS variable (or set `data-accent` on the form).
611
614
 
612
- The form named `consultation` is **auto-created on sync** like any form, and because it has a datetime input it gets default availability: 30-minute slots, one booking at a time, Mon–Fri 9 AM–5 PM in America/New_York, bookings confirmed instantly (email-confirmation off — the owner can require it in settings). The defaults are guesses — tell the user to review the timezone and open hours in the form's **Booking settings** (Customers › Inbox); appointments appear under **Customers › Bookings**. Reusing the same name across pages shares one booking form.
615
+ The form named `Consultation` is **auto-created on sync** like any form (the same name-to-key rule as §10 applies), and as a booking form it gets default availability: 30-minute slots, one booking at a time, Mon–Fri 9 AM–5 PM in America/New_York, bookings confirmed instantly (email-confirmation off — the owner can require it in settings). The defaults are guesses — tell the user to review the timezone and open hours in the form's **Booking settings** (Customers › Inbox); appointments appear under **Customers › Bookings**. Reusing the same name (or key) across pages shares one booking form. The visitor's confirm and manage links open pages on the site's own domain (`/_book/…`) — nothing to build.
613
616
 
614
617
  ## 12. RSS Feeds
615
618
 
package/dist/upload.mjs CHANGED
@@ -268,8 +268,18 @@ export async function uploadOne(opts) {
268
268
  const ext = extOf(name);
269
269
  if (TEXT_FILE_EXTS.includes(ext) && data.length <= MAX_INLINE_TEXT_BYTES) body.content = data.toString("utf-8");
270
270
  const created = await api(base, token, "POST", "/create_asset", body, fetchImpl);
271
- if (created.upload) await sendBytes(created.upload, data, fetchImpl);
272
- writeFilesIndex(ws, created.files);
271
+ let files = created.files;
272
+ if (created.upload) {
273
+ await sendBytes(created.upload, data, fetchImpl);
274
+ // The listing create_asset returned predates the bytes (a new file is only
275
+ // listed once stored), so ask again now that they are.
276
+ try {
277
+ files = await api(base, token, "GET", "/get_files?kind=files", undefined, fetchImpl);
278
+ } catch {
279
+ // keep the earlier listing; the next sync pull corrects it
280
+ }
281
+ }
282
+ writeFilesIndex(ws, files);
273
283
  return {
274
284
  source: filePath,
275
285
  type: "file",
@@ -277,7 +287,7 @@ export async function uploadOne(opts) {
277
287
  handle: created.handle || undefined,
278
288
  replaced: !!created.replaces_existing,
279
289
  url: created.url || undefined,
280
- indexed: !!(created.files || []).some((f) => f.path === FILES_INDEX),
290
+ indexed: !!(files || []).some((f) => f.path === FILES_INDEX),
281
291
  };
282
292
  }
283
293
 
@@ -290,7 +300,7 @@ export function describe(r) {
290
300
  return lines;
291
301
  }
292
302
  const lines = [`✅ ${r.replaced ? "Replaced" : "Uploaded"} file ${path.basename(r.source)} → "${r.ref}"${r.url ? ` (${r.url})` : ""}`];
293
- lines.push(` Use "${r.ref}"${r.handle ? ` (or its handle "${r.handle}")` : ""} as a file field's value, or getFile('${r.handle || r.ref}') in a view.`);
303
+ lines.push(` Use "${r.ref}" as a file field's value, or getFile('${r.ref}') in a view.`);
294
304
  return lines;
295
305
  }
296
306
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleekcms/sync",
3
- "version": "4.3.0",
3
+ "version": "4.5.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",