@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.1 — 2026-09-23
4
+
5
+ **A default window-style Dialog sits with the layer that opened it.** In 0.38.0 every non-blocking dialog took the older `z-[900]` layer, which put a dialog opened from a menu, a popover or a host window BEHIND its opener (caught by matrx-frontend's nested-portal guard before adoption). A dialog that is a window because the caller said nothing about `modal` now sits at `z-[10000]`, as a blocking dialog always did; a caller that passes `modal={false}` explicitly keeps `z-[900]` (the coexistence contract below a host window manager). Two tests.
6
+
7
+ **Consumer action:** none.
8
+
9
+ ## 0.38.0 — 2026-09-23
10
+
11
+ **On desktop a Dialog is a window, not a wall.** (register ARE-006, `common-docs/projects/ai-reachable-everywhere/REGISTER.md`.)
12
+
13
+ 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:
14
+
15
+ * a click outside never closes it — `DialogContent dismissOnOutsideClick` opts a quick picker back in;
16
+ * Escape closes it only when pressed inside it — Escape pressed in the page belongs to the page;
17
+ * `DialogHeader` drags it (clamped on screen, double-click recenters);
18
+ * it carries its own shadow and ring, since there is no scrim.
19
+
20
+ 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).
21
+
22
+ **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.
23
+
3
24
  ## 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
25
 
5
26
  **Four data-table changes that were on main and never published.** (published by lane
@@ -265,12 +265,14 @@ function useIsMobile() {
265
265
 
266
266
  // src/dialog.tsx
267
267
  var import_jsx_runtime5 = require("react/jsx-runtime");
268
+ var DialogWindowedByDefaultContext = React5.createContext(false);
268
269
  var Dialog = ({
269
270
  children,
270
271
  ...props
271
272
  }) => {
272
- const modal = props.modal ?? true;
273
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RadixDialogModalProvider, { modal, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DialogPrimitive.Root, { ...props, modal, children }) });
273
+ const isMobile = useIsMobile();
274
+ const modal = props.modal ?? isMobile;
275
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DialogWindowedByDefaultContext.Provider, { value: props.modal === void 0, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RadixDialogModalProvider, { modal, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DialogPrimitive.Root, { ...props, modal, children }) }) });
274
276
  };
275
277
  Dialog.displayName = "Dialog";
