@ims360/svelte-ivory 0.0.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.
Files changed (128) hide show
  1. package/LICENCE +23 -0
  2. package/README.md +15 -0
  3. package/dist/components/ai/AiMessage.svelte +115 -0
  4. package/dist/components/ai/AiMessage.svelte.d.ts +18 -0
  5. package/dist/components/ai/AttachedFile.svelte +28 -0
  6. package/dist/components/ai/AttachedFile.svelte.d.ts +9 -0
  7. package/dist/components/ai/Chat.svelte +150 -0
  8. package/dist/components/ai/Chat.svelte.d.ts +40 -0
  9. package/dist/components/ai/Markdown.svelte +59 -0
  10. package/dist/components/ai/Markdown.svelte.d.ts +16 -0
  11. package/dist/components/ai/UserMessage.svelte +53 -0
  12. package/dist/components/ai/UserMessage.svelte.d.ts +16 -0
  13. package/dist/components/ai/index.d.ts +5 -0
  14. package/dist/components/ai/index.js +5 -0
  15. package/dist/components/basic/checkbox/Checkbox.svelte +79 -0
  16. package/dist/components/basic/checkbox/Checkbox.svelte.d.ts +18 -0
  17. package/dist/components/basic/index.d.ts +2 -0
  18. package/dist/components/basic/index.js +2 -0
  19. package/dist/components/basic/toggle/Toggle.svelte +47 -0
  20. package/dist/components/basic/toggle/Toggle.svelte.d.ts +12 -0
  21. package/dist/components/buttons/CopyToClipboardButton.svelte +33 -0
  22. package/dist/components/buttons/CopyToClipboardButton.svelte.d.ts +9 -0
  23. package/dist/components/index.d.ts +0 -0
  24. package/dist/components/index.js +1 -0
  25. package/dist/components/layout/heading/Heading.svelte +33 -0
  26. package/dist/components/layout/heading/Heading.svelte.d.ts +16 -0
  27. package/dist/components/layout/heading/index.d.ts +5 -0
  28. package/dist/components/layout/heading/index.js +5 -0
  29. package/dist/components/layout/hiddenBackground/HiddenBackground.svelte +48 -0
  30. package/dist/components/layout/hiddenBackground/HiddenBackground.svelte.d.ts +13 -0
  31. package/dist/components/layout/hiddenBackground/index.js +6 -0
  32. package/dist/components/layout/index.d.ts +7 -0
  33. package/dist/components/layout/index.js +7 -0
  34. package/dist/components/layout/modal/Modal.svelte +114 -0
  35. package/dist/components/layout/modal/Modal.svelte.d.ts +30 -0
  36. package/dist/components/layout/modal/ModalTest.svelte +16 -0
  37. package/dist/components/layout/modal/ModalTest.svelte.d.ts +9 -0
  38. package/dist/components/layout/popover/Popover.svelte +108 -0
  39. package/dist/components/layout/popover/Popover.svelte.d.ts +36 -0
  40. package/dist/components/layout/portal/Portal.svelte +23 -0
  41. package/dist/components/layout/portal/Portal.svelte.d.ts +15 -0
  42. package/dist/components/layout/tabs/Tab.svelte +80 -0
  43. package/dist/components/layout/tabs/Tab.svelte.d.ts +21 -0
  44. package/dist/components/layout/tabs/TabPanel.svelte +23 -0
  45. package/dist/components/layout/tabs/TabPanel.svelte.d.ts +10 -0
  46. package/dist/components/layout/tabs/Tabs.svelte +86 -0
  47. package/dist/components/layout/tabs/Tabs.svelte.d.ts +21 -0
  48. package/dist/components/layout/tabs/index.d.ts +26 -0
  49. package/dist/components/layout/tabs/index.js +8 -0
  50. package/dist/components/layout/tooltip/Tooltip.svelte +111 -0
  51. package/dist/components/layout/tooltip/Tooltip.svelte.d.ts +32 -0
  52. package/dist/components/toast/Toast.svelte +100 -0
  53. package/dist/components/toast/Toast.svelte.d.ts +16 -0
  54. package/dist/components/toast/index.d.ts +2 -0
  55. package/dist/components/toast/index.js +2 -0
  56. package/dist/components/toast/toasts.svelte.d.ts +26 -0
  57. package/dist/components/toast/toasts.svelte.js +67 -0
  58. package/dist/index.d.ts +0 -0
  59. package/dist/index.js +2 -0
  60. package/dist/utils/actions/clickOutside.d.ts +11 -0
  61. package/dist/utils/actions/clickOutside.js +23 -0
  62. package/dist/utils/actions/focusTrap.d.ts +4 -0
  63. package/dist/utils/actions/focusTrap.js +64 -0
  64. package/dist/utils/actions/index.d.ts +5 -0
  65. package/dist/utils/actions/index.js +5 -0
  66. package/dist/utils/actions/portal.d.ts +9 -0
  67. package/dist/utils/actions/portal.js +39 -0
  68. package/dist/utils/actions/shortcut.d.ts +10 -0
  69. package/dist/utils/actions/shortcut.js +25 -0
  70. package/dist/utils/actions/visible.d.ts +5 -0
  71. package/dist/utils/actions/visible.js +14 -0
  72. package/dist/utils/functions/cookie.d.ts +12 -0
  73. package/dist/utils/functions/cookie.js +36 -0
  74. package/dist/utils/functions/index.d.ts +3 -0
  75. package/dist/utils/functions/index.js +3 -0
  76. package/dist/utils/functions/pseudoRandomId.d.ts +1 -0
  77. package/dist/utils/functions/pseudoRandomId.js +3 -0
  78. package/dist/utils/functions/queryParams.d.ts +1 -0
  79. package/dist/utils/functions/queryParams.js +14 -0
  80. package/package.json +107 -0
  81. package/src/lib/components/ai/AiMessage.svelte +115 -0
  82. package/src/lib/components/ai/AttachedFile.svelte +28 -0
  83. package/src/lib/components/ai/Chat.svelte +150 -0
  84. package/src/lib/components/ai/Markdown.svelte +59 -0
  85. package/src/lib/components/ai/UserMessage.svelte +53 -0
  86. package/src/lib/components/ai/index.ts +5 -0
  87. package/src/lib/components/basic/checkbox/Checkbox.svelte +79 -0
  88. package/src/lib/components/basic/checkbox/checkbox.svelte.spec.ts +39 -0
  89. package/src/lib/components/basic/index.ts +2 -0
  90. package/src/lib/components/basic/toggle/Toggle.svelte +47 -0
  91. package/src/lib/components/basic/toggle/toggle.svelte.spec.ts +19 -0
  92. package/src/lib/components/buttons/CopyToClipboardButton.svelte +33 -0
  93. package/src/lib/components/index.ts +0 -0
  94. package/src/lib/components/layout/heading/Heading.svelte +33 -0
  95. package/src/lib/components/layout/heading/index.ts +7 -0
  96. package/src/lib/components/layout/hiddenBackground/HiddenBackground.svelte +48 -0
  97. package/src/lib/components/layout/hiddenBackground/index.ts +8 -0
  98. package/src/lib/components/layout/index.ts +7 -0
  99. package/src/lib/components/layout/modal/Modal.svelte +114 -0
  100. package/src/lib/components/layout/modal/ModalTest.svelte +16 -0
  101. package/src/lib/components/layout/modal/modal.svelte.spec.ts +39 -0
  102. package/src/lib/components/layout/popover/Popover.svelte +108 -0
  103. package/src/lib/components/layout/portal/Portal.svelte +23 -0
  104. package/src/lib/components/layout/tabs/Tab.svelte +80 -0
  105. package/src/lib/components/layout/tabs/TabPanel.svelte +23 -0
  106. package/src/lib/components/layout/tabs/Tabs.svelte +86 -0
  107. package/src/lib/components/layout/tabs/Tabs.test.svelte +5 -0
  108. package/src/lib/components/layout/tabs/index.ts +10 -0
  109. package/src/lib/components/layout/tooltip/Tooltip.svelte +111 -0
  110. package/src/lib/components/toast/Toast.svelte +100 -0
  111. package/src/lib/components/toast/index.ts +2 -0
  112. package/src/lib/components/toast/toasts.svelte.ts +89 -0
  113. package/src/lib/index.ts +1 -0
  114. package/src/lib/utils/actions/clickOutside.svelte.spec.ts +67 -0
  115. package/src/lib/utils/actions/clickOutside.ts +38 -0
  116. package/src/lib/utils/actions/focusTrap.ts +65 -0
  117. package/src/lib/utils/actions/index.ts +5 -0
  118. package/src/lib/utils/actions/portal.ts +43 -0
  119. package/src/lib/utils/actions/shortcut.svelte.spec.ts +19 -0
  120. package/src/lib/utils/actions/shortcut.ts +35 -0
  121. package/src/lib/utils/actions/visible.ts +28 -0
  122. package/src/lib/utils/functions/cookie.svelte.spec.ts +55 -0
  123. package/src/lib/utils/functions/cookie.ts +46 -0
  124. package/src/lib/utils/functions/index.ts +3 -0
  125. package/src/lib/utils/functions/pseudoRandomId.spec.ts +19 -0
  126. package/src/lib/utils/functions/pseudoRandomId.ts +4 -0
  127. package/src/lib/utils/functions/queryParams.spec.ts +25 -0
  128. package/src/lib/utils/functions/queryParams.ts +15 -0
