@witchcraft/ui 0.3.18 → 0.3.20

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 (27) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/assets/animations.css +1 -1
  3. package/dist/runtime/components/LibDatePicker/LibRangeDatePicker.vue +8 -4
  4. package/dist/runtime/components/LibNotifications/LibNotification.vue +2 -2
  5. package/dist/runtime/components/LibNotifications/LibNotifications.d.vue.ts +1 -0
  6. package/dist/runtime/components/LibNotifications/LibNotifications.vue +34 -17
  7. package/dist/runtime/components/LibNotifications/LibNotifications.vue.d.ts +1 -0
  8. package/dist/runtime/components/LibNotifications/calculateNotificationProgress.d.ts +2 -0
  9. package/dist/runtime/components/LibNotifications/calculateNotificationProgress.js +4 -0
  10. package/dist/runtime/components/LibTable/LibTable.d.vue.ts +1 -0
  11. package/dist/runtime/components/LibTable/LibTable.vue +2 -2
  12. package/dist/runtime/components/LibTable/LibTable.vue.d.ts +1 -0
  13. package/dist/runtime/composables/useTimeConditionally.d.ts +16 -0
  14. package/dist/runtime/composables/useTimeConditionally.js +27 -0
  15. package/dist/runtime/directives/vResizableCols.js +20 -25
  16. package/dist/runtime/types/index.d.ts +1 -1
  17. package/package.json +1 -1
  18. package/src/runtime/assets/animations.css +50 -3
  19. package/src/runtime/components/LibDatePicker/LibRangeDatePicker.vue +8 -4
  20. package/src/runtime/components/LibNotifications/LibNotification.vue +2 -2
  21. package/src/runtime/components/LibNotifications/LibNotifications.vue +35 -18
  22. package/src/runtime/components/LibNotifications/calculateNotificationProgress.ts +8 -0
  23. package/src/runtime/components/LibTable/LibTable.stories.ts +5 -4
  24. package/src/runtime/components/LibTable/LibTable.vue +3 -2
  25. package/src/runtime/composables/useTimeConditionally.ts +51 -0
  26. package/src/runtime/directives/vResizableCols.ts +20 -28
  27. package/src/runtime/types/index.ts +1 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "witchcraftUi",
3
3
  "configKey": "witchcraftUi",
4
- "version": "0.3.18",
4
+ "version": "0.3.20",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1 +1 @@
1
- @theme{--animate-blinkInf:blink 1s linear-infinite;@keyframes blink{0%{opacity:0}25%{opacity:1}75%{opacity:1}to{opacity:0}}--animate-slideBgInf:slide-bg 10s linear-infinite;@keyframes slide{0%{background-position:0}to{background-position:100%}}--animate-hide:hide 500ms ease-in;@keyframes hide{0%{opacity:1}to{opacity:0}}--animate-slideIn:slideIn 500ms cubic-bezier(0.16,1,0.3,1);@keyframes slideIn{0%{opacity:0;transform:translateX(100%)}to{opacity:1;transform:translateX(0)}}--animate-overlayShow:overlayShow 500ms cubic-bezier(0.16,1,0.3,1);@keyframes overlayShow{0%{opacity:0}to{opacity:1}}--animate-contentShow:contentShow 500ms cubic-bezier(0.16,1,0.3,1);@keyframes contentShow{0%{opacity:0;transform:translateY(-10%) scale(.96)}to{opacity:1;transform:scale(1)}}}
1
+ @utility animate-from-*{--animate-from:--value([length,percentage]);--animate-from:calc(var(--spacing) * --value(integer))}@utility -animate-from-*{--animate-from:--value([length,percentage]);--animate-from:calc(var(--spacing) * -1 * --value(integer))}@theme{--animate-blinkInf:blink 1s linear-infinite;@keyframes blink{0%{opacity:0}25%{opacity:1}75%{opacity:1}to{opacity:0}}--animate-slideBgInf:slide-bg 10s linear-infinite;@keyframes slide{0%{background-position:0}to{background-position:100%}}--animate-hide:hide 500ms ease-in;@keyframes hide{0%{opacity:1}to{opacity:0}}--animate-slideInLeft:slideInLeft 500ms cubic-bezier(0.16,1,0.3,1);@keyframes slideInLeft{0%{opacity:0;transform:translateX(var(--animate-from,100%))}to{opacity:1;transform:translateX(0)}}--animate-slideInRight:slideInLeft 500ms cubic-bezier(0.16,1,0.3,1);@keyframes slideInRight{0%{opacity:0;transform:translateX(var(--animate-from,-100%))}to{opacity:1;transform:translateX(0)}}--animate-slideInUp:slideInUp 500ms cubic-bezier(0.16,1,0.3,1);@keyframes slideInUp{0%{opacity:0;transform:translateY(var(--animate-from,100%))}to{opacity:1;transform:translateY(0)}}--animate-slideInDown:slideInDown 500ms cubic-bezier(0.16,1,0.3,1);@keyframes slideInDown{0%{opacity:0;transform:translateY(var(--animate-from,-100%))}to{opacity:1;transform:translateY(0)}}--animate-overlayShow:overlayShow 500ms cubic-bezier(0.16,1,0.3,1);@keyframes overlayShow{0%{opacity:0}to{opacity:1}}--animate-contentShow:contentShow 500ms cubic-bezier(0.16,1,0.3,1);@keyframes contentShow{0%{opacity:0;transform:translateY(-10%) scale(.96)}to{opacity:1;transform:scale(1)}}}
@@ -209,10 +209,14 @@ const locale = useInjectedLocale().timeLocale;
209
209
  dark:border-neutral-700
210
210
  shadow-lg
211
211
  will-change-[transform,opacity]
