@hyvor/design 2.1.12 → 2.1.13

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.
@@ -0,0 +1,78 @@
1
+ <script lang="ts">
2
+ import IconArrowRightCircle from '@hyvor/icons/IconArrowRightCircle';
3
+ import Button from '../../Button/Button.svelte';
4
+ import toast from '../../Toast/toast.js';
5
+ import ExcalidrawComponent from './ExcalidrawComponent.svelte';
6
+ import type { LoadedExcalidraw } from './excalidraw-loader.js';
7
+ import { setSelectedFile } from '../file-uploader.js';
8
+
9
+ let loaded: LoadedExcalidraw | undefined;
10
+ let excalidrawAPI: any;
11
+
12
+ function handleReady(loadedLib: LoadedExcalidraw, api: any) {
13
+ loaded = loadedLib;
14
+ excalidrawAPI = api;
15
+ }
16
+
17
+ async function handleFinish() {
18
+ if (!loaded || !excalidrawAPI) {
19
+ toast.error('Excalidraw is not ready yet');
20
+ return;
21
+ }
22
+
23
+ const elements = excalidrawAPI.getSceneElements();
24
+ if (!elements || !elements.length) {
25
+ toast.error('Draw something first');
26
+ return;
27
+ }
28
+
29
+ const svg = await loaded.ExcalidrawLib.exportToSvg({
30
+ elements,
31
+ appState: excalidrawAPI.getAppState(),
32
+ files: excalidrawAPI.getFiles()
33
+ });
34
+
35
+ const data = new XMLSerializer().serializeToString(svg);
36
+ const blob = new Blob([data], { type: 'image/svg+xml;charset=utf-8' });
37
+
38
+ setSelectedFile({
39
+ type: 'image',
40
+ from: 'excalidraw',
41
+ upload: {
42
+ type: 'excalidraw',
43
+ blob
44
+ }
45
+ });
46
+ }
47
+ </script>
48
+
49
+ <div class="excalidraw-tab">
50
+ <div class="canvas">
51
+ <ExcalidrawComponent onReady={handleReady} />
52
+ </div>
53
+ <div class="footer">
54
+ <Button size="large" onclick={handleFinish}>
55
+ Finalize
56
+ {#snippet end()}
57
+ <IconArrowRightCircle />
58
+ {/snippet}
59
+ </Button>
60
+ </div>
61
+ </div>
62
+
63
+ <style>
64
+ .excalidraw-tab {
65
+ height: 100%;
66
+ display: flex;
67
+ flex-direction: column;
68
+ }
69
+ .canvas {
70
+ flex: 1;
71
+ min-height: 0;
72
+ }
73
+ .footer {
74
+ padding-top: 15px;
75
+ margin-bottom: 10px;
76
+ text-align: center;
77
+ }
78
+ </style>
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const Excalidraw: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type Excalidraw = InstanceType<typeof Excalidraw>;
18
+ export default Excalidraw;
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import { onDestroy, onMount } from 'svelte';
3
+ import Loader from '../../Loader/Loader.svelte';
4
+ import { loadExcalidraw, type LoadedExcalidraw } from './excalidraw-loader.js';
5
+
6
+ interface Props {
7
+ onReady: (loaded: LoadedExcalidraw, api: any) => void;
8
+ }
9
+
10
+ let { onReady }: Props = $props();
11
+
12
+ let mountEl: HTMLDivElement | undefined = $state();
13
+ let ready = $state(false);
14
+ let error = $state('');
15
+
16
+ let root: ReturnType<LoadedExcalidraw['createRoot']> | undefined;
17
+
18
+ onMount(async () => {
19
+ try {
20
+ const loaded = await loadExcalidraw();
21
+ if (!mountEl) return;
22
+
23
+ root = loaded.createRoot(mountEl);
24
+ root.render(
25
+ loaded.React.createElement(loaded.ExcalidrawLib.Excalidraw, {
26
+ excalidrawAPI: (api: any) => onReady(loaded, api)
27
+ })
28
+ );
29
+ ready = true;
30
+ } catch (e: any) {
31
+ error = e?.message || 'Failed to load Excalidraw';
32
+ }
33
+ });
34
+
35
+ onDestroy(() => {
36
+ root?.unmount();
37
+ });
38
+ </script>
39
+
40
+ <div class="excalidraw-wrap">
41
+ {#if !ready}
42
+ <div class="loading">
43
+ {#if error}
44
+ {error}
45
+ {:else}
46
+ <Loader full />
47
+ {/if}
48
+ </div>
49
+ {/if}
50
+ <div class="mount" bind:this={mountEl}></div>
51
+ </div>
52
+
53
+ <style>
54
+ .excalidraw-wrap {
55
+ position: relative;
56
+ height: 100%;
57
+ width: 100%;
58
+ }
59
+ .loading {
60
+ position: absolute;
61
+ inset: 0;
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ }
66
+ .mount {
67
+ height: 100%;
68
+ width: 100%;
69
+ }
70
+ </style>
@@ -0,0 +1,7 @@
1
+ import { type LoadedExcalidraw } from './excalidraw-loader.js';
2
+ interface Props {
3
+ onReady: (loaded: LoadedExcalidraw, api: any) => void;
4
+ }
5
+ declare const ExcalidrawComponent: import("svelte").Component<Props, {}, "">;
6
+ type ExcalidrawComponent = ReturnType<typeof ExcalidrawComponent>;
7
+ export default ExcalidrawComponent;
@@ -0,0 +1,21 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
2
+ <rect width="1000" height="1000" rx="200" ry="200" fill="#fff" />
3
+ <svg
4
+ viewBox="0 0 107 101"
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ xmlns:xlink="http://www.w3.org/1999/xlink"
7
+ xml:space="preserve"
8
+ style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"
9
+ >
10
+ <path
11
+ style="fill:none"
12
+ d="M24 17h121v121H24z"
13
+ transform="matrix(.8843 0 0 .83471 -21.223 -14.19)"
14
+ />
15
+ <path
16
+ d="M119.81 105.98a.549.549 0 0 0-.53-.12c-4.19-6.19-9.52-12.06-14.68-17.73l-.85-.93c0-.11-.05-.21-.12-.3a.548.548 0 0 0-.34-.2l-.17-.18-.12-.09c-.15-.32-.53-.56-.95-.35-1.58.81-3 1.97-4.4 3.04-1.87 1.43-3.7 2.92-5.42 4.52-.7.65-1.39 1.33-1.97 2.09-.28.37-.07.72.27.87-1.22 1.2-2.45 2.45-3.68 3.74-.11.12-.17.28-.16.44.01.16.09.31.22.41l2.16 1.65s.01.03.03.04c3.09 3.05 8.51 7.28 14.25 11.76.85.67 1.71 1.34 2.57 2.01.39.47.76.94 1.12 1.4.19.25.55.3.8.11.13.1.26.21.39.31a.57.57 0 0 0 .8-.1c.07-.09.1-.2.11-.31.04 0 .07.03.1.03.15 0 .31-.06.42-.18l10.18-11.12a.56.56 0 0 0-.04-.8l.01-.01Zm-29.23-3.85c.07.09.14.17.21.25 1.16.98 2.4 2.04 3.66 3.12l-5.12-3.91s-.32-.22-.52-.36c-.11-.08-.21-.16-.31-.24l-.38-.32s.07-.07.1-.11l.35-.35c1.72-1.74 4.67-4.64 6.19-6.06-1.61 1.62-4.87 6.37-4.17 7.98h-.01Zm17.53 13.81-4.22-3.22c-1.65-1.71-3.43-3.4-5.24-5.03 2.28 1.76 4.23 3.25 4.52 3.51 2.21 1.97 2.11 1.61 3.63 2.91l1.83 1.33c-.18.16-.36.33-.53.49l.01.01Zm1.06.81-.08-.06c.16-.13.33-.25.49-.38l-.4.44h-.01ZM42.24 51.45c.14.72.27 1.43.4 2.11.69 3.7 1.33 7.03 2.55 9.56l.48 1.92c.19.73.46 1.64.71 1.83 2.85 2.52 7.22 6.28 11.89 9.82.21.16.5.15.7-.01.01.02.03.03.04.04.11.1.24.15.38.15.16 0 .31-.06.42-.19 5.98-6.65 10.43-12.12 13.6-16.7.2-.25.3-.54.29-.84.2-.24.41-.48.6-.68a.558.558 0 0 0-.1-.86.578.578 0 0 0-.17-.36c-1.39-1.34-2.42-2.31-3.46-3.28-1.84-1.72-3.74-3.5-7.77-7.51-.02-.02-.05-.04-.07-.06a.555.555 0 0 0-.22-.14c-1.11-.39-3.39-.78-6.26-1.28-4.22-.72-10-1.72-15.2-3.27h-.04v-.01s-.02 0-.03.02h-.01l.04-.02s-.31.01-.37.04c-.08.04-.14.09-.19.15-.05.06-.09.12-.47.2-.38.08.08 0 .11 0h-.11v.03c.07.34.05.58.16.97-.02.1.21 1.02.24 1.11l1.83 7.26h.03Zm30.95 6.54s-.03.04-.04.05l-.64-.71c.22.21.44.42.68.66Zm-7.09 9.39s-.07.08-.1.12l-.02-.02c.04-.03.08-.07.13-.1h-.01Zm-7.07 8.47Zm3.02-28.57c.35.35 1.74 1.65 2.06 1.97-1.45-.66-5.06-2.34-6.74-2.88 1.65.29 3.93.66 4.68.91Zm-19.18-2.77c.84 1.44 1.5 6.49 2.16 11.4-.37-1.58-.69-3.12-.99-4.6-.52-2.56-1-4.85-1.67-6.88.14.01.31.03.49.05 0 .01 0 .02.02.03h-.01Zm-.29-1.21c-.23-.02-.44-.04-.62-.05-.02-.04-.03-.08-.04-.12l.66.18v-.01Zm-2.22.45v-.02.02ZM118.9 42.57c.04-.23-1.1-1.24-.74-1.26.85-.04.86-1.35 0-1.31-1.13.06-2.27.32-3.37.53-1.98.37-3.95.78-5.92 1.21-4.39.94-8.77 1.93-13.1 3.11-1.36.37-2.86.7-4.11 1.36-.42.22-.4.67-.17.95-.09.05-.18.08-.28.09-.37.07-.74.13-1.11.19a.566.566 0 0 0-.39.86c-2.32 3.1-4.96 6.44-7.82 9.95-2.81 3.21-5.73 6.63-8.72 10.14-9.41 11.06-20.08 23.6-31.9 34.64-.23.21-.24.57-.03.8.05.06.12.1.19.13-.16.15-.32.3-.48.44-.1.09-.14.2-.16.32-.08.08-.16.17-.23.25-.21.23-.2.59.03.8.23.21.59.2.8-.03.04-.04.08-.09.12-.13a.84.84 0 0 1 1.22 0c.69.74 1.34 1.44 1.95 2.09l-1.38-1.15a.57.57 0 0 0-.8.07c-.2.24-.17.6.07.8l14.82 12.43c.11.09.24.13.37.13.15 0 .29-.06.4-.17l.36-.36a.56.56 0 0 0 .63-.12c20.09-20.18 36.27-35.43 54.8-49.06.17-.12.25-.32.23-.51a.57.57 0 0 0 .48-.39c3.42-10.46 4.08-19.72 4.28-24.27 0-.03.01-.05.02-.07.02-.05.03-.1.04-.14.03-.11.05-.19.05-.19.26-.78.17-1.53-.15-2.15v.02ZM82.98 58.94c.9-1.03 1.79-2.04 2.67-3.02-5.76 7.58-15.3 19.26-28.81 33.14 9.2-10.18 18.47-20.73 26.14-30.12Zm-32.55 52.81-.03-.03c.11.02.19.04.2.04a.47.47 0 0 0-.17 0v-.01Zm6.9 6.42-.05-.04.03-.03c.02 0 .03.02.04.02 0 .02-.02.03-.03.05h.01Zm8.36-7.21 1.38-1.44c.01.01.02.03.03.05-.47.46-.94.93-1.42 1.39h.01Zm2.24-2.21c.26-.3.56-.65.87-1.02.01-.01.02-.03.04-.04 3.29-3.39 6.68-6.82 10.18-10.25.02-.02.05-.04.07-.06.86-.66 1.82-1.39 2.72-2.08-4.52 4.32-9.11 8.78-13.88 13.46v-.01Zm21.65-55.88c-1.86 2.42-3.9 5.56-5.63 8.07-5.46 7.91-23.04 27.28-23.43 27.65-2.71 2.62-10.88 10.46-16.09 15.37-.14.13-.25.24-.34.35a.794.794 0 0 1 .03-1.13c24.82-23.4 39.88-42.89 46-51.38-.13.33-.24.69-.55 1.09l.01-.02Zm16.51 7.1-.01.02c0-.02-.02-.07.01-.02Zm-.91-5.13Zm-5.89 9.45c-2.26-1.31-3.32-3.27-2.71-5.25l.19-.66c.08-.19.17-.38.28-.57.59-.98 1.49-1.85 2.52-2.36.05-.02.1-.03.15-.04a.795.795 0 0 1-.04-.43c.05-.31.25-.58.66-.58.67 0 2.75.62 3.54 1.3.24.19.47.4.68.63.3.35.74.92.96 1.33.13.06.23.62.38.91.14.46.2.93.18 1.4 0 .02 0 .02.01.03-.03.07 0 .37-.04.4-.1.72-.36 1.43-.75 2.05-.04.05-.07.11-.11.16 0 .01-.02.02-.03.04-.3.43-.65.83-1.08 1.13-1.26.89-2.73 1.16-4.2.79a6.33 6.33 0 0 1-.57-.25l-.02-.03Zm16.27-1.63c-.49 2.05-1.09 4.19-1.8 6.38-.03.08-.03.16-.03.23-.1.01-.19.05-.27.11-4.44 3.26-8.73 6.62-12.98 10.11 3.67-3.32 7.39-6.62 11.23-9.95a6.409 6.409 0 0 0 2.11-3.74l.56-3.37.03-.1c.25-.71 1.34-.4 1.17.33h-.02Z"
17
+ style="fill:currentColor;fill-rule:nonzero"
18
+ transform="matrix(1 0 0 1 -26.41 -29.49)"
19
+ />
20
+ </svg>
21
+ </svg>
@@ -0,0 +1,26 @@
1
+ export default ExcalidrawIcon;
2
+ type ExcalidrawIcon = SvelteComponent<{
3
+ [x: string]: never;
4
+ }, {
5
+ [evt: string]: CustomEvent<any>;
6
+ }, {}> & {
7
+ $$bindings?: string | undefined;
8
+ };
9
+ declare const ExcalidrawIcon: $$__sveltets_2_IsomorphicComponent<{
10
+ [x: string]: never;
11
+ }, {
12
+ [evt: string]: CustomEvent<any>;
13
+ }, {}, {}, string>;
14
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
15
+ new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
16
+ $$bindings?: Bindings;
17
+ } & Exports;
18
+ (internal: unknown, props: {
19
+ $$events?: Events;
20
+ $$slots?: Slots;
21
+ }): Exports & {
22
+ $set?: any;
23
+ $on?: any;
24
+ };
25
+ z_$$bindings?: Bindings;
26
+ }
@@ -0,0 +1,13 @@
1
+ export interface LoadedExcalidraw {
2
+ React: any;
3
+ createRoot: (container: Element) => {
4
+ render: (node: any) => void;
5
+ unmount: () => void;
6
+ };
7
+ ExcalidrawLib: any;
8
+ }
9
+ /**
10
+ * Loads Excalidraw directly in the browser, on demand, with no installed dependency.
11
+ * See https://docs.excalidraw.com/docs/@excalidraw/excalidraw/integration#browser
12
+ */
13
+ export declare function loadExcalidraw(): Promise<LoadedExcalidraw>;
@@ -0,0 +1,36 @@
1
+ const EXCALIDRAW_VERSION = '0.18.0';
2
+ const REACT_VERSION = '19.0.0';
3
+ const REACT_URL = `https://esm.sh/react@${REACT_VERSION}`;
4
+ const REACT_DOM_CLIENT_URL = `https://esm.sh/react-dom@${REACT_VERSION}/client`;
5
+ const EXCALIDRAW_ASSET_PATH = `https://esm.sh/@excalidraw/excalidraw@${EXCALIDRAW_VERSION}/dist/prod/`;
6
+ const EXCALIDRAW_JS_URL = `${EXCALIDRAW_ASSET_PATH}index.js?deps=react@${REACT_VERSION},react-dom@${REACT_VERSION}`;
7
+ const EXCALIDRAW_CSS_URL = `${EXCALIDRAW_ASSET_PATH}index.css`;
8
+ let loadPromise = null;
9
+ function ensureStylesheet() {
10
+ if (document.querySelector(`link[href="${EXCALIDRAW_CSS_URL}"]`))
11
+ return;
12
+ const link = document.createElement('link');
13
+ link.rel = 'stylesheet';
14
+ link.href = EXCALIDRAW_CSS_URL;
15
+ document.head.appendChild(link);
16
+ }
17
+ /**
18
+ * Loads Excalidraw directly in the browser, on demand, with no installed dependency.
19
+ * See https://docs.excalidraw.com/docs/@excalidraw/excalidraw/integration#browser
20
+ */
21
+ export function loadExcalidraw() {
22
+ if (loadPromise)
23
+ return loadPromise;
24
+ window.EXCALIDRAW_ASSET_PATH = EXCALIDRAW_ASSET_PATH;
25
+ ensureStylesheet();
26
+ loadPromise = Promise.all([
27
+ import(/* @vite-ignore */ REACT_URL),
28
+ import(/* @vite-ignore */ REACT_DOM_CLIENT_URL),
29
+ import(/* @vite-ignore */ EXCALIDRAW_JS_URL)
30
+ ]).then(([ReactModule, ReactDOMClientModule, ExcalidrawLib]) => ({
31
+ React: ReactModule.default ?? ReactModule,
32
+ createRoot: ReactDOMClientModule.createRoot,
33
+ ExcalidrawLib
34
+ }));
35
+ return loadPromise;
36
+ }
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import IconCaretLeft from '@hyvor/icons/IconCaretLeft';
3
3
  import IconCloudUpload from '@hyvor/icons/IconCloudUpload';
4
+ import IconCardImage from '@hyvor/icons/IconCardImage';
4
5
  import Button from '../Button/Button.svelte';
5
6
  import Modal from '../Modal/Modal.svelte';
6
7
  import TabNav from '../TabNav/TabNav.svelte';
@@ -13,9 +14,17 @@
13
14
  } from './file-uploader.js';