package/LICENCE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ims360 GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # Ivory, more than just a [skeleton](https://www.skeleton.dev/)
2
+
3
+ A svelte library with a bunch of components and utilities, building upon [skeletons](https://www.skeleton.dev/) design system.
4
+ Currently this library is primarily intended for our ([ims360](https://ims360.de/)) internal use, so documentation is sparse at the moment.
5
+ Eventually we will add more documentation and examples to make it easier for devs outside our organization to use.
6
+
7
+ ## Setup
8
+
9
+ Include this line in your `app.css` file, so the TW classes used in the lib are included:
10
+
11
+ ```css
12
+ @source "../node_modules/@ims360/svelte-lib";
13
+ ```
14
+
15
+ _You may need to adjust the path if your `app.css` is not located in `/src`_
@@ -0,0 +1,115 @@
1
+ <!--
2
+ @component
3
+ The default for AI Messages in the Chat component.
4
+ Can be customized using the `class` and `loading` props.
5
+ -->
6
+
7
+ <script lang="ts">
8
+ import { ThumbsDown, ThumbsUp } from '@lucide/svelte';
9
+ import clsx from 'clsx';
10
+ import type { Snippet } from 'svelte';
11
+ import type { ClassValue } from 'svelte/elements';
12
+ import { twMerge } from 'tailwind-merge';
13
+ import CopyToClipboardButton from '../buttons/CopyToClipboardButton.svelte';
14
+ import type { AiChatMessage } from './Chat.svelte';
15
+ import Markdown from './Markdown.svelte';
16
+
17
+ interface Props {
18
+ b_message: AiChatMessage;
19
+ messageText?: Snippet<[{ message: AiChatMessage }]>;
20
+ class?: ClassValue;
21
+ minHeight?: number;
22
+ }
23
+
24
+ let {
25
+ b_message = $bindable(),
26
+ class: clazz,
27
+ messageText = defaultMessage,
28
+ minHeight
29
+ }: Props = $props();
30
+
31
+ // const uuidRegex =
32
+ // /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g;
33
+ // const icon = `<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" class="h-[1em] origin-center overflow-visible align-[-0.125rem]" viewBox="0 0 512 512"><g transform="translate(256 256)" transform-origin="128 0"><path d="M336 0c-8.8 0-16 7.2-16 16s7.2 16 16 16l121.4 0L212.7 276.7c-6.2 6.2-6.2 16.4 0 22.6s16.4 6.2 22.6 0L480 54.6 480 176c0 8.8 7.2 16 16 16s16-7.2 16-16l0-160c0-8.8-7.2-16-16-16L336 0zM64 32C28.7 32 0 60.7 0 96L0 448c0 35.3 28.7 64 64 64l352 0c35.3 0 64-28.7 64-64l0-144c0-8.8-7.2-16-16-16s-16 7.2-16 16l0 144c0 17.7-14.3 32-32 32L64 480c-17.7 0-32-14.3-32-32L32 96c0-17.7 14.3-32 32-32l144 0c8.8 0 16-7.2 16-16s-7.2-16-16-16L64 32z" fill="currentColor" transform="translate(-256 -256)"></path><!----></g></svg>`;
34
+
35
+ // async function replacerFactory(): Promise<(match: string) => string> {
36
+ // const docs = await documents.value;
37
+ // return (match: string) => {
38
+ // const doc = docs.find((d) => d.id === match);
39
+
40
+ // return `<span class="bg-surface-100-900 py-1 px-2 rounded flex flex-row items-center gap-2 hover:shadow-lg w-fit transition-all">
41
+ // ${doc?.title ?? 'Unbekanntes Dokument'}
42
+ // <a href="/documents/${doc?.id}" target="_blank">
43
+ // ${icon}
44
+ // </a>
45
+ // </span>`;
46
+ // };
47
+ // }
48
+ </script>
49
+
50
+ <div
51
+ class={twMerge(clsx('group flex w-full flex-col items-start', clazz))}
52
+ style={minHeight ? `min-height: ${minHeight}px;` : undefined}
53
+ >
54
+ {@render messageText({
55
+ message: b_message
56
+ })}
57
+ <div
58
+ class={[
59
+ 'text-surface-500 flex -translate-x-3 flex-row items-center transition-all group-hover:opacity-100',
60
+ b_message.liked || b_message.disliked ? 'opacity-100' : 'opacity-0'
61
+ ]}
62
+ >
63
+ <CopyToClipboardButton
64
+ text={b_message.message}
65
+ toastMessage="Nachricht wurde in die Zwischenablage kopiert"
66
+ />
67
+ <button
68
+ type="button"
69
+ class="btn-icon"
70
+ onclick={() => {
71
+ b_message.liked = !b_message.liked;
72
+ b_message.disliked = false;
73
+ }}
74
+ >
75
+ <ThumbsUp class={[b_message.liked && 'fill-surface-500/50']} />
76
+ </button>
77
+ <button
78
+ type="button"
79
+ class="btn-icon"
80
+ onclick={() => {
81
+ b_message.liked = false;
82
+ b_message.disliked = !b_message.disliked;
83
+ }}
84
+ >
85
+ <ThumbsDown class={[b_message.disliked && 'fill-surface-500/50']} />
86
+ </button>
87
+ {#if b_message.time}
88
+ <p class="text-surface-400-600 pl-2">
89
+ {b_message.time.toLocaleString('de')}
90
+ </p>
91
+ {/if}
92
+ </div>
93
+ </div>
94
+
95
+ {#snippet defaultMessage({ message }: { message: AiChatMessage })}
96
+ {#if message}
97
+ <Markdown source={message.message} />
98
+ {:else}
99
+ <p class="flex flex-row">
100
+ <span class="h-4 animate-bounce rounded-full pl-1">.</span>
101
+ <span
102
+ class="h-4 animate-bounce rounded-full"
103
+ style="animation-delay: 125ms !important;"
104
+ >
105
+ .
106
+ </span>
107
+ <span
108
+ class="h-4 animate-bounce rounded-full"
109
+ style="animation-delay: 250ms !important;"
110
+ >
111
+ .
112
+ </span>
113
+ </p>
114
+ {/if}
115
+ {/snippet}
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ClassValue } from 'svelte/elements';
3
+ import type { AiChatMessage } from './Chat.svelte';
4
+ interface Props {
5
+ b_message: AiChatMessage;
6
+ messageText?: Snippet<[{
7
+ message: AiChatMessage;
8
+ }]>;
9
+ class?: ClassValue;
10
+ minHeight?: number;
11
+ }
12
+ /**
13
+ * The default for AI Messages in the Chat component.
14
+ * Can be customized using the `class` and `loading` props.
15
+ */
16
+ declare const AiMessage: import("svelte").Component<Props, {}, "b_message">;
17
+ type AiMessage = ReturnType<typeof AiMessage>;
18
+ export default AiMessage;
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import { X } from '@lucide/svelte';
3
+ import type { ClassValue } from 'svelte/elements';
4
+
5
+ interface Props {
6
+ file: File;
7
+ onremove?: (f: File) => void;
8
+ class?: ClassValue;
9
+ }
10
+
11
+ let { file, onremove, class: clazz }: Props = $props();
12
+ </script>
13
+
14
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
15
+ <svelte:element
16
+ this={onremove ? 'button' : 'div'}
17
+ type={onremove ? 'button' : undefined}
18
+ onclick={onremove ? () => onremove(file) : undefined}
19
+ class={[
20
+ 'bg-primary-500/25 group flex flex-row items-center gap-1 overflow-hidden rounded-full px-4 py-1 whitespace-nowrap',
21
+ clazz
22
+ ]}
23
+ >
24
+ <p>{file.name}</p>
25
+ {#if onremove}
26
+ <X size={16} class="group-hover:text-primary-500" />
27
+ {/if}
28
+ </svelte:element>
@@ -0,0 +1,9 @@
1
+ import type { ClassValue } from 'svelte/elements';
2
+ interface Props {
3
+ file: File;
4
+ onremove?: (f: File) => void;
5
+ class?: ClassValue;
6
+ }
7
+ declare const AttachedFile: import("svelte").Component<Props, {}, "">;
8
+ type AttachedFile = ReturnType<typeof AttachedFile>;
9
+ export default AttachedFile;
@@ -0,0 +1,150 @@
1
+ <!--
2
+ @component
3
+ An AI chat component that can be used to create a chatbot.
4
+ Comes with default styles for the chat messages, but can be customized with the `userMessage` and `systemMessage` props.
5
+ The input component has to be provided as a child component, and the `submit` function has to be provided as a callback.
6
+ -->
7
+
8
+ <script lang="ts" module>
9
+ import clsx from 'clsx';
10
+ import { tick, type Snippet } from 'svelte';
11
+ import type { ClassValue } from 'svelte/elements';
12
+ import { twMerge } from 'tailwind-merge';
13
+ import AiMessage from './AiMessage.svelte';
14
+ import UserMessage from './UserMessage.svelte';
15
+
16
+ export interface AiChatMessage {
17
+ from: 'user' | 'system';
18
+ message: string;
19
+ time?: Date;
20
+ liked?: boolean;
21
+ disliked?: boolean;
22
+ files?: File[];
23
+ }
24
+
25
+ export interface AiChat {
26
+ messages: AiChatMessage[];
27
+ loading?: boolean;
28
+ }
29
+ </script>
30
+
31
+ <script lang="ts">
32
+ interface Props {
33
+ class?: ClassValue;
34
+ b_chat: AiChat;
35
+ userMessage?: Snippet<[{ message: AiChatMessage; i: number }]>;
36
+ systemMessage?: Snippet<[{ message: AiChatMessage; i: number; minHeight?: number }]>;
37
+ placeholder?: Snippet;
38
+ children: Snippet<[{ onsubmit: (message: AiChatMessage) => Promise<void> }]>;
39
+ submit: (message: AiChatMessage) => Promise<void>;
40
+ }
41
+
42
+ let {
43
+ class: clazz,
44
+ b_chat: chat = $bindable(),
45
+ userMessage = defaultUserMessage,
46
+ systemMessage = defaultSystemMessage,
47
+ placeholder,
48
+ children,
49
+ submit: externalSubmit
50
+ }: Props = $props();
51
+
52
+ let chatContainer = $state<HTMLDivElement>();
53
+ let lastMessageMinHeight = $state(0);
54
+
55
+ function getLastMessageMinHeight() {
56
+ if (!chatContainer) return 0;
57
+ const secondToLastElement = chatContainer.children[chatContainer.children.length - 2];
58
+ const rect = secondToLastElement?.getBoundingClientRect();
59
+ const remainHeight = chatContainer.clientHeight - rect.height - 16;
60
+ return remainHeight;
61
+ }
62
+
63
+ async function scrollToBottom() {
64
+ if (!chatContainer) return;
65
+ await tick();
66
+ await tick();
67
+ lastMessageMinHeight = getLastMessageMinHeight();
68
+ await tick();
69
+ // ensure we don't scroll if the newly generated message is already in view
70
+ chatContainer.scrollTo({
71
+ top: chatContainer.scrollHeight,
72
+ behavior: 'smooth'
73
+ });
74
+ }
75
+
76
+ async function submit(message: AiChatMessage) {
77
+ if (chat.loading) {
78
+ return;
79
+ }
80
+
81
+ chat.messages.push({
82
+ ...message,
83
+ from: 'user',
84
+ time: new Date()
85
+ });
86
+ // prevent the user from sending another message while we are loading the ai response
87
+ chat.loading = true;
88
+
89
+ // add an empty system message to the chat, this will indicate a loading state
90
+ chat.messages.push({
91
+ from: 'system',
92
+ message: '',
93
+ time: new Date()
94
+ });
95
+
96
+ await scrollToBottom();
97
+
98
+ await externalSubmit(message);
99
+
100
+ chat.loading = false;
101
+ }
102
+ </script>
103
+
104
+ <div class={twMerge(clsx('flex grow flex-col gap-2 overflow-hidden', clazz))}>
105
+ <div
106
+ class="flex grow flex-col gap-4 overflow-auto pr-2 [scrollbar-gutter:stable]"
107
+ bind:this={chatContainer}
108
+ >
109
+ {#if chat.messages.length === 0 && placeholder}
110
+ {@render placeholder()}
111
+ {/if}
112
+ {#each chat.messages as _, i}
113
+ {@const message = chat.messages[i]}
114
+ {#if message.from === 'user'}
115
+ {@render userMessage({
116
+ message,
117
+ i
118
+ })}
119
+ {:else}
120
+ {@render systemMessage({
121
+ message,
122
+ i,
123
+ minHeight: i === chat.messages.length - 1 ? lastMessageMinHeight : 0
124
+ })}
125
+ {/if}
126
+ {/each}
127
+ </div>
128
+ {@render children({ onsubmit: submit })}
129
+ </div>
130
+
131
+ {#snippet defaultSystemMessage({
132
+ i,
133
+ minHeight
134
+ }: {
135
+ i: number;
136
+ message: AiChatMessage;
137
+ minHeight?: number;
138
+ })}
139
+ <AiMessage bind:b_message={chat.messages[i]} {minHeight} />
140
+ {/snippet}
141
+
142
+ {#snippet defaultUserMessage({
143
+ message
144
+ }: {
145
+ i: number;
146
+ message: AiChatMessage;
147
+ minHeight?: number;
148
+ })}
149
+ <UserMessage {message} />
150
+ {/snippet}
@@ -0,0 +1,40 @@
1
+ import { type Snippet } from 'svelte';
2
+ import type { ClassValue } from 'svelte/elements';
3
+ export interface AiChatMessage {
4
+ from: 'user' | 'system';
5
+ message: string;
6
+ time?: Date;
7
+ liked?: boolean;
8
+ disliked?: boolean;
9
+ files?: File[];
10
+ }
11
+ export interface AiChat {
12
+ messages: AiChatMessage[];
13
+ loading?: boolean;
14
+ }
15
+ interface Props {
16
+ class?: ClassValue;
17
+ b_chat: AiChat;
18
+ userMessage?: Snippet<[{
19
+ message: AiChatMessage;
20
+ i: number;
21
+ }]>;
22
+ systemMessage?: Snippet<[{
23
+ message: AiChatMessage;
24
+ i: number;
25
+ minHeight?: number;
26
+ }]>;
27
+ placeholder?: Snippet;
28
+ children: Snippet<[{
29
+ onsubmit: (message: AiChatMessage) => Promise<void>;
30
+ }]>;
31
+ submit: (message: AiChatMessage) => Promise<void>;
32
+ }
33
+ /**
34
+ * An AI chat component that can be used to create a chatbot.
35
+ * Comes with default styles for the chat messages, but can be customized with the `userMessage` and `systemMessage` props.
36
+ * The input component has to be provided as a child component, and the `submit` function has to be provided as a callback.
37
+ */
38
+ declare const Chat: import("svelte").Component<Props, {}, "b_chat">;
39
+ type Chat = ReturnType<typeof Chat>;
40
+ export default Chat;
@@ -0,0 +1,59 @@
1
+ <!--
2
+ @component
3
+ Renders markdown to html.
4
+ Uses the [marked](https://marked.js.org/) library for rendering and [dompurify](https://github.com/cure53/DOMPurify) for sanitizing the html.
5
+ -->
6
+
7
+ <script lang="ts">
8
+ import clsx from 'clsx';
9
+ import DomPurify from 'dompurify';
10
+ import { marked } from 'marked';
11
+ import type { ClassValue } from 'svelte/elements';
12
+ import { twMerge } from 'tailwind-merge';
13
+
14
+ interface Props {
15
+ source: string;
16
+ class?: ClassValue;
17
+ replace?: {
18
+ regex: RegExp;
19
+ replacerFactory: () => Promise<(match: string) => string> | ((match: string) => string);
20
+ };
21
+ }
22
+
23
+ let { source, replace, class: clazz }: Props = $props();
24
+
25
+ const html = $derived.by(async () => {
26
+ // replace 0 width characters
27
+ const cleanedSource = source.replace(
28
+ // eslint-disable-next-line no-misleading-character-class
29
+ /^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/,
30
+ ''
31
+ );
32
+
33
+ const html = await marked.parse(cleanedSource);
34
+ // const docs = await documents.value;
35
+
36
+ if (replace) {
37
+ const replacer = await replace.replacerFactory();
38
+ const replaced = html.replaceAll(replace.regex, replacer);
39
+ return DomPurify.sanitize(replaced);
40
+ } else {
41
+ return DomPurify.sanitize(html);
42
+ }
43
+ });
44
+ </script>
45
+
46
+ <div
47
+ class={twMerge(
48
+ clsx(
49
+ 'text-surface-950-50 prose prose-strong:text-surface-950-50 prose-p:my-1 flex flex-col items-start gap-1',
50
+ clazz
51
+ )
52
+ )}
53
+ >
54
+ {#await html then html}
55
+ <!-- this is fine since we purify the string -->
56
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
57
+ {@html html}
58
+ {/await}
59
+ </div>
@@ -0,0 +1,16 @@
1
+ import type { ClassValue } from 'svelte/elements';
2
+ interface Props {
3
+ source: string;
4
+ class?: ClassValue;
5
+ replace?: {
6
+ regex: RegExp;
7
+ replacerFactory: () => Promise<(match: string) => string> | ((match: string) => string);
8
+ };
9
+ }
10
+ /**
11
+ * Renders markdown to html.
12
+ * Uses the [marked](https://marked.js.org/) library for rendering and [dompurify](https://github.com/cure53/DOMPurify) for sanitizing the html.
13
+ */
14
+ declare const Markdown: import("svelte").Component<Props, {}, "">;
15
+ type Markdown = ReturnType<typeof Markdown>;
16
+ export default Markdown;
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import clsx from 'clsx';
3
+ import type { Snippet } from 'svelte';
4
+ import type { ClassValue } from 'svelte/elements';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import AttachedFile from './AttachedFile.svelte';
7
+ import type { AiChatMessage } from './Chat.svelte';
8
+ import Markdown from './Markdown.svelte';
9
+
10
+ interface Props {
11
+ class?: ClassValue;
12
+ message: AiChatMessage;
13
+ /** How attached files should be rendered */
14
+ attachedFile?: Snippet<[file: File]>;
15
+ /** How the message string should be rendered */
16
+ messageText?: Snippet<[{ message: AiChatMessage }]>;
17
+ }
18
+
19
+ let {
20
+ class: clazz,
21
+ message,
22
+ attachedFile = defaultAttachedFile,
23
+ messageText = defaultMessageText
24
+ }: Props = $props();
25
+ </script>
26
+
27
+ <div class={twMerge(clsx('flex w-full flex-col items-end gap-1', clazz))}>
28
+ {@render messageText({ message })}
29
+ {#if message.files}
30
+ <div class="flex flex-row items-center gap-2">
31
+ {#each message.files as file}
32
+ {@render attachedFile(file)}
33
+ {/each}
34
+ </div>
35
+ {/if}
36
+ <div class="flex flex-row items-center gap-2">
37
+ {#if message.time}
38
+ <p class="text-surface-400-600 text-sm">
39
+ {message.time.toLocaleString('de')}
40
+ </p>
41
+ {/if}
42
+ </div>
43
+ </div>
44
+
45
+ {#snippet defaultAttachedFile(file: File)}
46
+ <AttachedFile {file} />
47
+ {/snippet}
48
+
49
+ {#snippet defaultMessageText({ message }: { message: AiChatMessage })}
50
+ <div class="bg-surface-200-800 text-surface-contrast-200-800 rounded px-2 py-0.5">
51
+ <Markdown source={message.message} />
52
+ </div>
53
+ {/snippet}
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ClassValue } from 'svelte/elements';
3
+ import type { AiChatMessage } from './Chat.svelte';
4
+ interface Props {
5
+ class?: ClassValue;
6
+ message: AiChatMessage;
7
+ /** How attached files should be rendered */
8
+ attachedFile?: Snippet<[file: File]>;
9
+ /** How the message string should be rendered */
10
+ messageText?: Snippet<[{
11
+ message: AiChatMessage;
12
+ }]>;
13
+ }
14
+ declare const UserMessage: import("svelte").Component<Props, {}, "">;
15
+ type UserMessage = ReturnType<typeof UserMessage>;
16
+ export default UserMessage;
@@ -0,0 +1,5 @@
1
+ export { default as AiMessage } from './AiMessage.svelte';
2
+ export { default as AttachedFile } from './AttachedFile.svelte';
3
+ export { default as Chat, type AiChat, type AiChatMessage } from './Chat.svelte';
4
+ export { default as Markdown } from './Markdown.svelte';
5
+ export { default as UserMessage } from './UserMessage.svelte';
@@ -0,0 +1,5 @@
1
+ export { default as AiMessage } from './AiMessage.svelte';
2
+ export { default as AttachedFile } from './AttachedFile.svelte';
3
+ export { default as Chat } from './Chat.svelte';
4
+ export { default as Markdown } from './Markdown.svelte';
5
+ export { default as UserMessage } from './UserMessage.svelte';
@@ -0,0 +1,79 @@
1
+ <!--
2
+ @component
3
+ It's a checkbox
4
+ -->
5
+
6
+ <script lang="ts">
7
+ import { type Icon as LucideIcon, Minus, icons } from '@lucide/svelte';
8
+ import clsx from 'clsx';
9
+ import type { ClassValue } from 'svelte/elements';
10
+ import { twMerge } from 'tailwind-merge';
11
+
12
+ const Check = $derived(icons.Check);
13
+
14
+ type Props = {
15
+ class?: ClassValue;
16
+ /** `checked` has prioriy over `partial` */
17
+ checked?: boolean | null;
18
+ /** `checked` has prioriy over `partial` */
19
+ partial?: boolean | null;
20
+ id?: string;
21
+ /** if true, the onclick handler will not be called */
22
+ disabled?: boolean;
23
+ onclick?: () => void;
24
+ /** data-testid */
25
+ testId?: string;
26
+ };
27
+
28
+ let {
29
+ class: clazz,
30
+ checked = false,
31
+ partial = false,
32
+ id,
33
+ disabled = false,
34
+ onclick,
35
+ testId
36
+ }: Props = $props();
37
+
38
+ const {
39
+ icon: Icon,
40
+ innerClass,
41
+ style
42
+ }: { icon?: typeof LucideIcon; innerClass?: string; style?: string } = $derived.by(() => {
43
+ if (!checked && !partial) return { innerClass: 'border-surface-500' };
44
+ if (checked)
45
+ return {
46
+ icon: Check,
47
+ innerClass: 'bg-primary-500 border-primary-500 text-surface-50'
48
+ };
49
+ if (partial)
50
+ return {
51
+ icon: Minus,
52
+ innerClass: 'border-primary-700 text-primary-500'
53
+ };
54
+ return {};
55
+ });
56
+ </script>
57
+
58
+ <!-- svelte-ignore a11y_no_static_element_interactions-->
59
+ <svelte:element
60
+ this={onclick ? 'button' : 'div'}
61
+ type="button"
62
+ {id}
63
+ {disabled}
64
+ {style}
65
+ class={twMerge(
66
+ clsx(
67
+ 'box-border flex h-5 w-5 items-center justify-center overflow-hidden rounded border-2 transition-colors',
68
+ disabled && 'cursor-not-allowed opacity-70',
69
+ innerClass,
70
+ clazz
71
+ )
72
+ )}
73
+ {onclick}
74
+ data-testid={testId}
75
+ >
76
+ {#if Icon}
77
+ <Icon class="h-full w-full" size={16} strokeWidth={3} />
78
+ {/if}
79
+ </svelte:element>
@@ -0,0 +1,18 @@
1
+ import type { ClassValue } from 'svelte/elements';
2
+ type Props = {
3
+ class?: ClassValue;
4
+ /** `checked` has prioriy over `partial` */
5
+ checked?: boolean | null;
6
+ /** `checked` has prioriy over `partial` */
7
+ partial?: boolean | null;
8
+ id?: string;
9
+ /** if true, the onclick handler will not be called */
10
+ disabled?: boolean;
11
+ onclick?: () => void;
12
+ /** data-testid */
13
+ testId?: string;
14
+ };
15
+ /** It's a checkbox */
16
+ declare const Checkbox: import("svelte").Component<Props, {}, "">;
17
+ type Checkbox = ReturnType<typeof Checkbox>;
18
+ export default Checkbox;
@@ -0,0 +1,2 @@
1
+ export { default as Checkbox } from './checkbox/Checkbox.svelte';
2
+ export { default as Toggle } from './toggle/Toggle.svelte';
@@ -0,0 +1,2 @@
1
+ export { default as Checkbox } from './checkbox/Checkbox.svelte';
2
+ export { default as Toggle } from './toggle/Toggle.svelte';