@open-mercato/ui 0.6.7-develop.6595.1.e93ecd3cfa → 0.6.7-develop.6600.1.49a5df927f

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.
@@ -0,0 +1,9 @@
1
+ function computeMenuViewportShiftX(rect, viewportWidth, margin = 8) {
2
+ if (rect.left < margin) return Math.ceil(margin - rect.left);
3
+ if (rect.right > viewportWidth - margin) return Math.floor(viewportWidth - margin - rect.right);
4
+ return 0;
5
+ }
6
+ export {
7
+ computeMenuViewportShiftX
8
+ };
9
+ //# sourceMappingURL=viewport.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/utils/viewport.ts"],
4
+ "sourcesContent": ["// Horizontal shift (px) needed to keep an overlay fully inside the viewport.\n// Positive shifts right (overlay bleeds off the left edge), negative shifts left\n// (bleeds off the right edge), 0 when it already fits. Keeps a small margin.\n//\n// The two branches are mutually exclusive for any overlay narrower than the\n// viewport (minus the margins): clearing the left edge cannot then push the\n// right edge off-screen. Callers that could exceed the viewport width MUST cap\n// the overlay (e.g. `max-w-[calc(100vw-1rem)]`) \u2014 for a genuinely too-wide\n// overlay this falls back to left-aligning it, which is the sane default.\nexport function computeMenuViewportShiftX(\n rect: { left: number; right: number },\n viewportWidth: number,\n margin = 8,\n): number {\n if (rect.left < margin) return Math.ceil(margin - rect.left)\n if (rect.right > viewportWidth - margin) return Math.floor(viewportWidth - margin - rect.right)\n return 0\n}\n"],
5
+ "mappings": "AASO,SAAS,0BACd,MACA,eACA,SAAS,GACD;AACR,MAAI,KAAK,OAAO,OAAQ,QAAO,KAAK,KAAK,SAAS,KAAK,IAAI;AAC3D,MAAI,KAAK,QAAQ,gBAAgB,OAAQ,QAAO,KAAK,MAAM,gBAAgB,SAAS,KAAK,KAAK;AAC9F,SAAO;AACT;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.7-develop.6595.1.e93ecd3cfa",
3
+ "version": "0.6.7-develop.6600.1.49a5df927f",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -155,13 +155,13 @@
155
155
  "remark-gfm": "^4.0.1"
156
156
  },
157
157
  "peerDependencies": {
158
- "@open-mercato/shared": "0.6.7-develop.6595.1.e93ecd3cfa",
158
+ "@open-mercato/shared": "0.6.7-develop.6600.1.49a5df927f",
159
159
  "react": ">=18.0.0",
160
160
  "react-dom": ">=18.0.0",
161
161
  "react-is": ">=18.0.0"
162
162
  },
