@ims360/svelte-ivory 0.0.4 → 0.0.7

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 (153) hide show
  1. package/README.md +7 -0
  2. package/dist/components/ai/AiMessage.svelte.d.ts +1 -0
  3. package/dist/components/ai/AiMessage.svelte.d.ts.map +1 -0
  4. package/dist/components/ai/AttachedFile.svelte.d.ts +1 -0
  5. package/dist/components/ai/AttachedFile.svelte.d.ts.map +1 -0
  6. package/dist/components/ai/Chat.svelte.d.ts +1 -0
  7. package/dist/components/ai/Chat.svelte.d.ts.map +1 -0
  8. package/dist/components/ai/Markdown.svelte.d.ts +1 -0
  9. package/dist/components/ai/Markdown.svelte.d.ts.map +1 -0
  10. package/dist/components/ai/UserMessage.svelte.d.ts +1 -0
  11. package/dist/components/ai/UserMessage.svelte.d.ts.map +1 -0
  12. package/dist/components/ai/index.d.ts +1 -0
  13. package/dist/components/ai/index.d.ts.map +1 -0
  14. package/dist/components/basic/checkbox/Checkbox.svelte.d.ts +1 -0
  15. package/dist/components/basic/checkbox/Checkbox.svelte.d.ts.map +1 -0
  16. package/dist/components/basic/index.d.ts +1 -0
  17. package/dist/components/basic/index.d.ts.map +1 -0
  18. package/dist/components/basic/toggle/Toggle.svelte +5 -3
  19. package/dist/components/basic/toggle/Toggle.svelte.d.ts +1 -0
  20. package/dist/components/basic/toggle/Toggle.svelte.d.ts.map +1 -0
  21. package/dist/components/buttons/CopyToClipboardButton.svelte.d.ts +1 -0
  22. package/dist/components/buttons/CopyToClipboardButton.svelte.d.ts.map +1 -0
  23. package/dist/components/layout/drawer/Drawer.svelte.d.ts +1 -0
  24. package/dist/components/layout/drawer/Drawer.svelte.d.ts.map +1 -0
  25. package/dist/components/layout/heading/Heading.svelte +1 -1
  26. package/dist/components/layout/heading/Heading.svelte.d.ts +1 -0
  27. package/dist/components/layout/heading/Heading.svelte.d.ts.map +1 -0
  28. package/dist/components/layout/heading/index.d.ts +1 -0
  29. package/dist/components/layout/heading/index.d.ts.map +1 -0
  30. package/dist/components/layout/hiddenBackground/HiddenBackground.svelte +3 -3
  31. package/dist/components/layout/hiddenBackground/HiddenBackground.svelte.d.ts +2 -1
  32. package/dist/components/layout/hiddenBackground/HiddenBackground.svelte.d.ts.map +1 -0
  33. package/dist/components/layout/hiddenBackground/index.d.ts +7 -0
  34. package/dist/components/layout/hiddenBackground/index.d.ts.map +1 -0
  35. package/dist/components/layout/index.d.ts +2 -1
  36. package/dist/components/layout/index.d.ts.map +1 -0
  37. package/dist/components/layout/modal/Modal.svelte +60 -48
  38. package/dist/components/layout/modal/Modal.svelte.d.ts +1 -0
  39. package/dist/components/layout/modal/Modal.svelte.d.ts.map +1 -0
  40. package/dist/components/layout/modal/ModalTest.svelte.d.ts +1 -0
  41. package/dist/components/layout/modal/ModalTest.svelte.d.ts.map +1 -0
  42. package/dist/components/layout/popover/Popover.svelte.d.ts +1 -0
  43. package/dist/components/layout/popover/Popover.svelte.d.ts.map +1 -0
  44. package/dist/components/layout/portal/Portal.svelte.d.ts +1 -0
  45. package/dist/components/layout/portal/Portal.svelte.d.ts.map +1 -0
  46. package/dist/components/layout/tabs/Tab.svelte +3 -3
  47. package/dist/components/layout/tabs/Tab.svelte.d.ts +2 -1
  48. package/dist/components/layout/tabs/Tab.svelte.d.ts.map +1 -0
  49. package/dist/components/layout/tabs/TabPanel.svelte.d.ts +1 -0
  50. package/dist/components/layout/tabs/TabPanel.svelte.d.ts.map +1 -0
  51. package/dist/components/layout/tabs/Tabs.svelte.d.ts +1 -0
  52. package/dist/components/layout/tabs/Tabs.svelte.d.ts.map +1 -0
  53. package/dist/components/layout/tabs/index.d.ts +2 -1
  54. package/dist/components/layout/tabs/index.d.ts.map +1 -0
  55. package/dist/components/layout/tooltip/Tooltip.svelte +12 -11
  56. package/dist/components/layout/tooltip/Tooltip.svelte.d.ts +1 -0
  57. package/dist/components/layout/tooltip/Tooltip.svelte.d.ts.map +1 -0
  58. package/dist/components/table/index.d.ts +6 -0
  59. package/dist/components/table/index.d.ts.map +1 -0
  60. package/dist/components/table/index.js +5 -0
  61. package/dist/components/table/plugins/expandAll.svelte.d.ts +7 -0
  62. package/dist/components/table/plugins/expandAll.svelte.d.ts.map +1 -0
  63. package/dist/components/table/plugins/expandAll.svelte.js +24 -0
  64. package/dist/components/table/plugins/search.svelte.d.ts +13 -0
  65. package/dist/components/table/plugins/search.svelte.d.ts.map +1 -0
  66. package/dist/components/table/plugins/search.svelte.js +52 -0
  67. package/dist/components/table/table/Column.svelte +78 -0
  68. package/dist/components/table/table/Column.svelte.d.ts +16 -0
  69. package/dist/components/table/table/Column.svelte.d.ts.map +1 -0
  70. package/dist/components/table/table/ColumnHead.svelte +73 -0
  71. package/dist/components/table/table/ColumnHead.svelte.d.ts +11 -0
  72. package/dist/components/table/table/ColumnHead.svelte.d.ts.map +1 -0
  73. package/dist/components/table/table/Row.svelte +67 -0
  74. package/dist/components/table/table/Row.svelte.d.ts +13 -0
  75. package/dist/components/table/table/Row.svelte.d.ts.map +1 -0
  76. package/dist/components/table/table/Table.svelte +137 -0
  77. package/dist/components/table/table/Table.svelte.d.ts +52 -0
  78. package/dist/components/table/table/Table.svelte.d.ts.map +1 -0
  79. package/dist/components/table/table/VirtualList.svelte +101 -0
  80. package/dist/components/table/table/VirtualList.svelte.d.ts +41 -0
  81. package/dist/components/table/table/VirtualList.svelte.d.ts.map +1 -0
  82. package/dist/components/table/table/column.svelte.d.ts +21 -0
  83. package/dist/components/table/table/column.svelte.d.ts.map +1 -0
  84. package/dist/components/table/table/column.svelte.js +47 -0
  85. package/dist/components/table/table/index.js +11 -0
  86. package/dist/components/table/table/table.svelte.d.ts +36 -0
  87. package/dist/components/table/table/table.svelte.d.ts.map +1 -0
  88. package/dist/components/table/table/table.svelte.js +92 -0
  89. package/dist/components/toast/Toast.svelte.d.ts +1 -0
  90. package/dist/components/toast/Toast.svelte.d.ts.map +1 -0
  91. package/dist/components/toast/index.d.ts +1 -0
  92. package/dist/components/toast/index.d.ts.map +1 -0
  93. package/dist/components/toast/toasts.svelte.d.ts +1 -0
  94. package/dist/components/toast/toasts.svelte.d.ts.map +1 -0
  95. package/dist/index.d.ts +1 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/utils/actions/clickOutside.d.ts +1 -0
  98. package/dist/utils/actions/clickOutside.d.ts.map +1 -0
  99. package/dist/utils/actions/focusTrap.d.ts +1 -0
  100. package/dist/utils/actions/focusTrap.d.ts.map +1 -0
  101. package/dist/utils/actions/index.d.ts +2 -0
  102. package/dist/utils/actions/index.d.ts.map +1 -0
  103. package/dist/utils/actions/index.js +1 -0
  104. package/dist/utils/actions/portal.d.ts +1 -0
  105. package/dist/utils/actions/portal.d.ts.map +1 -0
  106. package/dist/utils/actions/resize.d.ts +6 -0
  107. package/dist/utils/actions/resize.d.ts.map +1 -0
  108. package/dist/utils/actions/resize.js +25 -0
  109. package/dist/utils/actions/shortcut.d.ts +1 -0
  110. package/dist/utils/actions/shortcut.d.ts.map +1 -0
  111. package/dist/utils/actions/visible.d.ts +1 -0
  112. package/dist/utils/actions/visible.d.ts.map +1 -0
  113. package/dist/utils/functions/cookie.d.ts +1 -0
  114. package/dist/utils/functions/cookie.d.ts.map +1 -0
  115. package/dist/utils/functions/index.d.ts +1 -0
  116. package/dist/utils/functions/index.d.ts.map +1 -0
  117. package/dist/utils/functions/pseudoRandomId.d.ts +1 -0
  118. package/dist/utils/functions/pseudoRandomId.d.ts.map +1 -0
  119. package/dist/utils/functions/queryParams.d.ts +1 -0
  120. package/dist/utils/functions/queryParams.d.ts.map +1 -0
  121. package/package.json +9 -2
  122. package/src/lib/components/basic/toggle/Toggle.svelte +5 -3
  123. package/src/lib/components/layout/heading/Heading.svelte +1 -1
  124. package/src/lib/components/layout/hiddenBackground/HiddenBackground.svelte +3 -3
  125. package/src/lib/components/layout/index.ts +1 -1
  126. package/src/lib/components/layout/modal/Modal.svelte +60 -48
  127. package/src/lib/components/layout/tabs/Tab.svelte +3 -3
  128. package/src/lib/components/layout/tooltip/Tooltip.svelte +12 -11
  129. package/src/lib/components/table/index.ts +5 -0
  130. package/src/lib/components/table/plugins/expandAll.svelte.ts +34 -0
  131. package/src/lib/components/table/plugins/search.svelte.ts +75 -0
  132. package/src/lib/components/table/table/Column.svelte +78 -0
  133. package/src/lib/components/table/table/ColumnHead.svelte +73 -0
  134. package/src/lib/components/table/table/Row.svelte +67 -0
  135. package/src/lib/components/table/table/Table.svelte +137 -0
  136. package/src/lib/components/table/table/VirtualList.svelte +101 -0
  137. package/src/lib/components/table/table/column.svelte.ts +59 -0
  138. package/src/lib/components/table/table/index.ts +15 -0
  139. package/src/lib/components/table/table/table.svelte.ts +124 -0
  140. package/src/lib/utils/actions/index.ts +1 -0
  141. package/src/lib/utils/actions/resize.ts +35 -0
  142. package/dist/components/index.d.ts +0 -0
  143. package/dist/components/index.js +0 -1
  144. package/src/lib/components/basic/checkbox/checkbox.svelte.spec.ts +0 -39
  145. package/src/lib/components/basic/toggle/toggle.svelte.spec.ts +0 -19
  146. package/src/lib/components/index.ts +0 -0
  147. package/src/lib/components/layout/modal/modal.svelte.spec.ts +0 -39
  148. package/src/lib/components/layout/tabs/Tabs.test.svelte +0 -5
  149. package/src/lib/utils/actions/clickOutside.svelte.spec.ts +0 -67
  150. package/src/lib/utils/actions/shortcut.svelte.spec.ts +0 -19
  151. package/src/lib/utils/functions/cookie.svelte.spec.ts +0 -55
  152. package/src/lib/utils/functions/pseudoRandomId.spec.ts +0 -19
  153. package/src/lib/utils/functions/queryParams.spec.ts +0 -25