212
- data-[state=open]:data-[side=top]:animate-slideDownAndFade
213
- data-[state=open]:data-[side=right]:animate-slideLeftAndFade
214
- data-[state=open]:data-[side=bottom]:animate-slideUpAndFade
215
- data-[state=open]:data-[side=left]:animate-slideRightAndFade
212
+ data-[side=top]:animate-from-3
213
+ data-[side=left]:animate-from-3
214
+ data-[side=right]:-animate-from-3
215
+ data-[side=bottom]:-animate-from-3
216
+ data-[state=open]:data-[side=top]:animate-slideInUp
217
+ data-[state=open]:data-[side=right]:animate-slideInRight
218
+ data-[state=open]:data-[side=bottom]:animate-slideInDown
219
+ data-[state=open]:data-[side=left]:animate-slideInLeft
216
220
  text-fg
217
221
  dark:text-neutral-200
218
222
  "
@@ -4,7 +4,6 @@
4
4
  :class="twMerge(
5
5
  `
6
6
  notification
7
- max-w-700px
8
7
  bg-neutral-50
9
8
  dark:bg-neutral-900
10
9
  text-fg
@@ -65,7 +64,7 @@
65
64
  </div>
66
65
  </slot>
67
66
  <div class="notification--spacer flex-1"/>
68
- <div class="actions flex">
67
+ <div class="notification--actions flex">
69
68
  <LibButton
70
69
  :border="false"
71
70
  class="
@@ -203,6 +202,7 @@ onMounted(() => {
203
202
  if (props.notification.isPaused) return;
204
203
  emit("pause", props.notification);
205
204
  } else {
205
+ if (!props.notification.isPaused) return;
206
206
  emit("resume", props.notification);
207
207
  }
208
208
  }, { signal: mousedownAbortController.signal });
@@ -4,6 +4,7 @@ import type { HTMLAttributes } from "vue";
4
4
  type RealProps = LinkableByIdProps & {
5
5
  /** If not provided, uses the global handler (this requires useNotificationHandler be called and configured). */
6
6
  handler?: NotificationHandler;
7
+ progressUpdateInterval?: number;
7
8
  };
8
9
  interface Props extends
9
10
  /** @vue-ignore */
@@ -5,17 +5,16 @@
5
5
  tag="div"
6
6
  :class="twMerge(`
7
7
  notifications
8
- [--notification-width:300px]
8
+ [--notification-width:calc(100dvw-var(--spacing)*4)]
9
+ sm:[--notification-width:300px]
9
10
  fixed
10
11
  top-0
11
12
  z-50
12
13
  right-[calc(var(--notification-width)*-1)]
13
14
  w-[calc(var(--spacing)*2+var(--notification-width)*2)]
14
- [&_.notification]:w-[var(--notification-width)]
15
15
  max-h-[100dvh]
16
16
  flex
17
17
  flex-col
18
- [&_.notification]:shrink-0
19
18
  gap-1
20
19
  list-none
21
20
  outline-none
@@ -29,7 +28,16 @@
29
28
  :handler="handler"
30
29
  tabindex="0"
31
30
  :notification="notification"
32
- class="overflow-hidden my-2 max-h-[25dvh] min-h-[300px]"
31
+ class="
32
+ overflow-hidden
33
+ my-2
34
+ max-h-[300px]
35
+ w-[var(--notification-width)]
36
+ shrink-0
37
+ max-sm:[&_.notification--button]:p-2
38
+ max-sm:[&_.notification--button]:py-1
39
+ max-sm:[&_.notification--header]:text-lg
40
+ "
33
41
  v-for="notification of notifications"
34
42
  :key="notification.id"
35
43
  @pause="handler.pause(notification)"
@@ -46,7 +54,7 @@
46
54
  -mx-[calc(var(--spacing)*2+2px)]
47
55
  rounded-none
48
56
  "
49
- :progress="100 - (notification.isPaused ? notification._timer.elapsedBeforePause : notification._timer.elapsedBeforePause + (time - notification.startTime)) / notification.timeout * 100"
57
+ :progress="calculateNotificationProgress(notification, time)"
50
58
  />
51
59
  </template>
52
60
  </lib-notification>
@@ -67,19 +75,27 @@
67
75
  <AlertDialogContent
68
76
  class="
69
77
  data-[state=open]:animate-contentShow
78
+ max-sm:data-[state=open]:animate-slideInUp
70
79
  fixed
71
80
  flex
81
+ max-h-[80dvh]
72
82
  top-[50%]
73
83
  left-[50%]
74
- translate-x-[-50%]
75
- translate-y-[-50%]
76
- max-h-[80dvh]
84
+ sm:translate-x-[-50%]
85
+ sm:translate-y-[-50%]
77
86
  max-w-[700px]
87
+ max-sm:bottom-2
88
+ max-sm:top-[unset]
89
+ max-sm:left-2
90
+ max-sm:right-2
91
+ max-sm:w-[calc(100%-var(--spacing)*4)]
78
92
  z-100
79
93
  "
80
94
  >
81
95
  <lib-notification
82
96
  class="
97
+ w-full
98
+ sm:max-w-[700px]
83
99
  max-w-full
84
100
  max-h-full
85
101
  top-notification
@@ -120,9 +136,11 @@ import {
120
136
  AlertDialogRoot,
121
137
  AlertDialogTitle
122
138
  } from "reka-ui";
123
- import { computed, ref } from "vue";
139
+ import { computed } from "vue";
140
+ import { calculateNotificationProgress } from "./calculateNotificationProgress.js";
124
141
  import LibNotification from "./LibNotification.vue";
125
142
  import { useNotificationHandler } from "../../composables/useNotificationHandler.js";
