@umbra.ui/core 0.1.26 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ import {
7
7
  watch,
8
8
  Teleport,
9
9
  computed,
10
+ type CSSProperties,
10
11
  } from "vue";
11
12
  import {
12
13
  offset,
@@ -25,7 +26,7 @@ export interface PopoverProps {
25
26
  /**
26
27
  * Controls whether the popover is visible
27
28
  */
28
- modelValue: boolean;
29
+ modelValue?: boolean;
29
30
  /**
30
31
  * Placement of the popover relative to the trigger
31
32
  * @default 'bottom'
@@ -56,6 +57,11 @@ export interface PopoverProps {
56
57
  * @default 0.5
57
58
  */
58
59
  overlayOpacity?: number;
60
+ /**
61
+ * Blur strength for overlay backdrop (0 = no blur)
62
+ * @default 0
63
+ */
64
+ overlayBlur?: number;
59
65
  /**
60
66
  * Animation duration in seconds
61
67
  * @default 0.2
@@ -96,6 +102,10 @@ export interface PopoverProps {
96
102
  * @default 0
97
103
  */
98
104
  hoverHideDelay?: number;
105
+ /**
106
+ * Background color of the popover
107
+ */
108
+ popoverBackground?: string;
99
109
  }
100
110
 
101
111
  const props = withDefaults(defineProps<PopoverProps>(), {
@@ -104,6 +114,7 @@ const props = withDefaults(defineProps<PopoverProps>(), {
104
114
  dismissOnClickOutside: true,
105
115
  showArrow: true,
106
116
  showOverlay: false,
117
+ overlayBlur: 0,
107
118
  overlayOpacity: 0.5,
108
119
  animationDuration: 0.2,
109
120
  zIndex: 1000,
@@ -113,6 +124,7 @@ const props = withDefaults(defineProps<PopoverProps>(), {
113
124
  trigger: "click",
114
125
  hoverShowDelay: 0,
115
126
  hoverHideDelay: 0,
127
+ popoverBackground: "var(--popover-bg)",
116
128
  });
117
129
 
118
130
  const emit = defineEmits<{
@@ -140,13 +152,19 @@ const popoverStyles = computed(() => ({
140
152
  maxWidth: props.maxWidth,
141
153
  maxHeight: props.maxHeight,
142
154
  zIndex: props.zIndex + 1,
155
+ backgroundColor: props.popoverBackground,
143
156
  }));
144
157
 
145
158
  const overlayStyles = computed(() => ({
146
- opacity: props.overlayOpacity,
147
159
  zIndex: props.zIndex,
148
160
  }));
149
161
 
162
+ const triggerStyles = computed<CSSProperties>(() =>
163
+ props.showOverlay && isVisible.value
164
+ ? { position: "relative", zIndex: props.zIndex + 1 }
165
+ : {}
166
+ );
167
+
150
168
  // Positioning
151
169
  const updatePosition = async () => {
152
170
  if (!triggerRef.value || !popoverRef.value) return;
@@ -196,6 +214,81 @@ const updatePosition = async () => {
196
214
  }
197
215
  };
198
216
 
217
+ const enableSmoothResize = () => {
218
+ if (!popoverRef.value) return;
219
+
220
+ let lastHeight = popoverRef.value.offsetHeight;
221
+ let isFirstRun = true;
222
+
223
+ const resizeObserver = new ResizeObserver((entries) => {
224
+ // Skip the initial observation
225
+ if (isFirstRun) {
226
+ isFirstRun = false;
227
+ return;
228
+ }
229
+
230
+ for (const entry of entries) {
231
+ if (!popoverRef.value) continue;
232
+
233
+ // Get the content wrapper's height
234
+ const contentWrapper = popoverRef.value.lastElementChild as HTMLElement;
235
+ if (!contentWrapper) continue;
236
+
237
+ const newContentHeight = contentWrapper.offsetHeight;
238
+
239
+ // Calculate total height including padding/borders
240
+ const computedStyle = window.getComputedStyle(popoverRef.value);
241
+ const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
242
+ const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
243
+ const borderTop = parseFloat(computedStyle.borderTopWidth) || 0;
244
+ const borderBottom = parseFloat(computedStyle.borderBottomWidth) || 0;
245
+
246
+ const newHeight =
247
+ newContentHeight +
248
+ paddingTop +
249
+ paddingBottom +
250
+ borderTop +
251
+ borderBottom;
252
+
253
+ // Only animate if height actually changed significantly
254
+ if (Math.abs(lastHeight - newHeight) > 1) {
255
+ // Kill any existing animations on this element
256
+ gsap.killTweensOf(popoverRef.value);
257
+
258
+ // Animate height change
259
+ gsap.fromTo(
260
+ popoverRef.value,
261
+ {
262
+ height: lastHeight,
263
+ },
264
+ {
265
+ height: newHeight,
266
+ duration: 0.3,
267
+ ease: "power2.inOut",
268
+ onComplete: () => {
269
+ lastHeight = newHeight;
270
+ updatePosition();
271
+ },
272
+ }
273
+ );
274
+
275
+ break; // Only process one change at a time
276
+ }
277
+ }
278
+ });
279
+
280
+ // Find the popoverContent div
281
+ const contentWrapper = popoverRef.value.lastElementChild as HTMLElement;
282
+ if (contentWrapper) {
283
+ // Only observe direct children, not the wrapper itself
284
+ Array.from(contentWrapper.children).forEach((child) => {
285
+ resizeObserver.observe(child as HTMLElement);
286
+ });
287
+ }
288
+
289
+ return resizeObserver;
290
+ };
291
+
199
292
  // Animation functions
200
293
  const open = async () => {
201
294
  if (isAnimating.value || isVisible.value) return;
@@ -258,10 +351,10 @@ const open = async () => {
258
351
  // NOW set initial state based on actual placement
259
352
  const side = actualPlacement.split("-")[0];
260
353
  const initialOffset = {
261
- top: { x: 0, y: 8 }, // slides up from below
262
- bottom: { x: 0, y: -8 }, // slides down from above
263
- left: { x: 8, y: 0 }, // slides left from right
264
- right: { x: -8, y: 0 }, // slides right from left
354
+ top: { x: 0, y: 8 },
355
+ bottom: { x: 0, y: -8 },
356
+ left: { x: 8, y: 0 },
357
+ right: { x: -8, y: 0 },
265
358
  }[side] || { x: 0, y: -8 };
266
359
 
267
360
  gsap.set(popoverRef.value, {
@@ -288,6 +381,13 @@ const open = async () => {
288
381
  onComplete: () => {
289
382
  isAnimating.value = false;
290
383
  emit("after-enter");
384
+
385
+ // Set up smooth resizing AFTER animation completes
386
+ if (popoverRef.value) {
387
+ const resizeObserver = enableSmoothResize();
388
+ // Store it for cleanup
389
+ (popoverRef.value as any)._resizeObserver = resizeObserver;
390
+ }
291
391
  },
292
392
  });
293
393
 
@@ -304,7 +404,7 @@ const open = async () => {
304
404
  tl.to(
305
405
  overlayRef.value,
306
406
  {
307
- opacity: 1,
407
+ opacity: props.overlayBlur > 0 ? 1 : props.overlayOpacity,
308
408
  duration: props.animationDuration,
309
409
  ease: "power2.out",
310
410
  },
@@ -320,6 +420,12 @@ const close = async () => {
320
420
  emit("update:modelValue", false);
321
421
  emit("close");
322
422
 
423
+ // Clean up resize observer
424
+ if (popoverRef.value && (popoverRef.value as any)._resizeObserver) {
425
+ (popoverRef.value as any)._resizeObserver.disconnect();
426
+ delete (popoverRef.value as any)._resizeObserver;
427
+ }
428
+
323
429
  if (!popoverRef.value) return;
324
430
 
325
431
  // Animate out
@@ -431,11 +537,12 @@ const handleEscape = (event: KeyboardEvent) => {
431
537
  watch(
432
538
  () => props.modelValue,
433
539
  (newValue) => {
434
- if (newValue) {
540
+ if (newValue === true) {
435
541
  open();
436
- } else {
542
+ } else if (newValue === false) {
437
543
  close();
438
544
  }
545
+ // When undefined, let the trigger handle it
439
546
  }
440
547
  );
441
548
 
@@ -472,6 +579,7 @@ defineExpose({
472
579
  <div
473
580
  ref="triggerRef"
474
581
  :class="$style.trigger"
582
+ :style="triggerStyles"
475
583
  @click="handleTriggerClick"
476
584
  @mouseenter="handleTriggerMouseEnter"
477
585
  @mouseleave="handleTriggerMouseLeave"
@@ -485,7 +593,10 @@ defineExpose({
485
593
  <div
486
594
  v-if="showOverlay && isVisible"
487
595
  ref="overlayRef"
488
- :class="$style.overlay"
596
+ :class="[
597
+ $style.overlay,
598
+ overlayBlur > 0 ? $style.overlayBlur : $style.overlayDim,
599
+ ]"
489
600
  :style="overlayStyles"
490
601
  @click="handleClickOutside"
491
602
  />
@@ -502,8 +613,10 @@ defineExpose({
502
613
  <!-- Arrow -->
503
614
  <div v-if="showArrow" ref="arrowRef" :class="$style.arrow" />
504
615
 
505
- <!-- Content -->
506
- <slot />
616
+ <!-- Content wrapper for proper sizing -->
617
+ <div :class="$style.popoverContent">
618
+ <slot />
619
+ </div>
507
620
  </div>
508
621
  </Teleport>
509
622
  </div>
@@ -522,15 +635,23 @@ defineExpose({
522
635
  .overlay {
523
636
  position: fixed;
524
637
  inset: 0;
525
- background-color: var(--popover-overlay-bg);
526
638
  will-change: opacity;
527
639
  }
528
640
 
641
+ .overlayBlur {
642
+ background-color: rgba(0, 0, 0, 0.2); /* Semi-transparent for blur effect */
643
+ backdrop-filter: blur(10px);
644
+ -webkit-backdrop-filter: blur(10px);
645
+ }
646
+
647
+ .overlayDim {
648
+ background-color: var(--popover-overlay-bg); /* Darker for dim effect */
649
+ }
650
+
529
651
  .popover {
530
652
  position: absolute;
531
653
  top: 0;
532
654
  left: 0;
533
- background-color: var(--popover-bg);
534
655
  border-radius: 0.353rem;
535
656
  box-shadow: 0px 1px 0px 0px var(--popover-shadow),
536
657
  inset 0px 1px 0px 0px var(--popover-inset-shadow);
@@ -538,6 +659,17 @@ defineExpose({
538
659
  will-change: transform, opacity;
539
660
  transform-origin: top center;
540
661
  overflow: hidden;
662
+ will-change: transform, opacity, height;
663
+ height: auto;
664
+ display: grid;
665
+ grid-template-rows: 1fr;
666
+ }
667
+
668
+ .popoverContent {
669
+ width: fit-content;
670
+ height: fit-content;
671
+ max-height: 80vh;
672
+ overflow: hidden;
541
673
  }
542
674
 
543
675
  .arrow {
@@ -21,12 +21,7 @@
21
21
  ); /* blackA3 - subtle shadow for light mode */
22
22
 
23
23
  /* Popover overlay colors */
24
- --popover-overlay-bg: rgba(
25
- 0,
26
- 0,
27
- 0,
28
- 0.5
29
- ); /* blackA8 - overlay for light mode */
24
+ --popover-overlay-bg: black; /* blackA8 - overlay for light mode */
30
25
  }
31
26
 
32
27
  /* Dark theme */
@@ -48,5 +43,5 @@
48
43
  --popover-arrow-shadow: rgba(0, 0, 0, 0.05); /* Original dark mode value */
49
44
 
50
45
  /* Popover overlay colors */
51
- --popover-overlay-bg: rgba(0, 0, 0, 0.5); /* Original dark mode value */
46
+ --popover-overlay-bg: black; /* Original dark mode value */
52
47
  }
@@ -1,4 +1,4 @@
1
- <!-- AdaptiveLayout2.vue -->
1
+ <!-- AdaptiveLayout.vue -->
2
2
  <script setup lang="ts">
3
3
  import {
4
4
  markRaw,
@@ -31,6 +31,7 @@ export interface Props {
31
31
  canHideViews?: boolean;
32
32
  canResizeViews?: boolean;
33
33
  layout?: ViewLayout;
34
+ height?: string;
34
35
  }
35
36
 
36
37
  const props = withDefaults(defineProps<Props>(), {
@@ -41,12 +42,17 @@ const props = withDefaults(defineProps<Props>(), {
41
42
  canHideViews: true,
42
43
  canResizeViews: true,
43
44
  layout: "adaptive",
45
+ height: "100%",
44
46
  });
45
47
 
46
48
  // View state management
47
49
  const layoutState = useAdaptiveLayoutState(props.instanceKey);
48
50
  const views = layoutState.views;
49
51
 
52
+ const allowViewResize = computed(() => {
53
+ return props.canResizeViews && props.layout !== "navstack";
54
+ });
55
+
50
56
  // Initialize views with the initial data
51
57
  views.value = props.initialViews.map((view) => ({
52
58
  ...view,
@@ -68,6 +74,20 @@ onMounted(() => {
68
74
  push,
69
75
  pop,
70
76
  });
77
+ switch (props.layout) {
78
+ case "navstack":
79
+ setupForBreakpoint(null, "mobile");
80
+ break;
81
+ case "slideover":
82
+ setupForBreakpoint(null, "tablet");
83
+ break;
84
+ case "splitview":
85
+ setupForBreakpoint(null, "desktop");
86
+ break;
87
+ default:
88
+ break;
89
+ }
90
+ setupForBreakpoint(null, "tablet");
71
91
  });
72
92
 
73
93
  // Container ref and monitoring
@@ -152,13 +172,13 @@ const replace = (id: string, view: View, animated: boolean = true) => {
152
172
  const show = (id: string, animated: boolean = true) => {
153
173
  switch (props.layout) {
154
174
  case "splitview":
155
- splitViewShow(id);
175
+ splitViewShow(id, animated);
156
176
  return;
157
177
  case "slideover":
158
- slideoverShow(id);
178
+ slideoverShow(id, animated);
159
179
  return;
160
180
  case "navstack":
161
- navstackShow(id);
181
+ navstackShow(id, animated);
162
182
  return;
163
183
  }
164
184
  switch (monitor.currentBreakpoint.value) {
@@ -411,6 +431,7 @@ const getViewMaxWidthOnscreen = (view: InternalView): string => {
411
431
  '--padding': props.padding,
412
432
  '--gap': props.gap,
413
433
  '--width': `${monitor.dimensions.value.width}px`,
434
+ height: props.height,
414
435
  }"
415
436
  >
416
437
  <div :id="getOnscreenId()" :class="$style.onscreen">
@@ -458,7 +479,7 @@ const getViewMaxWidthOnscreen = (view: InternalView): string => {
458
479
 
459
480
  <!-- Resize handle (not on last element) -->
460
481
  <div
461
- v-if="props.canResizeViews && index < onScreenViews.length - 1"
482
+ v-if="allowViewResize && index < onScreenViews.length - 1"
462
483
  :class="$style.resizeHandle"
463
484
  @mousedown="startResize(view.id, $event)"
464
485
  >
@@ -29,7 +29,8 @@ export const useViewAnimation = (
29
29
  const animate = async (
30
30
  targets: HTMLElement[],
31
31
  manipulateDOMFn: () => void,
32
- updateDataFn: () => void
32
+ updateDataFn: () => void,
33
+ animated: boolean = true
33
34
  ) => {
34
35
  if (animationInProgress.value) {
35
36
  console.warn("Animation already in progress");
@@ -43,17 +44,24 @@ export const useViewAnimation = (
43
44
  manipulateDOMFn();
44
45
 
45
46
  // Step 3: Animate
46
- return Flip.from(state, {
47
- duration: 0.3,
48
- ease: "power1.inOut",
49
- absolute: true,
50
- onComplete: () => {
51
- animationInProgress.value = false;
52
- },
53
- }).then(() => {
54
- // Step 5: Update Data
47
+ if (animated) {
48
+ return Flip.from(state, {
49
+ duration: 0.3,
50
+ ease: "power1.inOut",
51
+ absolute: true,
52
+ onComplete: () => {
53
+ animationInProgress.value = false;
54
+ },
55
+ }).then(() => {
56
+ // Step 5: Update Data
57
+ updateDataFn();
58
+ });
59
+ } else {
60
+ // Update Data
55
61
  updateDataFn();
56
- });
62
+ // Step 5: Complete
63
+ animationInProgress.value = false;
64
+ }
57
65
  };
58
66
 
59
67
  /*
@@ -235,10 +243,10 @@ export const useViewAnimation = (
235
243
 
236
244
  type NavigationType = "push" | "pop" | "show" | "hide";
237
245
 
238
- const splitViewShow = async (id: string) => {
246
+ const splitViewShow = async (id: string, animated: boolean = true) => {
239
247
  const view = views.value.find((v) => v.id === id);
240
248
  if (view?.location === "onscreen") return;
241
- await splitViewNavigate(id, "show");
249
+ await splitViewNavigate(id, "show", animated);
242
250
  };
243
251
 
244
252
  const splitViewHide = async (id: string) => {
@@ -287,7 +295,11 @@ export const useViewAnimation = (
287
295
  }
288
296
  };
289
297
 
290
- const splitViewNavigate = async (id: string, type: NavigationType) => {
298
+ const splitViewNavigate = async (
299
+ id: string,
300
+ type: NavigationType,
301
+ animated: boolean = true
302
+ ) => {
291
303
  const elements = getViewElements();
292
304
 
293
305
  // Get the appropriate view elements based on type
@@ -439,7 +451,7 @@ export const useViewAnimation = (
439
451
  }
440
452
  };
441
453
 
442
- await animate(elements, manipulateDOM, updateData);
454
+ await animate(elements, manipulateDOM, updateData, animated);
443
455
  };
444
456
 
445
457
  /*
@@ -451,7 +463,7 @@ export const useViewAnimation = (
451
463
  - pop dismisses a view from the overlay to the left.
452
464
  */
453
465
 
454
- const slideoverShow = async (id: string) => {
466
+ const slideoverShow = async (id: string, animated: boolean = true) => {
455
467
  // Get the view with the given id AND all views after it that have location "leading"
456
468
  const currentView = views.value.find((v) => v.id === id);
457
469
  if (currentView?.location === "onscreen") {
@@ -607,9 +619,9 @@ export const useViewAnimation = (
607
619
  - pop dismisses current view to the right, and pushes preceeding view fullscreen from the left.
608
620
  */
609
621
 
610
- const navstackShow = async (id: string) => {
622
+ const navstackShow = async (id: string, animated: boolean = true) => {
611
623
  const index = views.value.findIndex((view) => view.id === id);
612
- navstackNavigateTo(index);
624
+ navstackNavigateTo(index, animated);
613
625
  };
614
626
 
615
627
  const navstackPush = async () => {
@@ -626,7 +638,10 @@ export const useViewAnimation = (
626
638
  navstackNavigateTo(currentIndex - 1);
627
639
  };
628
640
 
629
- const navstackNavigateTo = async (index: number) => {
641
+ const navstackNavigateTo = async (
642
+ index: number,
643
+ animated: boolean = true
644
+ ) => {
630
645
  const onscreen = document.getElementById(getOnscreenId());
631
646
  const offscreenLeading = document.getElementById(getOffscreenLeadingId());
632
647
  const offscreenTrailing = document.getElementById(getOffscreenTrailingId());
@@ -685,7 +700,7 @@ export const useViewAnimation = (
685
700
  currentView.style.width = "";
686
701
  nextView.style.width = "";
687
702
  };
688
- await animate(targets, manipulateDOM, updateData);
703
+ await animate(targets, manipulateDOM, updateData, animated);
689
704
  };
690
705
 
691
706
  /*
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import "@umbra.ui/colors/semantic-colors.css";
2
+ import "@umbra.ui/typography/typography.css";
3
+
1
4
  // Core package exports
2
5
  export * from "@umbra.ui/colors";
3
6
  export * from "@umbra.ui/icons";