@@ -4,8 +4,7 @@
4
4
  import type { Snippet } from 'svelte';
5
5
  import type { ClassValue } from 'svelte/elements';
6
6
  import { twMerge } from 'tailwind-merge';
7
- import Heading from '../heading/Heading.svelte';
8
- import HiddenBackground from '../hiddenBackground/HiddenBackground.svelte';
7
+ import { Heading, HiddenBackground, Portal } from '..';
9
8
 
10
9
  /** Props for the modal, expose if you overwrite the defaults in a custom component */
11
10
  export interface ModalProps {
@@ -60,58 +59,71 @@
60
59
  A modal, comes with a title, close button and different variants per default.
61
60
  -->
62
61
  {#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
- >
62
+ <Portal>
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 -->
87
70
  <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
- ]}
71
+ class={clazz}
72
+ onclick={(e) => e.stopPropagation()}
73
+ data-testid={testId}
74
+ {style}
96
75
  >
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>
76
+ {@render modal()}
103
77
  </div>
78
+ {:else}
79
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
80
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
104
81
  <div
105
82
  class={twMerge(
106
- clsx(
107
- 'flex grow flex-col gap-4 overflow-hidden bg-inherit p-4 pt-2',
108
- innerClass
109
- )
83
+ clsx([
84
+ 'bg-surface-50-950 relative flex max-h-full max-w-full flex-col overflow-hidden rounded',
85
+ clazz
86
+ ])
110
87
  )}
