@ims360/svelte-ivory 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/colorTheme.svelte.d.ts +14 -0
  2. package/dist/colorTheme.svelte.d.ts.map +1 -0
  3. package/dist/colorTheme.svelte.js +59 -0
  4. package/dist/components/ai/index.d.ts +0 -4
  5. package/dist/components/ai/index.d.ts.map +1 -1
  6. package/dist/components/ai/index.js +0 -4
  7. package/dist/components/basic/ThemeController.svelte +49 -0
  8. package/dist/components/basic/ThemeController.svelte.d.ts +8 -0
  9. package/dist/components/basic/ThemeController.svelte.d.ts.map +1 -0
  10. package/dist/components/layout/popover/Popover.svelte +11 -7
  11. package/dist/components/layout/popover/Popover.svelte.d.ts.map +1 -1
  12. package/dist/components/layout/tooltip/Tooltip.svelte +1 -11
  13. package/dist/components/layout/tooltip/Tooltip.svelte.d.ts.map +1 -1
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -0
  17. package/package.json +9 -1
  18. package/src/lib/colorTheme.svelte.ts +65 -0
  19. package/src/lib/components/ai/index.ts +0 -4
  20. package/src/lib/components/basic/ThemeController.svelte +49 -0
  21. package/src/lib/components/layout/popover/Popover.svelte +11 -7
  22. package/src/lib/components/layout/tooltip/Tooltip.svelte +1 -11
  23. package/src/lib/index.ts +1 -0
  24. package/dist/components/ai/AiMessage.svelte +0 -114
  25. package/dist/components/ai/AiMessage.svelte.d.ts +0 -19
  26. package/dist/components/ai/AiMessage.svelte.d.ts.map +0 -1
  27. package/dist/components/ai/AttachedFile.svelte +0 -28
  28. package/dist/components/ai/AttachedFile.svelte.d.ts +0 -10
  29. package/dist/components/ai/AttachedFile.svelte.d.ts.map +0 -1
  30. package/dist/components/ai/Chat.svelte +0 -149
  31. package/dist/components/ai/Chat.svelte.d.ts +0 -43
  32. package/dist/components/ai/Chat.svelte.d.ts.map +0 -1
  33. package/dist/components/ai/UserMessage.svelte +0 -52
  34. package/dist/components/ai/UserMessage.svelte.d.ts +0 -17
  35. package/dist/components/ai/UserMessage.svelte.d.ts.map +0 -1
  36. package/src/lib/components/ai/AiMessage.svelte +0 -114
  37. package/src/lib/components/ai/AttachedFile.svelte +0 -28
  38. package/src/lib/components/ai/Chat.svelte +0 -149
  39. package/src/lib/components/ai/UserMessage.svelte +0 -52
@@ -0,0 +1,14 @@
1
+ import type { Handle } from '@sveltejs/kit';
2
+ export type ColorThemePreference = 'system' | 'light' | 'dark';
3
+ declare class ThemeController {
4
+ private currentTheme;
5
+ constructor();
6
+ get theme(): ColorThemePreference;
7
+ get preference(): ColorThemePreference;
8
+ set theme(value: ColorThemePreference);
9
+ /** Add to hook sequence to enable correct prerendering ot the theme */
10
+ handle: Handle;
11
+ }
12
+ export declare const ColorTheme: ThemeController;
13
+ export {};
14
+ //# sourceMappingURL=colorTheme.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"colorTheme.svelte.d.ts","sourceRoot":"","sources":["../src/lib/colorTheme.svelte.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAI5C,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAE/D,cAAM,eAAe;IACjB,OAAO,CAAC,YAAY,CAA0C;;IAY9D,IAAI,KAAK,IAQQ,oBAAoB,CANpC;IAED,IAAI,UAAU,yBAEb;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,oBAAoB,EAepC;IAED,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAcZ;CACL;AAED,eAAO,MAAM,UAAU,iBAAwB,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { browser } from '$app/environment';
2
+ import { cookie } from '@ims360/svelte-ivory/utils/functions';
3
+ const COOKIE_THEME_KEY = 'theme';
4
+ class ThemeController {
5
+ currentTheme = $state('system');
6
+ constructor() {
7
+ if (!browser)
8
+ return;
9
+ const value = cookie.get(COOKIE_THEME_KEY);
10
+ if (value === 'system' || value === 'dark' || value === 'light') {
11
+ this.theme = value;
12
+ }
13
+ else {
14
+ this.theme = 'system';
15
+ }
16
+ }
17
+ get theme() {
18
+ return this.currentTheme;
19
+ }
20
+ get preference() {
21
+ return this.currentTheme;
22
+ }
23
+ set theme(value) {
24
+ if (!browser)
25
+ return;
26
+ this.currentTheme = value;
27
+ cookie.set({
28
+ name: COOKIE_THEME_KEY,
29
+ value
30
+ });
31
+ if (value === 'system') {
32
+ const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
33
+ document.documentElement.classList.toggle('dark', systemDark);
34
+ }
35
+ else if (value === 'light') {
36
+ document.documentElement.classList.toggle('dark', false);
37
+ }
38
+ else {
39
+ document.documentElement.classList.toggle('dark', true);
40
+ }
41
+ }
42
+ /** Add to hook sequence to enable correct prerendering ot the theme */
43
+ handle = ({ event, resolve }) => {
44
+ const theme = event.cookies.get(COOKIE_THEME_KEY);
45
+ let isDark = false;
46
+ if (theme !== undefined && theme !== 'system') {
47
+ isDark = theme === 'dark';
48
+ }
49
+ else {
50
+ isDark = event.request.headers.get('sec-ch-prefers-color-scheme') === 'dark';
51
+ }
52
+ if (!isDark)
53
+ return resolve(event);
54
+ return resolve(event, {
55
+ transformPageChunk: ({ html }) => html.replace('class="', 'class="dark ')
56
+ });
57
+ };
58
+ }
59
+ export const ColorTheme = new ThemeController();
@@ -1,6 +1,2 @@
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
1
  export { default as Markdown } from './Markdown.svelte';