143
+ import { useTimeConditionally } from "../../composables/useTimeConditionally.js";
126
144
  import { NotificationHandler } from "../../helpers/NotificationHandler.js";
127
145
  import { twMerge } from "../../utils/twMerge.js";
128
146
  import LibProgressBar from "../LibProgressBar/LibProgressBar.vue";
@@ -132,17 +150,16 @@ defineOptions({
132
150
  });
133
151
  const props = defineProps({
134
152
  id: { type: String, required: false },
135
- handler: { type: Object, required: false }
153
+ handler: { type: Object, required: false },
154
+ progressUpdateInterval: { type: Number, required: false }
136
155
  });
156
+ const handler = props.handler ?? useNotificationHandler();
137
157
  const topNotifications = computed(() => handler.queue.filter((entry) => entry.requiresAction).reverse());
138
158
  const notifications = computed(() => handler.queue.filter((entry) => !entry.requiresAction));
139
- const time = ref(Date.now());
140
- setInterval(() => {
141
- requestAnimationFrame(() => {
142
- time.value = Date.now();
143
- });
144
- }, 50);
145
- const handler = props.handler ?? useNotificationHandler();
159
+ const fetchTime = computed(() => {
160
+ return notifications.value.filter((entry) => entry.timeout !== void 0 && !entry.isPaused).length > 0;
161
+ });
162
+ const { time } = useTimeConditionally(fetchTime, { refreshInterval: props.progressUpdateInterval });
146
163
  </script>
147
164
 
148
165
  <script>
@@ -4,6 +4,7 @@ import type { HTMLAttributes } from "vue";
4
4
  type RealProps = LinkableByIdProps & {
5
5
  /** If not provided, uses the global handler (this requires useNotificationHandler be called and configured). */
6
6
  handler?: NotificationHandler;
7
+ progressUpdateInterval?: number;
7
8
  };
8
9
  interface Props extends
9
10
  /** @vue-ignore */
