@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/dist/index.d.cts CHANGED
@@ -792,6 +792,20 @@ declare function CreatablePicker({ value, options, onSelect, placeholder, search
792
792
  * `aria-describedby` is dropped when there is no description rather than
793
793
  * pointing at nothing.
794
794
  *
795
+ * 6. ON DESKTOP A DIALOG IS A WINDOW, NOT A WALL (0.38.0, Arman 2026-09-23:
796
+ * "none of the built-in page ai integrations are accessible because the
797
+ * page is totally blocked"). Unless the caller passes `modal`, a desktop
798
+ * dialog does not block the page: no overlay, the page, the Agents menu,
799
+ * the assist dock and right-click AI stay usable beside it. It behaves like
800
+ * a window: a click outside never closes it (`dismissOnOutsideClick` opts a
801
+ * quick picker back in), Escape closes it only while focus is inside it
802
+ * (Escape in the page belongs to the page), and its `DialogHeader` drags
803
+ * it, clamped on screen, double-click to recenter. On a phone it stays the
804
+ * blocking bottom sheet — nothing useful sits behind a sheet. Confirmations
805
+ * are `AlertDialog`, a different component, and stay blocking. This is a
806
+ * LIGHT window: the host's heavy window manager (tray, persistence) is a
807
+ * separate system and must not be pulled into every dialog.
808
+ *
795
809
  * SEAM INVERSIONS. The host original resolved its portal target through
796
810
  * app-shaped hooks (a popped-out window-panel body). Here that is the
797
811
  * package's `PortalContainerProvider` seam, with an explicit `container` prop
@@ -828,6 +842,13 @@ declare const DialogOverlay: React.ForwardRefExoticComponent<Omit<DialogPrimitiv
828
842
  */
829
843
  declare const DialogContentPrimitive: React.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
830
844
  interface DialogContentProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
845
+ /**
846
+ * Windowed (non-blocking) dialogs never close from a click outside them —
847
+ * the person is reaching for the page or an AI control beside the window.
848
+ * Pass `true` for a quick picker that should close when the person clicks
849
+ * away. Ignored on a blocking dialog, which keeps Radix's behaviour.
850
+ */
851
+ dismissOnOutsideClick?: boolean;
831
852
  /** Render as a bottom sheet below the mobile breakpoint. Default `true`. */
832
853
  mobileSheet?: boolean;
833
854
  /** Render the built-in close control. Default `true`. */
package/dist/index.d.ts CHANGED
@@ -792,6 +792,20 @@ declare function CreatablePicker({ value, options, onSelect, placeholder, search
792
792
  * `aria-describedby` is dropped when there is no description rather than
793
793
  * pointing at nothing.
794
794
  *
795
+ * 6. ON DESKTOP A DIALOG IS A WINDOW, NOT A WALL (0.38.0, Arman 2026-09-23:
796
+ * "none of the built-in page ai integrations are accessible because the
797
+ * page is totally blocked"). Unless the caller passes `modal`, a desktop
798
+ * dialog does not block the page: no overlay, the page, the Agents menu,
799
+ * the assist dock and right-click AI stay usable beside it. It behaves like
800
+ * a window: a click outside never closes it (`dismissOnOutsideClick` opts a
801
+ * quick picker back in), Escape closes it only while focus is inside it
802
+ * (Escape in the page belongs to the page), and its `DialogHeader` drags
803
+ * it, clamped on screen, double-click to recenter. On a phone it stays the
804
+ * blocking bottom sheet — nothing useful sits behind a sheet. Confirmations
805
+ * are `AlertDialog`, a different component, and stay blocking. This is a
806
+ * LIGHT window: the host's heavy window manager (tray, persistence) is a
807
+ * separate system and must not be pulled into every dialog.
808
+ *
795
809
  * SEAM INVERSIONS. The host original resolved its portal target through
796
810
  * app-shaped hooks (a popped-out window-panel body). Here that is the
797
811
  * package's `PortalContainerProvider` seam, with an explicit `container` prop
@@ -828,6 +842,13 @@ declare const DialogOverlay: React.ForwardRefExoticComponent<Omit<DialogPrimitiv
828
842
  */
829
843
  declare const DialogContentPrimitive: React.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
830
844
  interface DialogContentProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
845
+ /**
846
+ * Windowed (non-blocking) dialogs never close from a click outside them —
847
+ * the person is reaching for the page or an AI control beside the window.
848
+ * Pass `true` for a quick picker that should close when the person clicks
849
+ * away. Ignored on a blocking dialog, which keeps Radix's behaviour.
850
+ */
851
+ dismissOnOutsideClick?: boolean;
831
852
  /** Render as a bottom sheet below the mobile breakpoint. Default `true`. */
832
853
  mobileSheet?: boolean;
833
854
  /** Render the built-in close control. Default `true`. */
package/dist/index.js CHANGED
@@ -1877,7 +1877,8 @@ var Dialog = ({
1877
1877
  children,
1878
1878
  ...props
1879
1879
  }) => {
1880
- const modal = props.modal ?? true;
1880
+ const isMobile = useIsMobile();
1881
+ const modal = props.modal ?? isMobile;
1881
1882
  return /* @__PURE__ */ jsx21(RadixDialogModalProvider, { modal, children: /* @__PURE__ */ jsx21(DialogPrimitive2.Root, { ...props, modal, children }) });
1882
1883
  };
1883
1884
  Dialog.displayName = "Dialog";
@@ -1926,6 +1927,11 @@ DialogContentPrimitive.displayName = "DialogContentPrimitive";
1926
1927
  var DIALOG_DESKTOP_CLASSES2 = "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";
1927
1928
  var DIALOG_MOBILE_SHEET_CLASSES2 = "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";
1928
1929
  var DIALOG_MOBILE_SHEET_OVERRIDE2 = "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";
1930
+ var DialogWindowHandleContext = React17.createContext(null);
1931
+ var WINDOW_KEEP_VISIBLE = 80;
1932
+ function isInteractiveTarget(target) {
1933
+ return target instanceof Element && Boolean(target.closest("button, a, input, textarea, select, [role='button'], [contenteditable='true']"));
1934
+ }
1929
1935
  var DialogContent = React17.forwardRef(
1930
1936
  ({
1931
1937
  className,
@@ -1934,15 +1940,50 @@ var DialogContent = React17.forwardRef(
1934
1940
  animated = true,
1935
1941
  showCloseButton = true,
1936
1942
  container,
1943
+ dismissOnOutsideClick = false,
1944
+ onInteractOutside,
1945
+ onEscapeKeyDown,
1946
+ style,
1937
1947
  ...props
1938
1948
  }, ref) => {
1939
1949
  const isMobile = useIsMobile();
1940
1950
  const isModal = useRadixDialogModal();
1941
1951
  const injectedContainer = usePortalContainer(container);
1942
1952
  const asSheet = mobileSheet && isMobile;
1953
+ const windowed = !isModal && !asSheet;
1943
1954
  const [containerEl, setContainerEl] = React17.useState(
1944
1955
  null
1945
1956
  );
1957
+ const [offset, setOffset] = React17.useState({ x: 0, y: 0 });
1958
+ const dragRef = React17.useRef(null);
1959
+ const handle = React17.useMemo(() => {
1960
+ if (!windowed) return null;
1961
+ const clamp = (value, limit) => Math.max(-limit, Math.min(limit, value));
1962
+ return {
1963
+ onPointerDown: (event) => {
1964
+ if (event.button !== 0 || isInteractiveTarget(event.target)) return;
1965
+ dragRef.current = { x: event.clientX, y: event.clientY, baseX: offset.x, baseY: offset.y };
1966
+ event.currentTarget.setPointerCapture?.(event.pointerId);
1967
+ },
1968
+ onPointerMove: (event) => {
1969
+ const start = dragRef.current;
1970
+ if (!start) return;
1971
+ setOffset({
1972
+ x: clamp(start.baseX + event.clientX - start.x, window.innerWidth / 2 - WINDOW_KEEP_VISIBLE / 2),
1973
+ y: clamp(start.baseY + event.clientY - start.y, window.innerHeight / 2 - WINDOW_KEEP_VISIBLE / 2)
1974
+ });
1975
+ },
1976
+ onPointerUp: (event) => {
1977
+ if (!dragRef.current) return;
1978
+ dragRef.current = null;
1979
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
1980
+ },
1981
+ onPointerCancel: () => {
1982
+ dragRef.current = null;
1983
+ },
1984
+ onDoubleClick: () => setOffset({ x: 0, y: 0 })
1985
+ };
1986
+ }, [windowed, offset.x, offset.y]);
1946
1987
  const mergedRef = React17.useCallback(
1947
1988
  (node) => {
1948
1989
  setContainerEl(node);
@@ -1970,19 +2011,33 @@ var DialogContent = React17.forwardRef(
1970
2011
  // can sit beside a host's floating window manager (which starts at
1971
2012
  // z=1000); staying below that boundary makes a newly focused window
1972
2013
  // usable without coupling the two systems.
1973
- !isModal && "z-[900]"
2014
+ !isModal && "z-[900]",
2015
+ // Without an overlay the card needs its own separation from the page.
2016
+ windowed && "shadow-2xl ring-1 ring-border/60"
1974
2017
  ),
2018
+ style: windowed && (offset.x !== 0 || offset.y !== 0) ? { ...style, translate: `${offset.x}px ${offset.y}px` } : style,
2019
+ onInteractOutside: (event) => {
2020
+ onInteractOutside?.(event);
2021
+ if (windowed && !dismissOnOutsideClick) event.preventDefault();
2022
+ },
2023
+ onEscapeKeyDown: (event) => {
2024
+ onEscapeKeyDown?.(event);
2025
+ const pressedIn = event.target instanceof Node && event.target !== document.body ? event.target : document.activeElement;
2026
+ if (windowed && containerEl && !(pressedIn && containerEl.contains(pressedIn))) {
2027
+ event.preventDefault();
2028
+ }
2029
+ },
1975
2030
  ...hasDescription ? {} : { "aria-describedby": void 0 },
1976
2031
  ...props,
1977
2032
  children: [
1978
2033
  hasTitle ? null : /* @__PURE__ */ jsx21(VisuallyHidden2.Root, { children: /* @__PURE__ */ jsx21(DialogPrimitive2.Title, { children: "Dialog" }) }),
1979
- /* @__PURE__ */ jsx21(DialogContainerContext.Provider, { value: containerEl, children: /* @__PURE__ */ jsx21(
2034
+ /* @__PURE__ */ jsx21(DialogContainerContext.Provider, { value: containerEl, children: /* @__PURE__ */ jsx21(DialogWindowHandleContext.Provider, { value: handle, children: /* @__PURE__ */ jsx21(
1980
2035
  PortalContainerProvider,
1981
2036
  {
1982
2037
  container: containerEl ?? injectedContainer ?? null,
1983
2038
  children
1984
2039
  }
1985
- ) }),
2040
+ ) }) }),
1986
2041
  showCloseButton ? /* @__PURE__ */ jsxs10(
1987
2042
  DialogPrimitive2.Close,
1988
2043
  {
@@ -2005,19 +2060,28 @@ DialogContent.displayName = DialogPrimitive2.Content.displayName;
2005
2060
  var DialogHeader = ({
2006
2061
  className,
2007
2062
  ...props
2008
- }) => /* @__PURE__ */ jsx21(
2009
- "div",
2010
- {
2011
- "data-slot": "dialog-header",
2012
- className: cn(
2013
- // DialogContent owns an absolute close control. Reserve its hit area in
2014
- // every header so trailing actions never render underneath it.
2015
- "flex flex-col space-y-1.5 pr-12 text-center sm:text-left",
2016
- className
2017
- ),
2018
- ...props
2019
- }
2020
- );
2063
+ }) => {
2064
+ const handle = React17.useContext(DialogWindowHandleContext);
2065
+ return /* @__PURE__ */ jsx21(
2066
+ "div",
2067
+ {
2068
+ "data-slot": "dialog-header",
2069
+ ...handle ? {
2070
+ ...handle,
2071
+ "data-window-handle": "",
2072
+ title: "Drag to move \xB7 double-click to recenter"
2073
+ } : {},
2074
+ className: cn(
2075
+ // DialogContent owns an absolute close control. Reserve its hit area in
2076
+ // every header so trailing actions never render underneath it.
2077
+ "flex flex-col space-y-1.5 pr-12 text-center sm:text-left",
2078
+ handle && "cursor-grab select-none active:cursor-grabbing",
2079
+ className
2080
+ ),
2081
+ ...props
2082
+ }
2083
+ );
2084
+ };
2021
2085
  DialogHeader.displayName = "DialogHeader";
2022
2086
  var DialogFooter = ({
2023
2087
  className,
@@ -2207,7 +2271,7 @@ var DropdownMenuShortcut = ({
2207
2271
  DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
2208
2272
 
2209
2273
  // src/editable-label.tsx
2210
- import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef2, useState as useState4 } from "react";
2274
+ import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef3, useState as useState4 } from "react";
2211
2275
  import { jsx as jsx23, jsxs as jsxs12 } from "react/jsx-runtime";
2212
2276
  function EditableLabel({
2213
2277
  value,
@@ -2233,7 +2297,7 @@ function EditableLabel({
2233
2297
  const [draft, setDraft] = useState4(value);
2234
2298
  const [error, setError] = useState4(null);
2235
2299
  const [busy, setBusy] = useState4(false);
2236
- const inputRef = useRef2(null);
2300
+ const inputRef = useRef3(null);
2237
2301
  useEffect2(() => {
2238
2302
  if (!editing) setDraft(value);
2239
2303
  }, [value, editing]);
@@ -2514,7 +2578,7 @@ var Label3 = React20.forwardRef(({ className, ...props }, ref) => /* @__PURE__ *
2514
2578
  Label3.displayName = LabelPrimitive.Root.displayName;
2515
2579
 
2516
2580
  // src/overflow-toolbar.tsx
2517
- import { useLayoutEffect, useMemo, useRef as useRef3, useState as useState5 } from "react";
2581
+ import { useLayoutEffect, useMemo as useMemo2, useRef as useRef4, useState as useState5 } from "react";
2518
2582
  import { Fragment, jsx as jsx26, jsxs as jsxs14 } from "react/jsx-runtime";
2519
2583
  var GAP_PX = 6;
2520
2584
  var TONE = {
@@ -2596,13 +2660,13 @@ function OverflowToolbar({
2596
2660
  renderOverflowMenu,
2597
2661
  renderTooltip
2598
2662
  }) {
2599
- const visibleActions = useMemo(
2663
+ const visibleActions = useMemo2(
2600
2664
  () => actions.filter((a) => !a.hidden),
2601
2665
  [actions]
2602
2666
  );
2603
- const containerRef = useRef3(null);
2604
- const ghostRef = useRef3(null);
2605
- const leadingRef = useRef3(null);
2667
+ const containerRef = useRef4(null);
2668
+ const ghostRef = useRef4(null);
2669
+ const leadingRef = useRef4(null);
2606
2670
  const [visibleCount, setVisibleCount] = useState5(visibleActions.length);
2607
2671
  const signature = visibleActions.map(
2608
2672
  (a) => `${a.id}:${a.hideLabel ? 1 : 0}:${a.running ? 1 : 0}:${a.runningLabel ?? ""}:${a.label}`
@@ -4005,15 +4069,15 @@ var TextareaWithPrefix = React32.forwardRef(
4005
4069
  TextareaWithPrefix.displayName = "TextareaWithPrefix";
4006
4070
 
4007
4071
  // src/use-scroll-fade.ts
4008
- import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef5, useState as useState8 } from "react";
4072
+ import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef6, useState as useState8 } from "react";
4009
4073
  var EPSILON = 2;
4010
4074
  function useScrollFade() {
4011
4075
  const [state, setState] = useState8({
4012
4076
  top: false,
4013
4077
  bottom: false
4014
4078
  });
4015
- const nodeRef = useRef5(null);
4016
- const cleanupRef = useRef5(null);
4079
+ const nodeRef = useRef6(null);
4080
+ const cleanupRef = useRef6(null);
4017
4081
  const measure = useCallback5(() => {
4018
4082
  const el = nodeRef.current;
4019
4083
  if (!el) return;