5
- export { default as UserMessage } from './UserMessage.svelte';
6
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/ai/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,KAAK,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AACjF,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/ai/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC"}
@@ -1,5 +1 @@
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
1
  export { default as Markdown } from './Markdown.svelte';
5
- export { default as UserMessage } from './UserMessage.svelte';
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import { ColorTheme, type ColorThemePreference } from '../../colorTheme.svelte';
3
+ import { merge } from '@ims360/svelte-ivory/utils/functions';
4
+ import { Icon, Monitor, Moon, Sun } from '@lucide/svelte';
5
+ import type { ClassValue } from 'svelte/elements';
6
+
7
+ type Props = {
8
+ class: ClassValue;
9
+ };
10
+
11
+ let { class: clazz }: Props = $props();
12
+
13
+ const options: { icon: typeof Icon; value: ColorThemePreference; ariaLabel: string }[] = [
14
+ {
15
+ value: 'system',
16
+ icon: Monitor,
17
+ ariaLabel: 'System Theme'
18
+ },
19
+ {
20
+ value: 'dark',
21
+ icon: Moon,
22
+ ariaLabel: 'Dark Theme'
23
+ },
24
+ {
25
+ value: 'light',
26
+ icon: Sun,
27
+ ariaLabel: 'Light Theme'
28
+ }
29
+ ];
30
+ </script>
31
+
32
+ <div class={merge('border-surface-300-700 grid grid-cols-3 rounded-full border', clazz)}>
33
+ {#each options as { value, icon: Icon, ariaLabel }}
34
+ {@const selected = value === ColorTheme.theme}
35
+ <button
36
+ type="button"
37
+ class={[
38
+ 'tranistion-all text-surface-600-400 hover:text-surface-900-100 h-6 w-6 rounded-full p-1 transition-colors',
39
+ selected && 'bg-surface-100-900'
40
+ ]}
41
+ onclick={() => {
42
+ ColorTheme.theme = value;
43
+ }}
44
+ aria-label={ariaLabel}
45
+ >
46
+ <Icon class="h-full w-full" />
47
+ </button>
48
+ {/each}
49
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { ClassValue } from 'svelte/elements';
2
+ type Props = {
3
+ class: ClassValue;
4
+ };
5
+ declare const ThemeController: import("svelte").Component<Props, {}, "">;
6
+ type ThemeController = ReturnType<typeof ThemeController>;
7
+ export default ThemeController;
8
+ //# sourceMappingURL=ThemeController.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeController.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/basic/ThemeController.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG9C,KAAK,KAAK,GAAG;IACT,KAAK,EAAE,UAAU,CAAC;CACrB,CAAC;AA6CN,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -40,11 +40,11 @@
40
40
  let {
41
41
  class: clazz,
42
42
  style: externalStyle,
43
- target,
44
43
  placement = 'bottom-start',
45
- children,
46
- autoplacement,
44
+ autoplacement = true,
47
45
  popover = 'auto',
46
+ target,
47
+ children,
48
48
  ...rest
49
49
  }: PopoverProps = $props();
50
50
 
@@ -173,8 +173,12 @@
173
173
  }
174
174
  </script>
175
175
 
176
- <div bind:this={popoverEl} {style} {popover} class="bg-transparent">
177
- <div class={twMerge(clsx(theme.current.popover?.class, clazz))} style={externalStyle} {...rest}>
178
- {@render children?.()}
179
- </div>
176
+ <div
177
+ bind:this={popoverEl}
178
+ style="{style} {externalStyle}"
179
+ {popover}
180
+ class={twMerge(clsx('bg-transparent', theme.current.popover?.class, clazz))}
181
+ {...rest}
182
+ >
183
+ {@render children?.()}
180
184
  </div>
@@ -1 +1 @@
1
- {"version":3,"file":"Popover.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/popover/Popover.svelte.ts"],"names":[],"mappings":"AAII,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAKjD,0CAA0C;AAC1C,MAAM,MAAM,gBAAgB,GACtB,KAAK,GACL,WAAW,GACX,SAAS,GACT,OAAO,GACP,aAAa,GACb,WAAW,GACX,QAAQ,GACR,cAAc,GACd,YAAY,GACZ,MAAM,GACN,YAAY,GACZ,UAAU,CAAC;AAEjB,MAAM,WAAW,YAAa,SAAQ,cAAc,CAAC,cAAc,CAAC;IAChE,6DAA6D;IAC7D,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC;;;;OAIG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AAqJL,QAAA,MAAM,OAAO;;;;;MAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"Popover.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/popover/Popover.svelte.ts"],"names":[],"mappings":"AAII,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAKjD,0CAA0C;AAC1C,MAAM,MAAM,gBAAgB,GACtB,KAAK,GACL,WAAW,GACX,SAAS,GACT,OAAO,GACP,aAAa,GACb,WAAW,GACX,QAAQ,GACR,cAAc,GACd,YAAY,GACZ,MAAM,GACN,YAAY,GACZ,UAAU,CAAC;AAEjB,MAAM,WAAW,YAAa,SAAQ,cAAc,CAAC,cAAc,CAAC;IAChE,6DAA6D;IAC7D,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC;;;;OAIG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AAmJL,QAAA,MAAM,OAAO;;;;;MAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -1,5 +1,4 @@
1
1
  <script lang="ts" module>