@@ -0,0 +1,2 @@
1
+ import type { NotificationEntry } from "../../helpers/NotificationHandler.js";
2
+ export declare function calculateNotificationProgress(notification: NotificationEntry, time: number): number;
@@ -0,0 +1,4 @@
1
+ export function calculateNotificationProgress(notification, time) {
2
+ if (notification.timeout === void 0) return 0;
3
+ return 100 - (notification.isPaused ? notification._timer.elapsedBeforePause : notification._timer.elapsedBeforePause + (time - notification.startTime)) / notification.timeout * 100;
4
+ }
@@ -11,6 +11,7 @@ type RealProps = {
11
11
  rounded?: boolean;
12
12
  border?: boolean;
13
13
  cellBorder?: boolean;
14
+ /** Disables the header. This also sets the selector to `tr:first-child > td` instead to avoid issues with the vResizableCols directive. */
14
15
  header?: boolean;
15
16
  colConfig?: TableColConfig<T>;
16
17
  /**
@@ -51,7 +51,7 @@
51
51
  mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' && `
52
52
  relative
53
53
  `,
54
- $attrs.wrapperClass
54
+ $attrs['wrapper-class']
55
55
  )"
56
56
  ref="parentRef"
57
57
  >
@@ -240,7 +240,7 @@ const isPostSetup = ref(false);
240
240
  const resizableOptions = computed(() => ({
241
241
  colCount: props.cols.length,
242
242
  widths,
243
- selector: ".cell",
243
+ selector: props.header ? void 0 : `tr:first-child > td`,
244
244
  ...props.resizable,
245
245
  onSetup: (el) => {
246
246
  isPostSetup.value = true;
@@ -11,6 +11,7 @@ type RealProps = {
11
11
  rounded?: boolean;
12
12
  border?: boolean;
13
13
  cellBorder?: boolean;
14
+ /** Disables the header. This also sets the selector to `tr:first-child > td` instead to avoid issues with the vResizableCols directive. */
14
15
  header?: boolean;
15
16
  colConfig?: TableColConfig<T>;
16
17
  /**
@@ -0,0 +1,16 @@
1
+ import { type Ref } from "vue";
2
+ /**
3
+ * Returns a ref with the current time if the given value ref is true or it's an array with 1 or more items.. It will update the time every 50ms (configurable) if so (within a requestAnimationFrame).
4
+ *
5
+ * When the value is anything else it will clear the interval and set the time to undefined.
6
+ *
7
+ * Useful for use with a progress bar.
8
+ *
9
+ * Updating the time all the time is expensive and not idea. This way we only set the interval if we really need it.
10
+ */
11
+ export declare function useTimeConditionally(val: Ref<any[] | boolean | any>, { refreshInterval }?: {
12
+ refreshInterval?: number;
13
+ }): {
14
+ time: Ref<undefined | number>;
15
+ refresh: () => void;
16
+ };
@@ -0,0 +1,27 @@
1
+ import { ref, watch } from "vue";
2
+ export function useTimeConditionally(val, {
3
+ refreshInterval = 50
4
+ } = {}) {
5
+ const time = ref(void 0);
6
+ let interval;
7
+ function refresh(v = val.value) {
8
+ if (v === true || Array.isArray(v) && v.length > 0) {
9
+ if (interval !== void 0) return;
10
+ time.value = Date.now();
11
+ interval = setInterval(() => {
12
+ requestAnimationFrame(() => {
13
+ time.value = Date.now();
14
+ });
15
+ }, refreshInterval);
16
+ } else {
17
+ if (interval === void 0) return;
18
+ clearInterval(interval);
19
+ interval = void 0;
20
+ }
21
+ }
22
+ watch(val, (val2) => {
23
+ refresh(val2);
24
+ });
25
+ refresh();
26
+ return { time, refresh };
27
+ }
@@ -10,14 +10,8 @@ const defaultOpts = {
10
10
  enabled: true
11
11
  };
12
12
  const callback = (_rect, el) => {
13
- const $el = getElInfo(el);
14
- if ($el.justResized) return;
15
13
  setColWidths(el);
16
- $el.justResized = true;
17
- setTimeout(() => {
18
- positionGrips(el);
19
- $el.justResized = false;
20
- }, 0);
14
+ positionGrips(el);
21
15
  };
22
16
  export const vResizableCols = {
23
17
  mounted(el, { value: opts = {} }) {
@@ -29,7 +23,7 @@ export const vResizableCols = {
29
23
  },
30
24
  updated(el, { value: opts = {} }) {
31
25
  const options = override({ ...defaultOpts }, opts);
32
- const info = el && options.enabled && getElInfo(el);
26
+ const info = el && options.enabled && getElInfo(el, { throwIfMissing: false });
33
27
  const hasGrips = el && options.enabled && elMap.get(el)?.grips;
34
28
  const colsNotEqual = info && info.colCount !== options.colCount;
35
29
  if (!options.enabled || colsNotEqual) {
@@ -54,7 +48,11 @@ function setWidth(col, amountInPx, el) {
54
48
  const width = Math.max($el.margin, amountInPx);
55
49
  const index = getColEls(el).findIndex((_) => col === _);
56
50
  if ($el.fitWidth) {
57
- $el.widths.value[index] = `${width / getBox(el).width * 100}%`;
51
+ if (amountInPx <= $el.margin) {
52
+ $el.widths.value[index] = `${$el.margin}px`;
53
+ } else {
54
+ $el.widths.value[index] = `${width / getBox(el).width * 100}%`;
55
+ }
58
56
  } else {
59
57
  $el.widths.value[index] = `${width}px`;
60
58
  }
@@ -86,6 +84,7 @@ function createPointerDownHandler(el) {
86
84
  castType(e.target);
87
85
  $el.target = e.target;
88
86
  $el.isDragging = true;
87
+ el.classList.add("dragging");
89
88
  e.preventDefault();
90
89
  document.addEventListener("pointerup", $el.pointerUpHandler);
91
90
  const { col, colNext } = getCols(el);
@@ -133,11 +132,7 @@ function createPointerMoveHandler(el) {
133
132
  setWidth(col, newWidth, el);
134
133
  }
135
134
  }
136
- $el.justResized = true;
137
- setTimeout(() => {
138
- positionGrips(el);
139
- $el.justResized = false;
140
- }, 0);
135
+ positionGrips(el);
141
136
  }
142
137
  }
143
138
  };
@@ -145,9 +140,11 @@ function createPointerMoveHandler(el) {
145
140
  function createPointerUpHandler(el) {
146
141
  return (e) => {
147
142
  const $el = getElInfo(el);
143
+ $el.pointerMoveHandler(e);
148
144
  if ($el.isDragging) {
149
145
  e.preventDefault();
150
146
  $el.isDragging = false;
147
+ el.classList.remove("dragging");
151
148
  el.classList.remove("resizable-cols-error");
152
149
  $el.offset = 0;
153
150
  delete $el.target;
@@ -187,7 +184,7 @@ function getElInfo(el, { throwIfMissing = true } = {}) {
187
184
  function getColEls(el) {
188
185
  const $el = elMap.get(el);
189
186
  if (!$el) unreachable("El went missing.");
190
- return [...el.querySelectorAll(`:scope ${$el.selector ? $el.selector : "tr > th, tr > td"}`)];
187
+ return [...el.querySelectorAll(`:scope ${$el.selector ?? "tr > th"}`)];
191
188
  }
192
189
  function setupColumns(el, opts) {
193
190
  const gripWidth = getTestGripSize(el);
@@ -206,8 +203,10 @@ function setupColumns(el, opts) {
206
203
  onTeardown: opts.onTeardown
207
204
  };
208
205
  elMap.set(el, $el);
209
- const els = getColEls(el);
210
- const headers = els.slice(0, opts.colCount);
206
+ const headers = getColEls(el);
207
+ if (headers.length !== opts.colCount) {
208
+ throw new Error(`Number of headers matched using selector ${opts.selector ?? "tr > th"} does not match number of columns.`);
209
+ }
211
210
  setColWidths(el, headers);
212
211
  el.style.width = $el.fitWidth ? "" : "min-content";
213
212
  const len = opts.colCount;
@@ -218,13 +217,9 @@ function setupColumns(el, opts) {
218
217
  el.appendChild(grip);
219
218
  $el.grips.set(grip, i);
220
219
  }
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);
220
+ positionGrips(el);
221
+ el.classList.add("resizable-cols-setup");
222
+ opts.onSetup?.(el);
228
223
  }
229
224
  function positionGrips(el) {
230
225
  let xPos = 0;
@@ -291,7 +286,7 @@ function setColWidths(el, children) {
291
286
  castType(col);
292
287
  const colBox = getBox(col);
293
288
  if ($el.fixedWidths[i] !== void 0) {
294
- setWidth(col, $el.fixedWidths[i], el);
289
+ $el.widths.value[i] = `${$el.fixedWidths[i]}px`;
295
290
  width += $el.fixedWidths[i];
296
291
  } else {
297
292
  if ($el.fitWidth) {
@@ -40,7 +40,7 @@ export type ResizableOptions = {
40
40
  * It can then be used as needed by the component.
41
41
  */
42
42
  widths: Ref<string[]>;
43
- /** The selector to use for the cells. "tr > td" by default. */
43
+ /** The selector to use for the header cells. "tr > th" by default. */
44
44
  selector: string;
45
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
46
  onSetup?: (el: Element) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@witchcraft/ui",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
4
4
  "description": "Vue component library.",
5
5
  "type": "module",
6
6
  "main": "./dist/runtime/main.lib.js",
@@ -1,3 +1,13 @@
1
+ @utility animate-from-* {
2
+ --animate-from: --value([length, percentage]);
3
+ --animate-from: calc(var(--spacing) * --value(integer));
4
+ }
5
+
6
+ @utility -animate-from-* {
7
+ --animate-from: --value([length, percentage]);
8
+ --animate-from: calc(var(--spacing) * -1 * --value(integer));
9
+ }
10
+
1
11
  @theme {
2
12
  --animate-blinkInf: blink 1s linear-infinite;
3
13
  @keyframes blink {
@@ -38,10 +48,10 @@
38
48
  }
39
49
  }
40
50
 
41
- --animate-slideIn: slideIn 500ms cubic-bezier(0.16, 1, 0.3, 1);
42
- @keyframes slideIn {
51
+ --animate-slideInLeft: slideInLeft 500ms cubic-bezier(0.16, 1, 0.3, 1);
52
+ @keyframes slideInLeft {
43
53
  from {
44
- transform: translateX(100%);
54
+ transform: translateX(var(--animate-from, 100%));
45
55
  opacity: 0;
46
56
  }
47
57
  to {
@@ -50,6 +60,43 @@
50
60
  }
51
61
  }
52
62
 
63
+ --animate-slideInRight: slideInLeft 500ms cubic-bezier(0.16, 1, 0.3, 1);
64
+ @keyframes slideInRight {
65
+ from {
66
+ transform: translateX(var(--animate-from, -100%));
67
+ opacity: 0;
68
+ }
69
+ to {
70
+ transform: translateX(0);
71
+ opacity: 1;
72
+ }
73
+ }
74
+
75
+ --animate-slideInUp: slideInUp 500ms cubic-bezier(0.16, 1, 0.3, 1);
76
+ @keyframes slideInUp {
77
+ from {
78
+ transform: translateY(var(--animate-from, 100%));
79
+ opacity: 0;
80
+ }
81
+ to {
82
+ transform: translateY(0);
83
+ opacity: 1;
84
+ }
85
+ }
86
+
87
+ --animate-slideInDown: slideInDown 500ms cubic-bezier(0.16, 1, 0.3, 1);
88
+ @keyframes slideInDown {
89
+ from {
90
+ transform: translateY(var(--animate-from, -100%));
91
+ opacity: 0;
92
+ }
93
+ to {
94
+ transform: translateY(0);
95
+ opacity: 1;
96
+ }
97
+ }
98
+
99
+
53
100
  --animate-overlayShow: overlayShow 500ms cubic-bezier(0.16, 1, 0.3, 1);
54
101
  @keyframes overlayShow {
55
102
  from {
@@ -237,10 +237,14 @@ const locale = useInjectedLocale().timeLocale
237
237
  dark:border-neutral-700
238
238
  shadow-lg
239
239
  will-change-[transform,opacity]
240
- data-[state=open]:data-[side=top]:animate-slideDownAndFade
241
- data-[state=open]:data-[side=right]:animate-slideLeftAndFade
242
- data-[state=open]:data-[side=bottom]:animate-slideUpAndFade
243
- data-[state=open]:data-[side=left]:animate-slideRightAndFade
240
+ data-[side=top]:animate-from-3
241
+ data-[side=left]:animate-from-3
242
+ data-[side=right]:-animate-from-3
243
+ data-[side=bottom]:-animate-from-3
244
+ data-[state=open]:data-[side=top]:animate-slideInUp
245
+ data-[state=open]:data-[side=right]:animate-slideInRight
246
+ data-[state=open]:data-[side=bottom]:animate-slideInDown
247
+ data-[state=open]:data-[side=left]:animate-slideInLeft
244
248
  text-fg
245
249
  dark:text-neutral-200
246
250
  "
@@ -3,7 +3,6 @@
3
3
  v-if="notification"
4
4
  :class="twMerge(`
5
5
  notification
6
- max-w-700px
7
6
  bg-neutral-50
8
7
  dark:bg-neutral-900
9
8
  text-fg
@@ -63,7 +62,7 @@
63
62
  </div>
64
63
  </slot>
65
64
  <div class="notification--spacer flex-1"/>
66
- <div class="actions flex">
65
+ <div class="notification--actions flex">
67
66
  <LibButton
68
67
  :border="false"
69
68
  class="
@@ -221,6 +220,7 @@ onMounted(() => {
221
220
  if (props.notification.isPaused) return
222
221
  emit("pause", props.notification)
223
222
  } else {
223
+ if (!props.notification.isPaused) return
224
224
  emit("resume", props.notification)
225
225
  }
226
226
  }, { signal: mousedownAbortController.signal })
@@ -5,17 +5,16 @@
5
5
  tag="div"
6
6
  :class="twMerge(`
7
7
  notifications
8
- [--notification-width:300px]
8
+ [--notification-width:calc(100dvw-var(--spacing)*4)]
9
+ sm:[--notification-width:300px]
9
10
  fixed
10
11
  top-0
11
12
  z-50
12
13
  right-[calc(var(--notification-width)*-1)]
13
14
  w-[calc(var(--spacing)*2+var(--notification-width)*2)]
14
- [&_.notification]:w-[var(--notification-width)]
15
15
  max-h-[100dvh]
16
16
  flex
17
17
  flex-col
18
- [&_.notification]:shrink-0
19
18
  gap-1
20
19
  list-none
21
20
  outline-none
@@ -29,7 +28,16 @@
29
28
  :handler="handler"
30
29
  tabindex="0"
31
30
  :notification="notification"
32
- class="overflow-hidden my-2 max-h-[25dvh] min-h-[300px]"
31
+ class="
32
+ overflow-hidden
33
+ my-2
34
+ max-h-[300px]
35
+ w-[var(--notification-width)]
36
+ shrink-0
37
+ max-sm:[&_.notification--button]:p-2
38
+ max-sm:[&_.notification--button]:py-1
39
+ max-sm:[&_.notification--header]:text-lg
40
+ "
33
41
  v-for="notification of notifications"
34
42
  :key="notification.id"
35
43
  @pause="handler.pause(notification)"
@@ -46,7 +54,7 @@
46
54
  -mx-[calc(var(--spacing)*2+2px)]
47
55
  rounded-none
48
56
  "
49
- :progress="100 - (((notification.isPaused ? (notification._timer.elapsedBeforePause): (notification._timer.elapsedBeforePause + (time - notification.startTime))) / notification.timeout) * 100)"
57
+ :progress="calculateNotificationProgress(notification, time!)"
50
58
  />
51
59
  </template>
52
60
  </lib-notification>
@@ -67,19 +75,27 @@
67
75
  <AlertDialogContent
68
76
  class="
69
77
  data-[state=open]:animate-contentShow
78
+ max-sm:data-[state=open]:animate-slideInUp
70
79
  fixed
71
80
  flex
81
+ max-h-[80dvh]
72
82
  top-[50%]
73
83
  left-[50%]
74
- translate-x-[-50%]
75
- translate-y-[-50%]
76
- max-h-[80dvh]
84
+ sm:translate-x-[-50%]
85
+ sm:translate-y-[-50%]
77
86
  max-w-[700px]
87
+ max-sm:bottom-2
88
+ max-sm:top-[unset]
89
+ max-sm:left-2
90
+ max-sm:right-2
91
+ max-sm:w-[calc(100%-var(--spacing)*4)]
78
92
  z-100
79
93
  "
80
94
  >
81
95
  <lib-notification
82
96
  class="
97
+ w-full
98
+ sm:max-w-[700px]
83
99
  max-w-full
84
100
  max-h-full
85
101
  top-notification
@@ -120,11 +136,13 @@ import {
120
136
  AlertDialogRoot,
121
137
  AlertDialogTitle
122
138
  } from "reka-ui"
123
- import { computed, ref } from "vue"
139
+ import { computed } from "vue"
124
140
 
141
+ import { calculateNotificationProgress } from "./calculateNotificationProgress.js"
125
142
  import LibNotification from "./LibNotification.vue"
126
143
 
127
144
  import { useNotificationHandler } from "../../composables/useNotificationHandler.js"
145
+ import { useTimeConditionally } from "../../composables/useTimeConditionally.js"
128
146
  import { NotificationHandler } from "../../helpers/NotificationHandler.js"
129
147
  import { twMerge } from "../../utils/twMerge.js"
130
148
  import LibProgressBar from "../LibProgressBar/LibProgressBar.vue"
@@ -137,18 +155,16 @@ defineOptions({
137
155
 
138
156
  const props = defineProps<Props>()
139
157
 
158
+ const handler = props.handler ?? useNotificationHandler()
159
+
140
160
  const topNotifications = computed(() => handler.queue.filter(entry => entry.requiresAction).reverse())
141
161
  const notifications = computed(() => handler.queue.filter(entry => !entry.requiresAction))
142
162
 
143
- const time = ref(Date.now())
144
- setInterval(() => {
145
- requestAnimationFrame(() => {
146
- time.value = Date.now()
147
- })
148
- }, 50)
149
-
163
+ const fetchTime = computed(() => {
164
+ return notifications.value.filter(entry => entry.timeout !== undefined && !entry.isPaused).length > 0
165
+ })
150
166
 
151
- const handler = props.handler ?? useNotificationHandler()
167
+ const { time } = useTimeConditionally(fetchTime, { refreshInterval: props.progressUpdateInterval })
152
168
  </script>
153
169
 
154
170
  <script lang="ts">
@@ -157,8 +173,9 @@ import type { HTMLAttributes } from "vue"
157
173
  type RealProps
158
174
  = & LinkableByIdProps
159
175
  & {
160
- /** If not provided, uses the global handler (this requires useNotificationHandler be called and configured). */
176
+ /** If not provided, uses the global handler (this requires useNotificationHandler be called and configured). */
161
177
  handler?: NotificationHandler
178
+ progressUpdateInterval?: number
162
179
  }
163
180
 
164
181
  interface Props
@@ -0,0 +1,8 @@
1
+ import type { NotificationEntry } from "../../helpers/NotificationHandler.js"
2
+
3
+ export function calculateNotificationProgress(notification: NotificationEntry, time: number) {
4
+ if (notification.timeout === undefined) return 0
5
+
6
+ return 100 - (((notification.isPaused ? (notification._timer.elapsedBeforePause) : (notification._timer.elapsedBeforePause + (time - notification.startTime))) / notification.timeout) * 100)
7
+ }
8
+
@@ -19,8 +19,9 @@ const meta: Meta<typeof LibTable> = {
19
19
  export default meta
20
20
  type Story = StoryObj<typeof LibTable> & { args: {
21
21
  slots?: string
22
- wrapperClass?: string
22
+ ["wrapper-class"]?: string
23
23
  } }
24
+
24
25
  export const Primary: Story = {
25
26
  render: args => ({
26
27
  components,
@@ -182,7 +183,7 @@ export const StickyHeader: Story = {
182
183
  // moving the border to the wrapper is to hide the little bits of border sticking out
183
184
  // added back the right straight border otherwise the scrollbar looks ass
184
185
  // this is ever so slightly visible if there is no scrollbar
185
- wrapperClass: `
186
+ ["wrapper-class"]: `
186
187
  max-h-[50dvh]
187
188
  `,
188
189
  values: Array.from({ length: 200 }).fill(0).map((_, i) => ({
@@ -205,7 +206,7 @@ export const VirtualizedFixedHeight: Story = {
205
206
  enabled: true
206
207
  },
207
208
  stickyHeader: true,
208
- wrapperClass: `
209
+ ["wrapper-class"]: `
209
210
  max-h-[50dvh]
210
211
  `,
211
212
  values: Array.from({ length: 10000 }).fill(0).map((_, i) => ({
@@ -258,7 +259,7 @@ export const VirtualizedFitWidthFalse: Story = {
258
259
  [&:not(.resizable-cols-setup)]:w-max
259
260
  [&:not(.resizable-cols-setup)_th]:w-max
260
261
  `,
261
- wrapperClass: `
262
+ ["wrapper-class"]: `
262
263
  max-h-[50dvh]
263
264
  `,
264
265
  values: Array.from({ length: 10000 }).fill(0).map((_, i) => ({
@@ -50,7 +50,7 @@
50
50
  mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' && `
51
51
  relative
52
52
  `,
53
- ($attrs as any).wrapperClass)"
53
+ ($attrs as any)['wrapper-class'])"
54
54
  ref="parentRef"
55
55
  >
56
56
  <div
@@ -256,7 +256,7 @@ const isPostSetup = ref(false)
256
256
  const resizableOptions = computed<MakeRequired<Partial<ResizableOptions>, "colCount" | "widths">>(() => ({
257
257
  colCount: props.cols.length,
258
258
  widths,
259
- selector: ".cell",
259
+ selector: props.header ? undefined : `tr:first-child > td`,
260
260
  ...props.resizable,
261
261
  onSetup: el => {
262
262
  isPostSetup.value = true
@@ -414,6 +414,7 @@ type RealProps = {
414
414
  rounded?: boolean
415
415
  border?: boolean
416
416
  cellBorder?: boolean
417
+ /** Disables the header. This also sets the selector to `tr:first-child > td` instead to avoid issues with the vResizableCols directive. */
417
418
  header?: boolean
418
419
  colConfig?: TableColConfig<T>
419
420
  /**
@@ -0,0 +1,51 @@
1
+ import { type Ref, ref, watch } from "vue"
2
+
3
+ /**
4
+ * Returns a ref with the current time if the given value ref is true or it's an array with 1 or more items.. It will update the time every 50ms (configurable) if so (within a requestAnimationFrame).
5
+ *
6
+ * When the value is anything else it will clear the interval and set the time to undefined.
7
+ *
8
+ * Useful for use with a progress bar.
9
+ *
10
+ * Updating the time all the time is expensive and not idea. This way we only set the interval if we really need it.
11
+ */
12
+ export function useTimeConditionally(
13
+ val: Ref<any[] | boolean | any>,
14
+ {
15
+ refreshInterval = 50
16
+ }: {
17
+ refreshInterval?: number
18
+ } = {}
19
+ ): {
20
+ time: Ref<undefined | number>
21
+ refresh: () => void
22
+ } {
23
+ const time = ref<undefined | number>(undefined)
24
+
25
+ let interval: ReturnType<typeof setInterval> | undefined
26
+ function refresh(
27
+ /** The value to use for the check. Do not pass unless you know what you're doing. */
28
+ v = val.value
29
+ ) {
30
+ if (v === true || (Array.isArray(v) && v.length > 0)) {
31
+ if (interval !== undefined) return
32
+ time.value = Date.now()
33
+ interval = setInterval(() => {
34
+ requestAnimationFrame(() => {
35
+ time.value = Date.now()
36
+ })
37
+ }, refreshInterval)
38
+ } else {
39
+ if (interval === undefined) return
40
+ clearInterval(interval)
41
+ interval = undefined
42
+ }
43
+ }
44
+ watch(val, val => {
45
+ refresh(val)
46
+ })
47
+
48
+ refresh()
49
+
50
+ return { time, refresh }
51
+ }
@@ -24,7 +24,6 @@ type Data = {
24
24
  onTeardown?: (el: Element) => void
25
25
  fixedWidths?: Record<number, number>
26
26
  fluidWidthsAsPercentOfFluidWidth?: Record<number, number>
27
- justResized?: boolean
28
27
  }
29
28
  const elMap = new WeakMap<HTMLElement, Data>()
30
29
  type RawOpts = { value: Partial<ResizableOptions> }
@@ -40,14 +39,8 @@ const defaultOpts: Omit<ResizableOptions, "colCount" | "widths" | "selector"> =
40
39
  // note that while it would be nice to throttle this it seems to loose the reference to the original element
41
40
  // haven't found where the issue is yet #future
42
41
  const callback: ResizeCallback = (_rect: DOMRectReadOnly, el: Element): void => {
43
- const $el = getElInfo(el as ResizableElement)
44
- if ($el.justResized) return
45
42
  setColWidths(el as ResizableElement)
46
- $el.justResized = true
47
- setTimeout(() => {
48
- positionGrips(el as ResizableElement)
49
- $el.justResized = false
50
- }, 0)
43
+ positionGrips(el as ResizableElement)
51
44
  }
52
45
 
53
46
  /**
@@ -119,7 +112,7 @@ export const vResizableCols: Directive = {
119
112
  },
120
113
  updated(el: ResizableElement, { value: opts = {} }: RawOpts) {
121
114
  const options = override({ ...defaultOpts }, opts) as ResizableOptions
122
- const info = el && options.enabled && getElInfo(el)
115
+ const info = el && options.enabled && getElInfo(el, { throwIfMissing: false })
123
116
  const hasGrips = el && options.enabled && elMap.get(el)?.grips
124
117
  // todo, we should probably check by name
125
118
  const colsNotEqual = (info && info.colCount !== options.colCount)
@@ -148,7 +141,11 @@ function setWidth(col: HTMLElement, amountInPx: number, el: ResizableElement): v
148
141
 
149
142
  const index = getColEls(el).findIndex(_ => col === _)
150
143
  if ($el.fitWidth) {
151
- $el.widths.value[index] = `${width / getBox(el).width * 100}%`
144
+ if (amountInPx <= $el.margin) {
145
+ $el.widths.value[index] = `${$el.margin}px`
146
+ } else {
147
+ $el.widths.value[index] = `${width / getBox(el).width * 100}%`
148
+ }
152
149
  } else {
153
150
  $el.widths.value[index] = `${width}px`
154
151
  }
@@ -186,6 +183,7 @@ function createPointerDownHandler(el: ResizableElement) {
186
183
  castType<HTMLElement>(e.target)
187
184
  $el.target = e.target
188
185
  $el.isDragging = true
186
+ el.classList.add("dragging")
189
187
  e.preventDefault()
190
188
 
191
189
  // in case any errors happen, we want the pointer up to still be called
@@ -245,11 +243,7 @@ function createPointerMoveHandler(el: ResizableElement) {
245
243
  }
246
244
  }
247
245
 
248
- $el.justResized = true
249
- setTimeout(() => {
250
- positionGrips(el)
251
- $el.justResized = false
252
- }, 0)
246
+ positionGrips(el)
253
247
  }
254
248
  }
255
249
  }
@@ -258,9 +252,11 @@ function createPointerMoveHandler(el: ResizableElement) {
258
252
  function createPointerUpHandler(el: ResizableElement) {
259
253
  return (e: PointerEvent) => {
260
254
  const $el = getElInfo(el)
255
+ $el.pointerMoveHandler(e)
261
256
  if ($el.isDragging) {
262
257
  e.preventDefault()
263
258
  $el.isDragging = false
259
+ el.classList.remove("dragging")
264
260
  el.classList.remove("resizable-cols-error")
265
261
  $el.offset = 0
266
262
  delete $el.target
@@ -303,7 +299,7 @@ function getElInfo<T extends boolean = true>(el: ResizableElement, { throwIfMiss
303
299
  function getColEls(el: ResizableElement): HTMLElement[] {
304
300
  const $el = elMap.get(el)
305
301
  if (!$el) unreachable("El went missing.")
306
- return [...el.querySelectorAll(`:scope ${$el.selector ? $el.selector : "tr > th, tr > td"}`)] as any
302
+ return [...el.querySelectorAll(`:scope ${$el.selector ?? "tr > th"}`)] as any
307
303
  }
308
304
 
309
305
  function setupColumns(el: ResizableElement, opts: ResizableOptions): void {
@@ -323,9 +319,10 @@ function setupColumns(el: ResizableElement, opts: ResizableOptions): void {
323
319
  onTeardown: opts.onTeardown
324
320
  }
325
321
  elMap.set(el, $el)
326
- const els = getColEls(el)
327
-
328
- const headers = els.slice(0, opts.colCount)
322
+ const headers = getColEls(el)
323
+ if (headers.length !== opts.colCount) {
324
+ throw new Error(`Number of headers matched using selector ${opts.selector ?? "tr > th"} does not match number of columns.`)
325
+ }
329
326
 
330
327
  setColWidths(el, headers)
331
328
  el.style.width = $el.fitWidth ? "" : "min-content"
@@ -338,13 +335,9 @@ function setupColumns(el: ResizableElement, opts: ResizableOptions): void {
338
335
  el.appendChild(grip)
339
336
  $el.grips.set(grip, i)
340
337
  }
341
- $el.justResized = true
342
- setTimeout(() => {
343
- positionGrips(el)
344
- $el.justResized = false
345
- el.classList.add("resizable-cols-setup")
346
- opts.onSetup?.(el)
347
- }, 0)
338
+ positionGrips(el)
339
+ el.classList.add("resizable-cols-setup")
340
+ opts.onSetup?.(el)
348
341
  }
349
342
 
350
343
  function positionGrips(el: ResizableElement): void {
@@ -412,7 +405,6 @@ function setColWidths(el: ResizableElement, children?: Element[]): void {
412
405
  const minFlexWidth = (totalFluidCount * $el.margin)
413
406
  const minTotalWidth = minFlexWidth + fixedTotalPx
414
407
 
415
-
416
408
  let leftOverFluidWidth = elWidth - fixedTotalPx
417
409
  if (leftOverFluidWidth < minFlexWidth) {
418
410
  leftOverFluidWidth = minFlexWidth
@@ -428,7 +420,7 @@ function setColWidths(el: ResizableElement, children?: Element[]): void {
428
420
  */
429
421
  const colBox = getBox(col)
430
422
  if ($el.fixedWidths![i] !== undefined) {
431
- setWidth(col, $el.fixedWidths![i]!, el)
423
+ $el.widths.value[i] = `${$el.fixedWidths![i]!}px`
432
424
  width += $el.fixedWidths![i]!
433
425
  } else {
434
426
  if ($el.fitWidth) {
@@ -41,7 +41,7 @@ export type ResizableOptions = {
41
41
  * It can then be used as needed by the component.
42
42
  */
43
43
  widths: Ref<string[]>
44
- /** The selector to use for the cells. "tr > td" by default. */
44
+ /** The selector to use for the header cells. "tr > th" by default. */
45
45
  selector: string
46
46
  /** 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. */
47
47
  onSetup?: (el: Element) => void