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.
- package/README.md +104 -0
- package/package.json +43 -3
- package/packages/churchsuite_categories/types.ts +9 -0
- package/packages/vite/dist/editor/anchor.d.ts +23 -0
- package/packages/vite/dist/editor/anchor.js +92 -0
- package/packages/vite/dist/editor/block_bar.svelte +624 -0
- package/packages/vite/dist/editor/block_bar.svelte.d.ts +36 -0
- package/packages/vite/dist/editor/client.d.ts +198 -0
- package/packages/vite/dist/editor/client.js +468 -0
- package/packages/vite/dist/editor/comments.svelte +1385 -0
- package/packages/vite/dist/editor/comments.svelte.d.ts +27 -0
- package/packages/vite/dist/editor/compare.svelte +712 -0
- package/packages/vite/dist/editor/compare.svelte.d.ts +11 -0
- package/packages/vite/dist/editor/content.d.ts +85 -0
- package/packages/vite/dist/editor/content.js +358 -0
- package/packages/vite/dist/editor/edit_bar.svelte +1706 -0
- package/packages/vite/dist/editor/edit_bar.svelte.d.ts +29 -0
- package/packages/vite/dist/editor/face.svelte +69 -0
- package/packages/vite/dist/editor/face.svelte.d.ts +15 -0
- package/packages/vite/dist/editor/image_picker.svelte +646 -0
- package/packages/vite/dist/editor/image_picker.svelte.d.ts +36 -0
- package/packages/vite/dist/editor/index.d.ts +2 -0
- package/packages/vite/dist/editor/index.js +2 -0
- package/packages/vite/dist/editor/inspector.svelte +2644 -0
- package/packages/vite/dist/editor/inspector.svelte.d.ts +85 -0
- package/packages/vite/dist/editor/live.d.ts +105 -0
- package/packages/vite/dist/editor/live.js +419 -0
- package/packages/vite/dist/editor/overlay.d.ts +81 -0
- package/packages/vite/dist/editor/overlay.js +1351 -0
- package/packages/vite/dist/editor/presence.d.ts +14 -0
- package/packages/vite/dist/editor/presence.js +399 -0
- package/packages/vite/dist/editor/richtext.d.ts +93 -0
- package/packages/vite/dist/editor/richtext.js +1144 -0
- package/packages/vite/dist/editor/server.d.ts +139 -0
- package/packages/vite/dist/editor/server.js +1320 -0
- package/packages/vite/dist/editor/time_machine.svelte +1213 -0
- package/packages/vite/dist/editor/time_machine.svelte.d.ts +12 -0
- package/packages/vite/dist/editor/together.d.ts +34 -0
- package/packages/vite/dist/editor/together.js +106 -0
- package/packages/vite/dist/editor/tools.svelte +2315 -0
- package/packages/vite/dist/editor/tools.svelte.d.ts +23 -0
- package/packages/vite/dist/hooks.d.ts +18 -2
- package/packages/vite/dist/hooks.js +24 -3
- package/packages/vite/dist/storyblok.svelte.d.ts +5 -1
- 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.
|
|
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
|
+
}
|