14
15
  import TabUpload from './TabUpload/TabUpload.svelte';
15
16
  import Preview from './Preview/Preview.svelte';
17
+ import Media from './Media/Media.svelte';
18
+ import Unsplash from './Unsplash/Unsplash.svelte';
19
+ import Excalidraw from './Excalidraw/Excalidraw.svelte';
20
+ import ExcalidrawIcon from './Excalidraw/ExcalidrawIcon.svelte';
16
21
 
17
22
  const config = getFileUploaderConfig();
18
23
 
24
+ const showMedia = config.type !== 'file' && !!config.mediaLoad;
25
+ const showUnsplash = config.type === 'image' && !!config.unsplashSearch;
26
+ const showExcalidraw = config.type === 'image' && !!config.excalidraw;
27
+
19
28
  let tab = $state('upload');
20
29
 
21
30
  function onClose() {
@@ -46,22 +55,28 @@
46
55
  </Button>
47
56
  {:else}
48
57
  <TabNav>
49
- <TabNavItem name="upload" active>
58
+ <TabNavItem name="upload" active={tab === 'upload'} onclick={() => (tab = 'upload')}>
50
59
  {#snippet start()}
51
60
  <IconCloudUpload />
52
61
  {/snippet}
53
62
  Upload
54
63
  </TabNavItem>
55
64
 
56
- <!-- <TabNavItem name="media">
57
- {#snippet start()}
58
- <IconCardImage />
59
- {/snippet}
60
- Media Library
61
- </TabNavItem> -->
65
+ {#if showMedia}
66
+ <TabNavItem name="media" active={tab === 'media'} onclick={() => (tab = 'media')}>
67
+ {#snippet start()}
68
+ <IconCardImage />
69
+ {/snippet}
70
+ Media Library
71
+ </TabNavItem>
72
+ {/if}
62
73
 
63
- {#if config.type === 'image'}
64
- <!-- <TabNavItem name="unsplash">
74
+ {#if showUnsplash}
75
+ <TabNavItem
76
+ name="unsplash"
77
+ active={tab === 'unsplash'}
78
+ onclick={() => (tab = 'unsplash')}
79
+ >
65
80
  {#snippet start()}
66
81
  <svg
67
82
  role="img"
@@ -70,44 +85,43 @@
70
85
  fill="currentColor"
71
86
  viewBox="0 0 24 24"
72
87
  xmlns="http://www.w3.org/2000/svg"
73
- ><path
74
- d="M7.5 6.75V0h9v6.75h-9zm9 3.75H24V24H0V10.5h7.5v6.75h9V10.5z"
75
- /></svg
88
+ ><path d="M7.5 6.75V0h9v6.75h-9zm9 3.75H24V24H0V10.5h7.5v6.75h9V10.5z" /></svg
76
89
  >
77
90
  {/snippet}
78
91
  Unsplash
79
92
  </TabNavItem>
80
- <TabNavItem name="excalidraw">
93
+ {/if}
94
+
95
+ {#if showExcalidraw}
96
+ <TabNavItem
97
+ name="excalidraw"
98
+ active={tab === 'excalidraw'}
99
+ onclick={() => (tab = 'excalidraw')}
100
+ >
81
101
  {#snippet start()}
82
102
  <ExcalidrawIcon />
83
103
  {/snippet}
84
104
  Excalidraw
85
- </TabNavItem> -->
105
+ </TabNavItem>
86
106
  {/if}
87
107
  </TabNav>
88
108
  {/if}
89
109
  </div>
90
110
  {/snippet}
91
- <div class="body" style:position={selectedFile ? 'relative' : undefined}>
111
+ <div class="body" style:position={$selectedFile ? 'relative' : undefined}>
92
112
  {#if tab === 'upload'}
93
113
  <TabUpload />
114
+ {:else if tab === 'media'}
115
+ <Media />
116
+ {:else if tab === 'unsplash'}
117
+ <Unsplash />
118
+ {:else if tab === 'excalidraw'}
119
+ <Excalidraw />
94
120
  {/if}
95
121
 
96
122
  {#if $selectedFile}
97
123
  <Preview />
98
124
  {/if}
99
-
100
- <!-- {#if tab === 'upload'}
101
- <TabUpload {type} on:select={handleSelect} />
102
- {:else if tab === 'media' && type !== 'any'}
103
- <Media {type} on:select={handleSelect} />
104
- {:else if tab === 'unsplash'}
105
- <Unsplash on:select={handleSelect} />
106
- {:else if tab === 'excalidraw'}
107
- <Excalidraw on:select={handleSelect} />
108
- {/if}
109
-
110
- -->
111
125
  </div>
112
126
  </Modal>
113
127
  </div>
@@ -0,0 +1,118 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import Loader from '../../Loader/Loader.svelte';
4
+ import LoadButton from '../../Loader/LoadButton.svelte';
5
+ import IconMessage from '../../IconMessage/IconMessage.svelte';
6
+ import toast from '../../Toast/toast.js';
7
+ import { getFileUploaderConfig, setSelectedFile, type MediaItem } from '../file-uploader.js';
8
+
9
+ const config = getFileUploaderConfig();
10
+
11
+ let items: MediaItem[] = $state([]);
12
+ let page = $state(1);
13
+ let isLoading = $state(true);
14
+ let isLoadingMore = $state(false);
15
+ let hasMore = $state(false);
16
+
17
+ function load(nextPage = 1) {
18
+ if (!config.mediaLoad) return;
19
+
20
+ nextPage === 1 ? (isLoading = true) : (isLoadingMore = true);
21
+
22
+ config
23
+ .mediaLoad(nextPage)
24
+ .then((results) => {
25
+ items = nextPage === 1 ? results : [...items, ...results];
26
+ hasMore = results.length > 0;
27
+ page = nextPage;
28
+ })
29
+ .catch((err) => {
30
+ toast.error(err.message || 'Failed to load media');
31
+ })
32
+ .finally(() => {
33
+ isLoading = false;
34
+ isLoadingMore = false;
35
+ });
36
+ }
37
+
38
+ function handleSelect(item: MediaItem) {
39
+ setSelectedFile({
40
+ type: config.type === 'audio' ? 'audio' : 'image',
41
+ from: 'media',
42
+ media: item
43
+ });
44
+ }
45
+
46
+ function handleLoadMore() {
47
+ load(page + 1);
48
+ }
49
+
50
+ onMount(() => load());
51
+ </script>
52
+
53
+ <div class="media">
54
+ {#if isLoading}
55
+ <Loader full />
56
+ {:else if items.length === 0}
57
+ <IconMessage empty message="No media found" />
58
+ {:else}
59
+ <div class="grid">
60
+ {#each items as item (item.id)}
61
+ <button type="button" class="item" onclick={() => handleSelect(item)}>
62
+ {#if config.type === 'audio'}
63
+ <div class="audio-name">{item.name}</div>
64
+ {:else}
65
+ <img src={item.url} alt={item.name} />
66
+ {/if}
67
+ </button>
68
+ {/each}
69
+ </div>
70
+
71
+ <LoadButton text="Load More" show={hasMore} loading={isLoadingMore} on:click={handleLoadMore} />
72
+ {/if}
73
+ </div>
74
+
75
+ <style>.media {
76
+ height: 100%;
77
+ display: flex;
78
+ flex-direction: column;
79
+ overflow-y: auto;
80
+ }
81
+
82
+ .grid {
83
+ display: grid;
84
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
85
+ gap: 10px;
86
+ }
87
+
88
+ .item {
89
+ display: block;
90
+ width: 100%;
91
+ aspect-ratio: 1;
92
+ padding: 0;
93
+ border: 1px solid var(--border);
94
+ border-radius: 5px;
95
+ background: var(--input);
96
+ cursor: pointer;
97
+ overflow: hidden;
98
+ transition: 0.2s box-shadow;
99
+ }
100
+ .item:hover {
101
+ box-shadow: 0 0 0 2px var(--accent-light);
102
+ }
103
+ .item img {
104
+ display: block;
105
+ width: 100%;
106
+ height: 100%;
107
+ object-fit: cover;
108
+ }
109
+ .item .audio-name {
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ height: 100%;
114
+ padding: 10px;
115
+ font-size: 13px;
116
+ text-align: center;
117
+ word-break: break-word;
118
+ }</style>
@@ -0,0 +1,3 @@
1
+ declare const Media: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Media = ReturnType<typeof Media>;
3
+ export default Media;
@@ -21,7 +21,13 @@
21
21
  if (file.upload) {
22
22
  return URL.createObjectURL(file.upload.blob);
23
23
  }
24
- return ''; // TODO
24
+ if (file.unsplash) {
25
+ return file.unsplash.url;
26
+ }
27
+ if (file.media) {
28
+ return file.media.url;
29
+ }
30
+ return '';
25
31
  }
26
32
 
27
33
  let imageSize = $state(getInitialImageSize());
@@ -39,7 +45,12 @@
39
45
  if (file.upload && file.upload.blob instanceof File) {
40
46
  return file.upload.blob.name;
41
47
  }
42
- // TODO: add other
48
+ if (file.unsplash) {
49
+ return file.unsplash.title || file.unsplash.alt;
50
+ }
51
+ if (file.media) {
52
+ return file.media.name;
53
+ }
43
54
  return null;
44
55
  }
45
56
 
@@ -129,7 +140,7 @@
129
140
  });
130
141
  } else {
131
142
  completeFileUpload({
132
- url: file.upload!.fetchedUrl!,
143
+ url: file.upload?.fetchedUrl || file.unsplash?.url || file.media?.url || '',
133
144
  selectedFile: file
134
145
  });
135
146
  }
@@ -0,0 +1,156 @@
1
+ <script lang="ts">
2
+ import IconArrowReturnLeft from '@hyvor/icons/IconArrowReturnLeft';
3
+ import Button from '../../Button/Button.svelte';
4
+ import TextInput from '../../TextInput/TextInput.svelte';
5
+ import Loader from '../../Loader/Loader.svelte';
6
+ import LoadButton from '../../Loader/LoadButton.svelte';
7
+ import IconMessage from '../../IconMessage/IconMessage.svelte';
8
+ import toast from '../../Toast/toast.js';
9
+ import { getFileUploaderConfig, setSelectedFile, type UnsplashImage } from '../file-uploader.js';
10
+
11
+ const config = getFileUploaderConfig();
12
+
13
+ let search = $state('');
14
+ let images: UnsplashImage[] = $state([]);
15
+ let page = $state(1);
16
+ let isLoading = $state(false);
17
+ let isLoadingMore = $state(false);
18
+ let hasMore = $state(false);
19
+ let hasSearched = $state(false);
20
+ let inputError = $state(false);
21
+
22
+ function performSearch(nextPage = 1) {
23
+ inputError = false;
24
+
25
+ if (search.trim() === '') {
26
+ inputError = true;
27
+ return;
28
+ }
29
+
30
+ if (!config.unsplashSearch) return;
31
+
32
+ nextPage === 1 ? (isLoading = true) : (isLoadingMore = true);
33
+
34
+ config
35
+ .unsplashSearch(search, nextPage)
36
+ .then((results) => {
37
+ images = nextPage === 1 ? results : [...images, ...results];
38
+ hasMore = results.length > 0;
39
+ page = nextPage;
40
+ hasSearched = true;
41
+ })
42
+ .catch((err) => {
43
+ toast.error(err.message || 'Failed to search Unsplash');
44
+ })
45
+ .finally(() => {
46
+ isLoading = false;
47
+ isLoadingMore = false;
48
+ });
49
+ }
50
+
51
+ function handleKeyup(e: KeyboardEvent) {
52
+ if (e.key === 'Enter') {
53
+ performSearch();
54
+ }
55
+ }
56
+
57
+ function handleSelect(image: UnsplashImage) {
58
+ setSelectedFile({
59
+ type: 'image',
60
+ from: 'unsplash',
61
+ unsplash: image
62
+ });
63
+ }
64
+
65
+ function handleLoadMore() {
66
+ performSearch(page + 1);
67
+ }
68
+ </script>
69
+
70
+ <div class="unsplash">
71
+ <div class="search-wrap">
72
+ <TextInput
73
+ bind:value={search}
74
+ placeholder="Search Unsplash"
75
+ autofocus
76
+ autocomplete="off"
77
+ onkeyup={handleKeyup}
78
+ block
79
+ state={inputError ? 'error' : 'default'}
80
+ />
81
+ <Button onclick={() => performSearch()}>
82
+ Search
83
+ {#snippet end()}
84
+ <IconArrowReturnLeft size={14} />
85
+ {/snippet}
86
+ </Button>
87
+ </div>
88
+
89
+ <div class="results">
90
+ {#if isLoading}
91
+ <Loader full />
92
+ {:else if images.length === 0 && hasSearched}
93
+ <IconMessage empty message="No images found" />
94
+ {:else}
95
+ <div class="cols">
96
+ {#each [0, 1, 2] as col (col)}
97
+ <div class="col">
98
+ {#each images as image, i (image.url)}
99
+ {#if i % 3 === col}
100
+ <button type="button" class="img-wrap" onclick={() => handleSelect(image)}>
101
+ <img src={image.url} alt={image.alt || image.title || ''} />
102
+ </button>
103
+ {/if}
104
+ {/each}
105
+ </div>
106
+ {/each}
107
+ </div>
108
+ {/if}
109
+
110
+ <LoadButton
111
+ text="Load More"
112
+ show={hasMore && !isLoading}
113
+ loading={isLoadingMore}
114
+ on:click={handleLoadMore}
115
+ />
116
+ </div>
117
+ </div>
118
+
119
+ <style>.unsplash {
120
+ display: flex;
121
+ flex-direction: column;
122
+ height: 100%;
123
+ }
124
+
125
+ .search-wrap {
126
+ display: flex;
127
+ align-items: center;
128
+ margin-bottom: 15px;
129
+ gap: 10px;
130
+ }
131
+
132
+ .results {
133
+ flex: 1;
134
+ overflow-y: auto;
135
+ }
136
+ .results .cols {
137
+ display: flex;
138
+ gap: 10px;
139
+ }
140
+ .results .col {
141
+ flex: 1;
142
+ }
143
+ .results .img-wrap {
144
+ display: block;
145
+ width: 100%;
146
+ margin-bottom: 10px;
147
+ padding: 0;
148
+ border: none;
149
+ background: none;
150
+ cursor: pointer;
151
+ }
152
+ .results .img-wrap img {
153
+ display: block;
154
+ width: 100%;
155
+ border-radius: 5px;
156
+ }</style>
@@ -0,0 +1,3 @@
1
+ declare const Unsplash: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Unsplash = ReturnType<typeof Unsplash>;
3
+ export default Unsplash;
@@ -1,15 +1,28 @@
1
1
  export declare let fileUploaderConfig: import("svelte/store").Writable<FileUploaderConfigInternal | null>;
2
2
  export declare let selectedFile: import("svelte/store").Writable<SelectedFile | null>;
3
3
  export type UploadType = 'image' | 'audio' | 'file';
4
+ export interface UnsplashImage {
5
+ url: string;
6
+ author: string;
7
+ author_url: string;
8
+ title: string | null;
9
+ alt: string | null;
10
+ }
11
+ export interface MediaItem {
12
+ id: string | number;
13
+ url: string;
14
+ name: string;
15
+ }
4
16
  export interface FileUploaderConfig {
5
17
  /**
6
18
  * image:
7
- * - shows unsplash and excalidraw tabs
19
+ * - shows unsplash and excalidraw tabs (if configured)
8
20
  * audio:
9
21
  * - shows audio preview
10
22
  * file
11
23
  * - allows any file type
12
24
  * - preview tries to detect file type (image/audio/other)
25
+ * - hides the media library tab
13
26
  */
14
27
  type: UploadType;
15
28
  uploader: (file: Blob, name: string | null) => Promise<{
@@ -17,16 +30,12 @@ export interface FileUploaderConfig {
17
30
  }>;
18
31
  allowedMimeTypes?: string[];
19
32
  maxFileSizeInMB?: number;
33
+ mediaLoad?: (page: number) => Promise<MediaItem[]>;
34
+ unsplashSearch?: (search: string, page: number) => Promise<UnsplashImage[]>;
35
+ excalidraw?: boolean;
20
36
  }
21
37
  export type SelectedFileFrom = 'upload' | 'media' | 'unsplash' | 'excalidraw';
22
- export type SelectedFileUploadType = 'paste' | 'dnd' | 'browse' | 'url';
23
- export interface UnsplashImage {
24
- url: string;
25
- author: string;
26
- author_url: string;
27
- title: string | null;
28
- alt: string | null;
29
- }
38
+ export type SelectedFileUploadType = 'paste' | 'dnd' | 'browse' | 'url' | 'excalidraw';
30
39
  export interface SelectedFile {
31
40
  type: UploadType;
32
41
  from: SelectedFileFrom;
@@ -36,12 +45,13 @@ export interface SelectedFile {
36
45
  fetchedUrl?: string;
37
46
  };
38
47
  unsplash?: UnsplashImage;
48
+ media?: MediaItem;
39
49
  }
40
50
  export interface UploadedFile {
41
51
  url: string;
42
52
  selectedFile: SelectedFile;
43
53
  }
44
- export type FileUploaderConfigInternal = Required<FileUploaderConfig> & {
54
+ export type FileUploaderConfigInternal = Required<Omit<FileUploaderConfig, 'mediaLoad' | 'unsplashSearch' | 'excalidraw'>> & Pick<FileUploaderConfig, 'mediaLoad' | 'unsplashSearch' | 'excalidraw'> & {
45
55
  onCancel: () => void;
46
56
  onUpload: (file: UploadedFile) => void;
47
57
  };
@@ -30,7 +30,7 @@ export { default as FormControl } from './FormControl/FormControl.svelte';
30
30
  export { default as InputGroup } from './FormControl/InputGroup.svelte';
31
31
  export { default as Label } from './FormControl/Label.svelte';
32
32
  export { default as Validation } from './FormControl/Validation.svelte';
33
- export { uploadFile, type FileUploaderConfig, type UploadedFile as FileUploaderUploadedFile, type SelectedFile as FileUploaderSelectedFile } from './FileUploader/file-uploader.js';
33
+ export { uploadFile, type FileUploaderConfig, type UploadedFile as FileUploaderUploadedFile, type SelectedFile as FileUploaderSelectedFile, type UnsplashImage as FileUploaderUnsplashImage, type MediaItem as FileUploaderMediaItem } from './FileUploader/file-uploader.js';
34
34
  export { default as IconButton } from './IconButton/IconButton.svelte';
35
35
  export { default as Kbd } from './Kbd/Kbd.svelte';
36
36
  export { default as Link } from './Link/Link.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyvor/design",
3
- "version": "2.1.12",
3
+ "version": "2.1.13",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "repository": {