163
163
  "devDependencies": {
164
- "@open-mercato/shared": "0.6.7-develop.6595.1.e93ecd3cfa",
164
+ "@open-mercato/shared": "0.6.7-develop.6600.1.49a5df927f",
165
165
  "@testing-library/dom": "^10.4.1",
166
166
  "@testing-library/jest-dom": "^6.9.1",
167
167
  "@testing-library/react": "^16.3.1",
@@ -36,6 +36,7 @@ import { apiCall, withScopedApiRequestHeaders } from './utils/apiCall'
36
36
  import { buildOptimisticLockHeader } from './utils/optimisticLock'
37
37
  import { useGuardedMutation } from './injection/useGuardedMutation'
38
38
  import { raiseCrudError } from './utils/serverErrors'
39
+ import { computeMenuViewportShiftX } from './utils/viewport'
39
40
  import { PerspectiveSidebar } from './PerspectiveSidebar'
40
41
  import { Popover, PopoverTrigger, PopoverContent } from '../primitives/popover'
41
42
  import { formatWithPublicDateFormat, normalizeDateFormatPattern } from '../primitives/date-format'
@@ -707,9 +708,33 @@ function ExportMenu({ config, sections }: { config: DataTableExportConfig; secti
707
708
  const disabled = Boolean(config.disabled)
708
709
  const hasSections = sections.length > 0
709
710
  const [open, setOpen] = React.useState(false)
711
+ const [menuOffsetX, setMenuOffsetX] = React.useState(0)
710
712
  const buttonRef = React.useRef<HTMLButtonElement>(null)
711
713
  const menuRef = React.useRef<HTMLDivElement>(null)
712
714
 
715
+ // Keep the menu inside the viewport on narrow screens: the trigger can sit
716
+ // near the left edge, and a `right-0` menu would otherwise render off-screen.
717
+ // Measure the untransformed rect and shift it back on-screen, re-measuring on
718
+ // resize/orientation change while the menu stays open.
719
+ React.useLayoutEffect(() => {
720
+ if (!open) {
721
+ setMenuOffsetX(0)
722
+ return
723
+ }
724
+ const measure = () => {
725
+ const el = menuRef.current
726
+ if (!el || typeof window === 'undefined') return
727
+ const previousTransform = el.style.transform
728
+ el.style.transform = 'none'
729
+ const rect = el.getBoundingClientRect()
730
+ el.style.transform = previousTransform
731
+ setMenuOffsetX(computeMenuViewportShiftX(rect, window.innerWidth))
732
+ }
733
+ measure()
734
+ window.addEventListener('resize', measure)
735
+ return () => window.removeEventListener('resize', measure)
736
+ }, [open])
737
+
713
738
  React.useEffect(() => {
714
739
  if (!open || !hasSections) return
715
740
  const onDocClick = (event: MouseEvent) => {
@@ -790,7 +815,8 @@ function ExportMenu({ config, sections }: { config: DataTableExportConfig; secti
790
815
  <div
791
816
  ref={menuRef}
792
817
  role="menu"
793
- className="absolute right-0 mt-2 w-60 rounded-md border bg-background py-2 shadow z-dropdown"
818
+ className="absolute right-0 mt-2 w-60 max-w-[calc(100vw-1rem)] rounded-md border bg-background py-2 shadow z-dropdown"
819
+ style={menuOffsetX ? { transform: `translateX(${menuOffsetX}px)` } : undefined}
794
820
  >
795
821
  {sections.map((section, idx) => (
796
822
  <div key={section.key} className={idx > 0 ? 'mt-2 border-t pt-3' : ''}>
@@ -0,0 +1,26 @@
1
+ import { computeMenuViewportShiftX } from '../viewport'
2
+
3
+ describe('computeMenuViewportShiftX', () => {
4
+ const viewport = 390
5
+
6
+ it('returns 0 when the overlay already fits within the viewport', () => {
7
+ expect(computeMenuViewportShiftX({ left: 100, right: 340 }, viewport)).toBe(0)
8
+ })
9
+
10
+ it('shifts right when the overlay bleeds off the left edge', () => {
11
+ // Export dropdown reproduction: left:-38 on a narrow phone.
12
+ expect(computeMenuViewportShiftX({ left: -38, right: 202 }, viewport)).toBe(46)
13
+ })
14
+
15
+ it('shifts left (negative) when the overlay bleeds off the right edge', () => {
16
+ expect(computeMenuViewportShiftX({ left: 200, right: 440 }, viewport)).toBe(-58)
17
+ })
18
+
19
+ it('respects a custom margin', () => {
20
+ expect(computeMenuViewportShiftX({ left: 2, right: 200 }, viewport, 16)).toBe(14)
21
+ })
22
+
23
+ it('leaves overlays flush to the margin untouched', () => {
24
+ expect(computeMenuViewportShiftX({ left: 8, right: 382 }, viewport)).toBe(0)
25
+ })
26
+ })
@@ -0,0 +1,18 @@
1
+ // Horizontal shift (px) needed to keep an overlay fully inside the viewport.
2
+ // Positive shifts right (overlay bleeds off the left edge), negative shifts left
3
+ // (bleeds off the right edge), 0 when it already fits. Keeps a small margin.
4
+ //
5
+ // The two branches are mutually exclusive for any overlay narrower than the
6
+ // viewport (minus the margins): clearing the left edge cannot then push the
7
+ // right edge off-screen. Callers that could exceed the viewport width MUST cap
8
+ // the overlay (e.g. `max-w-[calc(100vw-1rem)]`) — for a genuinely too-wide
9
+ // overlay this falls back to left-aligning it, which is the sane default.
10
+ export function computeMenuViewportShiftX(
11
+ rect: { left: number; right: number },
12
+ viewportWidth: number,
13
+ margin = 8,
14
+ ): number {
15
+ if (rect.left < margin) return Math.ceil(margin - rect.left)
16
+ if (rect.right > viewportWidth - margin) return Math.floor(viewportWidth - margin - rect.right)
17
+ return 0
18
+ }