@ai-matrx/design-system 0.37.1 → 0.38.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.0 — 2026-09-23
4
+
5
+ **On desktop a Dialog is a window, not a wall.** (register ARE-006, `common-docs/projects/ai-reachable-everywhere/REGISTER.md`.)
6
+
7
+ Arman, on a blocking settings dialog: *"none of the built-in page ai integrations are accessible because the page is totally blocked."* Unless the caller passes `modal`, a desktop `Dialog` no longer blocks the page: no overlay, and the page, its menus and every AI control beside it stay usable. It behaves like a light window:
8
+
9
+ * a click outside never closes it — `DialogContent dismissOnOutsideClick` opts a quick picker back in;
10
+ * Escape closes it only when pressed inside it — Escape pressed in the page belongs to the page;
11
+ * `DialogHeader` drags it (clamped on screen, double-click recenters);
12
+ * it carries its own shadow and ring, since there is no scrim.
13
+
14
+ Unchanged: the API (`open`, `onOpenChange`, `DialogTrigger`, `DialogClose`); `AlertDialog` / `ConfirmDialog` (a different component, still blocking); the phone bottom sheet (still blocking); any caller passing `modal` explicitly. `SavedViewsControl` no longer shows its error twice while its save window is open (the page is visible now).
15
+
16
+ **Consumer action:** none required. A screen that genuinely must block (a destructive step that is not a confirmation) passes `modal`. A screen whose content relied on closing when the person clicked away passes `dismissOnOutsideClick`. A test that queries the whole document for a role the page ALSO carries must scope to the dialog (`within(dialog)`), because the page behind is no longer hidden from assistive technology.
17
+
3
18
  ## 0.37.1 — 2026-09-23 (0.37.0 was tagged but its dts build failed under TypeScript 6 before the registry; same contents)
4
19
 
5
20
  **Four data-table changes that were on main and never published.** (published by lane
@@ -269,7 +269,8 @@ var Dialog = ({
269
269
  children,
270
270
  ...props
271
271
  }) => {
272
- const modal = props.modal ?? true;
272
+ const isMobile = useIsMobile();
273
+ const modal = props.modal ?? isMobile;
273
274
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RadixDialogModalProvider, { modal, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DialogPrimitive.Root, { ...props, modal, children }) });
274
275
  };
275
276
  Dialog.displayName = "Dialog";
@@ -315,6 +316,11 @@ DialogContentPrimitive.displayName = "DialogContentPrimitive";
315
316
  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";
316
317
  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";
317
318
  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";
