@ai-matrx/design-system 0.37.1 → 0.38.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.
@@ -214,12 +214,14 @@ function useIsMobile() {
214
214
 
215
215
  // src/dialog.tsx
216
216
  import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
217
+ var DialogWindowedByDefaultContext = React5.createContext(false);
217
218
  var Dialog = ({
218
219
  children,
219
220
  ...props
220
221
  }) => {
221
- const modal = props.modal ?? true;
222
- return /* @__PURE__ */ jsx5(RadixDialogModalProvider, { modal, children: /* @__PURE__ */ jsx5(DialogPrimitive.Root, { ...props, modal, children }) });
222
+ const isMobile = useIsMobile();
223
+ const modal = props.modal ?? isMobile;
224
+ return /* @__PURE__ */ jsx5(DialogWindowedByDefaultContext.Provider, { value: props.modal === void 0, children: /* @__PURE__ */ jsx5(RadixDialogModalProvider, { modal, children: /* @__PURE__ */ jsx5(DialogPrimitive.Root, { ...props, modal, children }) }) });
223
225
  };
224
226
  Dialog.displayName = "Dialog";
225
227
  var DialogPortal = ({
@@ -264,6 +266,11 @@ DialogContentPrimitive.displayName = "DialogContentPrimitive";
264
266
  var DIALOG_DESKTOP_CLASSES = "matrx-dialog-card fixed inset-0 z-[10000] m-auto grid h-fit min-w-0 w-full max-w-lg gap-4 max-h-[85dvh] overflow-y-auto overscroll-contain [--dialog-pad:1.5rem] overflow-x-clip border bg-background p-6 shadow-lg [overflow-wrap:anywhere] [&>*]:min-w-0 [&>*]:max-w-full sm:rounded-lg";
265
267
  var DIALOG_MOBILE_SHEET_CLASSES = "matrx-mobile-sheet matrx-dialog-card fixed inset-x-0 bottom-0 left-0 right-0 top-auto z-[10000] flex min-w-0 flex-col w-full max-w-full max-h-[90dvh] gap-4 [--dialog-pad:1rem] overflow-x-clip border-t bg-background p-4 pb-safe shadow-lg [overflow-wrap:anywhere] [&>*]:min-w-0 [&>*]:max-w-full rounded-t-2xl rounded-b-none overflow-y-auto overscroll-contain";
266
268
  var DIALOG_MOBILE_SHEET_OVERRIDE = "inset-x-0 bottom-0 left-0 right-0 top-auto translate-none w-full max-w-full max-h-[90dvh] rounded-b-none rounded-t-2xl overflow-y-auto";
269
+ var DialogWindowHandleContext = React5.createContext(null);
270
+ var WINDOW_KEEP_VISIBLE = 80;
271
+ function isInteractiveTarget(target) {
272
+ return target instanceof Element && Boolean(target.closest("button, a, input, textarea, select, [role='button'], [contenteditable='true']"));
273
+ }
267
274
  var DialogContent = React5.forwardRef(
268
275
  ({
269
276
  className,
@@ -272,15 +279,51 @@ var DialogContent = React5.forwardRef(
272
279
  animated = true,
273
280
  showCloseButton = true,
274
281
  container,
282
+ dismissOnOutsideClick = false,
283
+ onInteractOutside,
284
+ onEscapeKeyDown,
285
+ style,
275
286
  ...props
276
287
  }, ref) => {
277
288
  const isMobile = useIsMobile();
278
289
  const isModal = useRadixDialogModal();
279
290
  const injectedContainer = usePortalContainer(container);
280
291
  const asSheet = mobileSheet && isMobile;
292
+ const windowed = !isModal && !asSheet;
293
+ const windowedByDefault = React5.useContext(DialogWindowedByDefaultContext);
281
294
  const [containerEl, setContainerEl] = React5.useState(
282
295
  null
283
296
  );
297
+ const [offset, setOffset] = React5.useState({ x: 0, y: 0 });
298
+ const dragRef = React5.useRef(null);
299
+ const handle = React5.useMemo(() => {
300
+ if (!windowed) return null;
301
+ const clamp = (value, limit) => Math.max(-limit, Math.min(limit, value));
302
+ return {
303
+ onPointerDown: (event) => {
304
+ if (event.button !== 0 || isInteractiveTarget(event.target)) return;
305
+ dragRef.current = { x: event.clientX, y: event.clientY, baseX: offset.x, baseY: offset.y };
306
+ event.currentTarget.setPointerCapture?.(event.pointerId);
307
+ },
308
+ onPointerMove: (event) => {
309
+ const start = dragRef.current;
310
+ if (!start) return;
311
+ setOffset({
312
+ x: clamp(start.baseX + event.clientX - start.x, window.innerWidth / 2 - WINDOW_KEEP_VISIBLE / 2),
313
+ y: clamp(start.baseY + event.clientY - start.y, window.innerHeight / 2 - WINDOW_KEEP_VISIBLE / 2)
314
+ });
315
+ },
316
+ onPointerUp: (event) => {
317
+ if (!dragRef.current) return;
318
+ dragRef.current = null;
319
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
320
+ },
321
+ onPointerCancel: () => {
322
+ dragRef.current = null;
323
+ },
324
+ onDoubleClick: () => setOffset({ x: 0, y: 0 })
325
+ };
326
+ }, [windowed, offset.x, offset.y]);
284
327
  const mergedRef = React5.useCallback(
285
328
  (node) => {
286
329
  setContainerEl(node);
@@ -308,19 +351,33 @@ var DialogContent = React5.forwardRef(
308
351
  // can sit beside a host's floating window manager (which starts at
309
352
  // z=1000); staying below that boundary makes a newly focused window
310
353
  // usable without coupling the two systems.
311
- !isModal && "z-[900]"
354
+ !isModal && !windowedByDefault && "z-[900]",
355
+ // Without an overlay the card needs its own separation from the page.
356
+ windowed && "shadow-2xl ring-1 ring-border/60"
312
357
  ),
358
+ style: windowed && (offset.x !== 0 || offset.y !== 0) ? { ...style, translate: `${offset.x}px ${offset.y}px` } : style,
359
+ onInteractOutside: (event) => {
360
+ onInteractOutside?.(event);
361
+ if (windowed && !dismissOnOutsideClick) event.preventDefault();
362
+ },
363
+ onEscapeKeyDown: (event) => {
364
+ onEscapeKeyDown?.(event);
365
+ const pressedIn = event.target instanceof Node && event.target !== document.body ? event.target : document.activeElement;
366
+ if (windowed && containerEl && !(pressedIn && containerEl.contains(pressedIn))) {
367
+ event.preventDefault();
368
+ }
369
+ },
313
370
  ...hasDescription ? {} : { "aria-describedby": void 0 },
314
371
  ...props,
315
372
  children: [
316
373
  hasTitle ? null : /* @__PURE__ */ jsx5(VisuallyHidden.Root, { children: /* @__PURE__ */ jsx5(DialogPrimitive.Title, { children: "Dialog" }) }),
317
- /* @__PURE__ */ jsx5(DialogContainerContext.Provider, { value: containerEl, children: /* @__PURE__ */ jsx5(
374
+ /* @__PURE__ */ jsx5(DialogContainerContext.Provider, { value: containerEl, children: /* @__PURE__ */ jsx5(DialogWindowHandleContext.Provider, { value: handle, children: /* @__PURE__ */ jsx5(
318
375
  PortalContainerProvider,
319
376
  {
320
377
  container: containerEl ?? injectedContainer ?? null,
321
378
  children
322
379
  }
323
- ) }),
380
+ ) }) }),
324
381
  showCloseButton ? /* @__PURE__ */ jsxs2(
325
382
  DialogPrimitive.Close,
326
383
  {
@@ -343,19 +400,28 @@ DialogContent.displayName = DialogPrimitive.Content.displayName;
343
400
  var DialogHeader = ({
344
401
  className,
345
402
  ...props
346
- }) => /* @__PURE__ */ jsx5(
347
- "div",
348
- {
349
- "data-slot": "dialog-header",
350
- className: cn(
351
- // DialogContent owns an absolute close control. Reserve its hit area in
352
- // every header so trailing actions never render underneath it.
353
- "flex flex-col space-y-1.5 pr-12 text-center sm:text-left",
354
- className
355
- ),
356
- ...props
357
- }
358
- );
403
+ }) => {
404
+ const handle = React5.useContext(DialogWindowHandleContext);
405
+ return /* @__PURE__ */ jsx5(
406
+ "div",
407
+ {
408
+ "data-slot": "dialog-header",
409
+ ...handle ? {
410
+ ...handle,
411
+ "data-window-handle": "",
412
+ title: "Drag to move \xB7 double-click to recenter"
413
+ } : {},
414
+ className: cn(
415
+ // DialogContent owns an absolute close control. Reserve its hit area in
416
+ // every header so trailing actions never render underneath it.
417
+ "flex flex-col space-y-1.5 pr-12 text-center sm:text-left",
418
+ handle && "cursor-grab select-none active:cursor-grabbing",
419
+ className
420
+ ),
421
+ ...props
422
+ }
423
+ );
424
+ };
359
425
  DialogHeader.displayName = "DialogHeader";
360
426
  var DialogFooter = ({
361
427
  className,