@aiaiai-pt/design-system 0.51.0 → 0.51.1

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.
@@ -25,6 +25,8 @@
25
25
  * @typedef {'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'} Placement
26
26
  */
27
27
 
28
+ import { calculatePopoverPosition } from './popover-position.js';
29
+
28
30
  let {
29
31
  /** @type {boolean} */
30
32
  open = $bindable(false),
@@ -52,6 +54,7 @@
52
54
 
53
55
  let posX = $state(0);
54
56
  let posY = $state(0);
57
+ let posMaxHeight = /** @type {number | undefined} */ ($state(undefined));
55
58
  let width = $state(0);
56
59
 
57
60
  function reposition() {
@@ -66,27 +69,19 @@
66
69
  width = rect.width;
67
70
  }
68
71
 
69
- const isTop = placement.startsWith('top');
70
- const isEnd = placement.endsWith('end');
71
-
72
- let x = isEnd ? rect.right - (matchWidth ? rect.width : popRect.width) : rect.left;
73
- let y = isTop ? rect.top - popRect.height - offset : rect.bottom + offset;
74
-
75
- // Flip vertical if out of viewport
76
- if (!isTop && y + popRect.height > viewportH) {
77
- y = rect.top - popRect.height - offset;
78
- } else if (isTop && y < 0) {
79
- y = rect.bottom + offset;
80
- }
81
-
82
- // Clamp horizontal
83
- if (x + popRect.width > viewportW) {
84
- x = viewportW - popRect.width - offset;
85
- }
86
- if (x < 0) x = offset;
72
+ const { x, y, maxHeight } = calculatePopoverPosition(
73
+ rect,
74
+ { width: popRect.width, height: popRect.height },
75
+ viewportH,
76
+ viewportW,
77
+ placement,
78
+ offset,
79
+ matchWidth,
80
+ );
87
81
 
88
82
  posX = x;
89
83
  posY = y;
84
+ posMaxHeight = maxHeight;
90
85
  }
91
86
 
92
87
  // Position + close listeners
@@ -157,6 +152,8 @@
157
152
  style:left="{posX}px"
158
153
  style:top="{posY}px"
159
154
  style:width={matchWidth ? `${width}px` : undefined}
155
+ style:max-height={posMaxHeight != null ? `${posMaxHeight}px` : undefined}
156
+ style:overflow-y={posMaxHeight != null ? 'auto' : undefined}
160
157
  {...rest}
161
158
  >
