@witchcraft/ui 0.3.14 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "witchcraftUi",
3
3
  "configKey": "witchcraftUi",
4
- "version": "0.3.14",
4
+ "version": "0.3.16",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -22,7 +22,7 @@ const componentsInfo = globFiles([
22
22
  name: name.startsWith("Lib") ? name.replace("Lib", "PREFIX") : `PREFIX${name}`,
23
23
  filepath
24
24
  }));
25
- const module = defineNuxtModule({
25
+ const module$1 = defineNuxtModule({
26
26
  meta: {
27
27
  name: "witchcraftUi",
28
28
  configKey: "witchcraftUi"
@@ -154,4 +154,4 @@ const module = defineNuxtModule({
154
154
  }
155
155
  });
156
156
 
157
- export { module as default };
157
+ export { module$1 as default };
@@ -1 +1 @@
1
- @custom-variant dark (&:where(.dark, .dark *));@utility focus-outline-within{@apply outlined-within:outline-2 outlined-within:outline-accent-500 outlined-within:outline-offset-2}@utility focus-outline{@apply outlined:outline-2 outlined:outline-accent-500 outlined:outline-offset-2}@utility focus-outline-no-offset{@apply outlined:outline-2 outlined:outline-accent-500}@utility focus-outline-hidden{@apply outlined:outline-none}@utility bg-squares-gradient{--_square:var(--squareSize,5px);--_double_square:calc(var(--_square)*2);--_light_square:var(--lightSquare,var(--color-white));--_dark_square:var(--darkSquare,var(--color-black));background-color:var(--_light_square);background:repeating-conic-gradient(var(--_dark_square) 0 25%,var(--_light_square) 0 50%) 50% /var(--_double_square) var(--_double_square)}@utility square-light-*{--lightSquare:--value(--color- *)}@utility square-dark-*{--darkSquare:--value(--color- *)}@utility square-size-*{--squareSize:calc(--value(integer) * 1px)}@utility bg-bars-gradient{--_bg_color:var(--bars-bg-color,var(--color-accent-700));--_fg_color:var(--bars-fg-color,var(--color-accent-800));--_angle:var(--bars-angle,45deg);--_fg_width:var(--bars-fg-width,50%);--_bg_width:calc(100% - var(--_fg_width));background-color:var(--_bg_color);--_pos_1:calc(var(--_bg_width)/2);--_pos_2:calc(var(--_bg_width)/2 + var(--_fg_width)/2);--_pos_3:calc(var(--_bg_width) + var(--_fg_width)/2);background-image:repeating-linear-gradient(var(--_angle),var(--_bg_color),var(--_bg_color) var(--_pos_1),var(--_fg_color) var(--_pos_1),var(--_fg_color) var(--_pos_2),var(--_bg_color) var(--_pos_2),var(--_bg_color) var(--_pos_3),var(--_fg_color) var(--_pos_3),var(--_fg_color))}@utility bars-angle-*{--bars-angle:var(--value(integer) * 1deg)}@utility bars-fg-*{--bars-fg-color:--value(--color-*)}@utility bars-bg-*{--bars-bg-color:--value(--color-*)}@utility bars-w-*{--bars-fg-width:calc(--value(integer) * 1%, 50%)}@utility scrollbar-hidden{-ms-overflow-style:none;scrollbar-width:none;&::-webkit-scrollbar{display:none}}@utility styled-scrollbar{--_scrollbar_width:var(--scrollbar-width,calc(var(--spacing)*3));--_scrollbar_border_width:var(--scrollbar-border-width,calc(var(--spacing)/2));--_scrollbar_color:var(--scrollbar-color,--alpha(var(--color-accent-500)/40%));--_scrollbar_hover_color:var(--scrollbar-hover-color,--alpha(var(--color-accent-500)/80%));--_scrollbar_bg_color:var(--scrollbar-bg-color,var(--color-bg));.dark &{--_scrollbar_bg_color:var(--scrollbar-bg-color,var(--color-fg))}&::-webkit-scrollbar{height:var(--_scrollbar_width);width:var(--_scrollbar_width)}&::-webkit-scrollbar-corner,&::-webkit-scrollbar-track{background-color:transparent}&::-webkit-scrollbar-thumb,&::-webkit-scrollbar-track{border-radius:var(--_scrollbar_width)}&::-webkit-scrollbar-thumb{background-color:var(--_scrollbar_color);border:var(--_scrollbar_border_width) solid var(--_scrollbar_bg_color)}&::-webkit-scrollbar-thumb:hover{cursor:pointer}&::-webkit-scrollbar-thumb:active,&::-webkit-scrollbar-thumb:hover{background-color:var(--_scrollbar_hover_color);border-radius:var(--_scrollbar_width)}}@utility styled-scrollbar-w-*{--scrollbar-width:--value(integer)}@utility styled-scrollbar-border-w-*{--scrollbar-border-width:--value(integer)}@utility styled-scrollbar-*{--scrollbar-color:--value(--color-*)}@utility styled-scrollbar-bg-*{--scrollbar-bg-color:--value(--color-*)}@utility styled-resizer{--_resizer_width:var(--resizer-width,8px);--_resizer_color:var(--resizer-color,var(--color-neutral-300));.dark &{--_resizer_color:var(--resizer-color,var(--color-neutral-700))}&::-webkit-resizer{border-bottom-color:var(--_resizer_color);border-left-color:transparent;border-right-color:var(--_resizer_color);border-style:solid;border-top-color:transparent;border-width:var(--_resizer_width)}}@utility styled-resizer-w-*{--resizer-width:--value(integer)}@utility styled-resizer-color-*{--resizer-color:--value(--color-*)}@utility content-vertical-holder{--tw-content:"\200b";content:var(--tw-content)}@utility no-touch-action{touch-action:none}@utility bg-transparency-squares{@apply bg-squares-gradient square-light-white square-dark-neutral-300 square-size-6}@utility link-like{@apply cursor-pointer hover:text-accent-500}
1
+ @custom-variant dark (&:where(.dark, .dark *));@utility focus-outline-within{@apply outlined-within:outline-2 outlined-within:outline-accent-500 outlined-within:outline-offset-2}@utility focus-outline{@apply outlined:outline-2 outlined:outline-accent-500 outlined:outline-offset-2}@utility focus-outline-no-offset{@apply outlined:outline-2 outlined:outline-accent-500}@utility focus-outline-hidden{@apply outlined:outline-none}@utility bg-squares-gradient{--_square:var(--squareSize,5px);--_double_square:calc(var(--_square)*2);--_light_square:var(--lightSquare,var(--color-white));--_dark_square:var(--darkSquare,var(--color-black));background-color:var(--_light_square);background:repeating-conic-gradient(var(--_dark_square) 0 25%,var(--_light_square) 0 50%) 50% /var(--_double_square) var(--_double_square)}@utility square-light-*{--lightSquare:--value(--color- *)}@utility square-dark-*{--darkSquare:--value(--color- *)}@utility square-size-*{--squareSize:calc(--value(integer) * 1px)}@utility bg-bars-gradient{--_bg_color:var(--bars-bg-color,var(--color-accent-700));--_fg_color:var(--bars-fg-color,var(--color-accent-800));--_angle:var(--bars-angle,45deg);--_fg_width:var(--bars-fg-width,50%);--_bg_width:calc(100% - var(--_fg_width));background-color:var(--_bg_color);--_pos_1:calc(var(--_bg_width)/2);--_pos_2:calc(var(--_bg_width)/2 + var(--_fg_width)/2);--_pos_3:calc(var(--_bg_width) + var(--_fg_width)/2);background-image:repeating-linear-gradient(var(--_angle),var(--_bg_color),var(--_bg_color) var(--_pos_1),var(--_fg_color) var(--_pos_1),var(--_fg_color) var(--_pos_2),var(--_bg_color) var(--_pos_2),var(--_bg_color) var(--_pos_3),var(--_fg_color) var(--_pos_3),var(--_fg_color))}@utility bars-angle-*{--bars-angle:var(--value(integer) * 1deg)}@utility bars-fg-*{--bars-fg-color:--value(--color-*)}@utility bars-bg-*{--bars-bg-color:--value(--color-*)}@utility bars-w-*{--bars-fg-width:calc(--value(integer) * 1%, 50%)}@utility scrollbar-hidden{-ms-overflow-style:none;scrollbar-width:none;&::-webkit-scrollbar{display:none}}@utility styled-scrollbar{--_scrollbar_width:var(--scrollbar-width,calc(var(--spacing)*3));--_scrollbar_border_width:var(--scrollbar-border-width,calc(var(--spacing)/2));--_scrollbar_color:var(--scrollbar-color,--alpha(var(--color-accent-500)/40%));--_scrollbar_hover_color:var(--scrollbar-hover-color,--alpha(var(--color-accent-500)/80%));--_scrollbar_bg_color:var(--scrollbar-bg-color,var(--color-bg));.dark &{--_scrollbar_bg_color:var(--scrollbar-bg-color,var(--color-fg))}&::-webkit-scrollbar{height:var(--_scrollbar_width);width:var(--_scrollbar_width)}&::-webkit-scrollbar-corner,&::-webkit-scrollbar-track{background-color:transparent}&::-webkit-scrollbar-thumb,&::-webkit-scrollbar-track{border-radius:var(--_scrollbar_width)}&::-webkit-scrollbar-thumb{background-color:var(--_scrollbar_color);border:var(--_scrollbar_border_width) solid var(--_scrollbar_bg_color)}&::-webkit-scrollbar-thumb:hover{cursor:pointer}&::-webkit-scrollbar-thumb:active,&::-webkit-scrollbar-thumb:hover{background-color:var(--_scrollbar_hover_color);border-radius:var(--_scrollbar_width)}}@utility styled-scrollbar-w-*{--scrollbar-width:--value(integer)}@utility styled-scrollbar-border-w-*{--scrollbar-border-width:--value(integer)}@utility styled-scrollbar-*{--scrollbar-color:--value(--color-*)}@utility styled-scrollbar-bg-*{--scrollbar-bg-color:--value(--color-*)}@utility styled-resizer{--_resizer_width:var(--resizer-width,8px);--_resizer_color:var(--resizer-color,var(--color-neutral-300));.dark &{--_resizer_color:var(--resizer-color,var(--color-neutral-700))}&::-webkit-resizer{border-bottom-color:var(--_resizer_color);border-left-color:transparent;border-right-color:var(--_resizer_color);border-style:solid;border-top-color:transparent;border-width:var(--_resizer_width)}}@utility styled-resizer-w-*{--resizer-width:--value(integer)}@utility styled-resizer-color-*{--resizer-color:--value(--color-*)}@utility content-vertical-holder{--tw-content:"\200b";content:var(--tw-content)}@utility no-touch-action{touch-action:none}@utility bg-transparency-squares{@apply bg-squares-gradient square-light-white square-dark-neutral-300 square-size-6}@utility link-like{@apply cursor-pointer hover:text-accent-500}@utility no-truncate{overflow:visible;text-overflow:unset;white-space:normal}
@@ -1,3 +1,4 @@
1
+ import { type VirtualizerOptions } from "@tanstack/vue-virtual";
1
2
  import { type TableHTMLAttributes } from "vue";
2
3
  import type { ResizableOptions, TableColConfig } from "../../types/index.js";
3
4
  import type { TailwindClassProp } from "../shared/props.js";
@@ -5,7 +6,6 @@ type T = any;
5
6
  type RealProps = {
6
7
  resizable?: Partial<ResizableOptions>;
7
8
  values?: T[];
8
- itemKey?: keyof T | ((item: T) => string);
9
9
  /** Let's the table know the shape of the data since values might be empty. */
10
10
  cols?: (keyof T)[];
11
11
  rounded?: boolean;
@@ -13,6 +13,56 @@ type RealProps = {
13
13
  cellBorder?: boolean;
14
14
  header?: boolean;
15
15
  colConfig?: TableColConfig<T>;
16
+ /**
17
+ * See tanstack/vue-virtual {@link https://tanstack.com/virtual/latest/docs/api/virtualizer}
18
+ *
19
+ * The defaults are:
20
+ *
21
+ * - enabled: false
22
+ * - method: "fixed"
23
+ * - overscan: (50 if fixed, 10 if dynamic)
24
+ * - estimateSize: () => { return 33 }
25
+ *
26
+ * This also has an additional option, `method`, which can be set to `fixed` or `dynamic` (experimental).
27
+ *
28
+ * Notes:
29
+ *
30
+ * - Because of how virtualization works, initial layout (before .resizable-cols-setup class is applied) will only have access to the headers and not the rows. This can cause cols to look very small, especially if using resizable.fitWidth false.
31
+ *
32
+ * ### Fixed
33
+ *
34
+ * `fixed` is the default and will set the height of ALL items to the height of the first item onMounted (tanstack does not do this and if your estimateSize if off, the scrolling is weird).
35
+ *
36
+ * Since the table now truncates rows by default, they will always be the same height unless you change the inner styling. In fixed mode, `forceRecalculateFixedVirtualizer` is exposed if you need to force re-calculation.
37
+ *
38
+ * If using slots, be sure to at least pass the `class` slot prop to the td element. `style` with width is also supplied but is not required if you're displaying the table as a table.
39
+ *
40
+ * ### Dynamic (experimental)
41
+ *
42
+ * In `dynamic` mode we use tanstack's measureElement method. This is more expensive, but it will work with any heights.
43
+ *
44
+ * Dynamic mode also requires the table displays itself using grid and flex post setup as otherwise dynamic mode doesn't work.
45
+ *
46
+ * You don't need to do anything unless using slots. If using slots, pass the given `ref` slot prop to ref (internally this is tanstack's measureElement) and the class and style slot props at the very least:
47
+ * ```vue
48
+ * <template #[`${colName}`]="slotProps">
49
+ * <td
50
+ * :ref="slotProps.ref"
51
+ * :class="slotProps.class"
52
+ * :style="slotProps.style"
53
+ * >
54
+ * {{ slotProps.value }}
55
+ * </td>
56
+ * </template>
57
+ * ```
58
+ */
59
+ virtualizerOptions?: Partial<VirtualizerOptions<any, any>> & {
60
+ method?: "fixed" | "dynamic";
61
+ };
62
+ /** Whether to enable sticky header styles. This requires `border:false`. This moves the border to the wrapper and styles a straight border between the scroll bar and the rounded border. */
63
+ stickyHeader?: boolean;
64
+ /** Which key to use for the rows (only if not using virtualization). */
65
+ itemKey?: keyof T | ((item: T) => string);
16
66
  };
17
67
  interface Props extends
18
68
  /** @vue-ignore */
@@ -20,18 +70,25 @@ Partial<Omit<TableHTMLAttributes, "class" | "readonly" | "disabled"> & TailwindC
20
70
  }
21
71
  declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
22
72
  props: __VLS_PrettifyLocal<Pick<Partial<{}> & Omit<{} & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, never> & Props & {}> & import("vue").PublicProps;
23
- expose(exposed: import("vue").ShallowUnwrapRef<{}>): void;
73
+ expose(exposed: import("vue").ShallowUnwrapRef<{
74
+ forceRecalculateFixedVirtualizer: () => void;
75
+ }>): void;
24
76
  attrs: any;
25
77
  slots: {
26
78
  [x: string]: ((props: {
27
79
  colKey: any;
28
80
  config: any;
29
- style: string;
30
- class: string;
81
+ style: {
82
+ width: any;
83
+ };
84
+ class: any;
31
85
  }) => any) | undefined;
32
86
  } & {
33
87
  [x: string]: ((props: {
34
- class: string;
88
+ class: any;
89
+ style: {
90
+ width: any;
91
+ };
35
92
  item: any;
36
93
  value: any;
37
94
  }) => any) | undefined;
@@ -1,135 +1,329 @@
1
1
  <template>
2
- <!-- Assumes no scrollbars on children -->
3
- <table
2
+ <!--
3
+ - moving the border to the wrapper is to hide the little bits of border sticking out
4
+ added back the right straight border otherwise the scrollbar looks ass
5
+ this is ever so slightly visible if there is no scrollbar
6
+
7
+ - relative is for the sticky header in dynamic mode
8
+
9
+ - dynamic mode REQUIRES grid since otherwise the transforms don't work because of how tanstack calculates them
10
+ - tried pre-calculating the transforms to take into account the previous elements (e.g. virtual.start - (height of previous rows)) but this was way to slow and buggy
11
+ -->
12
+ <div
4
13
  :class="twMerge(
5
- `table
6
- table-fixed
7
- border-separate
8
- border-spacing-0
9
- overflow-x-scroll
10
- scrollbar-hidden
11
- [&_.grip]:w-[5px]
12
- relative
13
- w-full
14
- box-content
15
- [&_thead]:font-bold
16
- [&_td]:p-1
17
- [&_td]:overflow-x-hidden
18
- [&.resizable-cols-error]:cursor-not-allowed
19
- [&.resizable-cols-error]:user-select-none
20
- `,
21
- cellBorder && `
22
- [&_td]:border-neutral-500
23
- [&_td:not(.last-row)]:border-b
24
- [&_td:not(.first-col)]:border-l
25
- `,
14
+ `
15
+ table--container
16
+ overflow-auto
17
+ `,
18
+ hasScrollbar.vertical && `has-scrollbar-vertical`,
19
+ hasScrollbar.horizontal && `has-scrollbar-horizontal`,
20
+ stickyHeader && `
21
+ [&_thead]:sticky
22
+ [&_thead]:top-0
23
+ [&_thead]:z-1
24
+ [&_.grip]:z-2
25
+ `,
26
+ isPostSetup && `resizable-cols-setup`,
26
27
  border && `
27
- [&_thead_td]:bg-neutral-200
28
- [&_td]:border-neutral-500
29
- dark:[&_thead_td]:bg-neutral-800
30
- dark:[&_td]:border-neutral-500
31
- [&_td.first-row]:border-t
32
- [&_td.last-row]:border-b
33
- [&_td.last-col]:border-r
34
- [&_td.first-col]:border-l
35
- `,
28
+ border
29
+ border-neutral-500
30
+ `,
31
+ border && cellBorder && `
32
+ [&.has-scrollbar-horizontal_.last-row]:border-b
33
+ [&.has-scrollbar-horizontal_.last-row]:border-neutral-500
34
+ [&.has-scrollbar-vertical_.last-col]:border-r
35
+ [&.has-scrollbar-vertical_.last-col]:border-neutral-500
36
+ `,
37
+ (!resizableOptions.fitWidth || stickyHeader) && `
38
+ [&_td.tr]:rounded-tr-none!
39
+ [&_td.br]:rounded-br-none!
40
+ `,
41
+ // this combo prevents the x-scrollbar from showing up when it shouldn\'t
42
+ // and max-w-fit allows the border to shrink with the table columns
43
+ resizableOptions.fitWidth === false && `
44
+ [&_.grip]:last:translate-x-[-5px]
45
+ mr-1
46
+ max-w-fit
47
+ `,
36
48
  rounded && `
37
- [&_td.br]:rounded-br-sm
38
- [&_td.bl]:rounded-bl-sm
39
- [&_td.tr]:rounded-tr-sm
40
- [&_td.tl]:rounded-tl-sm
49
+ rounded-md
50
+ `,
51
+ mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' && `
52
+ relative
41
53
  `,
42
- $attrs.class
54
+ $attrs.wrapperClass
43
55
  )"
44
- v-resizable-cols="resizableOptions"
56
+ ref="parentRef"
45
57
  >
46
- <thead
47
- v-if="header"
48
- class="table--header"
58
+ <div
59
+ class="table--inner-container"
60
+ :style="{
61
+ ...mergedVirtualizerOpts.enabled ? { height: `${totalSize}px` } : {}
62
+ }"
49
63
  >
50
- <tr class="table--row">
51
- <template
52
- v-for="col, i of cols"
53
- :key="col"
64
+ <!-- https://github.com/TanStack/virtual/issues/640#issuecomment-2795731690 -->
65
+ <table
66
+ :style="{
67
+ ...stickyHeader && mergedVirtualizerOpts.enabled ? { '--table-sticky-fix': `${totalSize - tableHeight}px` } : {},
68
+ ...$attrs.style ?? {}
69
+ }"
70
+ :class="twMerge(
71
+ `
72
+ table
73
+ table-fixed
74
+ border-separate
75
+ border-spacing-0
76
+ scrollbar-hidden
77
+ [&_.grip]:w-[5px]
78
+ relative
79
+ w-full
80
+ box-content
81
+ [&_thead]:font-bold
82
+ [&_td]:p-1
83
+ [&_th]:p-1
84
+ [&.resizable-cols-error]:cursor-not-allowed
85
+ [&.resizable-cols-error]:user-select-none
86
+ [&_thead_th]:bg-neutral-200
87
+ dark:[&_thead_th]:bg-neutral-800
88
+ `,
89
+ isPostSetup && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' && `
90
+ grid
91
+ `,
92
+ stickyHeader && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'fixed' && `
93
+ after:inline-block
94
+ after:h-(--table-sticky-fix)
95
+ `,
96
+ cellBorder && `
97
+ [&_td]:border-neutral-500
98
+ [&_td:not(.last-row)]:border-b
99
+ [&_td:not(.first-col)]:border-l
100
+ [&_th]:border-neutral-500
101
+ [&_th:not(.last-row)]:border-b
102
+ [&_th:not(.first-col)]:border-l
103
+ `,
104
+ !cellBorder && `
105
+ [&_.grip]:hover:bg-neutral-300
106
+ dark:[&_.grip]:hover:bg-neutral-700
107
+ `,
108
+ $attrs.class
109
+ )"
110
+ v-resizable-cols="resizableOptions"
111
+ >
112
+ <thead
113
+ v-if="header"
114
+ :class="twMerge(
115
+ `table--header`,
116
+ isPostSetup && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' && `
117
+ grid
118
+ top-0
119
+ `
120
+ )"
54
121
  >
55
- <slot
56
- :name="`header-${col.toString()}`"
57
- :class="[
58
- extraClasses[`-1-${i}`],
59
- 'cell table--header-cell',
60
- colConfig[col]?.resizable === false ? 'no-resize' : ''
61
- ].join(' ')"
62
- :style="`width:${widths.length > 0 ? widths[i] : ``}; `"
63
- :col-key="col"
64
- :config="colConfig[col]"
122
+ <tr
123
+ :class="twMerge(
124
+ `table--row`,
125
+ isPostSetup && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' && `flex w-full`
126
+ )"
65
127
  >
66
- <td
67
- :class="[
68
- extraClasses[`-1-${i}`],
69
- 'cell table--header-cell',
70
- colConfig[col]?.resizable === false ? 'no-resize' : ''
71
- ].join(' ')"
72
- :style="`width:${widths.length > 0 ? widths[i] : ``}; `"
128
+ <template
129
+ v-for="col, i of cols"
130
+ :key="col"
73
131
  >
74
- {{ colConfig[col]?.name ?? col }}
75
- </td>
76
- </slot>
77
- </template>
78
- </tr>
79
- </thead>
80
- <tbody class="table--body">
81
- <template
82
- v-for="item, i of values"
83
- :key="typeof itemKey === 'function' ? itemKey(item) : item[itemKey]"
84
- >
85
- <tr class="table--row">
132
+ <slot
133
+ :name="`header-${col.toString()}`"
134
+ :class="classes[`-1-${i}`]"
135
+ :style="{ width: widths[i] }"
136
+ :col-key="col"
137
+ :config="colConfig[col]"
138
+ >
139
+ <th
140
+ :class="classes[`-1-${i}`]"
141
+ :style="{ width: widths[i] }"
142
+ >
143
+ {{ colConfig[col]?.name ?? col }}
144
+ </th>
145
+ </slot>
146
+ </template>
147
+ </tr>
148
+ </thead>
149
+ <tbody
150
+ :class="twMerge(
151
+ `table--body`,
152
+ isPostSetup && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' && `
153
+ grid
154
+ relative
155
+ `
156
+ )"
157
+ :style="{
158
+ ...mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' ? { height: `${totalSize}px` } : {}
159
+ }"
160
+ >
86
161
  <template
87
- v-for="col, j of cols"
88
- :key="(typeof itemKey === 'function' ? itemKey(item) : item[itemKey]) + col.toString()"
162
+ v-for="(virtual, index) in virtualList"
163
+ :key="virtual.key"
89
164
  >
90
- <slot
91
- :name="col"
92
- :item="item"
93
- :value="item[col]"
94
- :class="extraClasses[`${i}-${j}`] + 'table--cell cell'"
165
+ <tr
166
+ :class="twMerge(
167
+ `
168
+ table--row
169
+ `,
170
+ isPostSetup && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' && `
171
+ flex
172
+ absolute
173
+ w-full
174
+ `,
175
+ isPostSetup && mergedVirtualizerOpts.enabled && ` will-change-transform `
176
+ )"
177
+ :style="{
178
+ ...mergedVirtualizerOpts.enabled ? {
179
+ transform: mergedVirtualizerOpts.method === 'fixed' ? `translateY(${virtual.start - index * virtual.size}px)` : `translateY(${virtual.start}px)`,
180
+ height: virtual.size
181
+ } : {}
182
+ }"
183
+ :data-index="virtual.index"
184
+ :ref="measureElement"
95
185
  >
96
- <td :class="extraClasses[`${i}-${j}`] + 'table--cell cell'">
97
- {{ item[col] }}
98
- </td>
99
- </slot>
186
+ <template
187
+ v-for="col, j of cols"
188
+ :key="virtual.key + '-' + col.toString()"
189
+ >
190
+ <slot
191
+ :name="col"
192
+ :item="values[virtual.index]"
193
+ :value="values[virtual.index][col]"
194
+ :style="{ width: widths[j] }"
195
+ :class="classes[`${virtual.index}-${j}`]"
196
+ >
197
+ <td
198
+ :style="{ width: widths[j] }"
199
+ :class="classes[`${virtual.index}-${j}`]"
200
+ >
201
+ {{ values[virtual.index][col] }}
202
+ </td>
203
+ </slot>
204
+ </template>
205
+ </tr>
100
206
  </template>
101
- </tr>
102
- </template>
103
- </tbody>
104
- </table>
207
+ </tbody>
208
+ </table>
209
+ </div>
210
+ </div>
105
211
  </template>
106
212
 
107
213
  <script setup>
108
214
  import { keys } from "@alanscodelog/utils/keys";
109
- import { computed, ref } from "vue";
215
+ import { throttle } from "@alanscodelog/utils/throttle";
216
+ import { useVirtualizer } from "@tanstack/vue-virtual";
217
+ import { computed, onMounted, ref } from "vue";
218
+ import { useGlobalResizeObserver } from "../../composables/useGlobalResizeObserver.js";
110
219
  import { vResizableCols } from "../../directives/vResizableCols.js";
111
220
  import { twMerge } from "../../utils/twMerge.js";
112
221
  defineOptions({
113
- name: "LibTable"
222
+ name: "LibTable",
223
+ inheritAttrs: false
114
224
  });
115
225
  const props = defineProps({
116
226
  resizable: { type: Object, required: false, default: () => ({}) },
117
227
  values: { type: Array, required: false, default: () => [] },
118
- itemKey: { type: [String, Number, Symbol, Function], required: false, default: "" },
119
228
  cols: { type: Array, required: false, default: () => [] },
120
229
  rounded: { type: Boolean, required: false, default: true },
121
230
  border: { type: Boolean, required: false, default: true },
122
231
  cellBorder: { type: Boolean, required: false, default: true },
123
232
  header: { type: Boolean, required: false, default: true },
124
- colConfig: { type: Object, required: false, default: () => ({}) }
233
+ colConfig: { type: Object, required: false, default: () => ({}) },
234
+ virtualizerOptions: { type: Object, required: false, default: () => ({}) },
235
+ stickyHeader: { type: Boolean, required: false },
236
+ itemKey: { type: [String, Number, Symbol, Function], required: false, default: "" }
125
237
  });
126
238
  const widths = ref([]);
239
+ const isPostSetup = ref(false);
127
240
  const resizableOptions = computed(() => ({
128
241
  colCount: props.cols.length,
129
242
  widths,
130
243
  selector: ".cell",
131
- ...props.resizable
244
+ ...props.resizable,
245
+ onSetup: (el) => {
246
+ isPostSetup.value = true;
247
+ if (props.resizable.onSetup) {
248
+ props.resizable.onSetup(el);
249
+ }
250
+ },
251
+ onTeardown: (el) => {
252
+ isPostSetup.value = false;
253
+ if (props.resizable.onTeardown) {
254
+ props.resizable.onTeardown(el);
255
+ }
256
+ }
132
257
  }));
258
+ const parentRef = ref(null);
259
+ const mergedVirtualizerOpts = computed(() => {
260
+ return {
261
+ // we have to put the defaults here as they can't reference local variables
262
+ count: props.values.length,
263
+ getScrollElement: () => parentRef.value,
264
+ estimateSize: () => {
265
+ return 33;
266
+ },
267
+ overscan: props.virtualizerOptions?.overscan ?? (props.virtualizerOptions?.method === "dynamic" ? 10 : 50),
268
+ method: "fixed",
269
+ enabled: false,
270
+ ...props.virtualizerOptions
271
+ };
272
+ });
273
+ const rowVirtualizer = useVirtualizer(mergedVirtualizerOpts);
274
+ const virtualList = computed(() => {
275
+ return mergedVirtualizerOpts.value.enabled ? rowVirtualizer.value.getVirtualItems() : props.values.map((_, i) => ({
276
+ index: i,
277
+ size: void 0,
278
+ start: 0,
279
+ end: 0,
280
+ key: typeof props.itemKey === "function" ? props.itemKey(_) : props.itemKey ? props.values[props.itemKey] : i
281
+ }));
282
+ });
283
+ const totalSize = computed(() => rowVirtualizer.value.getTotalSize());
284
+ function measureElement(el) {
285
+ if (!el || !mergedVirtualizerOpts.value.enabled) return;
286
+ if (mergedVirtualizerOpts.value?.method === "dynamic") {
287
+ rowVirtualizer.value.measureElement(el);
288
+ }
289
+ }
290
+ function forceRecalculateFixedVirtualizer() {
291
+ if (mergedVirtualizerOpts.value?.method === "dynamic" || !mergedVirtualizerOpts.value.enabled) return;
292
+ if (!parentRef.value) {
293
+ throw new Error("forceRecalculateFixedVirtualizer cannot be called before the table is mounted.");
294
+ }
295
+ const height = parentRef.value.querySelector("td")?.getBoundingClientRect().height;
296
+ if (!height) return;
297
+ for (let i = 0; i < props.values.length; i++) {
298
+ rowVirtualizer.value.resizeItem(i, height);
299
+ }
300
+ }
301
+ const tableHeight = ref(0);
302
+ function updateTableHeight() {
303
+ if (!parentRef.value) return;
304
+ const el = parentRef.value.querySelector("tbody");
305
+ if (!el) return;
306
+ if (tableHeight.value === el.getBoundingClientRect().height) return;
307
+ tableHeight.value = el.getBoundingClientRect().height;
308
+ }
309
+ const throttledUpdateTableHeight = throttle(updateTableHeight, 100, { leading: true });
310
+ onMounted(() => {
311
+ throttledUpdateTableHeight();
312
+ forceRecalculateFixedVirtualizer();
313
+ useGlobalResizeObserver(parentRef, onResize);
314
+ });
315
+ const hasScrollbar = ref({ vertical: false, horizontal: false });
316
+ function onResize() {
317
+ const el = parentRef.value;
318
+ if (!el) return;
319
+ hasScrollbar.value = {
320
+ vertical: el.scrollHeight > el.clientHeight,
321
+ horizontal: el.scrollWidth > el.clientWidth
322
+ };
323
+ if (hasScrollbar.value.vertical) {
324
+ throttledUpdateTableHeight();
325
+ }
326
+ }
133
327
  const getExtraClasses = (row, col, isHeader) => {
134
328
  const res = {
135
329
  bl: !isHeader && row === props.values.length - 1 && col === 0,
@@ -143,12 +337,28 @@ const getExtraClasses = (row, col, isHeader) => {
143
337
  };
144
338
  return keys(res).filter((key) => res[key]);
145
339
  };
146
- const extraClasses = computed(() => Object.fromEntries(
147
- [...Array(props.values.length + 1).keys()].map((row) => [...Array(props.cols.length).keys()].map((col) => [
148
- `${row - 1}-${col}`,
149
- " " + getExtraClasses(row <= 0 ? 0 : row - 1, col, row === 0).join(" ") + " "
150
- ])).flat()
151
- ));
340
+ const classes = computed(() => {
341
+ const res = {};
342
+ const headerTdClass = `table--header-cell cell truncate`;
343
+ const bodyTdClass = `table--cell cell truncate`;
344
+ for (let i = -1; i < props.values.length + 1; i++) {
345
+ for (let j = 0; j < props.cols.length; j++) {
346
+ const col = props.cols[j];
347
+ const colConfig = props.colConfig[col];
348
+ const key = `${i}-${j}`;
349
+ res[key] = twMerge(
350
+ getExtraClasses(i, j, i === -1).join(" "),
351
+ i === -1 ? headerTdClass : bodyTdClass,
352
+ i === -1 ? colConfig?.resizable === false && `no-resize` : void 0,
353
+ i !== -1 && mergedVirtualizerOpts.value.enabled && mergedVirtualizerOpts.value.method === "dynamic" && `flex`
354
+ );
355
+ }
356
+ }
357
+ return res;
358
+ });
359
+ defineExpose({
360
+ forceRecalculateFixedVirtualizer
361
+ });
152
362
  </script>
153
363
 
154
364
  <script>