2
- import { merge } from '../../../utils/functions';
3
2
  import type { Snippet } from 'svelte';
4
3
  import type { ClassValue, MouseEventHandler } from 'svelte/elements';
5
4
  import Popover, { type PopoverPlacement } from '../popover/Popover.svelte';
@@ -42,7 +41,6 @@
42
41
  }: TooltipProps = $props();
43
42
 
44
43
  let target = $state<HTMLElement>();
45
-
46
44
  let popover = $state<Popover>();
47
45
 
48
46
  let currentTimeout: number;
@@ -79,15 +77,7 @@
79
77
  {@render children?.()}
80
78
  </svelte:element>
81
79
 
82
- <Popover
83
- bind:this={popover}
84
- {target}
85
- {placement}
86
- class={merge(
87
- 'bg-surface-50-950 max-w-96 -translate-y-0.5 rounded px-4 py-1 shadow-lg',
88
- tooltipClass
89
- )}
90
- >
80
+ <Popover bind:this={popover} {target} {placement} class={tooltipClass}>
91
81
  {#if typeof tooltip === 'string'}
92
82
  {tooltip}
93
83
  {:else}
@@ -1 +1 @@
1
- {"version":3,"file":"Tooltip.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/tooltip/Tooltip.svelte.ts"],"names":[],"mappings":"AAII,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAE3E,MAAM,WAAW,YAAY;IACzB,OAAO,CAAC,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAC;IACzC,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iCAAiC;IACjC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,sCAAsC;IACtC,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAuDL,kEAAkE;AAClE,QAAA,MAAM,OAAO,kDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"Tooltip.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/tooltip/Tooltip.svelte.ts"],"names":[],"mappings":"AAGI,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAE3E,MAAM,WAAW,YAAY;IACzB,OAAO,CAAC,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAC;IACzC,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iCAAiC;IACjC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,sCAAsC;IACtC,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAmDL,kEAAkE;AAClE,QAAA,MAAM,OAAO,kDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { ColorTheme, type ColorThemePreference } from './colorTheme.svelte';
1
2
  export { theme, type Theme } from './theme.svelte';
2
3
  export { type IvoryComponent } from './types';
3
4
  export type Variant = 'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'error' | 'surface';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,OAAO,GACb,SAAS,GACT,WAAW,GACX,UAAU,GACV,SAAS,GACT,SAAS,GACT,OAAO,GACP,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,OAAO,GACb,SAAS,GACT,WAAW,GACX,UAAU,GACV,SAAS,GACT,SAAS,GACT,OAAO,GACP,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  // Reexport your entry components here
2
+ export { ColorTheme } from './colorTheme.svelte';
2
3
  export { theme } from './theme.svelte';
3
4
  export {} from './types';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ims360/svelte-ivory",
3
- "version": "0.3.6",
3
+ "version": "0.4.0",
4
4
  "keywords": [
5
5
  "svelte"
6
6
  ],
@@ -9,6 +9,14 @@
9
9
  "**/*.css"
10
10
  ],