162
159
  {#if children}{@render children()}{/if}
@@ -1,4 +1,5 @@
1
1
  export default Popover;
2
+ export type Placement = "bottom-start" | "bottom-end" | "top-start" | "top-end";
2
3
  type Popover = {
3
4
  $on?(type: string, callback: (e: any) => void): () => void;
4
5
  $set?(props: Partial<$$ComponentProps>): void;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * calculatePopoverPosition — pure positioning algorithm for Popover.svelte.
3
+ *
4
+ * Returns the fixed-position coordinate (x, y) for the popover, plus an
5
+ * optional maxHeight cap (in px) when neither the preferred nor the
6
+ * opposite side fits and we must scroll the popover content in-place.
7
+ *
8
+ * Preference order:
9
+ * 1. Preferred side (from `placement`) if it fits.
10
+ * 2. Opposite side if that fits.
11
+ * 3. Side with more room — capped to that room, overflow-y scrollable.
12
+ *
13
+ * @param {{ top: number, bottom: number, left: number, right: number, width: number }} anchorRect
14
+ * @param {{ width: number, height: number }} popRect
15
+ * @param {number} viewportH
16
+ * @param {number} viewportW
17
+ * @param {string} placement — 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
18
+ * @param {number} offset — gap between anchor and popover (px)
19
+ * @param {boolean} [matchWidth]
20
+ * @returns {{ x: number, y: number, maxHeight: number | undefined }}
21
+ */
22
+ export function calculatePopoverPosition(anchorRect: {
23
+ top: number;
24
+ bottom: number;
25
+ left: number;
26
+ right: number;
27
+ width: number;
28
+ }, popRect: {
29
+ width: number;
30
+ height: number;
31
+ }, viewportH: number, viewportW: number, placement: string, offset: number, matchWidth?: boolean): {
32
+ x: number;
33
+ y: number;
34
+ maxHeight: number | undefined;
35
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * calculatePopoverPosition — pure positioning algorithm for Popover.svelte.
3
+ *
4
+ * Returns the fixed-position coordinate (x, y) for the popover, plus an
5
+ * optional maxHeight cap (in px) when neither the preferred nor the
6
+ * opposite side fits and we must scroll the popover content in-place.
7
+ *
8
+ * Preference order:
9
+ * 1. Preferred side (from `placement`) if it fits.
10
+ * 2. Opposite side if that fits.
11
+ * 3. Side with more room — capped to that room, overflow-y scrollable.
12
+ *
13
+ * @param {{ top: number, bottom: number, left: number, right: number, width: number }} anchorRect
14
+ * @param {{ width: number, height: number }} popRect
15
+ * @param {number} viewportH
16
+ * @param {number} viewportW
17
+ * @param {string} placement — 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
18
+ * @param {number} offset — gap between anchor and popover (px)
19
+ * @param {boolean} [matchWidth]
20
+ * @returns {{ x: number, y: number, maxHeight: number | undefined }}
21
+ */
22
+ export function calculatePopoverPosition(
23
+ anchorRect,
24
+ popRect,
25
+ viewportH,
26
+ viewportW,
27
+ placement,
28
+ offset,
29
+ matchWidth = false,
30
+ ) {
31
+ const isTop = placement.startsWith("top");
32
+ const isEnd = placement.endsWith("end");
33
+
34
+ // Horizontal: preserve matchWidth behaviour for end-aligned placement
35
+ let x = isEnd
36
+ ? anchorRect.right - (matchWidth ? anchorRect.width : popRect.width)
37
+ : anchorRect.left;
38
+
39
+ // Available room on each side (gap to viewport edge after the offset)
40
+ const roomAbove = anchorRect.top - offset;
41
+ const roomBelow = viewportH - anchorRect.bottom - offset;
42
+
43
+ const fitsAbove = popRect.height <= roomAbove;
44
+ const fitsBelow = popRect.height <= roomBelow;
45
+
46
+ let y;
47
+ let maxHeight = undefined;
48
+
49
+ if (isTop) {
50
+ if (fitsAbove) {
51
+ // Preferred side fits
52
+ y = anchorRect.top - popRect.height - offset;
53
+ } else if (fitsBelow) {
54
+ // Flip to below
55
+ y = anchorRect.bottom + offset;
56
+ } else {
57
+ // Neither fits: open on the side with more room and cap
58
+ if (roomAbove >= roomBelow) {
59
+ maxHeight = roomAbove;
60
+ y = anchorRect.top - maxHeight - offset; // = anchorRect.top - (anchorRect.top - offset) - offset = 0
61
+ } else {
62
+ y = anchorRect.bottom + offset;
63
+ maxHeight = roomBelow;
64
+ }
65
+ }
66
+ } else {
67
+ // Bottom placement
68
+ if (fitsBelow) {
69
+ // Preferred side fits
70
+ y = anchorRect.bottom + offset;
71
+ } else if (fitsAbove) {
72
+ // Flip to above
73
+ y = anchorRect.top - popRect.height - offset;
74
+ } else {
75
+ // Neither fits: open on the side with more room and cap
76
+ if (roomBelow >= roomAbove) {
77
+ y = anchorRect.bottom + offset;
78
+ maxHeight = roomBelow;
79
+ } else {
80
+ maxHeight = roomAbove;
81
+ y = anchorRect.top - maxHeight - offset; // = 0
82
+ }
83
+ }
84
+ }
85
+
86
+ // Clamp horizontal
87
+ if (x + popRect.width > viewportW) {
88
+ x = viewportW - popRect.width - offset;
89
+ }
90
+ if (x < 0) x = offset;
91
+
92
+ return { x, y, maxHeight };
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.51.0",
3
+ "version": "0.51.1",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",