@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
@@ -0,0 +1,114 @@
1
+ <script lang="ts" module>
2
+ import { X } from '@lucide/svelte';
3
+ import clsx from 'clsx';
4
+ import type { Snippet } from 'svelte';
5
+ import type { ClassValue } from 'svelte/elements';
6
+ import { twMerge } from 'tailwind-merge';
7
+ import Heading from '../heading/Heading.svelte';
8
+ import HiddenBackground from '../hiddenBackground/HiddenBackground.svelte';
9
+
10
+ /** Props for the modal, expose if you overwrite the defaults in a custom component */
11
+ export interface ModalProps {
12
+ /** Class of the modal itself, does not apply to the inner div */
13
+ class?: ClassValue;
14
+ /** Class of the div wrapping the children */
15
+ innerClass?: ClassValue;
16
+ /** If `true`, the modal will be open */
17
+ b_open: boolean;
18
+ /** Content of the modal */
19
+ children?: Snippet;
20
+ /** Style applied to the */
21
+ style?: string;
22
+ /** If `true` the modal will not close when clicking outside of it */
23
+ preventClosing?: boolean;
24
+ /** Variant of the modal, applies styling to the header */
25
+ variant?: ModalVariant;
26
+ title?: string;
27
+ }
28
+
29
+ export type ModalVariant = 'success' | 'warning' | 'error' | 'info';
30
+ </script>
31
+
32
+ <script lang="ts">
33
+ interface Props extends ModalProps {
34
+ /** If you don't want the title and close button to be included you can overwrite the default modal */
35
+ modal?: Snippet;
36
+ testId?: string;
37
+ }
38
+
39
+ let {
40
+ class: clazz = 'flex ',
41
+ style,
42
+ title,
43
+ b_open = $bindable(),
44
+ children,
45
+ modal,
46
+ preventClosing = false,
47
+ variant,
48
+ innerClass,
49
+ testId
50
+ }: Props = $props();
51
+
52
+ function close() {
53
+ if (preventClosing) return;
54
+ b_open = false;
55
+ }
56
+ </script>
57
+
58
+ <!--
59
+ @component
60
+ A modal, comes with a title, close button and different variants per default.
61
+ -->
62
+ {#if b_open}
63
+ <HiddenBackground
64
+ onclose={close}
65
+ class="flex h-full w-full flex-col items-center justify-start p-16"
66
+ >
67
+ {#if modal}
68
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
69
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
70
+ <div class={clazz} onclick={(e) => e.stopPropagation()} data-testid={testId} {style}>
71
+ {@render modal()}
72
+ </div>
73
+ {:else}
74
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
75
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
76
+ <div
77
+ class={twMerge(
78
+ clsx([
79
+ 'bg-surface-50-950 relative flex max-h-full max-w-full flex-col overflow-hidden rounded',
80
+ clazz
81
+ ])
82
+ )}
83
+ {style}
84
+ onclick={(e) => e.stopPropagation()}
85
+ data-testid={testId}
86
+ >
87
+ <div
88
+ class={[
89
+ 'flex flex-row items-center justify-between gap-4 px-4 py-3',
90
+ // !variant && 'pt-3',
91
+ variant === 'success' && 'preset-tonal-success',
92
+ variant === 'warning' && 'preset-tonal-warning',
93
+ variant === 'error' && 'preset-tonal-error',
94
+ variant === 'info' && 'preset-tonal-primary'
95
+ ]}
96
+ >
97
+ {#if title}
98
+ <Heading>{title}</Heading>
99
+ {/if}
100
+ <button class="group ml-auto flex justify-end" type="button" onclick={close}>
101
+ <X class="h-full w-auto transition-[stroke-width] group-hover:stroke-3" />
102
+ </button>
103
+ </div>
104
+ <div
105
+ class={twMerge(
106
+ clsx('flex flex-col gap-4 overflow-hidden bg-inherit p-4 pt-2', innerClass)
107
+ )}
108
+ >
109
+ {@render children?.()}
110
+ </div>
111
+ </div>
112
+ {/if}
113
+ </HiddenBackground>
114
+ {/if}
@@ -0,0 +1,16 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Modal from './Modal.svelte';
4
+
5
+ interface Props {
6
+ open: boolean;
7
+ testId: string;
8
+ children: Snippet;
9
+ }
10
+
11
+ let { open, ...props }: Props = $props();
12
+
13
+ let b_open = $state(open);
14
+ </script>
15
+
16
+ <Modal bind:b_open {...props} />
@@ -0,0 +1,39 @@
1
+ import '@testing-library/jest-dom/vitest';
2
+ import { render, screen } from '@testing-library/svelte';
3
+ import { createRawSnippet } from 'svelte';
4
+ import { describe, expect, it } from 'vitest';
5
+ import ModalTest from './ModalTest.svelte';
6
+
7
+ const testId = 'modal';
8
+ const contentTestId = 'modal-content';
9
+
10
+ const children = createRawSnippet(() => ({
11
+ render: () => `<p data-testId="${contentTestId}">Content</p>`
12
+ }));
13
+
14
+ describe('Layout/Modal', () => {
15
+ it('renders', async () => {
16
+ render(ModalTest, {
17
+ open: true,
18
+ testId,
19
+ children
20
+ });
21
+ const modal = screen.getByTestId(testId);
22
+ expect(modal).toBeVisible();
23
+ const content = screen.getByTestId(contentTestId);
24
+ expect(content).toBeVisible();
25
+ });
26
+
27
+ // this is bugged for some f'ing reason
28
+ // it('closes when clicking outside', async () => {
29
+ // render(ModalTest, {
30
+ // open: true,
31
+ // testId,
32
+ // children
33
+ // });
34
+ // const modal = screen.getByTestId(testId);
35
+ // const background = screen.getByTestId(HiddenBackground.TEST_ID);
36
+ // await userEvent.click(background!);
37
+ // expect(modal).not.toBeVisible();
38
+ // });
39
+ });
@@ -0,0 +1,108 @@
1
+ <script lang="ts" module>
2
+ import { browser } from '$app/environment';
3
+ import {
4
+ autoPlacement,
5
+ autoUpdate,
6
+ computePosition,
7
+ flip,
8
+ shift,
9
+ type ComputePositionConfig
10
+ } from '@floating-ui/dom';
11
+ import clsx from 'clsx';
12
+ import type { Snippet } from 'svelte';
13
+ import type { ClassValue } from 'svelte/elements';
14
+ import { twMerge } from 'tailwind-merge';
15
+ import { clickOutside } from '../../../utils/actions/clickOutside';
16
+
17
+ /** Possible placements for the popover */
18
+ export type PopoverPlacement = ComputePositionConfig['placement'];
19
+ </script>
20
+
21
+ <script lang="ts">
22
+ type Props = {
23
+ class?: ClassValue;
24
+ /** Whether the popover is open or not */
25
+ b_open: boolean;
26
+ style?: string;
27
+ /** The element the popover will be positioned relative to */
28
+ target: Element | undefined;
29
+ /**
30
+ * Where the popover should be positioned relative to the target.
31
+ *
32
+ * default: `bottom-start`
33
+ */
34
+ placement?: PopoverPlacement;
35
+ /**
36
+ * Callback that is called when the user clicks outside the popover or the target element.
37
+ */
38
+ onClickOutside?: (e: MouseEvent) => void;
39
+ /** If set to `true`, the nested component will not be unmounted when the popover is closed */
40
+ keepMounted?: boolean;
41
+ children: Snippet;
42
+ /**
43
+ * Whether to place the popover automatically
44
+ *
45
+ * [Further reading](https://floating-ui.com/docs/autoPlacement)
46
+ */
47
+ autoplacement?: boolean;
48
+ };
49
+
50
+ let {
51
+ class: clazz,
52
+ b_open = $bindable(false),
53
+ style: externalStyle,
54
+ target,
55
+ placement = 'bottom-start',
56
+ onClickOutside = () => {
57
+ b_open = false;
58
+ },
59
+ keepMounted = false,
60
+ children,
61
+ autoplacement
62
+ }: Props = $props();
63
+
64
+ let style: string = $state('');
65
+ let popover: HTMLDivElement | undefined = $state();
66
+
67
+ const postion = async (open: boolean) => {
68
+ if (!open || !popover || !browser || !target) return;
69
+ const { x, y } = await computePosition(target, popover, {
70
+ middleware: [shift(), ...(autoplacement ? [autoPlacement()] : [flip()])],
71
+ placement
72
+ });
73
+ style = `top: ${y}px; left: ${x}px;`;
74
+ };
75
+
76
+ let cleanup: () => void = () => {};
77
+ $effect(() => {
78
+ if (browser && popover && target)
79
+ if (b_open) {
80
+ cleanup = autoUpdate(target, popover, () => postion(b_open));
81
+ } else {
82
+ cleanup();
83
+ }
84
+ });
85
+
86
+ // TODO: this is kinda hacky
87
+ $effect(() => {
88
+ [popover, target];
89
+ postion(b_open);
90
+ });
91
+ </script>
92
+
93
+ <!--
94
+ @component
95
+ A popover, positions itself relative to a target element.
96
+ -->
97
+ {#if b_open || keepMounted}
98
+ <div
99
+ class={twMerge(
100
+ clsx('absolute z-30', !keepMounted && clazz, keepMounted && !b_open ? 'hidden' : clazz)
101
+ )}
102
+ style={style + ' ' + externalStyle}
103
+ bind:this={popover}
104
+ use:clickOutside={{ callback: onClickOutside, target }}
105
+ >
106
+ {@render children()}
107
+ </div>
108
+ {/if}
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { portal } from '../../../utils/actions/index';
4
+
5
+ interface Props {
6
+ children: Snippet;
7
+ target?: string | HTMLElement;
8
+ }
9
+
10
+ let { children, target = 'body' }: Props = $props();
11
+ </script>
12
+
13
+ <!--
14
+ @component
15
+ A portal, renders its children in a different DOM element.
16
+
17
+ Wrapps the `use:portal` action.
18
+
19
+ **Use sparingy as it can make the DOM structure confusing**
20
+ -->
21
+ <div use:portal={target} hidden>
22
+ {@render children()}
23
+ </div>
@@ -0,0 +1,80 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import { pseudoRandomId } from '$lib/utils/functions/index';
4
+ import clsx from 'clsx';
5
+ import { onMount, type Snippet } from 'svelte';
6
+ import type { ClassValue } from 'svelte/elements';
7
+ import { twMerge } from 'tailwind-merge';
8
+ import { getTabContext } from './Tabs.svelte';
9
+
10
+ type Props = {
11
+ class?: (selected: boolean) => ClassValue;
12
+ id?: string | undefined;
13
+ /**
14
+ * If this is set the element will be a link.
15
+ *
16
+ * This is useful if your tabs are in a `+layout.svelte` and the Panels are seperate pages.
17
+ */
18
+ href?: string | undefined;
19
+ children: Snippet<[{ selected: boolean }]>;
20
+ testId?: string;
21
+ /** If `href` is set, this can be used to highlight an active tab */
22
+ active?: boolean;
23
+ };
24
+
25
+ let {
26
+ class: clazz = (selected: boolean) => [selected && 'text-primary-500 underline'],
27
+ id,
28
+ href,
29
+ children,
30
+ testId,
31
+ active
32
+ }: Props = $props();
33
+
34
+ const tab = pseudoRandomId('tab-');
35
+ const tabs = getTabContext();
36
+
37
+ const tabId = id || tab;
38
+
39
+ const selected = $derived.by(() => {
40
+ if (typeof active === 'boolean') {
41
+ return active;
42
+ } else if (href) {
43
+ return page.url.pathname.startsWith(href);
44
+ } else if (tabs && tabs.selectedTab === tabId) {
45
+ return true;
46
+ } else {
47
+ return false;
48
+ }
49
+ });
50
+
51
+ onMount(() => {
52
+ if (href) return;
53
+ tabs.registerTab(tabId);
54
+ });
55
+ </script>
56
+
57
+ <svelte:element
58
+ this={href ? 'a' : 'button'}
59
+ class={twMerge(
60
+ clsx(
61
+ 'btn flex h-fit w-fit items-center justify-center px-0 text-xl font-bold',
62
+ clazz(selected)
63
+ )
64
+ )}
65
+ onclick={href
66
+ ? undefined
67
+ : () => {
68
+ tabs.selectedTab = tabId;
69
+ }}
70
+ type={href ? undefined : 'button'}
71
+ {href}
72
+ data-testid={testId}
73
+ role="tab"
74
+ tabindex="0"
75
+ aria-selected={selected}
76
+ >
77
+ {@render children({
78
+ selected
79
+ })}
80
+ </svelte:element>
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import { pseudoRandomId } from '$lib/utils/functions/index';
3
+ import { type Snippet } from 'svelte';
4
+ import { getTabContext } from './Tabs.svelte';
5
+
6
+ type Props = {
7
+ keepMounted?: boolean;
8
+ children: Snippet<[{ visible: boolean }]>;
9
+ };
10
+
11
+ let { keepMounted = false, children }: Props = $props();
12
+
13
+ const panel = pseudoRandomId('tab-panel-');
14
+ const tabs = getTabContext();
15
+
16
+ tabs.registerPanel(panel);
17
+ </script>
18
+
19
+ {#if keepMounted}
20
+ {@render children({ visible: tabs.selectedPanel === panel })}
21
+ {:else if tabs.selectedPanel === panel}
22
+ {@render children({ visible: true })}
23
+ {/if}
@@ -0,0 +1,86 @@
1
+ <script lang="ts" module>
2
+ import { getContext, onDestroy, setContext, type Snippet } from 'svelte';
3
+ import type { ClassValue } from 'svelte/elements';
4
+
5
+ interface TabContext {
6
+ registerTab: (tab: string) => void;
7
+ registerPanel: (panel: string) => void;
8
+ selectedTab: string | undefined;
9
+ tabs: string[];
10
+ selectedPanel: string | undefined;
11
+ }
12
+ const TABS = {};
13
+
14
+ function setTabContext(context: TabContext) {
15
+ setContext<TabContext>(TABS, context);
16
+ }
17
+
18
+ export function getTabContext() {
19
+ return getContext<TabContext>(TABS);
20
+ }
21
+ </script>
22
+
23
+ <script lang="ts">
24
+ type Props = {
25
+ class?: ClassValue;
26
+ children: Snippet;
27
+ b_index?: number;
28
+ };
29
+
30
+ let { class: clazz = '', children, b_index = $bindable(0) }: Props = $props();
31
+
32
+ let allTabs: string[] = $state([]);
33
+ let panels: string[] = $state([]);
34
+
35
+ export const forward = () => {
36
+ if (b_index >= panels.length - 1) {
37
+ b_index = 0;
38
+ } else {
39
+ b_index++;
40
+ }
41
+ };
42
+
43
+ export const back = () => {
44
+ if (b_index === 0) {
45
+ b_index = panels.length - 1;
46
+ } else {
47
+ b_index--;
48
+ }
49
+ };
50
+
51
+ setTabContext({
52
+ registerTab: (tab: string) => {
53
+ allTabs.push(tab);
54
+ onDestroy(() => {
55
+ const i = allTabs.indexOf(tab);
56
+ allTabs.splice(i, 1);
57
+ });
58
+ },
59
+
60
+ registerPanel: (panel: string) => {
61
+ panels.push(panel);
62
+ b_index = b_index;
63
+ onDestroy(() => {
64
+ panels.filter((p) => p !== panel);
65
+ });
66
+ },
67
+
68
+ get selectedTab() {
69
+ return allTabs[b_index];
70
+ },
71
+ set selectedTab(tab: string) {
72
+ b_index = allTabs.indexOf(tab);
73
+ },
74
+ get selectedPanel() {
75
+ return panels[b_index];
76
+ },
77
+ set selectedPanel(panel: string) {
78
+ b_index = panels.indexOf(panel);
79
+ },
80
+ tabs: allTabs
81
+ });
82
+ </script>
83
+
84
+ <div class={[clazz]}>
85
+ {@render children()}
86
+ </div>
@@ -0,0 +1,5 @@
1
+ <script lang="ts">
2
+ interface Props {}
3
+
4
+ let {}: Props = $props();
5
+ </script>
@@ -0,0 +1,10 @@
1
+ import Tab from './Tab.svelte';
2
+ import TabPanel from './TabPanel.svelte';
3
+ import { default as TabsComponent } from './Tabs.svelte';
4
+
5
+ const Tabs = Object.assign(TabsComponent, {
6
+ Tab: Tab,
7
+ Panel: TabPanel
8
+ });
9
+
10
+ export default Tabs;
@@ -0,0 +1,111 @@
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
+ import Popover, { type PopoverPlacement } from '../popover/Popover.svelte';
7
+ import Portal from '../portal/Portal.svelte';
8
+
9
+ export interface Props {
10
+ children?: Snippet;
11
+ /** The content of the tooltip */
12
+ tooltip: string | Snippet;
13
+ /** The class of the element that triggers the tooltip */
14
+ class?: ClassValue;
15
+ /** The class of the tooltip itself */
16
+ tooltipClass?: ClassValue;
17
+ style?: string;
18
+ onclick?: (e: Event) => void;
19
+ /** If the href is set, the resulting element will be a link to the href */
20
+ href?: string;
21
+ /**
22
+ * The delay before the tooltip is shown in ms.
23
+ *
24
+ * default: `500`
25
+ */
26
+ timeout?: number;
27
+ /**
28
+ * Where the tooltip should be placed
29
+ *
30
+ * default: `top`
31
+ */
32
+ placement?: PopoverPlacement;
33
+ }
34
+ </script>
35
+
36
+ <script lang="ts">
37
+ let {
38
+ children,
39
+ tooltip,
40
+ class: clazz,
41
+ style,
42
+ onclick,
43
+ href,
44
+ timeout = 500,
45
+ tooltipClass
46
+ }: Props = $props();
47
+
48
+ let target = $state<HTMLElement>();
49
+
50
+ let open = $state(false);
51
+
52
+ let showTimeout: number;
53
+ function onpointerenter() {
54
+ clearTimeout(timeout);
55
+ if (timeout === 0) {
56
+ open = true;
57
+ } else {
58
+ showTimeout = setTimeout(() => {
59
+ open = true;
60
+ }, timeout) as unknown as number;
61
+ }
62
+ }
63
+
64
+ function onpointerleave() {
65
+ clearTimeout(timeout);
66
+ open = false;
67
+ }
68
+ </script>
69
+
70
+ <!--
71
+ @component
72
+ Shows additional information when hovering over an element.
73
+ -->
74
+
75
+ <!-- Ignoring this error is fine since it's a false positive -->
76
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
77
+ <svelte:element
78
+ this={href ? 'a' : onclick ? 'button' : 'div'}
79
+ {href}
80
+ type={onclick ? 'button' : undefined}
81
+ class={clazz}
82
+ bind:this={target}
83
+ {onpointerenter}
84
+ {onpointerleave}
85
+ {style}
86
+ {onclick}
87
+ >
88
+ {@render children?.()}
89
+ </svelte:element>
90
+
91
+ {#if open}
92
+ <Portal>
93
+ <Popover
94
+ bind:b_open={open}
95
+ {target}
96
+ placement="top"
97
+ class={twMerge(
98
+ clsx(
99
+ 'bg-surface-100-800-token max-w-96 -translate-y-0.5 rounded px-4 py-1 shadow-lg',
100
+ tooltipClass
101
+ )
102
+ )}
103
+ >
104
+ {#if typeof tooltip === 'string'}
105
+ {tooltip}
106
+ {:else}
107
+ {@render tooltip()}
108
+ {/if}
109
+ </Popover>
110
+ </Portal>
111
+ {/if}