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