@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.
@@ -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,6 +1,5 @@
1
1
  import { castType } from "@alanscodelog/utils/castType";
2
2
  import { override } from "@alanscodelog/utils/override";
3
- import { throttle } from "@alanscodelog/utils/throttle";
4
3
  import { unreachable } from "@alanscodelog/utils/unreachable";
5
4
  import { globalResizeObserver } from "../globalResizeObserver.js";
6
5
  const observer = globalResizeObserver;
@@ -11,38 +10,40 @@ const defaultOpts = {
11
10
  enabled: true
12
11
  };
13
12
  const callback = (_rect, el) => {
13
+ const $el = getElInfo(el);
14
+ if ($el.justResized) return;
14
15
  setColWidths(el);
15
- positionGrips(el);
16
+ $el.justResized = true;
17
+ setTimeout(() => {
18
+ positionGrips(el);
19
+ $el.justResized = false;
20
+ }, 0);
16
21
  };
17
- const throttledCallback = throttle(callback);
18
22
  export const vResizableCols = {
19
23
  mounted(el, { value: opts = {} }) {
20
24
  const options = override({ ...defaultOpts }, opts);
21
25
  if (options.enabled) {
22
26
  setupColumns(el, options);
23
- observer.observe(el, throttledCallback);
27
+ observer.observe(el, callback);
24
28
  }
25
29
  },
26
30
  updated(el, { value: opts = {} }) {
27
31
  const options = override({ ...defaultOpts }, opts);
28
- const info = el && getElInfo(el);
29
- const hasGrips = el && elMap.get(el).grips;
32
+ const info = el && options.enabled && getElInfo(el);
33
+ const hasGrips = el && options.enabled && elMap.get(el)?.grips;
30
34
  const colsNotEqual = info && info.colCount !== options.colCount;
31
- if (hasGrips && !options.enabled || colsNotEqual) {
35
+ if (!options.enabled || colsNotEqual) {
32
36
  teardownColumns(el);
33
- observer.unobserve(el, throttledCallback);
37
+ observer.unobserve(el, callback);
34
38
  }
35
39
  if (!hasGrips && options.enabled || colsNotEqual) {
36
40
  setupColumns(el, options);
37
- observer.observe(el, throttledCallback);
41
+ observer.observe(el, callback);
38
42
  }
39
43
  },
40
44
  unmounted(el) {
41
- const hasGrips = elMap.has(el) && elMap.get(el).grips;
42
- if (hasGrips) {
43
- teardownColumns(el);
44
- globalResizeObserver.unobserve(el, throttledCallback);
45
- }
45
+ teardownColumns(el);
46
+ globalResizeObserver.unobserve(el, callback);
46
47
  },
47
48
  getSSRProps() {
48
49
  return {};
@@ -88,7 +89,7 @@ function createPointerDownHandler(el) {
88
89
  e.preventDefault();
89
90
  document.addEventListener("pointerup", $el.pointerUpHandler);
90
91
  const { col, colNext } = getCols(el);
91
- if (col === null || colNext === null) {
92
+ if (col === null || colNext === null && $el.fitWidth) {
92
93
  el.classList.add("resizable-cols-error");
93
94
  } else {
94
95
  document.addEventListener("pointermove", $el.pointerMoveHandler);
@@ -105,6 +106,7 @@ function createPointerMoveHandler(el) {
105
106
  const $el = getElInfo(el);
106
107
  if ($el.isDragging) {
107
108
  e.preventDefault();
109
+ $el.fluidWidthsAsPercentOfFluidWidth = void 0;
108
110
  const { col, colNext } = getCols(el);
109
111
  if (col !== null) {
110
112
  const leftBox = getBox(col);
@@ -131,7 +133,11 @@ function createPointerMoveHandler(el) {
131
133
  setWidth(col, newWidth, el);
132
134
  }
133
135
  }
134
- positionGrips(el);
136
+ $el.justResized = true;
137
+ setTimeout(() => {
138
+ positionGrips(el);
139
+ $el.justResized = false;
140
+ }, 0);
135
141
  }
136
142
  }
137
143
  };
@@ -173,15 +179,15 @@ function getTestGripSize(el) {
173
179
  el.removeChild(testGrip);
174
180
  return dynamicMinWidth;
175
181
  }
176
- function getElInfo(el) {
182
+ function getElInfo(el, { throwIfMissing = true } = {}) {
177
183
  const $el = elMap.get(el);
178
- if (!$el) unreachable("El went missing.");
184
+ if (!$el && throwIfMissing) unreachable("El went missing.");
179
185
  return $el;
180
186
  }
181
187
  function getColEls(el) {
182
188
  const $el = elMap.get(el);
183
189
  if (!$el) unreachable("El went missing.");
184
- return [...el.querySelectorAll(`:scope ${$el.selector ? $el.selector : "tr > td"}`)];
190
+ return [...el.querySelectorAll(`:scope ${$el.selector ? $el.selector : "tr > th, tr > td"}`)];
185
191
  }
186
192
  function setupColumns(el, opts) {
187
193
  const gripWidth = getTestGripSize(el);
@@ -196,7 +202,8 @@ function setupColumns(el, opts) {
196
202
  margin: opts.margin === "dynamic" ? gripWidth : opts.margin,
197
203
  colCount: opts.colCount,
198
204
  widths: opts.widths,
199
- selector: opts.selector
205
+ selector: opts.selector,
206
+ onTeardown: opts.onTeardown
200
207
  };
201
208
  elMap.set(el, $el);
202
209
  const els = getColEls(el);
@@ -211,17 +218,22 @@ function setupColumns(el, opts) {
211
218
  el.appendChild(grip);
212
219
  $el.grips.set(grip, i);
213
220
  }
214
- positionGrips(el);
215
- el.classList.add("resizable-cols-setup");
221
+ $el.justResized = true;
222
+ setTimeout(() => {
223
+ positionGrips(el);
224
+ $el.justResized = false;
225
+ el.classList.add("resizable-cols-setup");
226
+ opts.onSetup?.(el);
227
+ }, 0);
216
228
  }
217
229
  function positionGrips(el) {
218
230
  let xPos = 0;
219
231
  const $el = getElInfo(el);
220
232
  for (const grip of $el.grips.keys()) {
221
233
  const col = $el.grips.get(grip);
222
- const colEls = getColEls(el)[col];
223
- if (!colEls) unreachable();
224
- const colBox = getBox(colEls);
234
+ const colEl = getColEls(el)[col];
235
+ if (!colEl) unreachable();
236
+ const colBox = getBox(colEl);
225
237
  const gripBox = getBox(grip);
226
238
  grip.style.left = `${xPos + colBox.width - gripBox.width / 2}px`;
227
239
  xPos += colBox.width;
@@ -231,14 +243,71 @@ function setColWidths(el, children) {
231
243
  const $el = getElInfo(el);
232
244
  const header = children ?? getColEls(el).slice(0, $el.colCount);
233
245
  const len = $el.colCount;
246
+ const elWidth = getBox(el).width;
247
+ let fluidTotalPx = 0;
248
+ const fluid = {};
249
+ const doCalculateFixed = $el.fixedWidths === void 0;
250
+ const doCalculateFluid = $el.fluidWidthsAsPercentOfFluidWidth === void 0;
251
+ if (doCalculateFixed) {
252
+ $el.fixedWidths = { [-1]: 0 };
253
+ }
254
+ if (doCalculateFluid) {
255
+ $el.fluidWidthsAsPercentOfFluidWidth = {};
256
+ }
257
+ for (let i = 0; i < len; i++) {
258
+ const col = header[i];
259
+ castType(col);
260
+ if (col.classList.contains("no-resize")) {
261
+ if (doCalculateFixed) {
262
+ const w = getBox(col).width;
263
+ $el.fixedWidths[i] = w;
264
+ $el.fixedWidths[-1] += $el.fixedWidths[i];
265
+ }
266
+ } else {
267
+ if (doCalculateFluid) {
268
+ const w = getBox(col).width;
269
+ fluid[i] = w;
270
+ fluidTotalPx += w;
271
+ }
272
+ }
273
+ }
274
+ const totalFluidCount = len - Object.keys($el.fixedWidths).length;
275
+ if (doCalculateFluid) {
276
+ for (let i = 0; i < len; i++) {
277
+ if ($el.fixedWidths[i] !== void 0) continue;
278
+ $el.fluidWidthsAsPercentOfFluidWidth[i] = fluid[i] / fluidTotalPx;
279
+ }
280
+ }
281
+ const fixedTotalPx = $el.fixedWidths[-1];
282
+ const minFlexWidth = totalFluidCount * $el.margin;
283
+ const minTotalWidth = minFlexWidth + fixedTotalPx;
284
+ let leftOverFluidWidth = elWidth - fixedTotalPx;
285
+ if (leftOverFluidWidth < minFlexWidth) {
286
+ leftOverFluidWidth = minFlexWidth;
287
+ }
234
288
  let width = 0;
235
- const minTotalWidth = len * $el.margin;
236
289
  for (let i = 0; i < len; i++) {
237
290
  const col = header[i];
238
291
  castType(col);
239
292
  const colBox = getBox(col);
240
- setWidth(col, colBox.width, el);
241
- width += getBox(col).width;
293
+ if ($el.fixedWidths[i] !== void 0) {
294
+ setWidth(col, $el.fixedWidths[i], el);
295
+ width += $el.fixedWidths[i];
296
+ } else {
297
+ if ($el.fitWidth) {
298
+ if (!$el.widths.value[i]) {
299
+ setWidth(col, colBox.width, el);
300
+ width += getBox(col).width;
301
+ continue;
302
+ }
303
+ const newInPx = $el.fluidWidthsAsPercentOfFluidWidth[i] * leftOverFluidWidth;
304
+ setWidth(col, newInPx, el);
305
+ width += getBox(col).width;
306
+ } else {
307
+ setWidth(col, colBox.width, el);
308
+ width += getBox(col).width;
309
+ }
310
+ }
242
311
  }
243
312
  if (width < minTotalWidth) {
244
313
  el.style.minWidth = `${minTotalWidth}px`;
@@ -247,14 +316,17 @@ function setColWidths(el, children) {
247
316
  }
248
317
  }
249
318
  function teardownColumns(el) {
250
- const $el = getElInfo(el);
251
- el.removeEventListener("pointerdown", $el.pointerDownHandler);
252
- document.removeEventListener("pointermove", $el.pointerMoveHandler);
253
- document.removeEventListener("pointerup", $el.pointerUpHandler);
254
- for (const key of Object.keys($el)) {
255
- delete $el[key];
319
+ const $el = getElInfo(el, { throwIfMissing: false });
320
+ if ($el) {
321
+ el.removeEventListener("pointerdown", $el.pointerDownHandler);
322
+ document.removeEventListener("pointermove", $el.pointerMoveHandler);
323
+ document.removeEventListener("pointerup", $el.pointerUpHandler);
324
+ for (const key of Object.keys($el)) {
325
+ delete $el[key];
326
+ }
327
+ $el.onTeardown?.(el);
328
+ elMap.delete(el);
256
329
  }
257
- elMap.delete(el);
258
330
  el.classList.remove("resizable-cols-setup");
259
331
  removeGrips(el);
260
332
  }
@@ -2,9 +2,7 @@ import type { ErrorW } from "@alanscodelog/utils";
2
2
  import type { Ref } from "vue";
3
3
  export type ResizableOptions = {
4
4
  /**
5
- * Defaults to true.
6
- *
7
- * ### true
5
+ * ### true (default)
8
6
  * The directive will shrink/expand the columns when the table is resized and will use percentage widths on the table cells. This disables resizing of the last column (from the right handle).
9
7
  *
10
8
  * Additionally because of the way `table-layout:fixed` works, a min-width cannot be set on the elements via css, so instead, if the table shrinks past `opts.margin * col #`, `min-width` is set on the table until it's resized larger.
@@ -16,6 +14,8 @@ export type ResizableOptions = {
16
14
  * The table can be resized past it's normal width and uses pixel widths on the table cells. You might want to set `overscroll-x: scroll` on a parent wrapping element.
17
15
  *
18
16
  * This will set the table width to `min-content`, else it doesn't work. Note that it does this after the initial reading/setting of sizes so you can, for example, layout the table with `width: 100%`.
17
+ *
18
+ * @default true
19
19
  */
20
20
  fitWidth: boolean;
21
21
  /**
@@ -42,6 +42,10 @@ export type ResizableOptions = {
42
42
  widths: Ref<string[]>;
43
43
  /** The selector to use for the cells. "tr > td" by default. */
44
44
  selector: string;
45
+ /** Is called just after the `resizable-cols-setup` class is added. Can be useful for controlling the styling of wrappers or doing additional things post-setup. The default table element uses it to set the class on the wrapper also. */
46
+ onSetup?: (el: Element) => void;
47
+ /** Is called on teardown (after the `resizable-cols-setup` class is removed). */
48
+ onTeardown?: (el: Element) => void;
45
49
  };
46
50
  export type TableColConfig<T = {}> = Record<keyof T, {
47
51
  name?: string;
@@ -5,6 +5,7 @@ export declare const twMergeExtend: {
5
5
  "focus-outline": {
6
6
  "focus-outline": string[];
7
7
  }[];
8
+ "no-truncate": string[];
8
9
  };
9
10
  };
10
11
  };
@@ -2,7 +2,8 @@ import { extendTailwindMerge } from "tailwind-merge";
2
2
  const _twMergeExtend = {
3
3
  extend: {
4
4
  classGroups: {
5
- "focus-outline": [{ "focus-outline": ["", "no-offset", "none"] }]
5
+ "focus-outline": [{ "focus-outline": ["", "no-offset", "none"] }],
6
+ "no-truncate": ["truncate", "no-truncate"]
6
7
  }
7
8
  }
8
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@witchcraft/ui",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "Vue component library.",
5
5
  "type": "module",
6
6
  "main": "./dist/runtime/main.lib.js",
@@ -89,6 +89,7 @@
89
89
  "@chromatic-com/storybook": "^3.2.7",
90
90
  "@commitlint/cli": "^19.8.1",
91
91
  "@internationalized/date": "^3.9.0",
92
+ "@faker-js/faker": "^10.0.0",
92
93
  "@nuxt/eslint-config": "^1.9.0",
93
94
  "@nuxt/module-builder": "^1.0.2",
94
95
  "@nuxtjs/i18n": "^9.5.6",
@@ -106,6 +107,7 @@
106
107
  "@storybook/test-runner": "^0.22.1",
107
108
  "@storybook/vue3": "^8.6.14",
108
109
  "@storybook/vue3-vite": "^8.6.14",
110
+ "@tanstack/vue-virtual": "^3.13.0",
109
111
  "@tailwindcss/cli": "^4.1.12",
110
112
  "@tailwindcss/postcss": "^4.1.12",
111
113
  "@types/node": "^24.3.0",
@@ -176,7 +178,7 @@
176
178
  "storybook": "BROWSER=none storybook dev -p 6006",
177
179
  "storybook:clear-cache": "BROWSER=none storybook dev -p 6006 --no-manager-cache",
178
180
  "storybook:build": "pnpm nuxt prepare && storybook build -o docs/storybook",
179
- "storybook:test": "pnpm storybook:build && pnpm concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"pnpm http-server docs/storybook --port 6006 --silent\" \"pnpm wait-on tcp:6006 && pnpm test-storybook\"",
181
+ "storybook:test": "pnpm storybook:build && pnpm concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"pnpm http-server docs/storybook --port 6006 --silent\" \"pnpm wait-on tcp:6006 && pnpm test-storybook --excludeTags 'skip-smoke-test'\"",
180
182
  "test": "pnpm storybook:test && pnpm lint:types",
181
183
  "test:dev": "pnpm concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"pnpm storybook\" \"pnpm wait-on tcp:6006 && pnpm test-storybook --watch\"",
182
184
  "doc": "pnpm test && pnpm storybook:build",
package/src/module.ts CHANGED
@@ -177,7 +177,9 @@ export default defineNuxtModule<ModuleOptions>({
177
177
  nuxt.hook("vite:extendConfig", async config => {
178
178
  if (options.includeUnpluginIconsPlugins) {
179
179
  logger.info(`Adding unplugin-icons`)
180
+ // @ts-expect-error .
180
181
  config.plugins ??= []
182
+ // @ts-expect-error .
181
183
  config.plugins = [
182
184
  ...(
183
185
  options.includeUnpluginIconsPlugins
@@ -199,6 +201,7 @@ export default defineNuxtModule<ModuleOptions>({
199
201
  tailwindcss() as any,
200
202
  ...config.plugins
201
203
  ]
204
+ // @ts-expect-error .
202
205
  config.optimizeDeps ??= {}
203
206
  config.optimizeDeps.exclude ??= []
204
207
  config.optimizeDeps.exclude.push("~icons")
@@ -224,3 +224,8 @@
224
224
  @apply cursor-pointer hover:text-accent-500;
225
225
  }
226
226
 
227
+ @utility no-truncate {
228
+ text-overflow: unset;
229
+ overflow: visible;
230
+ white-space: normal;
231
+ }