88
+ {style}
89
+ onclick={(e) => e.stopPropagation()}
90
+ data-testid={testId}
111
91
  >
112
- {@render children?.()}
92
+ <div
93
+ class={[
94
+ 'flex flex-row items-center justify-between gap-4 px-4 py-3',
95
+ !variant && 'pb-0',
96
+ variant === 'success' && 'preset-tonal-success',
97
+ variant === 'warning' && 'preset-tonal-warning',
98
+ variant === 'error' && 'preset-tonal-error',
99
+ variant === 'info' && 'preset-tonal-primary'
100
+ ]}
101
+ >
102
+ {#if title}
103
+ <Heading>{title}</Heading>
104
+ {/if}
105
+ <button
106
+ class="group ml-auto flex justify-end"
107
+ type="button"
108
+ onclick={close}
109
+ >
110
+ <X
111
+ class="h-full w-auto transition-[stroke-width] group-hover:stroke-3"
112
+ />
113
+ </button>
114
+ </div>
115
+ <div
116
+ class={twMerge(
117
+ clsx(
118
+ 'flex grow flex-col gap-4 overflow-hidden bg-inherit p-4 pt-3',
119
+ innerClass
120
+ )
121
+ )}
122
+ >
123
+ {@render children?.()}
124
+ </div>
113
125
  </div>
114
- </div>
115
- {/if}
116
- </HiddenBackground>
126
+ {/if}
127
+ </HiddenBackground>
128
+ </Portal>
117
129
  {/if}
@@ -8,7 +8,7 @@
8
8
  import { getTabContext } from './Tabs.svelte';
9
9
 
10
10
  type Props = {
11
- class?: (selected: boolean) => ClassValue;
11
+ class?: ClassValue | ((selected: boolean) => ClassValue);
12
12
  id?: string | undefined;
13
13
  /**
14
14
  * If this is set the element will be a link.
@@ -58,8 +58,8 @@
58
58
  this={href ? 'a' : 'button'}
59
59
  class={twMerge(
60
60
  clsx(
61
- 'btn flex h-fit w-fit items-center justify-center px-0 text-xl font-bold',
62
- clazz(selected)
61
+ 'btn flex h-fit w-fit shrink-0 items-center justify-center px-0 text-xl font-bold select-none',
62
+ typeof clazz === 'function' ? clazz(selected) : clazz
63
63
  )
64
64
  )}
65
65
  onclick={href
@@ -42,27 +42,28 @@
42
42
  onclick,
43
43
  href,
44
44
  timeout = 500,
45
- tooltipClass
45
+ tooltipClass,
46
+ placement = 'top'
46
47
  }: Props = $props();
47
48
 
48
49
  let target = $state<HTMLElement>();
49
50
 
50
51
  let open = $state(false);
51
52
 
52
- let showTimeout: number;
53
- function onpointerenter() {
54
- clearTimeout(timeout);
53
+ let currentTimeout: number;
54
+ function onmouseenter() {
55
+ clearTimeout(currentTimeout);
55
56
  if (timeout === 0) {
56
57
  open = true;
57
58
  } else {
58
- showTimeout = setTimeout(() => {
59
+ currentTimeout = setTimeout(() => {
59
60
  open = true;
60
61
  }, timeout) as unknown as number;
61
62
  }
62
63
  }
63
64
 
64
- function onpointerleave() {
65
- clearTimeout(timeout);
65
+ function onmouseleave() {
66
+ clearTimeout(currentTimeout);
66
67
  open = false;
67
68
  }
68
69
  </script>
@@ -80,8 +81,8 @@
80
81
  type={onclick ? 'button' : undefined}
81
82
  class={clazz}
82
83
  bind:this={target}
83
- {onpointerenter}
84
- {onpointerleave}
84
+ {onmouseenter}
85
+ {onmouseleave}
85
86
  {style}
86
87
  {onclick}
87
88
  >
@@ -93,10 +94,10 @@
93
94
  <Popover
94
95
  bind:b_open={open}
95
96
  {target}
96
- placement="top"
97
+ {placement}
97
98
  class={twMerge(
98
99
  clsx(
99
- 'bg-surface-100-800-token max-w-96 -translate-y-0.5 rounded px-4 py-1 shadow-lg',
100
+ 'bg-surface-50-950 max-w-96 -translate-y-0.5 rounded px-4 py-1 shadow-lg',
100
101
  tooltipClass
101
102
  )
102
103
  )}
@@ -0,0 +1,5 @@
1
+ export { expandAllPlugin } from './plugins/expandAll.svelte';
2
+ export { searchPlugin } from './plugins/search.svelte';
3
+ export { default as Table } from './table';
4
+ export { default as Column, type ColumnProps } from './table/Column.svelte';
5
+ export { getColumnHeadContext } from './table/ColumnHead.svelte';
@@ -0,0 +1,34 @@
1
+ import { SvelteSet } from 'svelte/reactivity';
2
+ import { getAllIds, type TablePlugin, type TableRow } from '../table/table.svelte';
3
+
4
+ interface ExpandAllConfig {
5
+ enabled: boolean;
6
+ }
7
+
8
+ const DEFAULT_CONFIG: ExpandAllConfig = {
9
+ enabled: true
10
+ };
11
+
12
+ export function expandAllPlugin<T extends TableRow<T>>(
13
+ conf: Partial<ExpandAllConfig>
14
+ ): TablePlugin<T> {
15
+ let initialized = false;
16
+
17
+ const config: ExpandAllConfig = {
18
+ ...DEFAULT_CONFIG,
19
+ ...conf
20
+ };
21
+
22
+ const middleware: TablePlugin<T> = (state) => {
23
+ if (initialized || !config.enabled) return state;
24
+ initialized = true;
25
+ const allIds = getAllIds(...state.data);
26
+ const newExpanded = new SvelteSet(allIds);
27
+ return {
28
+ data: state.data,
29
+ expanded: newExpanded
30
+ };
31
+ };
32
+
33
+ return middleware;
34
+ }
@@ -0,0 +1,75 @@
1
+ import { SvelteSet } from 'svelte/reactivity';
2
+ import type { TablePlugin, TableRow } from '../table/table.svelte';
3
+
4
+ interface SearchConfig<T extends TableRow<T>> {
5
+ search: string;
6
+ matches: (row: T) => boolean;
7
+ }
8
+
9
+ export function searchPlugin<T extends TableRow<T>>(conf: SearchConfig<T>): TablePlugin<T> {
10
+ let prevSearch: string | undefined = undefined;
11
+ let expandedBeforeSearch: Set<string> | undefined = undefined;
12
+
13
+ const middleware: TablePlugin<T> = (state) => {
14
+ // ensure that the state before the search is saved and restored when the user types
15
+ if (prevSearch && !conf.search && expandedBeforeSearch) {
16
+ prevSearch = conf.search;
17
+ return {
18
+ ...state,
19
+ expanded: expandedBeforeSearch
20
+ };
21
+ }
22
+
23
+ if (!conf.search) return state;
24
+
25
+ // ensure we store the state before the we started searching
26
+ if (conf.search && !prevSearch) {
27
+ expandedBeforeSearch = state.expanded;
28
+ }
29
+
30
+ // figure out which nodes to expand and hide
31
+ const { expanded, hidden } = search(state.data, conf.search, conf.matches);
32
+
33
+ prevSearch = conf.search;
34
+ return {
35
+ data: state.data.filter((d) => !hidden.has(d.id)),
36
+ expanded: new SvelteSet(expanded)
37
+ };
38
+ };
39
+ return middleware;
40
+ }
41
+
42
+ /** collapses everything that doesnt match the searchString expands direct search hit */
43
+ export const search = <T extends TableRow<T>>(
44
+ nodes: T[],
45
+ searchString: string,
46
+ stringsMatch: (a: T, b: string) => boolean
47
+ ) => {
48
+ const search = searchString.trim().toLowerCase();
49
+ const hidden = new Set<string>();
50
+ const expanded = new Set<string>();
51
+
52
+ const recursor = (node: T, childOfMatch = false): boolean => {
53
+ const matches = stringsMatch(node, search);
54
+
55
+ const intermediateNode =
56
+ node.children?.some((c) => recursor(c, matches || childOfMatch)) ?? false;
57
+
58
+ if (intermediateNode) {
59
+ console.log('intermediateNode', node);
60
+
61
+ expanded.add(node.id);
62
+ } else if (!childOfMatch) {
63
+ hidden.add(node.id);
64
+ }
65
+
66
+ return matches || intermediateNode;
67
+ };
68
+
69
+ nodes.forEach((n) => recursor(n));
70
+
71
+ return {
72
+ hidden,
73
+ expanded
74
+ };
75
+ };
@@ -0,0 +1,78 @@
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 type { ColumnConfig } from './column.svelte';
7
+ import { getTableContext } from './Table.svelte';
8
+
9
+ let defaultClasses = $state<ClassValue>();
10
+
11
+ export function setClasses(c: ClassValue) {
12
+ defaultClasses = c;
13
+ }
14
+
15
+ export interface ColumnProps extends ColumnConfig {
16
+ class?: ClassValue;
17
+ /** If the type is incorrect pass the "row" property with the right type */
18
+ children: Snippet;
19
+ onclick?: (e: Event) => void | Promise<void>;
20
+ /** Cannot be used with resizable columns*/
21
+ ignoreWidth?: boolean;
22
+ }
23
+ </script>
24
+
25
+ <script lang="ts">
26
+ let {
27
+ class: clazz = 'py-2 flex flex-row items-center',
28
+ children,
29
+ onclick,
30
+ ignoreWidth = false,
31
+ // ColumnConfig
32
+ resizable = true,
33
+ ...props
34
+ }: ColumnProps = $props();
35
+
36
+ // Register the new column if this is the first table row that was rendered
37
+ const table = getTableContext();
38
+ const column = table.registerColumn({ resizable, ...props });
39
+ const allowClicking = $derived(!!onclick);
40
+
41
+ // passes updated props to the column
42
+ $effect(() => {
43
+ column.updateConfig({ resizable, ...props });
44
+ });
45
+
46
+ // this must be separate to the above effect, since otherwise the width would be reset on every scroll
47
+ $effect(() => {
48
+ if (!column.resizable) column.resize(props.width);
49
+ });
50
+
51
+ function onClick(e: MouseEvent) {
52
+ e.stopPropagation();
53
+ e.preventDefault();
54
+ onclick?.(e);
55
+ }
56
+ </script>
57
+
58
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
59
+ <svelte:element
60
+ this={allowClicking ? 'button' : 'div'}
61
+ onclick={allowClicking ? onClick : undefined}
62
+ type={onclick ? 'button' : undefined}
63
+ style={ignoreWidth ? '' : `width: ${column.width}px !important;`}
64
+ class={twMerge(
65
+ clsx(
66
+ 'flex shrink-0 flex-row items-stretch justify-start truncate',
67
+ !ignoreWidth && [
68
+ 'border-r border-transparent',
69
+ column.dragging && 'border-primary-400-600',
70
+ !column.dragging && column.hovering && 'border-surface-300-700'
71
+ ],
72
+ defaultClasses,
73
+ clazz
74
+ )
75
+ )}
76
+ >
77
+ {@render children()}
78
+ </svelte:element>
@@ -0,0 +1,73 @@
1
+ <script lang="ts" module>
2
+ import { getContext, setContext, type Snippet } from 'svelte';
3
+ import { resize } from '../../../utils/actions';
4
+ import type { Column } from './column.svelte';
5
+
6
+ const CONTEXT = {};
7
+ function setColumnHeadContext(column: Column) {
8
+ setContext(CONTEXT, column);
9
+ }
10
+
11
+ export function getColumnHeadContext(): Column {
12
+ return getContext(CONTEXT);
13
+ }
14
+ </script>
15
+
16
+ <script lang="ts">
17
+ type Props = {
18
+ column: Column;
19
+ children: Snippet;
20
+ };
21
+
22
+ let { column, children }: Props = $props();
23
+ setColumnHeadContext(column);
24
+
25
+ let target = $state<HTMLElement | undefined>();
26
+ let dragging = $state(false);
27
+
28
+ const onHoverStart = () => {
29
+ column.hovering = true;
30
+ };
31
+
32
+ const onHoverEnd = () => {
33
+ column.hovering = false;
34
+ };
35
+
36
+ const onResize = (mouseX: number) => {
37
+ if (!target) return;
38
+ let newWidth = mouseX - target.getBoundingClientRect().left;
39
+ column.resize(newWidth + 2);
40
+ };
41
+
42
+ const onDragging = (d: boolean) => {
43
+ dragging = d;
44
+ column.dragging = d;
45
+ };
46
+ </script>
47
+
48
+ <div
49
+ class={['group flex shrink-0 flex-row justify-start']}
50
+ bind:this={target}
51
+ style="width: {column.width}px;"
52
+ >
53
+ {@render children()}
54
+ {#if column.resizable}
55
+ <button
56
+ type="button"
57
+ class={[
58
+ 'ml-auto flex h-full w-4 shrink-0 translate-x-px cursor-col-resize justify-center border-r bg-inherit',
59
+ dragging
60
+ ? '!border-primary-400-600'
61
+ : 'group-hover:!border-surface-300-700 border-transparent'
62
+ ]}
63
+ use:resize={{ resized: onResize, dragging: onDragging }}
64
+ onmouseenter={onHoverStart}
65
+ onmouseleave={onHoverEnd}
66
+ onfocusin={onHoverStart}
67
+ onfocusout={onHoverEnd}
68
+ tabindex="-1"
69
+ aria-label="Resize column"
70
+ >
71
+ </button>
72
+ {/if}
73
+ </div>
@@ -0,0 +1,67 @@
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
+ let defaultClasses = $state<ClassValue>();
8
+
9
+ export function setClasses(c: ClassValue) {
10
+ defaultClasses = c;
11
+ }
12
+ </script>
13
+
14
+ <script lang="ts">
15
+ interface Props {
16
+ class?: ClassValue;
17
+ onclick?: () => void;
18
+ href?: string;
19
+ children: Snippet;
20
+ }
21
+
22
+ let {
23
+ class: clazz = 'hover:bg-surface-950-50/10 transition-colors',
24
+ onclick,
25
+ href,
26
+ children
27
+ }: Props = $props();
28
+
29
+ const elementProps: {
30
+ this: 'button' | 'a' | 'div';
31
+ type?: 'button';
32
+ onclick?: () => void;
33
+ href?: string;
34
+ } = $derived.by(() => {
35
+ if (onclick) {
36
+ return {
37
+ this: 'button',
38
+ type: 'button',
39
+ onclick
40
+ };
41
+ } else if (href) {
42
+ return {
43
+ this: 'a',
44
+ href: href
45
+ };
46
+ } else {
47
+ return {
48
+ this: 'div'
49
+ };
50
+ }
51
+ });
52
+ </script>
53
+
54
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
55
+ <svelte:element
56
+ this={elementProps.this}
57
+ {...elementProps}
58
+ class={twMerge(
59
+ clsx(
60
+ 'flex h-full min-w-full grow flex-row items-stretch gap-2 overflow-hidden pr-4 pl-2',
61
+ defaultClasses,
62
+ clazz
63
+ )
64
+ )}
65
+ >
66
+ {@render children()}
67
+ </svelte:element>
@@ -0,0 +1,137 @@
1
+ <script lang="ts" module>
2
+ import { ChevronRight } from '@lucide/svelte';
3
+ import clsx from 'clsx';
4
+ import { getContext, setContext, type Snippet } from 'svelte';
5
+ import type { ClassValue } from 'svelte/elements';
6
+ import { twMerge } from 'tailwind-merge';
7
+ import Column from './Column.svelte';
8
+ import ColumnHead from './ColumnHead.svelte';
9
+ import Row from './Row.svelte';
10
+ import { TableController, type TablePlugin, type TableRow } from './table.svelte';
11
+ import VirtualList from './VirtualList.svelte';
12
+
13
+ export interface TableProps<T extends { id: string }> {
14
+ class?: ClassValue;
15
+ data: T[];
16
+ onclick?: (row: T) => void;
17
+ href?: (row: T) => string | undefined;
18
+ rowHeight?: number;
19
+ }
20
+
21
+ const TABLE_CONTEXT = {};
22
+ function setTableContext<T extends { id: string; children?: T[] | undefined }>(
23
+ table: TableController<T>
24
+ ) {
25
+ setContext(TABLE_CONTEXT, table);
26
+ }
27
+
28
+ export function getTableContext<
29
+ T extends { id: string; children?: T[] | undefined }
30
+ >(): TableController<T> {
31
+ return getContext<TableController<T>>(TABLE_CONTEXT);
32
+ }
33
+ </script>
34
+
35
+ <script lang="ts" generics="T extends TableRow<T>">
36
+ interface Props<TI extends { id: string }> extends TableProps<TI> {
37
+ /** Renders the rows */
38
+ children: Snippet<[{ row: TI; nestingLevel?: number; index: number }]>;
39
+ /** Add columns in front of the tree-indicator */
40
+ firstColumn?: Snippet<[{ row: TI }]>;
41
+ rowClass?: ClassValue;
42
+ headerClass?: ClassValue;
43
+ plugins?: TablePlugin<TI>[];
44
+ controller?: TableController<TI>;
45
+ }
46
+
47
+ let {
48
+ class: clazz,
49
+ data,
50
+ children: passedChildren,
51
+ firstColumn,
52
+ rowClass,
53
+ headerClass,
54
+ rowHeight = 64,
55
+ onclick,
56
+ href,
57
+ plugins = [],
58
+ controller: table = new TableController()
59
+ }: Props<T> = $props();
60
+
61
+ $effect(() => {
62
+ table.refresh({
63
+ data,
64
+ plugins
65
+ });
66
+ });
67
+
68
+ setTableContext(table);
69
+ const treeIndicatorId = crypto.randomUUID();
70
+ </script>
71
+
72
+ <VirtualList
73
+ data={table.results.entries}
74
+ class={['border-transparent', clazz, 'flex flex-col overflow-hidden']}
75
+ bind:b_scrollTop={table.scrollTop}
76
+ {rowHeight}
77
+ >
78
+ {#snippet header()}
79
+ <div
80
+ class={twMerge(
81
+ clsx(
82
+ 'flex w-fit min-w-full flex-row gap-2 border-b border-inherit pr-4 pl-2',
83
+ headerClass
84
+ )
85
+ )}
86
+ >
87
+ {#each table.columns as column (column.id)}
88
+ <ColumnHead {column}>
89
+ {#if typeof column.header === 'function'}
90
+ {@render column.header()}
91
+ {:else}
92
+ <div
93
+ class="flex grow flex-row items-center justify-start gap-4 py-2 text-start select-none"
94
+ >
95
+ {column.header}
96
+ </div>
97
+ {/if}
98
+ </ColumnHead>
99
+ {/each}
100
+ </div>
101
+ {/snippet}
102
+ {#snippet children({ row: { node, id, nestingLevel }, index })}
103
+ <Row
104
+ onclick={onclick ? () => onclick(node) : undefined}
105
+ href={href?.(node)}
106
+ class={rowClass}
107
+ >
108
+ {@render firstColumn?.({ row: node })}
109
+ <Column
110
+ id={treeIndicatorId}
111
+ resizable={false}
112
+ header=""
113
+ onclick={() => {
114
+ table.toggleExpansion(node.id);
115
+ }}
116
+ ignoreWidth={table.results.someHaveChildren}
117
+ width={table.results.someHaveChildren ? 24 : 0}
118
+ minWidth={0}
119
+ >
120
+ <div
121
+ class="flex h-full items-center justify-end"
122
+ style="width: calc(var(--spacing) * {nestingLevel * 4} + 24px);"
123
+ >
124
+ {#if node.children}
125
+ <ChevronRight
126
+ class={[
127
+ 'ml-auto aspect-square shrink-0 transition-transform duration-100',
128
+ table.expanded.has(id) && 'rotate-90'
129
+ ]}
130
+ />
131
+ {/if}
132
+ </div>
133
+ </Column>
134
+ {@render passedChildren?.({ row: node, nestingLevel, index })}
135
+ </Row>
136
+ {/snippet}
137
+ </VirtualList>