storyloco 2.9.0 → 2.10.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 (45) hide show
  1. package/README.md +104 -0
  2. package/package.json +43 -3
  3. package/packages/churchsuite_categories/types.ts +9 -0
  4. package/packages/vite/dist/editor/anchor.d.ts +23 -0
  5. package/packages/vite/dist/editor/anchor.js +92 -0
  6. package/packages/vite/dist/editor/block_bar.svelte +624 -0
  7. package/packages/vite/dist/editor/block_bar.svelte.d.ts +36 -0
  8. package/packages/vite/dist/editor/client.d.ts +198 -0
  9. package/packages/vite/dist/editor/client.js +468 -0
  10. package/packages/vite/dist/editor/comments.svelte +1385 -0
  11. package/packages/vite/dist/editor/comments.svelte.d.ts +27 -0
  12. package/packages/vite/dist/editor/compare.svelte +712 -0
  13. package/packages/vite/dist/editor/compare.svelte.d.ts +11 -0
  14. package/packages/vite/dist/editor/content.d.ts +85 -0
  15. package/packages/vite/dist/editor/content.js +358 -0
  16. package/packages/vite/dist/editor/edit_bar.svelte +1706 -0
  17. package/packages/vite/dist/editor/edit_bar.svelte.d.ts +29 -0
  18. package/packages/vite/dist/editor/face.svelte +69 -0
  19. package/packages/vite/dist/editor/face.svelte.d.ts +15 -0
  20. package/packages/vite/dist/editor/image_picker.svelte +646 -0
  21. package/packages/vite/dist/editor/image_picker.svelte.d.ts +36 -0
  22. package/packages/vite/dist/editor/index.d.ts +2 -0
  23. package/packages/vite/dist/editor/index.js +2 -0
  24. package/packages/vite/dist/editor/inspector.svelte +2644 -0
  25. package/packages/vite/dist/editor/inspector.svelte.d.ts +85 -0
  26. package/packages/vite/dist/editor/live.d.ts +105 -0
  27. package/packages/vite/dist/editor/live.js +419 -0
  28. package/packages/vite/dist/editor/overlay.d.ts +81 -0
  29. package/packages/vite/dist/editor/overlay.js +1351 -0
  30. package/packages/vite/dist/editor/presence.d.ts +14 -0
  31. package/packages/vite/dist/editor/presence.js +399 -0
  32. package/packages/vite/dist/editor/richtext.d.ts +93 -0
  33. package/packages/vite/dist/editor/richtext.js +1144 -0
  34. package/packages/vite/dist/editor/server.d.ts +139 -0
  35. package/packages/vite/dist/editor/server.js +1320 -0
  36. package/packages/vite/dist/editor/time_machine.svelte +1213 -0
  37. package/packages/vite/dist/editor/time_machine.svelte.d.ts +12 -0
  38. package/packages/vite/dist/editor/together.d.ts +34 -0
  39. package/packages/vite/dist/editor/together.js +106 -0
  40. package/packages/vite/dist/editor/tools.svelte +2315 -0
  41. package/packages/vite/dist/editor/tools.svelte.d.ts +23 -0
  42. package/packages/vite/dist/hooks.d.ts +18 -2
  43. package/packages/vite/dist/hooks.js +24 -3
  44. package/packages/vite/dist/storyblok.svelte.d.ts +5 -1
  45. package/packages/vite/dist/storyblok.svelte.js +23 -5
package/README.md CHANGED
@@ -17,6 +17,7 @@ A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, a
17
17
  - **⏰ Time** - Simple time input field with step control
18
18
  - **💷 Currency** - Amount input with a currency selector; configure allowed codes via the `currencies` and `default_currency` options
19
19
  - **📅 Dates** - List of dates, each with an optional time, end date and end time, plus a timezone picker; configure via the `min`, `max` and `auto_sort` options