276
278
  var DialogPortal = ({
@@ -315,6 +317,11 @@ DialogContentPrimitive.displayName = "DialogContentPrimitive";
315
317
  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
318
  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
319
  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";
320
+ var DialogWindowHandleContext = React5.createContext(null);
321
+ var WINDOW_KEEP_VISIBLE = 80;
322
+ function isInteractiveTarget(target) {
323
+ return target instanceof Element && Boolean(target.closest("button, a, input, textarea, select, [role='button'], [contenteditable='true']"));
324
+ }
318
325
  var DialogContent = React5.forwardRef(
319
326
  ({
320
327
  className,
@@ -323,15 +330,51 @@ var DialogContent = React5.forwardRef(
323
330
  animated = true,
324
331
  showCloseButton = true,
325
332
  container,
333
+ dismissOnOutsideClick = false,
334
+ onInteractOutside,
335
+ onEscapeKeyDown,
336
+ style,
326
337
  ...props
327
338
  }, ref) => {
328
339
  const isMobile = useIsMobile();
329
340
  const isModal = useRadixDialogModal();
330
341
  const injectedContainer = usePortalContainer(container);
331
342
  const asSheet = mobileSheet && isMobile;
343
+ const windowed = !isModal && !asSheet;
344
+ const windowedByDefault = React5.useContext(DialogWindowedByDefaultContext);
332
345
  const [containerEl, setContainerEl] = React5.useState(
333
346
  null
334
347
  );
348
+ const [offset, setOffset] = React5.useState({ x: 0, y: 0 });
349
+ const dragRef = React5.useRef(null);
350
+ const handle = React5.useMemo(() => {
351
+ if (!windowed) return null;
352
+ const clamp = (value, limit) => Math.max(-limit, Math.min(limit, value));
353
+ return {
354
+ onPointerDown: (event) => {
355
+ if (event.button !== 0 || isInteractiveTarget(event.target)) return;
356
+ dragRef.current = { x: event.clientX, y: event.clientY, baseX: offset.x, baseY: offset.y };
357
+ event.currentTarget.setPointerCapture?.(event.pointerId);
358
+ },
359
+ onPointerMove: (event) => {
360
+ const start = dragRef.current;
361
+ if (!start) return;
362
+ setOffset({
363
+ x: clamp(start.baseX + event.clientX - start.x, window.innerWidth / 2 - WINDOW_KEEP_VISIBLE / 2),
364
+ y: clamp(start.baseY + event.clientY - start.y, window.innerHeight / 2 - WINDOW_KEEP_VISIBLE / 2)
365
+ });
366
+ },
367
+ onPointerUp: (event) => {
368
+ if (!dragRef.current) return;
369
+ dragRef.current = null;
370
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
371
+ },
372
+ onPointerCancel: () => {
373
+ dragRef.current = null;
374
+ },
375
+ onDoubleClick: () => setOffset({ x: 0, y: 0 })
376
+ };
377
+ }, [windowed, offset.x, offset.y]);
335
378
  const mergedRef = React5.useCallback(
336
379
  (node) => {
337
380
  setContainerEl(node);
@@ -359,19 +402,33 @@ var DialogContent = React5.forwardRef(
359
402
  // can sit beside a host's floating window manager (which starts at
360
403
  // z=1000); staying below that boundary makes a newly focused window
361
404
  // usable without coupling the two systems.
362
- !isModal && "z-[900]"
405
+ !isModal && !windowedByDefault && "z-[900]",
406
+ // Without an overlay the card needs its own separation from the page.
407
+ windowed && "shadow-2xl ring-1 ring-border/60"
363
408
  ),
409
+ style: windowed && (offset.x !== 0 || offset.y !== 0) ? { ...style, translate: `${offset.x}px ${offset.y}px` } : style,
410
+ onInteractOutside: (event) => {
411
+ onInteractOutside?.(event);
412
+ if (windowed && !dismissOnOutsideClick) event.preventDefault();
413
+ },
414
+ onEscapeKeyDown: (event) => {
415
+ onEscapeKeyDown?.(event);
416
+ const pressedIn = event.target instanceof Node && event.target !== document.body ? event.target : document.activeElement;
417
+ if (windowed && containerEl && !(pressedIn && containerEl.contains(pressedIn))) {
418
+ event.preventDefault();
419
+ }
420
+ },
364
421
  ...hasDescription ? {} : { "aria-describedby": void 0 },
365
422
  ...props,
366
423
  children: [
367
424
  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)(
425
+ /* @__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
426
  PortalContainerProvider,
370
427
  {
371
428
  container: containerEl ?? injectedContainer ?? null,
372
429
  children
373
430
  }
374
- ) }),
431
+ ) }) }),
375
432
  showCloseButton ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
376
433
  DialogPrimitive.Close,
377
434
  {
@@ -394,19 +451,28 @@ DialogContent.displayName = DialogPrimitive.Content.displayName;
394
451
  var DialogHeader = ({
395
452
  className,
396
453
  ...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
- );
454
+ }) => {
455
+ const handle = React5.useContext(DialogWindowHandleContext);
456
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
457
+ "div",
458
+ {
459
+ "data-slot": "dialog-header",
460
+ ...handle ? {
461
+ ...handle,
462
+ "data-window-handle": "",
463
+ title: "Drag to move \xB7 double-click to recenter"
464
+ } : {},
465
+ className: cn(
466
+ // DialogContent owns an absolute close control. Reserve its hit area in
467
+ // every header so trailing actions never render underneath it.
468
+ "flex flex-col space-y-1.5 pr-12 text-center sm:text-left",
469
+ handle && "cursor-grab select-none active:cursor-grabbing",
470
+ className
471
+ ),
472
+ ...props
473
+ }
474
+ );
475
+ };
410
476
  DialogHeader.displayName = "DialogHeader";
411
477
  var DialogFooter = ({
412
478
  className,