@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.
- package/LICENCE +23 -0
- package/README.md +15 -0
- package/dist/components/ai/AiMessage.svelte +115 -0
- package/dist/components/ai/AiMessage.svelte.d.ts +18 -0
- package/dist/components/ai/AttachedFile.svelte +28 -0
- package/dist/components/ai/AttachedFile.svelte.d.ts +9 -0
- package/dist/components/ai/Chat.svelte +150 -0
- package/dist/components/ai/Chat.svelte.d.ts +40 -0
- package/dist/components/ai/Markdown.svelte +59 -0
- package/dist/components/ai/Markdown.svelte.d.ts +16 -0
- package/dist/components/ai/UserMessage.svelte +53 -0
- package/dist/components/ai/UserMessage.svelte.d.ts +16 -0
- package/dist/components/ai/index.d.ts +5 -0
- package/dist/components/ai/index.js +5 -0
- package/dist/components/basic/checkbox/Checkbox.svelte +79 -0
- package/dist/components/basic/checkbox/Checkbox.svelte.d.ts +18 -0
- package/dist/components/basic/index.d.ts +2 -0
- package/dist/components/basic/index.js +2 -0
- package/dist/components/basic/toggle/Toggle.svelte +47 -0
- package/dist/components/basic/toggle/Toggle.svelte.d.ts +12 -0
- package/dist/components/buttons/CopyToClipboardButton.svelte +33 -0
- package/dist/components/buttons/CopyToClipboardButton.svelte.d.ts +9 -0
- package/dist/components/index.d.ts +0 -0
- package/dist/components/index.js +1 -0
- package/dist/components/layout/heading/Heading.svelte +33 -0
- package/dist/components/layout/heading/Heading.svelte.d.ts +16 -0
- package/dist/components/layout/heading/index.d.ts +5 -0
- package/dist/components/layout/heading/index.js +5 -0
- package/dist/components/layout/hiddenBackground/HiddenBackground.svelte +48 -0
- package/dist/components/layout/hiddenBackground/HiddenBackground.svelte.d.ts +13 -0
- package/dist/components/layout/hiddenBackground/index.js +6 -0
- package/dist/components/layout/index.d.ts +7 -0
- package/dist/components/layout/index.js +7 -0
- package/dist/components/layout/modal/Modal.svelte +114 -0
- package/dist/components/layout/modal/Modal.svelte.d.ts +30 -0
- package/dist/components/layout/modal/ModalTest.svelte +16 -0
- package/dist/components/layout/modal/ModalTest.svelte.d.ts +9 -0
- package/dist/components/layout/popover/Popover.svelte +108 -0
- package/dist/components/layout/popover/Popover.svelte.d.ts +36 -0
- package/dist/components/layout/portal/Portal.svelte +23 -0
- package/dist/components/layout/portal/Portal.svelte.d.ts +15 -0
- package/dist/components/layout/tabs/Tab.svelte +80 -0
- package/dist/components/layout/tabs/Tab.svelte.d.ts +21 -0
- package/dist/components/layout/tabs/TabPanel.svelte +23 -0
- package/dist/components/layout/tabs/TabPanel.svelte.d.ts +10 -0
- package/dist/components/layout/tabs/Tabs.svelte +86 -0
- package/dist/components/layout/tabs/Tabs.svelte.d.ts +21 -0
- package/dist/components/layout/tabs/index.d.ts +26 -0
- package/dist/components/layout/tabs/index.js +8 -0
- package/dist/components/layout/tooltip/Tooltip.svelte +111 -0
- package/dist/components/layout/tooltip/Tooltip.svelte.d.ts +32 -0
- package/dist/components/toast/Toast.svelte +100 -0
- package/dist/components/toast/Toast.svelte.d.ts +16 -0
- package/dist/components/toast/index.d.ts +2 -0
- package/dist/components/toast/index.js +2 -0
- package/dist/components/toast/toasts.svelte.d.ts +26 -0
- package/dist/components/toast/toasts.svelte.js +67 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.js +2 -0
- package/dist/utils/actions/clickOutside.d.ts +11 -0
- package/dist/utils/actions/clickOutside.js +23 -0
- package/dist/utils/actions/focusTrap.d.ts +4 -0
- package/dist/utils/actions/focusTrap.js +64 -0
- package/dist/utils/actions/index.d.ts +5 -0
- package/dist/utils/actions/index.js +5 -0
- package/dist/utils/actions/portal.d.ts +9 -0
- package/dist/utils/actions/portal.js +39 -0
- package/dist/utils/actions/shortcut.d.ts +10 -0
- package/dist/utils/actions/shortcut.js +25 -0
- package/dist/utils/actions/visible.d.ts +5 -0
- package/dist/utils/actions/visible.js +14 -0
- package/dist/utils/functions/cookie.d.ts +12 -0
- package/dist/utils/functions/cookie.js +36 -0
- package/dist/utils/functions/index.d.ts +3 -0
- package/dist/utils/functions/index.js +3 -0
- package/dist/utils/functions/pseudoRandomId.d.ts +1 -0
- package/dist/utils/functions/pseudoRandomId.js +3 -0
- package/dist/utils/functions/queryParams.d.ts +1 -0
- package/dist/utils/functions/queryParams.js +14 -0
- package/package.json +107 -0
- package/src/lib/components/ai/AiMessage.svelte +115 -0
- package/src/lib/components/ai/AttachedFile.svelte +28 -0
- package/src/lib/components/ai/Chat.svelte +150 -0
- package/src/lib/components/ai/Markdown.svelte +59 -0
- package/src/lib/components/ai/UserMessage.svelte +53 -0
- package/src/lib/components/ai/index.ts +5 -0
- package/src/lib/components/basic/checkbox/Checkbox.svelte +79 -0
- package/src/lib/components/basic/checkbox/checkbox.svelte.spec.ts +39 -0
- package/src/lib/components/basic/index.ts +2 -0
- package/src/lib/components/basic/toggle/Toggle.svelte +47 -0
- package/src/lib/components/basic/toggle/toggle.svelte.spec.ts +19 -0
- package/src/lib/components/buttons/CopyToClipboardButton.svelte +33 -0
- package/src/lib/components/index.ts +0 -0
- package/src/lib/components/layout/heading/Heading.svelte +33 -0
- package/src/lib/components/layout/heading/index.ts +7 -0
- package/src/lib/components/layout/hiddenBackground/HiddenBackground.svelte +48 -0
- package/src/lib/components/layout/hiddenBackground/index.ts +8 -0
- package/src/lib/components/layout/index.ts +7 -0
- package/src/lib/components/layout/modal/Modal.svelte +114 -0
- package/src/lib/components/layout/modal/ModalTest.svelte +16 -0
- package/src/lib/components/layout/modal/modal.svelte.spec.ts +39 -0
- package/src/lib/components/layout/popover/Popover.svelte +108 -0
- package/src/lib/components/layout/portal/Portal.svelte +23 -0
- package/src/lib/components/layout/tabs/Tab.svelte +80 -0
- package/src/lib/components/layout/tabs/TabPanel.svelte +23 -0
- package/src/lib/components/layout/tabs/Tabs.svelte +86 -0
- package/src/lib/components/layout/tabs/Tabs.test.svelte +5 -0
- package/src/lib/components/layout/tabs/index.ts +10 -0
- package/src/lib/components/layout/tooltip/Tooltip.svelte +111 -0
- package/src/lib/components/toast/Toast.svelte +100 -0
- package/src/lib/components/toast/index.ts +2 -0
- package/src/lib/components/toast/toasts.svelte.ts +89 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/utils/actions/clickOutside.svelte.spec.ts +67 -0
- package/src/lib/utils/actions/clickOutside.ts +38 -0
- package/src/lib/utils/actions/focusTrap.ts +65 -0
- package/src/lib/utils/actions/index.ts +5 -0
- package/src/lib/utils/actions/portal.ts +43 -0
- package/src/lib/utils/actions/shortcut.svelte.spec.ts +19 -0
- package/src/lib/utils/actions/shortcut.ts +35 -0
- package/src/lib/utils/actions/visible.ts +28 -0
- package/src/lib/utils/functions/cookie.svelte.spec.ts +55 -0
- package/src/lib/utils/functions/cookie.ts +46 -0
- package/src/lib/utils/functions/index.ts +3 -0
- package/src/lib/utils/functions/pseudoRandomId.spec.ts +19 -0
- package/src/lib/utils/functions/pseudoRandomId.ts +4 -0
- package/src/lib/utils/functions/queryParams.spec.ts +25 -0
- package/src/lib/utils/functions/queryParams.ts +15 -0
|
@@ -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,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,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,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,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,39 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/svelte';
|
|
3
|
+
import { userEvent } from '@testing-library/user-event';
|
|
4
|
+
import { fn } from '@vitest/spy';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { Checkbox } from '../index';
|
|
7
|
+
|
|
8
|
+
const testId = 'checkbox';
|
|
9
|
+
|
|
10
|
+
describe('Basic/Checkbox', () => {
|
|
11
|
+
it('renders the checked icon', async () => {
|
|
12
|
+
render(Checkbox, { testId, checked: true });
|
|
13
|
+
|
|
14
|
+
const checkbox = screen.getByTestId(testId);
|
|
15
|
+
expect(checkbox).toBeVisible();
|
|
16
|
+
const icon = checkbox.querySelector('svg');
|
|
17
|
+
expect(icon).toBeVisible();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('fires click event when clicked', async () => {
|
|
21
|
+
const callback = fn();
|
|
22
|
+
render(Checkbox, { testId, onclick: callback });
|
|
23
|
+
|
|
24
|
+
const checkbox = screen.getByTestId(testId);
|
|
25
|
+
expect(checkbox).toBeVisible();
|
|
26
|
+
checkbox.click();
|
|
27
|
+
expect(callback).toHaveBeenCalledOnce();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("doesn't fire click event when disabled", async () => {
|
|
31
|
+
const callback = fn();
|
|
32
|
+
render(Checkbox, { testId, onclick: callback, disabled: true });
|
|
33
|
+
|
|
34
|
+
const checkbox = screen.getByTestId(testId);
|
|
35
|
+
expect(checkbox).toBeVisible();
|
|
36
|
+
await userEvent.click(checkbox);
|
|
37
|
+
expect(callback).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
value?: boolean;
|
|
9
|
+
class?: ClassValue;
|
|
10
|
+
onclick?: () => void;
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
testId?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let { value, class: clazz, onclick, children, testId }: Props = $props();
|
|
16
|
+
|
|
17
|
+
let thumbWidth = $state(0);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<svelte:element
|
|
21
|
+
this={onclick ? 'button' : 'div'}
|
|
22
|
+
class={twMerge(
|
|
23
|
+
clsx(
|
|
24
|
+
'flex h-5 w-9 items-center rounded-full border p-0.5',
|
|
25
|
+
value ? 'bg-primary-500 border-primary-500' : 'bg-surface-500/30 border-surface-500/60',
|
|
26
|
+
clazz
|
|
27
|
+
)
|
|
28
|
+
)}
|
|
29
|
+
type={onclick ? 'button' : undefined}
|
|
30
|
+
role={onclick ? 'button' : undefined}
|
|
31
|
+
{onclick}
|
|
32
|
+
tabindex="0"
|
|
33
|
+
data-testid={testId}
|
|
34
|
+
>
|
|
35
|
+
<div class="relative flex h-full w-full flex-row items-center">
|
|
36
|
+
<div
|
|
37
|
+
class={[
|
|
38
|
+
'relative flex aspect-square h-full items-center justify-center rounded-full transition-all',
|
|
39
|
+
value ? 'bg-surface-50' : 'bg-surface-600'
|
|
40
|
+
]}
|
|
41
|
+
style={value ? `left: calc(100% - ${thumbWidth}px);` : 'left: 0;'}
|
|
42
|
+
bind:clientWidth={thumbWidth}
|
|
43
|
+
>
|
|
44
|
+
{@render children?.()}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</svelte:element>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/svelte';
|
|
3
|
+
import { fn } from '@vitest/spy';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import Toggle from './Toggle.svelte';
|
|
6
|
+
|
|
7
|
+
const testId = 'toggle';
|
|
8
|
+
|
|
9
|
+
describe('Basic/Toggle', () => {
|
|
10
|
+
it('fires click event when clicked', async () => {
|
|
11
|
+
const callback = fn();
|
|
12
|
+
render(Toggle, { testId, onclick: callback });
|
|
13
|
+
|
|
14
|
+
const toggle = screen.getByTestId(testId);
|
|
15
|
+
expect(toggle).toBeVisible();
|
|
16
|
+
toggle.click();
|
|
17
|
+
expect(callback).toHaveBeenCalledOnce();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { Copy } from '@lucide/svelte';
|
|
3
|
+
import type { ClassValue } from 'svelte/elements';
|
|
4
|
+
import { Toasts } from '../toast';
|
|
5
|
+
|
|
6
|
+
let lastCopied = $state<string>();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
interface Props {
|
|
11
|
+
text: string;
|
|
12
|
+
class?: ClassValue;
|
|
13
|
+
toastMessage?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { text, class: clazz = 'text-xl', toastMessage }: Props = $props();
|
|
17
|
+
|
|
18
|
+
async function copyText() {
|
|
19
|
+
await navigator.clipboard.writeText(text);
|
|
20
|
+
lastCopied = text;
|
|
21
|
+
if (toastMessage) {
|
|
22
|
+
Toasts.trigger({
|
|
23
|
+
variant: 'success',
|
|
24
|
+
message: toastMessage,
|
|
25
|
+
icon: Copy
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<button type="button" class={['text-surface-500 btn', clazz]} onclick={copyText}>
|
|
32
|
+
<Copy class={['h-5 w-5']} />
|
|
33
|
+
</button>
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
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
|
+
|
|
7
|
+
export interface Props {
|
|
8
|
+
children: Snippet;
|
|
9
|
+
class?: ClassValue;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function setClasses(c: ClassValue) {
|
|
13
|
+
defaultClasses = c;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let defaultClasses = $state<ClassValue>('');
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script lang="ts">
|
|
20
|
+
let { children, class: clazz }: Props = $props();
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<!--
|
|
24
|
+
@component
|
|
25
|
+
A component for unified headings.
|
|
26
|
+
Classes that are set using the `setDefaultClasses` function will be applied to all headings,
|
|
27
|
+
including the ones used in other components of this lib, e.g. the `Modal`.
|
|
28
|
+
If you set the `defaultClasses`, make sure to call it before using a component that uses the heading component (e.g. your root `+layout.svelte`).
|
|
29
|
+
-->
|
|
30
|
+
|
|
31
|
+
<h2 class={twMerge(clsx('truncate text-lg font-bold select-none', defaultClasses, clazz))}>
|
|
32
|
+
{@render children()}
|
|
33
|
+
</h2>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import type { ClassValue } from 'svelte/elements';
|
|
5
|
+
import { fade } from 'svelte/transition';
|
|
6
|
+
import { twMerge } from 'tailwind-merge';
|
|
7
|
+
import { focusTrap, shortcut } from '../../../utils/actions/index';
|
|
8
|
+
|
|
9
|
+
let globalClass = $state<ClassValue>();
|
|
10
|
+
|
|
11
|
+
export function setClasses(value: ClassValue) {
|
|
12
|
+
globalClass = value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const TEST_ID = 'background';
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
interface Props {
|
|
20
|
+
class?: ClassValue;
|
|
21
|
+
/** Gets called when the dialog is clicked */
|
|
22
|
+
onclose?: () => void;
|
|
23
|
+
children: Snippet;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { class: clazz, onclose, children }: Props = $props();
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<dialog
|
|
30
|
+
class={twMerge(
|
|
31
|
+
clsx(
|
|
32
|
+
'bg-surface-950-50/40 absolute top-0 left-0 z-40 m-0 h-full w-full p-0',
|
|
33
|
+
globalClass,
|
|
34
|
+
clazz
|
|
35
|
+
)
|
|
36
|
+
)}
|
|
37
|
+
open
|
|
38
|
+
use:focusTrap={true}
|
|
39
|
+
use:shortcut={{
|
|
40
|
+
code: 'Escape',
|
|
41
|
+
callback: onclose ?? (() => {})
|
|
42
|
+
}}
|
|
43
|
+
onclick={onclose}
|
|
44
|
+
transition:fade={{ duration: 200 }}
|
|
45
|
+
data-testid={TEST_ID}
|
|
46
|
+
>
|
|
47
|
+
{@render children()}
|
|
48
|
+
</dialog>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as Heading } from './heading/index';
|
|
2
|
+
export { default as HiddenBackground } from './hiddenBackground/index';
|
|
3
|
+
export { default as Modal } from './modal/Modal.svelte';
|
|
4
|
+
export { default as Popover } from './popover/Popover.svelte';
|
|
5
|
+
export { default as Portal } from './portal/Portal.svelte';
|
|
6
|
+
export { default as Tabs } from './tabs/index';
|
|
7
|
+
export { default as Tooltip } from './tooltip/Tooltip.svelte';
|