11
11
  "type": "module",
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "provenance": true
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/ims360ca/svelte-ivory.git"
19
+ },
12
20
  "exports": {
13
21
  ".": {
14
22
  "types": "./dist/index.d.ts",
@@ -0,0 +1,65 @@
1
+ import { browser } from '$app/environment';
2
+ import { cookie } from '@ims360/svelte-ivory/utils/functions';
3
+ import type { Handle } from '@sveltejs/kit';
4
+
5
+ const COOKIE_THEME_KEY = 'theme';
6
+
7
+ export type ColorThemePreference = 'system' | 'light' | 'dark';
8
+
9
+ class ThemeController {
10
+ private currentTheme = $state<ColorThemePreference>('system');
11
+
12
+ constructor() {
13
+ if (!browser) return;
14
+ const value = cookie.get(COOKIE_THEME_KEY);
15
+ if (value === 'system' || value === 'dark' || value === 'light') {
16
+ this.theme = value;
17
+ } else {
18
+ this.theme = 'system';
19
+ }
20
+ }
21
+
22
+ get theme() {
23
+ return this.currentTheme;
24
+ }
25
+
26
+ get preference() {
27
+ return this.currentTheme;
28
+ }
29
+
30
+ set theme(value: ColorThemePreference) {
31
+ if (!browser) return;
32
+ this.currentTheme = value;
33
+ cookie.set({
34
+ name: COOKIE_THEME_KEY,
35
+ value
36
+ });
37
+ if (value === 'system') {
38
+ const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
39
+ document.documentElement.classList.toggle('dark', systemDark);
40
+ } else if (value === 'light') {
41
+ document.documentElement.classList.toggle('dark', false);
42
+ } else {
43
+ document.documentElement.classList.toggle('dark', true);
44
+ }
45
+ }
46
+
47
+ /** Add to hook sequence to enable correct prerendering ot the theme */
48
+ handle: Handle = ({ event, resolve }) => {
49
+ const theme = event.cookies.get(COOKIE_THEME_KEY);
50
+ let isDark = false;
51
+ if (theme !== undefined && (theme as ColorThemePreference) !== 'system') {
52
+ isDark = theme === 'dark';
53
+ } else {
54
+ isDark = event.request.headers.get('sec-ch-prefers-color-scheme') === 'dark';
55
+ }
56
+
57
+ if (!isDark) return resolve(event);
58
+
59
+ return resolve(event, {
60
+ transformPageChunk: ({ html }) => html.replace('class="', 'class="dark ')
61
+ });
62
+ };
63
+ }
64
+
65
+ export const ColorTheme = new ThemeController();
@@ -1,5 +1 @@
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
1
  export { default as Markdown } from './Markdown.svelte';
5
- export { default as UserMessage } from './UserMessage.svelte';
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import { ColorTheme, type ColorThemePreference } from '$lib/colorTheme.svelte';
3
+ import { merge } from '@ims360/svelte-ivory/utils/functions';
4
+ import { Icon, Monitor, Moon, Sun } from '@lucide/svelte';
5
+ import type { ClassValue } from 'svelte/elements';
6
+
7
+ type Props = {
8
+ class: ClassValue;
9
+ };
10
+
11
+ let { class: clazz }: Props = $props();
12
+
13
+ const options: { icon: typeof Icon; value: ColorThemePreference; ariaLabel: string }[] = [
14
+ {
15
+ value: 'system',
16
+ icon: Monitor,
17
+ ariaLabel: 'System Theme'
18
+ },
19
+ {
20
+ value: 'dark',
21
+ icon: Moon,
22
+ ariaLabel: 'Dark Theme'
23
+ },
24
+ {
25
+ value: 'light',
26
+ icon: Sun,
27
+ ariaLabel: 'Light Theme'
28
+ }
29
+ ];
30
+ </script>
31
+
32
+ <div class={merge('border-surface-300-700 grid grid-cols-3 rounded-full border', clazz)}>
33
+ {#each options as { value, icon: Icon, ariaLabel }}
34
+ {@const selected = value === ColorTheme.theme}
35
+ <button
36
+ type="button"
37
+ class={[
38
+ 'tranistion-all text-surface-600-400 hover:text-surface-900-100 h-6 w-6 rounded-full p-1 transition-colors',
39
+ selected && 'bg-surface-100-900'
40
+ ]}
41
+ onclick={() => {
42
+ ColorTheme.theme = value;
43
+ }}
44
+ aria-label={ariaLabel}
45
+ >
46
+ <Icon class="h-full w-full" />
47
+ </button>
48
+ {/each}
49
+ </div>
@@ -40,11 +40,11 @@
40
40
  let {
41
41
  class: clazz,
42
42
  style: externalStyle,
43
- target,
44
43
  placement = 'bottom-start',
45
- children,
46
- autoplacement,
44
+ autoplacement = true,
47
45
  popover = 'auto',
46
+ target,
47
+ children,
48
48
  ...rest
49
49
  }: PopoverProps = $props();
50
50
 
@@ -173,8 +173,12 @@
173
173
  }
174
174
  </script>
175
175
 
176
- <div bind:this={popoverEl} {style} {popover} class="bg-transparent">
177
- <div class={twMerge(clsx(theme.current.popover?.class, clazz))} style={externalStyle} {...rest}>
178
- {@render children?.()}
179
- </div>
176
+ <div
177
+ bind:this={popoverEl}
178
+ style="{style} {externalStyle}"
179
+ {popover}
180
+ class={twMerge(clsx('bg-transparent', theme.current.popover?.class, clazz))}
181
+ {...rest}
182
+ >
183
+ {@render children?.()}
180
184
  </div>
@@ -1,5 +1,4 @@
1
1
  <script lang="ts" module>
2
- import { merge } from '$lib/utils/functions';
3
2
  import type { Snippet } from 'svelte';
4
3
  import type { ClassValue, MouseEventHandler } from 'svelte/elements';
5
4
  import Popover, { type PopoverPlacement } from '../popover/Popover.svelte';
@@ -42,7 +41,6 @@
42
41
  }: TooltipProps = $props();
43
42
 
44
43
  let target = $state<HTMLElement>();
45
-
46
44
  let popover = $state<Popover>();
47
45
 
48
46
  let currentTimeout: number;
@@ -79,15 +77,7 @@
79
77
  {@render children?.()}
80
78
  </svelte:element>
81
79
 
82
- <Popover
83
- bind:this={popover}
84
- {target}
85
- {placement}
86
- class={merge(
87
- 'bg-surface-50-950 max-w-96 -translate-y-0.5 rounded px-4 py-1 shadow-lg',
88
- tooltipClass
89
- )}
90
- >
80
+ <Popover bind:this={popover} {target} {placement} class={tooltipClass}>
91
81
  {#if typeof tooltip === 'string'}
92
82
  {tooltip}
93
83
  {:else}
package/src/lib/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  // Reexport your entry components here
2
+ export { ColorTheme, type ColorThemePreference } from './colorTheme.svelte';
2
3
  export { theme, type Theme } from './theme.svelte';
3
4
  export { type IvoryComponent } from './types';
4
5
 
@@ -1,114 +0,0 @@
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 { merge } from '../../utils/functions';
9
- import { ThumbsDown, ThumbsUp } from '@lucide/svelte';
10
- import type { Snippet } from 'svelte';
11
- import type { ClassValue } from 'svelte/elements';
12
- import CopyToClipboardButton from '../buttons/CopyToClipboardButton.svelte';
13
- import type { AiChatMessage } from './Chat.svelte';
14
- import Markdown from './Markdown.svelte';
15
-
16
- interface Props {
17
- b_message: AiChatMessage;
18
- messageText?: Snippet<[{ message: AiChatMessage }]>;
19
- class?: ClassValue;
20
- minHeight?: number;
21
- }
22
-
23
- let {
24
- b_message = $bindable(),
25
- class: clazz,
26
- messageText = defaultMessage,
27
- minHeight
28
- }: Props = $props();
29
-
30
- // const uuidRegex =
31
- // /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g;
32
- // 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>`;
33
-
34
- // async function replacerFactory(): Promise<(match: string) => string> {
35
- // const docs = await documents.value;
36
- // return (match: string) => {
37
- // const doc = docs.find((d) => d.id === match);
38
-
39
- // 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">
40
- // ${doc?.title ?? 'Unbekanntes Dokument'}
41
- // <a href="/documents/${doc?.id}" target="_blank">
42
- // ${icon}
43
- // </a>
44
- // </span>`;
45
- // };
46
- // }
47
- </script>
48
-
49
- <div
50
- class={merge('group flex w-full flex-col items-start', clazz)}
51
- style={minHeight ? `min-height: ${minHeight}px;` : undefined}
52
- >
53
- {@render messageText({
54
- message: b_message
55
- })}
56
- <div
57
- class={[
58
- 'text-surface-500 flex -translate-x-3 flex-row items-center transition-all group-hover:opacity-100',
59
- b_message.liked || b_message.disliked ? 'opacity-100' : 'opacity-0'
60
- ]}
61
- >
62
- <CopyToClipboardButton
63
- text={b_message.message}
64
- toastMessage="Nachricht wurde in die Zwischenablage kopiert"
65
- />
66
- <button
67
- type="button"
68
- class="btn-icon"
69
- onclick={() => {
70
- b_message.liked = !b_message.liked;
71
- b_message.disliked = false;
72
- }}
73
- >
74
- <ThumbsUp class={[b_message.liked && 'fill-surface-500/50']} />
75
- </button>
76
- <button
77
- type="button"
78
- class="btn-icon"
79
- onclick={() => {
80
- b_message.liked = false;
81
- b_message.disliked = !b_message.disliked;
82
- }}
83
- >
84
- <ThumbsDown class={[b_message.disliked && 'fill-surface-500/50']} />
85
- </button>
86
- {#if b_message.time}
87
- <p class="text-surface-400-600 pl-2">
88
- {b_message.time.toLocaleString('de')}
89
- </p>
90
- {/if}
91
- </div>
92
- </div>
93
-
94
- {#snippet defaultMessage({ message }: { message: AiChatMessage })}
95
- {#if message}
96
- <Markdown source={message.message} />
97
- {:else}
98
- <p class="flex flex-row">
99
- <span class="h-4 animate-bounce rounded-full pl-1">.</span>
100
- <span
101
- class="h-4 animate-bounce rounded-full"
102
- style="animation-delay: 125ms !important;"
103
- >
104
- .
105
- </span>
106
- <span
107
- class="h-4 animate-bounce rounded-full"
108
- style="animation-delay: 250ms !important;"
109
- >
110
- .
111
- </span>
112
- </p>
113
- {/if}
114
- {/snippet}
@@ -1,19 +0,0 @@
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;
19
- //# sourceMappingURL=AiMessage.svelte.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AiMessage.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/ai/AiMessage.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAI/C,UAAU,KAAK;IACX,SAAS,EAAE,aAAa,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,OAAO,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC,CAAC;IACpD,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAsFL;;;GAGG;AACH,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -1,28 +0,0 @@
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>
@@ -1,10 +0,0 @@
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;
10
- //# sourceMappingURL=AttachedFile.svelte.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AttachedFile.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/ai/AttachedFile.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG9C,UAAU,KAAK;IACX,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,KAAK,IAAI,CAAC;IAC7B,KAAK,CAAC,EAAE,UAAU,CAAC;CACtB;AAsBL,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -1,149 +0,0 @@
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 { merge } from '../../utils/functions';
10
- import { tick, type Snippet } from 'svelte';
11
- import type { ClassValue } from 'svelte/elements';
12
- import AiMessage from './AiMessage.svelte';
13
- import UserMessage from './UserMessage.svelte';
14
-
15
- export interface AiChatMessage {
16
- from: 'user' | 'system';
17
- message: string;
18
- time?: Date;
19
- liked?: boolean;
20
- disliked?: boolean;
21
- files?: File[];
22
- }
23
-
24
- export interface AiChat {
25
- messages: AiChatMessage[];
26
- loading?: boolean;
27
- }
28
- </script>
29
-
30
- <script lang="ts">
31
- interface Props {
32
- class?: ClassValue;
33
- b_chat: AiChat;
34
- userMessage?: Snippet<[{ message: AiChatMessage; i: number }]>;
35
- systemMessage?: Snippet<[{ message: AiChatMessage; i: number; minHeight?: number }]>;
36
- placeholder?: Snippet;
37
- children: Snippet<[{ onsubmit: (message: AiChatMessage) => Promise<void> }]>;
38
- submit: (message: AiChatMessage) => Promise<void>;
39
- }
40
-
41
- let {
42
- class: clazz,
43
- b_chat = $bindable(),
44
- userMessage = defaultUserMessage,
45
- systemMessage = defaultSystemMessage,
46
- placeholder,
47
- children,
48
- submit: externalSubmit
49
- }: Props = $props();
50
-
51
- let chatContainer = $state<HTMLDivElement>();
52
- let lastMessageMinHeight = $state(0);
53
-
54
- function getLastMessageMinHeight() {
55
- if (!chatContainer) return 0;
56
- const secondToLastElement = chatContainer.children[chatContainer.children.length - 2];
57
- const rect = secondToLastElement?.getBoundingClientRect();
58
- const remainHeight = chatContainer.clientHeight - rect.height - 16;
59
- return remainHeight;
60
- }
61
-
62
- export async function scrollToBottom() {
63
- if (!chatContainer) return;
64
- await tick();
65
- await tick();
66
- lastMessageMinHeight = getLastMessageMinHeight();
67
- await tick();
68
- // ensure we don't scroll if the newly generated message is already in view
69
- chatContainer.scrollTo({
70
- top: chatContainer.scrollHeight,
71
- behavior: 'smooth'
72
- });
73
- }
74
-
75
- async function submit(message: AiChatMessage) {
76
- if (b_chat.loading) {
77
- return;
78
- }
79
-
80
- b_chat.messages.push({
81
- ...message,
82
- from: 'user',
83
- time: new Date()
84
- });
85
- // prevent the user from sending another message while we are loading the ai response
86
- b_chat.loading = true;
87
-
88
- // add an empty system message to the chat, this will indicate a loading state
89
- b_chat.messages.push({
90
- from: 'system',
91
- message: '',
92
- time: new Date()
93
- });
94
-
95
- await scrollToBottom();
96
-
97
- await externalSubmit(message);
98
-
99
- b_chat.loading = false;
100
- }
101
- </script>
102
-
103
- <div class={merge('flex grow flex-col gap-2 overflow-hidden', clazz)}>
104
- <div
105
- class="flex grow flex-col gap-4 overflow-auto pr-2 [scrollbar-gutter:stable]"
106
- bind:this={chatContainer}
107
- >
108
- {#if b_chat.messages.length === 0 && placeholder}
109
- {@render placeholder()}
110
- {/if}
111
- {#each b_chat.messages as _, i (i)}
112
- {@const message = b_chat.messages[i]}
113
- {#if message.from === 'user'}
114
- {@render userMessage({
115
- message,
116
- i
117
- })}
118
- {:else}
119
- {@render systemMessage({
120
- message,
121
- i,
122
- minHeight: i === b_chat.messages.length - 1 ? lastMessageMinHeight : 0
123
- })}
124
- {/if}
125
- {/each}
126
- </div>
127
- {@render children({ onsubmit: submit })}
128
- </div>
129
-
130
- {#snippet defaultSystemMessage({
131
- i,
132
- minHeight
133
- }: {
134
- i: number;
135
- message: AiChatMessage;
136
- minHeight?: number;
137
- })}
138
- <AiMessage bind:b_message={b_chat.messages[i]} {minHeight} />
139
- {/snippet}
140
-
141
- {#snippet defaultUserMessage({
142
- message
143
- }: {
144
- i: number;
145
- message: AiChatMessage;
146
- minHeight?: number;
147
- })}
148
- <UserMessage {message} />
149
- {/snippet}
@@ -1,43 +0,0 @@
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, {
39
- scrollToBottom: () => Promise<void>;
40
- }, "b_chat">;
41
- type Chat = ReturnType<typeof Chat>;
42
- export default Chat;
43
- //# sourceMappingURL=Chat.svelte.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Chat.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/ai/Chat.svelte.ts"],"names":[],"mappings":"AAII,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIlD,MAAM,WAAW,aAAa;IAC1B,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,MAAM;IACnB,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,KAAK;IACX,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,OAAO,EAAE,aAAa,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IAC/D,aAAa,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,OAAO,EAAE,aAAa,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACrF,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC,CAAC;QAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC,CAAC,CAAC;IAC7E,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AAqHL;;;;GAIG;AACH,QAAA,MAAM,IAAI;;YAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
@@ -1,52 +0,0 @@
1
- <script lang="ts">
2
- import { merge } from '../../utils/functions';
3
- import type { Snippet } from 'svelte';
4
- import type { ClassValue } from 'svelte/elements';
5
- import AttachedFile from './AttachedFile.svelte';
6
- import type { AiChatMessage } from './Chat.svelte';
7
- import Markdown from './Markdown.svelte';
8
-
9
- interface Props {
10
- class?: ClassValue;
11
- message: AiChatMessage;
12
- /** How attached files should be rendered */
13
- attachedFile?: Snippet<[file: File]>;
14
- /** How the message string should be rendered */
15
- messageText?: Snippet<[{ message: AiChatMessage }]>;
16
- }
17
-
18
- let {
19
- class: clazz,
20
- message,
21
- attachedFile = defaultAttachedFile,
22
- messageText = defaultMessageText
23
- }: Props = $props();
24
- </script>
25
-
26
- <div class={merge('flex w-full flex-col items-end gap-1', clazz)}>
27
- {@render messageText({ message })}
28
- {#if message.files}
29
- <div class="flex flex-row items-center gap-2">
30
- {#each message.files as file, i (i)}
31
- {@render attachedFile(file)}
32
- {/each}
33
- </div>
34
- {/if}
35
- <div class="flex flex-row items-center gap-2">
36
- {#if message.time}
37
- <p class="text-surface-400-600 text-sm">
38
- {message.time.toLocaleString('de')}
39
- </p>
40
- {/if}
41
- </div>
42
- </div>
43
-
44
- {#snippet defaultAttachedFile(file: File)}
45
- <AttachedFile {file} />
46
- {/snippet}
47
-
48
- {#snippet defaultMessageText({ message }: { message: AiChatMessage })}
49
- <div class="bg-surface-200-800 text-surface-contrast-200-800 rounded px-2 py-0.5">
50
- <Markdown source={message.message} />
51
- </div>
52
- {/snippet}
@@ -1,17 +0,0 @@
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;
17
- //# sourceMappingURL=UserMessage.svelte.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"UserMessage.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/ai/UserMessage.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAI/C,UAAU,KAAK;IACX,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;IACvB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IACrC,gDAAgD;IAChD,WAAW,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,OAAO,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC,CAAC;CACvD;AAgDL,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -1,114 +0,0 @@
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 { merge } from '$lib/utils/functions';
9
- import { ThumbsDown, ThumbsUp } from '@lucide/svelte';
10
- import type { Snippet } from 'svelte';
11
- import type { ClassValue } from 'svelte/elements';
12
- import CopyToClipboardButton from '../buttons/CopyToClipboardButton.svelte';
13
- import type { AiChatMessage } from './Chat.svelte';
14
- import Markdown from './Markdown.svelte';
15
-
16
- interface Props {
17
- b_message: AiChatMessage;
18
- messageText?: Snippet<[{ message: AiChatMessage }]>;
19
- class?: ClassValue;
20
- minHeight?: number;
21
- }
22
-
23
- let {
24
- b_message = $bindable(),
25
- class: clazz,
26
- messageText = defaultMessage,
27
- minHeight
28
- }: Props = $props();
29
-
30
- // const uuidRegex =
31
- // /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g;
32
- // 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>`;
33
-
34
- // async function replacerFactory(): Promise<(match: string) => string> {
35
- // const docs = await documents.value;
36
- // return (match: string) => {
37
- // const doc = docs.find((d) => d.id === match);
38
-
39
- // 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">
40
- // ${doc?.title ?? 'Unbekanntes Dokument'}
41
- // <a href="/documents/${doc?.id}" target="_blank">
42
- // ${icon}
43
- // </a>
44
- // </span>`;
45
- // };
46
- // }
47
- </script>
48
-
49
- <div
50
- class={merge('group flex w-full flex-col items-start', clazz)}
51
- style={minHeight ? `min-height: ${minHeight}px;` : undefined}
52
- >
53
- {@render messageText({
54
- message: b_message
55
- })}
56
- <div
57
- class={[
58
- 'text-surface-500 flex -translate-x-3 flex-row items-center transition-all group-hover:opacity-100',
59
- b_message.liked || b_message.disliked ? 'opacity-100' : 'opacity-0'
60
- ]}
61
- >
62
- <CopyToClipboardButton
63
- text={b_message.message}
64
- toastMessage="Nachricht wurde in die Zwischenablage kopiert"
65
- />
66
- <button
67
- type="button"
68
- class="btn-icon"
69
- onclick={() => {
70
- b_message.liked = !b_message.liked;
71
- b_message.disliked = false;
72
- }}
73
- >
74
- <ThumbsUp class={[b_message.liked && 'fill-surface-500/50']} />
75
- </button>
76
- <button
77
- type="button"
78
- class="btn-icon"
79
- onclick={() => {
80
- b_message.liked = false;
81
- b_message.disliked = !b_message.disliked;
82
- }}
83
- >
84
- <ThumbsDown class={[b_message.disliked && 'fill-surface-500/50']} />
85
- </button>
86
- {#if b_message.time}
87
- <p class="text-surface-400-600 pl-2">
88
- {b_message.time.toLocaleString('de')}
89
- </p>
90
- {/if}
91
- </div>
92
- </div>
93
-
94
- {#snippet defaultMessage({ message }: { message: AiChatMessage })}
95
- {#if message}
96
- <Markdown source={message.message} />
97
- {:else}
98
- <p class="flex flex-row">
99
- <span class="h-4 animate-bounce rounded-full pl-1">.</span>
100
- <span
101
- class="h-4 animate-bounce rounded-full"
102
- style="animation-delay: 125ms !important;"
103
- >
104
- .
105
- </span>
106
- <span
107
- class="h-4 animate-bounce rounded-full"
108
- style="animation-delay: 250ms !important;"
109
- >
110
- .
111
- </span>
112
- </p>
113
- {/if}
114
- {/snippet}
@@ -1,28 +0,0 @@
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>
@@ -1,149 +0,0 @@
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 { merge } from '$lib/utils/functions';
10
- import { tick, type Snippet } from 'svelte';
11
- import type { ClassValue } from 'svelte/elements';
12
- import AiMessage from './AiMessage.svelte';
13
- import UserMessage from './UserMessage.svelte';
14
-
15
- export interface AiChatMessage {
16
- from: 'user' | 'system';
17
- message: string;
18
- time?: Date;
19
- liked?: boolean;
20
- disliked?: boolean;
21
- files?: File[];
22
- }
23
-
24
- export interface AiChat {
25
- messages: AiChatMessage[];
26
- loading?: boolean;
27
- }
28
- </script>
29
-
30
- <script lang="ts">
31
- interface Props {
32
- class?: ClassValue;
33
- b_chat: AiChat;
34
- userMessage?: Snippet<[{ message: AiChatMessage; i: number }]>;
35
- systemMessage?: Snippet<[{ message: AiChatMessage; i: number; minHeight?: number }]>;
36
- placeholder?: Snippet;
37
- children: Snippet<[{ onsubmit: (message: AiChatMessage) => Promise<void> }]>;
38
- submit: (message: AiChatMessage) => Promise<void>;
39
- }
40
-
41
- let {
42
- class: clazz,
43
- b_chat = $bindable(),
44
- userMessage = defaultUserMessage,
45
- systemMessage = defaultSystemMessage,
46
- placeholder,
47
- children,
48
- submit: externalSubmit
49
- }: Props = $props();
50
-
51
- let chatContainer = $state<HTMLDivElement>();
52
- let lastMessageMinHeight = $state(0);
53
-
54
- function getLastMessageMinHeight() {
55
- if (!chatContainer) return 0;
56
- const secondToLastElement = chatContainer.children[chatContainer.children.length - 2];
57
- const rect = secondToLastElement?.getBoundingClientRect();
58
- const remainHeight = chatContainer.clientHeight - rect.height - 16;
59
- return remainHeight;
60
- }
61
-
62
- export async function scrollToBottom() {
63
- if (!chatContainer) return;
64
- await tick();
65
- await tick();
66
- lastMessageMinHeight = getLastMessageMinHeight();
67
- await tick();
68
- // ensure we don't scroll if the newly generated message is already in view
69
- chatContainer.scrollTo({
70
- top: chatContainer.scrollHeight,
71
- behavior: 'smooth'
72
- });
73
- }
74
-
75
- async function submit(message: AiChatMessage) {
76
- if (b_chat.loading) {
77
- return;
78
- }
79
-
80
- b_chat.messages.push({
81
- ...message,
82
- from: 'user',
83
- time: new Date()
84
- });
85
- // prevent the user from sending another message while we are loading the ai response
86
- b_chat.loading = true;
87
-
88
- // add an empty system message to the chat, this will indicate a loading state
89
- b_chat.messages.push({
90
- from: 'system',
91
- message: '',
92
- time: new Date()
93
- });
94
-
95
- await scrollToBottom();
96
-
97
- await externalSubmit(message);
98
-
99
- b_chat.loading = false;
100
- }
101
- </script>
102
-
103
- <div class={merge('flex grow flex-col gap-2 overflow-hidden', clazz)}>
104
- <div
105
- class="flex grow flex-col gap-4 overflow-auto pr-2 [scrollbar-gutter:stable]"
106
- bind:this={chatContainer}
107
- >
108
- {#if b_chat.messages.length === 0 && placeholder}
109
- {@render placeholder()}
110
- {/if}
111
- {#each b_chat.messages as _, i (i)}
112
- {@const message = b_chat.messages[i]}
113
- {#if message.from === 'user'}
114
- {@render userMessage({
115
- message,
116
- i
117
- })}
118
- {:else}
119
- {@render systemMessage({
120
- message,
121
- i,
122
- minHeight: i === b_chat.messages.length - 1 ? lastMessageMinHeight : 0
123
- })}
124
- {/if}
125
- {/each}
126
- </div>
127
- {@render children({ onsubmit: submit })}
128
- </div>
129
-
130
- {#snippet defaultSystemMessage({
131
- i,
132
- minHeight
133
- }: {
134
- i: number;
135
- message: AiChatMessage;
136
- minHeight?: number;
137
- })}
138
- <AiMessage bind:b_message={b_chat.messages[i]} {minHeight} />
139
- {/snippet}
140
-
141
- {#snippet defaultUserMessage({
142
- message
143
- }: {
144
- i: number;
145
- message: AiChatMessage;
146
- minHeight?: number;
147
- })}
148
- <UserMessage {message} />
149
- {/snippet}
@@ -1,52 +0,0 @@
1
- <script lang="ts">
2
- import { merge } from '$lib/utils/functions';
3
- import type { Snippet } from 'svelte';
4
- import type { ClassValue } from 'svelte/elements';
5
- import AttachedFile from './AttachedFile.svelte';
6
- import type { AiChatMessage } from './Chat.svelte';
7
- import Markdown from './Markdown.svelte';
8
-
9
- interface Props {
10
- class?: ClassValue;
11
- message: AiChatMessage;
12
- /** How attached files should be rendered */
13
- attachedFile?: Snippet<[file: File]>;
14
- /** How the message string should be rendered */
15
- messageText?: Snippet<[{ message: AiChatMessage }]>;
16
- }
17
-
18
- let {
19
- class: clazz,
20
- message,
21
- attachedFile = defaultAttachedFile,
22
- messageText = defaultMessageText
23
- }: Props = $props();
24
- </script>
25
-
26
- <div class={merge('flex w-full flex-col items-end gap-1', clazz)}>
27
- {@render messageText({ message })}
28
- {#if message.files}
29
- <div class="flex flex-row items-center gap-2">
30
- {#each message.files as file, i (i)}
31
- {@render attachedFile(file)}
32
- {/each}
33
- </div>
34
- {/if}
35
- <div class="flex flex-row items-center gap-2">
36
- {#if message.time}
37
- <p class="text-surface-400-600 text-sm">
38
- {message.time.toLocaleString('de')}
39
- </p>
40
- {/if}
41
- </div>
42
- </div>
43
-
44
- {#snippet defaultAttachedFile(file: File)}
45
- <AttachedFile {file} />
46
- {/snippet}
47
-
48
- {#snippet defaultMessageText({ message }: { message: AiChatMessage })}
49
- <div class="bg-surface-200-800 text-surface-contrast-200-800 rounded px-2 py-0.5">
50
- <Markdown source={message.message} />
51
- </div>
52
- {/snippet}