319
+ var DialogWindowHandleContext = React5.createContext(null);
320
+ var WINDOW_KEEP_VISIBLE = 80;
321
+ function isInteractiveTarget(target) {
322
+ return target instanceof Element && Boolean(target.closest("button, a, input, textarea, select, [role='button'], [contenteditable='true']"));
323
+ }
318
324
  var DialogContent = React5.forwardRef(
319
325
  ({
320
326
  className,
@@ -323,15 +329,50 @@ var DialogContent = React5.forwardRef(
323
329
  animated = true,
324
330
  showCloseButton = true,
325
331
  container,
332
+ dismissOnOutsideClick = false,
333
+ onInteractOutside,
334
+ onEscapeKeyDown,
335
+ style,
326
336
  ...props
327
337
  }, ref) => {
328
338
  const isMobile = useIsMobile();
329
339
  const isModal = useRadixDialogModal();
330
340
  const injectedContainer = usePortalContainer(container);
331
341
  const asSheet = mobileSheet && isMobile;
342
+ const windowed = !isModal && !asSheet;
332
343
  const [containerEl, setContainerEl] = React5.useState(
333
344
  null
334
345
  );
346
+ const [offset, setOffset] = React5.useState({ x: 0, y: 0 });
347
+ const dragRef = React5.useRef(null);
348
+ const handle = React5.useMemo(() => {
349
+ if (!windowed) return null;
350
+ const clamp = (value, limit) => Math.max(-limit, Math.min(limit, value));
351
+ return {
352
+ onPointerDown: (event) => {
353
+ if (event.button !== 0 || isInteractiveTarget(event.target)) return;
354
+ dragRef.current = { x: event.clientX, y: event.clientY, baseX: offset.x, baseY: offset.y };
355
+ event.currentTarget.setPointerCapture?.(event.pointerId);
356
+ },
357
+ onPointerMove: (event) => {
358
+ const start = dragRef.current;
359
+ if (!start) return;
360
+ setOffset({
361
+ x: clamp(start.baseX + event.clientX - start.x, window.innerWidth / 2 - WINDOW_KEEP_VISIBLE / 2),
362
+ y: clamp(start.baseY + event.clientY - start.y, window.innerHeight / 2 - WINDOW_KEEP_VISIBLE / 2)
363
+ });
364
+ },
365
+ onPointerUp: (event) => {
366
+ if (!dragRef.current) return;
367
+ dragRef.current = null;
368
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
369
+ },
370
+ onPointerCancel: () => {
371
+ dragRef.current = null;
372
+ },
373
+ onDoubleClick: () => setOffset({ x: 0, y: 0 })
374
+ };
375
+ }, [windowed, offset.x, offset.y]);
335
376
  const mergedRef = React5.useCallback(
336
377
  (node) => {
337
378
  setContainerEl(node);
@@ -359,19 +400,33 @@ var DialogContent = React5.forwardRef(
359
400
  // can sit beside a host's floating window manager (which starts at
360
401
  // z=1000); staying below that boundary makes a newly focused window
361
402
  // usable without coupling the two systems.
362
- !isModal && "z-[900]"
403
+ !isModal && "z-[900]",
404
+ // Without an overlay the card needs its own separation from the page.
405
+ windowed && "shadow-2xl ring-1 ring-border/60"
363
406
  ),
407
+ style: windowed && (offset.x !== 0 || offset.y !== 0) ? { ...style, translate: `${offset.x}px ${offset.y}px` } : style,
408
+ onInteractOutside: (event) => {
409
+ onInteractOutside?.(event);
410
+ if (windowed && !dismissOnOutsideClick) event.preventDefault();
411
+ },
412
+ onEscapeKeyDown: (event) => {
413
+ onEscapeKeyDown?.(event);
414
+ const pressedIn = event.target instanceof Node && event.target !== document.body ? event.target : document.activeElement;
415
+ if (windowed && containerEl && !(pressedIn && containerEl.contains(pressedIn))) {
416
+ event.preventDefault();
417
+ }
418
+ },
364
419
  ...hasDescription ? {} : { "aria-describedby": void 0 },
365
420
  ...props,
366
421
  children: [
367
422
  hasTitle ? null : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(VisuallyHidden.Root, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DialogPrimitive.Title, { children: "Dialog" }) }),
368
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DialogContainerContext.Provider, { value: containerEl, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
423
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DialogContainerContext.Provider, { value: containerEl, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DialogWindowHandleContext.Provider, { value: handle, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
369
424
  PortalContainerProvider,
370
425
  {
371
426
  container: containerEl ?? injectedContainer ?? null,
372
427
  children
373
428
  }
374
- ) }),
429
+ ) }) }),
375
430
  showCloseButton ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
376
431
  DialogPrimitive.Close,
377
432
  {
@@ -394,19 +449,28 @@ DialogContent.displayName = DialogPrimitive.Content.displayName;
394
449
  var DialogHeader = ({
395
450
  className,
396
451
  ...props
397
- }) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
398
- "div",
399
- {
400
- "data-slot": "dialog-header",
401
- className: cn(
402
- // DialogContent owns an absolute close control. Reserve its hit area in
403
- // every header so trailing actions never render underneath it.
404
- "flex flex-col space-y-1.5 pr-12 text-center sm:text-left",
405
- className
406
- ),
407
- ...props
408
- }
409
- );
452
+ }) => {
453
+ const handle = React5.useContext(DialogWindowHandleContext);
454
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
455
+ "div",
456
+ {
457
+ "data-slot": "dialog-header",
458
+ ...handle ? {
459
+ ...handle,
460
+ "data-window-handle": "",
461
+ title: "Drag to move \xB7 double-click to recenter"
462
+ } : {},
463
+ className: cn(
464
+ // DialogContent owns an absolute close control. Reserve its hit area in
465
+ // every header so trailing actions never render underneath it.
466
+ "flex flex-col space-y-1.5 pr-12 text-center sm:text-left",
467
+ handle && "cursor-grab select-none active:cursor-grabbing",
468
+ className
469
+ ),
470
+ ...props
471
+ }
472
+ );
473
+ };
410
474
  DialogHeader.displayName = "DialogHeader";
411
475
  var DialogFooter = ({
412
476
  className,