storyloco 1.4.1 → 1.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyloco",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, and Tailwind CSS",
5
5
  "keywords": [
6
6
  "storyblok",
@@ -20,10 +20,9 @@
20
20
  "types": "./src/index.ts",
21
21
  "files": [
22
22
  "src",
23
- "packages/mux/src/app.svelte",
24
23
  "packages/mux/types.ts",
25
- "packages/heading/src/app.svelte",
26
24
  "packages/heading/types.ts",
25
+ "packages/plans/index.ts",
27
26
  "packages/plans/types.ts",
28
27
  "packages/theme/src/app.svelte",
29
28
  "packages/shared/dist",
@@ -36,15 +35,15 @@
36
35
  },
37
36
  "./mux": {
38
37
  "types": "./packages/mux/types.ts",
39
- "import": "./packages/mux/src/app.svelte"
38
+ "import": "./packages/mux/types.ts"
40
39
  },
41
40
  "./heading": {
42
41
  "types": "./packages/heading/types.ts",
43
- "import": "./packages/heading/src/app.svelte"
42
+ "import": "./packages/heading/types.ts"
44
43
  },
45
44
  "./plans": {
46
- "types": "./packages/plans/types.ts",
47
- "import": "./packages/plans/types.ts"
45
+ "types": "./packages/plans/index.ts",
46
+ "import": "./packages/plans/index.ts"
48
47
  },
49
48
  "./shared": {
50
49
  "types": "./packages/shared/dist/index.d.ts",
@@ -0,0 +1 @@
1
+ export type { Types, Type, Floor, PlotDimension, Room, UUID } from './types.js'
@@ -1,57 +0,0 @@
1
- <script lang="ts">
2
- import { createFieldPlugin, type FieldPluginResponse } from '@storyblok/field-plugin'
3
- import { onMount } from 'svelte'
4
- import { Input } from 'shared'
5
- import type { Heading } from '../types.js'
6
-
7
- type Plugin = FieldPluginResponse<Heading | null>
8
-
9
- let plugin: Plugin | null = $state(null)
10
- let content: Heading = $state({ text: '', level: 1 })
11
-
12
- onMount(() => {
13
- createFieldPlugin<Heading>({
14
- validateContent: (content) => {
15
- if (typeof content !== 'object' || content === null) {
16
- return { content: { text: '', level: 1 } }
17
- }
18
-
19
- const validated = content as Heading
20
-
21
- // ensure level is one of the allowed values
22
- if (![1, 2, 3, 4, 5, 6].includes(validated.level)) {
23
- validated.level = 1
24
- }
25
-
26
- // ensure text is a string
27
- if (typeof validated.text !== 'string') validated.text = ''
28
-
29
- return { content: validated }
30
- },
31
- onUpdateState: (state) => {
32
- plugin = state as Plugin
33
- },
34
- })
35
- })
36
-
37
- function update() {
38
- if (plugin?.type !== 'loaded') return
39
- const state = $state.snapshot(content)
40
- plugin.actions.setContent(state)
41
- }
42
- </script>
43
-
44
- {#if plugin?.type === 'loaded'}
45
- <fieldset class="flex gap-2">
46
- <Input class="flex-1" bind:value={content.text} oninput={update} />
47
- <select
48
- class="rounded outline-none focus-visible:border-ring border-input border bg-input-background min-h-11.5"
49
- bind:value={() => content.level, (v) => (content.level = Number(v) as Heading['level'])}
50
- onchange={update}
51
- >
52
- {#each [1, 2, 3, 4, 5, 6] as level}
53
- <option value={level}>H{level}</option>
54
- {/each}
55
- </select>
56
- </fieldset>
57
- {/if}
@@ -1,551 +0,0 @@
1
- <script lang="ts">
2
- import { createFieldPlugin, type FieldPluginResponse } from '@storyblok/field-plugin'
3
- import { onMount } from 'svelte'
4
- import { flip } from 'svelte/animate'
5
- import { fly, fade, slide } from 'svelte/transition'
6
- import Mux from '@mux/mux-node'
7
- import { format_date, format_elapse } from 'kitto'
8
- import {
9
- VideoIcon,
10
- VideoOffIcon,
11
- HourglassIcon,
12
- Trash2Icon,
13
- RefreshCwIcon,
14
- CheckCircle2Icon,
15
- EllipsisIcon,
16
- Settings2Icon,
17
- } from '@lucide/svelte'
18
- import '@mux/mux-uploader'
19
- import { cn } from 'shared/utils'
20
- import { Input, Label, Skeleton, Switch } from 'shared'
21
- import type { Video } from '../types.js'
22
-
23
- type MuxAsset = Mux.Video.Assets.Asset
24
- type Plugin = FieldPluginResponse<Video | null>
25
-
26
- let content: Video | null = $state(null)
27
- let plugin: Plugin | null = $state(null)
28
- let assets: Array<MuxAsset> | null = $state(null)
29
- let open_actions: string | null = $state(null)
30
- let timeout: NodeJS.Timeout | null = $state(null)
31
- let video_options_open = $state(false)
32
-
33
- const is_mux_poster = $derived.by(() => content?.poster?.startsWith('https://image.mux.com/'))
34
- const poster = $derived.by(() => {
35
- if (is_mux_poster) return `${content?.poster}?width=558&height=314&fit_mode=smartcrop`
36
- if (content?.poster?.endsWith('.svg')) return content.poster
37
- return `${content?.poster}/m/558x314/smart`
38
- })
39
-
40
- onMount(() => {
41
- createFieldPlugin<Video>({
42
- enablePortalModal: true,
43
- onUpdateState: (state) => {
44
- plugin = state as Plugin
45
- },
46
- })
47
- })
48
-
49
- const mux = $derived.by(() => {
50
- if (plugin?.type !== 'loaded' || !plugin.data.options.MOXY_SECRET_ID) return null
51
-
52
- return new Mux({
53
- baseURL: 'https://moxy.admin-54b.workers.dev/api/',
54
- tokenId: '',
55
- tokenSecret: '',
56
- defaultHeaders: {
57
- authorization: `Bearer ${plugin.data.options.MOXY_SECRET_ID}`,
58
- },
59
- })
60
- })
61
-
62
- function get_poster(video: MuxAsset) {
63
- const playback_id = video.playback_ids?.[0]?.id
64
- return playback_id ? `https://image.mux.com/${playback_id}/thumbnail.jpg` : undefined
65
- }
66
-
67
- const actions = {
68
- update(c?: Partial<Video>) {
69
- if (plugin?.type !== 'loaded') return
70
- const state = $state.snapshot({ ...content, ...c })
71
- plugin.actions.setContent(state)
72
- },
73
- set_video(video: MuxAsset | null) {
74
- if (!video) {
75
- content = null
76
- actions.update()
77
- return
78
- }
79
-
80
- const playback_id = video.playback_ids?.[0]?.id
81
-
82
- content = {
83
- ...content,
84
- playback_id,
85
- title: video.meta?.title,
86
- m3u8_url: playback_id ? `https://stream.mux.com/${playback_id}.m3u8` : undefined,
87
- poster: get_poster(video),
88
- mux_video: video,
89
- }
90
-
91
- actions.update()
92
- },
93
- async list() {
94
- if (!mux) throw new Error('Mux not initialised')
95
-
96
- assets = (await mux.video.assets.list()).data
97
- },
98
- async delete(id: string) {
99
- if (plugin?.type !== 'loaded' || !plugin.data.options.MOXY_SECRET_ID || !mux)
100
- throw new Error('Mux not initialised')
101
-
102
- const confirm = window.confirm('Are you sure you want to delete this video?')
103
-
104
- if (!confirm) return
105
-
106
- // Update local state, for smooth transition
107
- if (assets?.length) assets = assets.filter((asset) => asset.id !== id)
108
-
109
- // Delete the video
110
- await mux.video.assets.delete(id)
111
-
112
- // Update content if the deleted video was the current one
113
- if (content?.mux_video?.id === id) actions.set_video(null)
114
-
115
- // Refresh the list
116
- await actions.list()
117
- },
118
- async get_upload_endpoint() {
119
- if (!mux) throw new Error('Mux not initialised')
120
-
121
- return (
122
- await mux.video.uploads.create({
123
- cors_origin: 'https://storyblok.com',
124
- new_asset_settings: {
125
- playback_policy: ['public'],
126
- encoding_tier: 'baseline',
127
- },
128
- })
129
- ).url
130
- },
131
- toggle_actions(id: string) {
132
- open_actions = open_actions === id ? null : id
133
- },
134
- async set_title(title: string, id: string) {
135
- if (timeout) clearTimeout(timeout)
136
-
137
- actions.update({ title })
138
-
139
- timeout = setTimeout(async () => {
140
- if (!mux) return
141
- await mux.video.assets.update(id, {
142
- meta: {
143
- title,
144
- },
145
- })
146
- }, 1000)
147
- },
148
- async select_poster() {
149
- if (!content?.mux_video) return
150
- const asset = await plugin?.actions?.selectAsset()
151
-
152
- if (!asset) actions.update({ poster: get_poster(content.mux_video) })
153
- else actions.update({ poster: asset.filename })
154
- },
155
- async delete_poster() {
156
- if (!content?.mux_video) return
157
- actions.update({ poster: get_poster(content.mux_video) })
158
- },
159
- }
160
-
161
- function format_duration(seconds: number): string {
162
- const hours = Math.floor(seconds / 3600)
163
- const minutes = Math.floor((seconds % 3600) / 60)
164
- const remaining_seconds = Math.floor(seconds % 60)
165
-
166
- if (hours > 0) {
167
- return `${hours}:${minutes.toString().padStart(2, '0')}:${remaining_seconds.toString().padStart(2, '0')}`
168
- }
169
-
170
- return `${minutes}:${remaining_seconds.toString().padStart(2, '0')}`
171
- }
172
-
173
- function date(date: string): string {
174
- const d = new Date(Number(date) * 1000)
175
- return format_elapse(d) || format_date('{MMM} {D}, {YYYY}', d)
176
- }
177
-
178
- const is_modal_open = $derived.by(() => plugin?.type === 'loaded' && plugin.data.isModalOpen)
179
-
180
- $effect(() => {
181
- document.documentElement.setAttribute('data-modal-open', is_modal_open ? 'true' : 'false')
182
- })
183
-
184
- const actions_menu_classes = `
185
- absolute
186
- right-4
187
- top-4
188
- flex
189
- items-center
190
- bg-card
191
- text-card-foreground
192
- border
193
- rounded
194
- divide-x
195
- opacity-0
196
- pointer-events-none
197
- -translate-y-10
198
- transition-[translate,opacity]
199
- overflow-hidden
200
-
201
- [&>li]:flex
202
- [&_button]:p-2
203
- [&_button]:hover:bg-muted
204
- [&_button]:outline-none
205
- [&_button]:focus:bg-muted
206
-
207
- group-hover:opacity-100
208
- group-hover:pointer-events-auto
209
- group-hover:translate-y-0
210
-
211
- group-focus-within:opacity-100
212
- group-focus-within:pointer-events-auto
213
- group-focus-within:translate-y-0
214
- `
215
- </script>
216
-
217
- <svelte:window
218
- onclick={(event) => {
219
- if (!(event.target instanceof HTMLElement)) return
220
-
221
- // reset target when we click outside of actions
222
- if (event.target.closest('.actions')) return
223
-
224
- open_actions = null
225
- }}
226
- />
227
-
228
- {#snippet skeleton()}
229
- <ol class="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-2">
230
- {#each Array(12)}
231
- <li>
232
- <div class="p-3 grid gap-2">
233
- <Skeleton class="aspect-video size-full" />
234
- <div class="flex gap-2 justify-between">
235
- <Skeleton class="w-24 h-4" />
236
- <Skeleton class="w-16 h-4" />
237
- </div>
238
- </div>
239
- </li>
240
- {/each}
241
- </ol>
242
- {/snippet}
243
-
244
- {#snippet asset_preview(video?: MuxAsset)}
245
- {@const playback_id = video?.playback_ids?.[0]?.id}
246
- {@const is_selected = content?.mux_video?.id === video?.id}
247
-
248
- <figure
249
- class={cn([
250
- 'flex items-center justify-center [&>svg]:absolute [&>svg]:size-5 bg-muted text-muted-foreground rounded w-full aspect-video relative border',
251
- { 'bg-primary [&>svg]:text-primary-foreground': video && is_selected },
252
- ])}
253
- >
254
- {#if playback_id && video.status === 'ready'}
255
- <img
256
- class={['rounded shrink-0 size-full absolute cover', { 'opacity-50': is_selected }]}
257
- src="https://image.mux.com/{playback_id}/thumbnail.jpg?width=240&height=135&fit_mode=smartcrop"
258
- alt={video.meta?.title}
259
- />
260
- <img
261
- class="rounded shrink-0 size-full absolute cover hover:opacity-100 transition-opacity opacity-0"
262
- src="https://image.mux.com/{playback_id}/animated.gif?width=240&height=135&fit_mode=smartcrop"
263
- alt={video.meta?.title}
264
- />
265
- {/if}
266
- {#if !video}
267
- <VideoIcon />
268
- {:else if video.status === 'errored'}
269
- <VideoOffIcon />
270
- {:else if video.status === 'preparing'}
271
- <HourglassIcon />
272
- {:else if is_selected}
273
- <CheckCircle2Icon />
274
- {/if}
275
- </figure>
276
- {/snippet}
277
-
278
- {#snippet asset_meta()}
279
- <p class="font-medium truncate">
280
- {content?.title}
281
- </p>
282
- <span class="justify-between flex text-muted-foreground text-xs">
283
- {#if content?.mux_video?.status === 'errored'}
284
- {content.mux_video.errors?.messages}
285
- {:else if content?.mux_video?.status === 'preparing'}
286
- Preparing...
287
- {:else if content?.mux_video?.duration}
288
- <span>
289
- {format_duration(content.mux_video.duration)}
290
- </span>
291
- <time datetime={content.mux_video.created_at}>
292
- {date(content.mux_video.created_at)}
293
- </time>
294
- {/if}
295
- </span>
296
- {/snippet}
297
-
298
- {#if plugin?.type === 'loaded' && mux}
299
- {#if plugin.data.isModalOpen}
300
- <div class="p-8 grid gap-8">
301
- <mux-uploader onsuccess={actions.list} endpoint={actions.get_upload_endpoint}></mux-uploader>
302
- {#await actions.list()}
303
- {@render skeleton()}
304
- {:then}
305
- {#if !assets || assets.length === 0}
306
- <p class="text-muted-foreground">No videos found</p>
307
- {:else}
308
- <ol class="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-2">
309
- {#each assets as video (video.id)}
310
- {@const actions_open = open_actions === video.id}
311
- <li
312
- class="relative group size-full"
313
- class:active={actions_open}
314
- animate:flip={{ duration: 300 }}
315
- in:fly={{ y: 20, duration: 300, delay: 100 }}
316
- out:fade={{ duration: 200 }}
317
- >
318
- {#if video.id}
319
- <button
320
- class="
321
- absolute
322
- top-6
323
- right-6
324
- bg-muted
325
- text-muted-foreground
326
- rounded
327
- p-1
328
- z-10
329
-
330
- opacity-0
331
- pointer-events-none
332
- -translate-y-10
333
- transition-[translate,opacity]
334
-
335
- group-hover:opacity-100
336
- group-hover:pointer-events-auto
337
- group-hover:translate-y-0
338
-
339
- group-focus-within:opacity-100
340
- group-focus-within:pointer-events-auto
341
- group-focus-within:translate-y-0
342
- "
343
- onclick={() => actions.toggle_actions(video.id)}
344
- >
345
- <EllipsisIcon size={18} />
346
- </button>
347
- {#if actions_open}
348
- <ol
349
- class="absolute top-14 w-full bg-card text-card-foreground rounded shadow-md z-10 border"
350
- transition:fly={{ y: 20, duration: 300, delay: 100 }}
351
- >
352
- <li>
353
- <button
354
- class="p-3 w-full text-start hover:bg-muted transition-colors"
355
- onclick={() => {
356
- plugin?.actions?.setModalOpen(false)
357
- actions.set_video(video)
358
- }}
359
- >
360
- Select
361
- </button>
362
- </li>
363
- <li>
364
- <button
365
- class="p-3 w-full text-start hover:bg-muted transition-colors"
366
- onclick={() => actions.delete(video.id)}
367
- >
368
- Delete
369
- </button>
370
- </li>
371
- </ol>
372
- {/if}
373
- {/if}
374
- <div class="grid gap-1 rounded hover:bg-muted transition-colors p-3 w-full">
375
- <button
376
- onclick={() => {
377
- actions.set_video(video)
378
- plugin?.actions?.setModalOpen(false)
379
- }}
380
- >
381
- {@render asset_preview(video)}
382
- </button>
383
- {@render asset_meta()}
384
- </div>
385
- </li>
386
- {/each}
387
- </ol>
388
- {/if}
389
- {:catch}
390
- An error occurred while loading videos.
391
- {/await}
392
- </div>
393
- {:else if content?.mux_video}
394
- <div
395
- class="p-4 grid grid-cols-[140px_1fr] w-full border border rounded hover:border-primary transition-colors bg-card text-card-foreground items-center gap-x-5 group"
396
- >
397
- {@render asset_preview(content.mux_video)}
398
- <div class="grid">{@render asset_meta()}</div>
399
- <ul class={actions_menu_classes}>
400
- <li>
401
- <button
402
- onclick={() => plugin?.actions?.setModalOpen(true)}
403
- title="Replace video"
404
- aria-label="Replace video"
405
- >
406
- <RefreshCwIcon size={18} />
407
- </button>
408
- </li>
409
- <li>
410
- <button
411
- title="Settings"
412
- aria-label="Settings"
413
- onclick={() => (video_options_open = !video_options_open)}
414
- >
415
- <Settings2Icon size={18} />
416
- </button>
417
- </li>
418
- <li>
419
- <button
420
- onclick={() => actions.set_video(null)}
421
- title="Remove video"
422
- aria-label="Remove video"
423
- >
424
- <Trash2Icon size={18} />
425
- </button>
426
- </li>
427
- </ul>
428
- {#if video_options_open}
429
- <div class="grid gap-5 mt-5 col-span-full" transition:slide={{ duration: 300 }}>
430
- <div class="grid gap-2">
431
- <Label for="title">Title</Label>
432
- <Input
433
- id="title"
434
- placeholder="Video title…"
435
- value={content.mux_video.meta?.title}
436
- oninput={(event) => {
437
- if (!event.target || !(event.target instanceof HTMLInputElement)) return
438
- if (!content?.mux_video) return
439
-
440
- actions.set_title(event.target.value, content.mux_video.id)
441
- }}
442
- />
443
- </div>
444
- <div class="flex items-center space-x-2">
445
- <Switch
446
- id="autoplay"
447
- checked={content.autoplay}
448
- onCheckedChange={(autoplay) => actions.update({ autoplay })}
449
- />
450
- <Label for="autoplay">Autoplay</Label>
451
- </div>
452
- <div class="flex items-center space-x-2">
453
- <Switch
454
- id="playsinline"
455
- checked={content.playsinline}
456
- onCheckedChange={(playsinline) => actions.update({ playsinline })}
457
- />
458
- <Label for="playsinline">Playsinline</Label>
459
- </div>
460
- <div class="flex items-center space-x-2">
461
- <Switch
462
- id="muted"
463
- checked={content.muted}
464
- onCheckedChange={(muted) => actions.update({ muted })}
465
- />
466
- <Label for="muted">Muted</Label>
467
- </div>
468
- <div class="flex items-center space-x-2">
469
- <Switch
470
- id="loop"
471
- checked={content.loop}
472
- onCheckedChange={(loop) => actions.update({ loop })}
473
- />
474
- <Label for="loop">Loop</Label>
475
- </div>
476
- <div class="grid gap-2">
477
- <Label for="preload">Preload</Label>
478
- <select
479
- class="bg-background border rounded min-h-11.5 outline-none focus-visible:border-ring transition-colors w-full"
480
- bind:value={content.preload}
481
- onchange={(event) => {
482
- if (!event.target || !(event.target instanceof HTMLSelectElement)) return
483
- actions.update({ preload: event.target.value as 'auto' | 'metadata' | 'none' })
484
- }}
485
- id="preload"
486
- >
487
- <option disabled selected value={undefined}>Select preload…</option>
488
- <option value="auto">Auto</option>
489
- <option value="metadata">Metadata (default)</option>
490
- <option value="none">None</option>
491
- </select>
492
- <p class="text-muted-foreground text-xs">
493
- Note: The preload attribute is ignored if autoplay is present.
494
- </p>
495
- </div>
496
- <div class="grid gap-2">
497
- <Label for="poster">Poster</Label>
498
- <figure class="relative rounded overflow-hidden border">
499
- <ul class={actions_menu_classes}>
500
- <li>
501
- <button
502
- onclick={actions.select_poster}
503
- title="Select poster"
504
- aria-label="Select poster"
505
- >
506
- <RefreshCwIcon size={18} />
507
- </button>
508
- </li>
509
- {#if !is_mux_poster}
510
- <li>
511
- <button
512
- onclick={actions.delete_poster}
513
- title="Delete poster"
514
- aria-label="Delete poster"
515
- >
516
- <Trash2Icon size={18} />
517
- </button>
518
- </li>
519
- {/if}
520
- </ul>
521
- <img
522
- class="shrink-0 aspect-video object-contain"
523
- src={poster}
524
- width={558}
525
- height={314}
526
- alt={content.mux_video.meta?.title}
527
- />
528
- {#if is_mux_poster}
529
- <figcaption
530
- class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black to-transparent p-4 h-20 flex items-end text-muted-foreground text-xs"
531
- >
532
- Auto-generated poster from video source
533
- </figcaption>
534
- {/if}
535
- </figure>
536
- </div>
537
- </div>
538
- {/if}
539
- </div>
540
- {:else}
541
- <button
542
- class="p-4 grid grid-cols-[140px_1fr] w-full border border-input rounded hover:border-primary transition-colors bg-input-background text-card-foreground items-center justify-items-start gap-5 font-medium"
543
- onclick={() => plugin?.actions?.setModalOpen(true)}
544
- title="Add video"
545
- aria-label="Add video"
546
- >
547
- {@render asset_preview()}
548
- + Add Video
549
- </button>
550
- {/if}
551
- {/if}