20
+ - **⛪ ChurchSuite Categories** - Multi-select of ChurchSuite calendar categories, read from your site's endpoint via the `endpoint` option
20
21
 
21
22
  ### Vite Plugins
22
23
 
@@ -270,6 +271,109 @@ await client.get('cdn/datasource_entries', { version })
270
271
 
271
272
  Errors go through `client.handle_error`, so a missing story is a 404 and anything else a 500, shaped by the client's `error` option.
272
273
 
274
+ ### Edit on the Site
275
+
276
+ Anyone with a role in the space can edit the real site in place, signed in with their own
277
+ Storyblok account, so their permissions apply and every change is credited to them. Changes
278
+ save as drafts; Publish in the corner badge lists what will go live, block by block, first.
279
+
280
+ It needs the **Site Editor** app installed in the space (Storyblok › Apps), with the site's
281
+ origin allowed on the app's page. `localhost` always is, and a pattern like
282
+ `https://*-team.vercel.app` covers every preview. Then three lines:
283
+
284
+ ```typescript
285
+ // hooks.server.ts
286
+ import { editor } from 'storyloco/editor/server'
287
+
288
+ export const handle = sequence(editor, version /* , ...the rest */)
289
+ ```
290
+
291
+ ```svelte
292
+ <!-- +layout.svelte -->
293
+ <script lang="ts">
294
+ import { EditBar } from 'storyloco/editor'
295
+ </script>
296
+
297
+ <EditBar />
298
+ ```
299
+
300
+ ```typescript
301
+ // app.d.ts
302
+ interface Locals {
303
+ editor?: import('storyloco/editor/server').Editor
304
+ }
305
+ ```
306
+
307
+ A site with a Content Security Policy needs `wss://moxy.uilo.co` in its `connect-src`, for the
308
+ multiplayer socket, and `'self'` in its `frame-ancestors`, for the phone and tablet preview.
309
+
310
+ A site that picks its own content version rather than using `version` asks `editor_version(event)`
311
+ first: draft for editors and guests, published for the live view. A site that caches pages at
312
+ its CDN (`s-maxage`) needs editors, and guests (`sl_guest…`), on a cache key of their own, or
313
+ whoever signs in on the live site is handed the cached visitor page, with no edit bar. The CDN
314
+ answers before the hook runs and won't vary on cookies, so it takes Routing Middleware: when a
315
+ request carries an `sl_editor…` cookie, rewrite it onto a `?_storyloco=1` URL, and never cache
316
+ responses there.
317
+
318
+ No secrets: sign-in runs through moxy, and the space comes from `STORYBLOK_SPACE_ID` or the
319
+ site's public token. Blocks that spread `attrs(blok)` are editable as they are; nothing marks
320
+ fields up. With edit mode on (the badge's switch, or **E**):
321
+
322
+ - **Text** edits where it is, matched to its field by what it shows. Body copy edits in place
323
+ as rich text, with the site's own styles.
324
+ - **Images** offer Replace: the space's library or an upload, with alt text and a focus point.
325
+ - **Blocks** get a toolbar on hover to move, duplicate, add after or delete (with Undo), and a
326
+ click opens every field of the block from its schema, nested blocks included. Drag a block
327
+ by its name in the toolbar to put it anywhere in its list.
328
+ - **AI** suggests an image's alt text in the image picker, and rewrites a selection or
329
+ paragraph of rich text shorter, friendlier, clearer, or with its spelling fixed.
330
+ - **Other stories** shown on the page, like cards listing the latest articles, edit as those
331
+ stories: their text and images where they show, and a click opens them.
332
+ - **Video** uploads to Mux when the space has the Mux app connected.
333
+ - **Undo** takes back any change made from the page, and Redo makes it again: **⌘Z** and
334
+ **⇧⌘Z** (Ctrl+Z and Ctrl+Y) outside a field, or the badge's arrows.
335
+ - **Someone else** saving the page in Storyblok while it's open shows a note with Refresh.
336
+ - **⌘K** (Ctrl+K), or the badge's ⌘ button, finds a page, a block on this one, or something
337
+ to do: make a new page, ask someone to review this one, or preview it at phone or tablet size.
338
+ - **History** goes back through the page's saves on a timeline, the page showing itself as it
339
+ was at each one, and restores any of them as the draft (with Undo). Pages that `connect()`
340
+ their story show it in place; others list what restoring would change.
341
+ - **Compare with live** (⌘K, or from Publish) flips the page between the draft and what
342
+ visitors see now, scrolled to the same place (**L**), with the blocks that aren't live yet
343
+ marked on the draft.
344
+ - **Share** the draft, like a Google Doc, with people who don't edit in Storyblok: invite them
345
+ by email (they sign in from the link, once), or let anyone with the link look. Guests see the
346
+ draft and, if they can comment, comment; someone from the link confirms their email with a
347
+ code first, so no comment is anonymous. Guests never edit, resolve, or get a Storyblok token,
348
+ and their pages aren't cached or indexed.
349
+
350
+ Everyone editing a site at once sees each other. Each person has a colour: their pointer and
351
+ name move over the page, what they've selected is outlined, and their avatars sit by the
352
+ badge. Text and rich text type together, letters merging as they're typed with everyone's
353
+ caret showing, and saves to the same story take turns, so no one's change is lost.
354
+
355
+ - **Follow** someone by clicking their avatar: the page goes where they go and scrolls with
356
+ them. Scroll yourself, or press Escape, to stop.
357
+ - **Say something** by your pointer with **/**; the others see it as it's typed.
358
+ - **Comment** with **C**, or the badge's speech bubble, then click where it goes. Threads take
359
+ replies, @mentions and Resolve. Anyone in one, or mentioned, finds it under the badge's bell,
360
+ its unread count on the badge, and gets a browser notification too once they've chosen
361
+ Notify me. A review request lands there too, and anything left unread for 15 minutes is
362
+ emailed, guests' included.
363
+
364
+ Visitors never see any of it. On the live site, press **⌘⇧E** (Ctrl+Shift+E) or add `?edit` to
365
+ any address to sign in; once signed in, the badge shows on every page, and ⌘⇧E or **E** turns
366
+ edit mode on and off. In development the badge is always there.
367
+
368
+ Links inside content don't navigate in edit mode; hold ⌘ or Ctrl to follow one. Match the
369
+ editor to a site with `--storyloco-bar-bg`, `-ink`, `-muted`, `-line`, `-font` and
370
+ `--storyloco-accent`, or build a bar of your own from `editor`, `set_editing`, `publish`,
371
+ `signin` and `signout`.
372
+
373
+ `bun run smoke <site url>…` (in `packages/vite`) checks each site loads and, signed in, that a
374
+ click on a block opens its settings. Sign in once per site with `--signin`, which opens a
375
+ window; later runs reuse it without one.
376
+
273
377
  ### Use Shared Components
274
378
 
275
379
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyloco",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, and Tailwind CSS",
5
5
  "keywords": [
6
6
  "storyblok",
@@ -36,7 +36,8 @@
36
36
  "packages/seo/types.ts",
37
37
  "packages/hubspot_form/types.ts",
38
38
  "packages/currency/types.ts",
39
- "packages/dates/types.ts"
39
+ "packages/dates/types.ts",
40
+ "packages/churchsuite_categories/types.ts"
40
41
  ],
41
42
  "exports": {
42
43
  ".": {
@@ -83,6 +84,15 @@
83
84
  "types": "./packages/vite/dist/server.d.ts",
84
85
  "import": "./packages/vite/dist/server.js"
85
86
  },
87
+ "./editor": {
88
+ "types": "./packages/vite/dist/editor/index.d.ts",
89
+ "svelte": "./packages/vite/dist/editor/index.js",
90
+ "import": "./packages/vite/dist/editor/index.js"
91
+ },
92
+ "./editor/server": {
93
+ "types": "./packages/vite/dist/editor/server.d.ts",
94
+ "import": "./packages/vite/dist/editor/server.js"
95
+ },
86
96
  "./time": {
87
97
  "types": "./packages/time/types.ts",
88
98
  "svelte": "./packages/time/src/app.svelte.ts"
@@ -105,6 +115,9 @@
105
115
  "./dates": {
106
116
  "types": "./packages/dates/types.ts",
107
117
  "svelte": "./packages/dates/src/app.svelte.ts"
118
+ },
119
+ "./churchsuite_categories": {
120
+ "types": "./packages/churchsuite_categories/types.ts"
108
121
  }
109
122
  },
110
123
  "devDependencies": {
@@ -134,7 +147,34 @@
134
147
  "packages/*"
135
148
  ],
136
149
  "dependencies": {
150
+ "@tiptap/core": "^3.32.0",
151
+ "@tiptap/extension-blockquote": "^3.32.0",
152
+ "@tiptap/extension-bold": "^3.32.0",
153
+ "@tiptap/extension-code": "^3.32.0",
154
+ "@tiptap/extension-code-block": "^3.32.0",
155
+ "@tiptap/extension-collaboration": "^3.32.0",
156
+ "@tiptap/extension-collaboration-caret": "^3.32.0",
157
+ "@tiptap/extension-document": "^3.32.0",
158
+ "@tiptap/extension-hard-break": "^3.32.0",
159
+ "@tiptap/extension-heading": "^3.32.0",
160
+ "@tiptap/extension-highlight": "^3.32.0",
161
+ "@tiptap/extension-horizontal-rule": "^3.32.0",
162
+ "@tiptap/extension-image": "^3.32.0",
163
+ "@tiptap/extension-italic": "^3.32.0",
164
+ "@tiptap/extension-link": "^3.32.0",
165
+ "@tiptap/extension-list": "^3.32.0",
166
+ "@tiptap/extension-paragraph": "^3.32.0",
167
+ "@tiptap/extension-strike": "^3.32.0",
168
+ "@tiptap/extension-subscript": "^3.32.0",
169
+ "@tiptap/extension-superscript": "^3.32.0",
170
+ "@tiptap/extension-table": "^3.32.0",
171
+ "@tiptap/extension-text": "^3.32.0",
172
+ "@tiptap/extension-underline": "^3.32.0",
173
+ "@tiptap/pm": "^3.32.0",
174
+ "@tiptap/y-tiptap": "^3.0.10",
137
175
  "ora": "9.4.1",
138
- "path-to-regexp": "8.4.2"
176
+ "path-to-regexp": "8.4.2",
177
+ "y-protocols": "^1.0.7",
178
+ "yjs": "^13.6.33"
139
179
  }
140
180
  }
@@ -0,0 +1,9 @@
1
+ export interface ChurchsuiteCategory {
2
+ id: number
3
+ name: string
4
+ colour?: string
5
+ }
6
+
7
+ // the chosen ChurchSuite calendar category ids; the same shape the legacy
8
+ // churchsuite-event-categories plugin saved, so old values load as they are
9
+ export type ChurchsuiteCategories = Array<number>
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Where a comment's pin sits inside its block, finer than a fraction of the block: the element it's on, by
3
+ * its place under the block, and the character when it's on text. A block reflows at another width, its
4
+ * text rewrapping, and the pin follows the word it was put on.
5
+ */
6
+ export type Anchor = {
7
+ /** Child indexes from the block down to the element */
8
+ path: Array<number>;
9
+ tag: string;
10
+ /** Where on the element, or on the character, as a fraction of its box */
11
+ fx: number;
12
+ fy: number;
13
+ /** The character, as an offset into the element's text, and the text from there to find it again by */
14
+ at?: number;
15
+ near?: string;
16
+ };
17
+ /** The anchor for a point on an element inside a block; null when it isn't inside */
18
+ export declare function anchor_at(block: Element, under: Element, x: number, y: number): Anchor | null;
19
+ /** Where an anchor is on screen now; null when the block's changed so it isn't there any more */
20
+ export declare function anchored(block: Element, anchor: Anchor): {
21
+ x: number;
22
+ y: number;
23
+ } | null;
@@ -0,0 +1,92 @@
1
+ const NEAR = 16;
2
+ const within = (box, x, y) => x >= box.left && x <= box.right && y >= box.top && y <= box.bottom;
3
+ /** The character under a point among an element's text, as its text node and offset */
4
+ function character_at(el, x, y) {
5
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
6
+ const range = document.createRange();
7
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
8
+ range.selectNodeContents(node);
9
+ if (![...range.getClientRects()].some((box) => within(box, x, y)))
10
+ continue;
11
+ for (let i = 0; i < node.length; i++) {
12
+ range.setStart(node, i);
13
+ range.setEnd(node, i + 1);
14
+ const box = [...range.getClientRects()].find((b) => within(b, x, y));
15
+ if (box)
16
+ return { node, i, box };
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+ /** The anchor for a point on an element inside a block; null when it isn't inside */
22
+ export function anchor_at(block, under, x, y) {
23
+ if (!block.contains(under))
24
+ return null;
25
+ const hit = character_at(under, x, y);
26
+ const el = hit?.node.parentElement ?? under;
27
+ const path = [];
28
+ for (let at = el; at !== block; at = at.parentElement)
29
+ path.unshift([...at.parentElement.children].indexOf(at));
30
+ let on = null;
31
+ if (hit) {
32
+ const before = document.createRange();
33
+ before.setStart(el, 0);
34
+ before.setEnd(hit.node, hit.i);
35
+ const at = before.toString().length;
36
+ const near = (el.textContent ?? "").slice(at, at + NEAR);
37
+ // all space is nothing to find it by
38
+ if (near.trim())
39
+ on = { at, near };
40
+ }
41
+ const box = on ? hit.box : el.getBoundingClientRect();
42
+ return {
43
+ path,
44
+ tag: el.localName,
45
+ fx: box.width ? (x - box.left) / box.width : 0,
46
+ fy: box.height ? (y - box.top) / box.height : 0,
47
+ ...on,
48
+ };
49
+ }
50
+ /** Where an anchor is on screen now; null when the block's changed so it isn't there any more */
51
+ export function anchored(block, anchor) {
52
+ let el = block;
53
+ for (const i of anchor.path)
54
+ el = el?.children[i];
55
+ if (!el || el.localName !== anchor.tag)
56
+ return null;
57
+ if (anchor.at === undefined || !anchor.near) {
58
+ const box = el.getBoundingClientRect();
59
+ if (!box.width && !box.height)
60
+ return null;
61
+ return { x: box.left + anchor.fx * box.width, y: box.top + anchor.fy * box.height };
62
+ }
63
+ // the same words, where they were or as near there as they've moved to with an edit
64
+ const text = el.textContent ?? "";
65
+ const after = text.indexOf(anchor.near, anchor.at);
66
+ const before = text.lastIndexOf(anchor.near, anchor.at);
67
+ const at = after < 0
68
+ ? before
69
+ : before < 0
70
+ ? after
71
+ : after - anchor.at < anchor.at - before
72
+ ? after
73
+ : before;
74
+ if (at < 0)
75
+ return null;
76
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
77
+ let left = at;
78
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
79
+ if (left >= node.length) {
80
+ left -= node.length;
81
+ continue;
82
+ }
83
+ const range = document.createRange();
84
+ range.setStart(node, left);
85
+ range.setEnd(node, left + 1);
86
+ const box = range.getClientRects()[0];
87
+ if (!box)
88
+ return null;
89
+ return { x: box.left + anchor.fx * box.width, y: box.top + anchor.fy * box.height };
90
+ }
91
+ return null;
92
+ }