@deadragdoll/reactnu 0.1.52 → 0.1.56

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.
@@ -1,3 +1,4 @@
1
+ import { ReactNode } from "react";
1
2
  export type MainMenuFlags = {
2
3
  checkable?: boolean;
3
4
  checked?: boolean;
@@ -11,6 +12,8 @@ export type MainMenuDivider = {
11
12
  };
12
13
  export type MainMenuItem = MainMenuFlags & {
13
14
  id: string;
15
+ /** Decorative mark shown before the item label in popup menus. */
16
+ icon?: ReactNode;
14
17
  items?: MainMenuNode[];
15
18
  modifier?: string;
16
19
  onSelect?: () => void;
@@ -1,4 +1,4 @@
1
- import { HTMLAttributes, PropsWithChildren, ReactNode } from "react";
1
+ import { CSSProperties, HTMLAttributes, PropsWithChildren, ReactNode } from "react";
2
2
  import { WindowTitleButtonDefinition } from "./WindowTitleButton";
3
3
  import { NuWindowBounds } from "../../windowing/windowing.types";
4
4
  export type WindowMode = "dialog" | "window";
@@ -13,12 +13,18 @@ export type WindowSize = {
13
13
  };
14
14
  export type WindowProps = PropsWithChildren<Omit<HTMLAttributes<HTMLElement>, "title"> & {
15
15
  active?: boolean;
16
+ /** Locks pointer resizing to width / height when set to a positive number. */
17
+ aspectRatio?: number;
16
18
  bodyClassName?: string;
17
19
  border?: WindowBorder;
18
20
  closeable?: boolean;
19
21
  draggable?: boolean;
22
+ /** Decorative visual rendered in the fixed left title-bar slot. */
23
+ icon?: ReactNode;
20
24
  maximizable?: boolean;
21
25
  maximized?: boolean;
26
+ minHeight?: CSSProperties["minHeight"];
27
+ minWidth?: CSSProperties["minWidth"];
22
28
  minimizable?: boolean;
23
29
  minimized?: boolean;
24
30
  mode?: WindowMode;
@@ -36,6 +42,6 @@ export type WindowProps = PropsWithChildren<Omit<HTMLAttributes<HTMLElement>, "t
36
42
  titleButtons?: WindowTitleButtonDefinition[];
37
43
  title: string;
38
44
  }>;
39
- declare function WindowInner({ active, bodyClassName, border, children, className, closeable, draggable, maximizable, maximized, minimizable, minimized, mode, onActivate, onBoundsChange, onClose, onPointerDownCapture, onPositionChange, onSizeChange, onToggleMaximized, onToggleMinimized, resizable, scrollable, statusBarClassName, statusBar, style, titleButtons, title, ...props }: WindowProps): import("react/jsx-runtime").JSX.Element;
45
+ declare function WindowInner({ active, aspectRatio, bodyClassName, border, children, className, closeable, draggable, icon, maximizable, maximized, minHeight, minWidth, minimizable, minimized, mode, onActivate, onBoundsChange, onClose, onPointerDownCapture, onPositionChange, onSizeChange, onToggleMaximized, onToggleMinimized, resizable, scrollable, statusBarClassName, statusBar, style, titleButtons, title, ...props }: WindowProps): import("react/jsx-runtime").JSX.Element;
40
46
  export declare const Window: import("react").MemoExoticComponent<typeof WindowInner>;
41
47
  export {};
@@ -1,10 +1,11 @@
1
- import { PointerEventHandler } from "react";
1
+ import { PointerEventHandler, ReactNode } from "react";
2
2
  import { WindowTitleButtonDefinition } from "../WindowTitleButton";
3
3
  type WindowTitleBarProps = {
4
4
  draggable: boolean;
5
+ icon?: ReactNode;
5
6
  onDragStart?: PointerEventHandler<HTMLElement>;
6
7
  titleButtons: WindowTitleButtonDefinition[];
7
8
  title: string;
8
9
  };
9
- export declare function WindowTitleBar({ draggable, onDragStart, titleButtons, title }: WindowTitleBarProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function WindowTitleBar({ draggable, icon, onDragStart, titleButtons, title }: WindowTitleBarProps): import("react/jsx-runtime").JSX.Element;
10
11
  export {};
package/dist/index.cjs CHANGED
@@ -471,6 +471,7 @@ function MainMenuList({
471
471
  ) : isChecked ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(NuGlyph, { name: "check-mark" }) : null
472
472
  }
473
473
  ) : null,
474
+ isPopupRow ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { "aria-hidden": "true", className: "nu-main-menu__icon", children: item.icon }) : null,
474
475
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "nu-main-menu__label", children: renderItemLabel(item) }),
475
476
  isPopupRow ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "nu-main-menu__shortcut", children: resolveItemShortcut(item) }) : null,
476
477
  isPopupRow ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "nu-main-menu__submenu-arrow", children: hasChildren ? "\u25B6" : "" }) : null
@@ -1731,6 +1732,7 @@ function MdiWindowPickerDialog({
1731
1732
  items: resolvedWindows.map((windowEntry, index) => ({
1732
1733
  id: windowEntry.id,
1733
1734
  name: {
1735
+ icon: windowEntry.icon,
1734
1736
  text: `${index + 1} ${formatPickerWindowTitle(windows, windowEntry)}`
1735
1737
  },
1736
1738
  selected: windowEntry.id === activeWindowId
@@ -1846,6 +1848,7 @@ function buildStandardMdiMenuItems(bridge) {
1846
1848
  ...mdiWindows.map((windowEntry, index) => ({
1847
1849
  checked: windowEntry.id === activeWindowId,
1848
1850
  id: `mdi.window.${windowEntry.id}`,
1851
+ icon: windowEntry.icon,
1849
1852
  onSelect: () => bridge?.activateWindow(windowEntry.id),
1850
1853
  text: `${index + 1} ${windowEntry.title}`
1851
1854
  })),
@@ -2069,6 +2072,7 @@ function WindowTitleButton({
2069
2072
  var import_jsx_runtime20 = require("react/jsx-runtime");
2070
2073
  function WindowTitleBar({
2071
2074
  draggable,
2075
+ icon,
2072
2076
  onDragStart,
2073
2077
  titleButtons,
2074
2078
  title
@@ -2084,6 +2088,7 @@ function WindowTitleBar({
2084
2088
  "--nu-window-title-controls-width": controlsWidth
2085
2089
  },
2086
2090
  children: [
2091
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { "aria-hidden": true, className: "nu-window__title-icon", children: icon }),
2087
2092
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "nu-window__title", children: renderMnemonicText(title) }),
2088
2093
  titleButtons.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "nu-window__title-controls", children: titleButtons.map((button, index) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2089
2094
  WindowTitleButton,
@@ -2184,16 +2189,23 @@ function getWindowGeometry(node) {
2184
2189
  width: rect.width
2185
2190
  };
2186
2191
  }
2192
+ function getValidAspectRatio(value) {
2193
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
2194
+ }
2187
2195
  function WindowInner({
2188
2196
  active = true,
2197
+ aspectRatio,
2189
2198
  bodyClassName,
2190
2199
  border = "single",
2191
2200
  children,
2192
2201
  className,
2193
2202
  closeable = true,
2194
2203
  draggable,
2204
+ icon,
2195
2205
  maximizable,
2196
2206
  maximized = false,
2207
+ minHeight,
2208
+ minWidth,
2197
2209
  minimizable,
2198
2210
  minimized = false,
2199
2211
  mode = "dialog",
@@ -2222,7 +2234,8 @@ function WindowInner({
2222
2234
  const menuState = useMainMenuState();
2223
2235
  const windowManager = (0, import_react13.useContext)(NuWindowContext);
2224
2236
  const isDraggable = draggable ?? mode === "window";
2225
- const isMaximizable = maximizable ?? mode === "window";
2237
+ const resolvedAspectRatio = getValidAspectRatio(aspectRatio);
2238
+ const isMaximizable = resolvedAspectRatio === void 0 && (maximizable ?? mode === "window");
2226
2239
  const isMinimizable = minimizable ?? mode === "window";
2227
2240
  const isResizable = resizable ?? mode === "window";
2228
2241
  const mdiBridge = (0, import_react13.useMemo)(
@@ -2372,22 +2385,16 @@ function WindowInner({
2372
2385
  } = getWindowGeometry(windowNode);
2373
2386
  const layerBounds = getWindowLayerBounds(windowNode);
2374
2387
  const computedStyle = window.getComputedStyle(windowNode);
2375
- const minWidth = Math.max(
2376
- Number.parseFloat(computedStyle.minWidth || "0") || 0,
2377
- 240
2378
- );
2379
- const minHeight = Math.max(
2380
- Number.parseFloat(computedStyle.minHeight || "0") || 0,
2381
- 120
2382
- );
2383
- const maxWidth = Math.max(
2384
- minWidth,
2385
- (layerBounds?.width ?? window.innerWidth) - left
2386
- );
2387
- const maxHeight = Math.max(
2388
- minHeight,
2389
- (layerBounds?.height ?? window.innerHeight) - top
2390
- );
2388
+ const minWidth2 = Number.parseFloat(computedStyle.minWidth || "0") || 0;
2389
+ const minHeight2 = Number.parseFloat(computedStyle.minHeight || "0") || 0;
2390
+ const availableWidth = (layerBounds?.width ?? window.innerWidth) - left;
2391
+ const availableHeight = (layerBounds?.height ?? window.innerHeight) - top;
2392
+ const ratioMinWidth = resolvedAspectRatio ? Math.max(minWidth2, minHeight2 * resolvedAspectRatio) : minWidth2;
2393
+ const maxWidth = resolvedAspectRatio ? Math.max(
2394
+ ratioMinWidth,
2395
+ Math.min(availableWidth, availableHeight * resolvedAspectRatio)
2396
+ ) : Math.max(minWidth2, availableWidth);
2397
+ const maxHeight = resolvedAspectRatio ? maxWidth / resolvedAspectRatio : Math.max(minHeight2, availableHeight);
2391
2398
  const hadTransform = computedStyle.transform !== "none";
2392
2399
  let didResize = false;
2393
2400
  if (hadTransform) {
@@ -2412,14 +2419,16 @@ function WindowInner({
2412
2419
  resizeFrameRef.current = window.requestAnimationFrame(flushResize);
2413
2420
  }
2414
2421
  function handlePointerMove(moveEvent) {
2415
- const nextWidth = Math.min(
2422
+ const widthDelta = moveEvent.clientX - startClientX;
2423
+ const heightDelta = moveEvent.clientY - startClientY;
2424
+ const nextWidth = resolvedAspectRatio ? Math.min(
2416
2425
  maxWidth,
2417
- Math.max(minWidth, startWidth + moveEvent.clientX - startClientX)
2418
- );
2419
- const nextHeight = Math.min(
2420
- maxHeight,
2421
- Math.max(minHeight, startHeight + moveEvent.clientY - startClientY)
2422
- );
2426
+ Math.max(
2427
+ ratioMinWidth,
2428
+ startWidth + (Math.abs(widthDelta) >= Math.abs(heightDelta * resolvedAspectRatio) ? widthDelta : heightDelta * resolvedAspectRatio)
2429
+ )
2430
+ ) : Math.min(maxWidth, Math.max(minWidth2, startWidth + widthDelta));
2431
+ const nextHeight = resolvedAspectRatio ? nextWidth / resolvedAspectRatio : Math.min(maxHeight, Math.max(minHeight2, startHeight + heightDelta));
2423
2432
  didResize = true;
2424
2433
  scheduleResize({
2425
2434
  height: nextHeight,
@@ -2459,7 +2468,11 @@ function WindowInner({
2459
2468
  "data-mode": mode,
2460
2469
  onPointerDownCapture: handleRootPointerDownCapture,
2461
2470
  ref: windowRef,
2462
- style,
2471
+ style: {
2472
+ ...style,
2473
+ minHeight: minHeight ?? style?.minHeight ?? 120,
2474
+ minWidth: minWidth ?? style?.minWidth ?? 240
2475
+ },
2463
2476
  children: [
2464
2477
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2465
2478
  "span",
@@ -2480,6 +2493,7 @@ function WindowInner({
2480
2493
  WindowTitleBar,
2481
2494
  {
2482
2495
  draggable: isDraggable,
2496
+ icon,
2483
2497
  onDragStart: handleTitlePointerDown,
2484
2498
  title,
2485
2499
  titleButtons: resolvedTitleButtons
@@ -2613,6 +2627,7 @@ function buildWindowBarGroups(items) {
2613
2627
  groups.push({
2614
2628
  active: Boolean(item.active),
2615
2629
  id: item.id,
2630
+ icon: item.icon,
2616
2631
  items: [item],
2617
2632
  label: item.title
2618
2633
  });
@@ -2624,6 +2639,7 @@ function buildWindowBarGroups(items) {
2624
2639
  active: Boolean(item.active),
2625
2640
  domain: item.domain,
2626
2641
  id: item.id,
2642
+ icon: item.icon,
2627
2643
  items: [item],
2628
2644
  label: item.title
2629
2645
  };
@@ -2633,10 +2649,16 @@ function buildWindowBarGroups(items) {
2633
2649
  }
2634
2650
  existingGroup.items.push(item);
2635
2651
  existingGroup.active = existingGroup.active || Boolean(item.active);
2652
+ if (item.active) {
2653
+ existingGroup.icon = item.icon;
2654
+ }
2636
2655
  existingGroup.label = `${item.domain} (${existingGroup.items.length})`;
2637
2656
  });
2638
2657
  return groups;
2639
2658
  }
2659
+ function getGroupIcon(group) {
2660
+ return group.items.find((item) => item.active)?.icon ?? group.icon;
2661
+ }
2640
2662
  function WindowBar({ items, onActivateWindow }) {
2641
2663
  const windowManager = (0, import_react15.useContext)(NuWindowContext);
2642
2664
  const groups = buildWindowBarGroups(items);
@@ -2677,17 +2699,23 @@ function WindowBar({ items, onActivateWindow }) {
2677
2699
  };
2678
2700
  windowManager.openDialog(dialogDefinition);
2679
2701
  }
2680
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "nu-window-bar", role: "group", "aria-label": "Open windows", children: groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2681
- AppBarItem,
2682
- {
2683
- active: group.active,
2684
- className: "nu-window-bar__item",
2685
- interactive: true,
2686
- onClick: () => activateGroup(group),
2687
- children: group.label
2688
- },
2689
- group.id
2690
- )) });
2702
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "nu-window-bar", role: "group", "aria-label": "Open windows", children: groups.map((group) => {
2703
+ const icon = getGroupIcon(group);
2704
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
2705
+ AppBarItem,
2706
+ {
2707
+ active: group.active,
2708
+ className: "nu-window-bar__item",
2709
+ interactive: true,
2710
+ onClick: () => activateGroup(group),
2711
+ children: [
2712
+ icon ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { "aria-hidden": true, className: "nu-window-bar__icon", children: icon }) : null,
2713
+ group.label
2714
+ ]
2715
+ },
2716
+ group.id
2717
+ );
2718
+ }) });
2691
2719
  }
2692
2720
 
2693
2721
  // src/windowing/dialogHelpers.tsx
@@ -3127,6 +3155,7 @@ function buildWindowRecord(definition, currentWindows, windowBoundsById, creatio
3127
3155
  ownerCenteredStyle,
3128
3156
  index
3129
3157
  );
3158
+ const hasAspectRatio = typeof definition.aspectRatio === "number" && Number.isFinite(definition.aspectRatio) && definition.aspectRatio > 0;
3130
3159
  return {
3131
3160
  ...definition,
3132
3161
  appModal,
@@ -3135,18 +3164,18 @@ function buildWindowRecord(definition, currentWindows, windowBoundsById, creatio
3135
3164
  creationOrder,
3136
3165
  draggable: mode === "window",
3137
3166
  id,
3138
- maximizable: definition.maximizable ?? mode === "window",
3167
+ maximizable: !hasAspectRatio && (definition.maximizable ?? mode === "window"),
3139
3168
  modalOwnerId,
3140
3169
  minimizable: definition.minimizable ?? mode === "window",
3141
3170
  mode,
3142
3171
  resizable: definition.resizable ?? mode === "window",
3143
- restoreStyle: savedWindowState.restoreStyle,
3172
+ restoreStyle: hasAspectRatio ? void 0 : savedWindowState.restoreStyle,
3144
3173
  title: resolveManagedWindowTitle(currentWindows, titleBase),
3145
3174
  titleBase,
3146
3175
  style: {
3147
3176
  ...savedWindowState.style
3148
3177
  },
3149
- maximized: savedWindowState.maximized,
3178
+ maximized: hasAspectRatio ? false : savedWindowState.maximized,
3150
3179
  minimized: savedWindowState.minimized
3151
3180
  };
3152
3181
  }
@@ -3361,7 +3390,7 @@ function NuWindowProvider({
3361
3390
  const toggleWindowMaximized = (0, import_react18.useCallback)((id) => {
3362
3391
  setWindows(
3363
3392
  (currentWindows) => currentWindows.map((windowEntry) => {
3364
- if (windowEntry.id !== id) {
3393
+ if (windowEntry.id !== id || !windowEntry.maximizable) {
3365
3394
  return windowEntry;
3366
3395
  }
3367
3396
  if (windowEntry.maximized) {
@@ -3527,21 +3556,32 @@ function NuWindowProvider({
3527
3556
  const topmostAppModalIndex = findTopmostAppModalIndex(visibleWindows);
3528
3557
  const topmostAppModalId = topmostAppModalIndex >= 0 ? visibleWindows[topmostAppModalIndex]?.id : void 0;
3529
3558
  const activeWindowId = topmostAppModalIndex >= 0 ? visibleWindows[topmostAppModalIndex]?.id : visibleWindows[visibleWindows.length - 1]?.id;
3559
+ const activeWindow = activeWindowId ? windows.find((windowEntry) => windowEntry.id === activeWindowId) : void 0;
3560
+ const activeActivationGroup = topmostAppModalIndex >= 0 ? void 0 : activeWindow?.activationGroup;
3530
3561
  const hasAppModal = topmostAppModalIndex >= 0;
3562
+ const isWindowActive = (0, import_react18.useCallback)(
3563
+ (windowEntry) => windowEntry.id === activeWindowId || activeActivationGroup !== void 0 && windowEntry.activationGroup === activeActivationGroup,
3564
+ [activeActivationGroup, activeWindowId]
3565
+ );
3531
3566
  const windowsInfo = (0, import_react18.useMemo)(
3532
3567
  () => [...windows].sort(
3533
3568
  (leftWindow, rightWindow) => leftWindow.creationOrder - rightWindow.creationOrder
3534
3569
  ).map((windowEntry) => ({
3535
- active: windowEntry.id === activeWindowId,
3570
+ active: isWindowActive(windowEntry),
3571
+ activationGroup: windowEntry.activationGroup,
3572
+ aspectRatio: windowEntry.aspectRatio,
3536
3573
  bodyClassName: windowEntry.bodyClassName,
3537
3574
  appModal: windowEntry.appModal,
3538
3575
  border: windowEntry.border,
3539
3576
  className: windowEntry.className,
3540
3577
  closeable: windowEntry.closeable,
3541
3578
  domain: windowEntry.domain,
3579
+ icon: windowEntry.icon,
3542
3580
  id: windowEntry.id,
3543
3581
  maximizable: windowEntry.maximizable,
3544
3582
  maximized: windowEntry.maximized,
3583
+ minHeight: windowEntry.minHeight,
3584
+ minWidth: windowEntry.minWidth,
3545
3585
  modal: windowEntry.modalOwnerId ?? false,
3546
3586
  minimizable: windowEntry.minimizable,
3547
3587
  minimized: windowEntry.minimized,
@@ -3552,7 +3592,7 @@ function NuWindowProvider({
3552
3592
  title: windowEntry.title,
3553
3593
  titleButtons: windowEntry.titleButtons
3554
3594
  })),
3555
- [activeWindowId, windows]
3595
+ [isWindowActive, windows]
3556
3596
  );
3557
3597
  const mdiBridge = (0, import_react18.useMemo)(
3558
3598
  () => ({
@@ -3612,7 +3652,7 @@ function NuWindowProvider({
3612
3652
  return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(NuWindowContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: ["nu-window-host", className].filter(Boolean).join(" "), children: [
3613
3653
  children,
3614
3654
  /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "nu-window-layer", children: renderWindows.map((windowEntry) => {
3615
- const isActiveWindow = windowEntry.id === activeWindowId;
3655
+ const isActiveWindow = isWindowActive(windowEntry);
3616
3656
  const stackIndex = stackIndexById.get(windowEntry.id);
3617
3657
  const isTopmostAppModal = windowEntry.id === topmostAppModalId;
3618
3658
  const ownerBounds = windowEntry.modalOwnerId ? windowBoundsById[windowEntry.modalOwnerId] : void 0;
@@ -3658,12 +3698,16 @@ function NuWindowProvider({
3658
3698
  Window,
3659
3699
  {
3660
3700
  active: isActiveWindow,
3701
+ aspectRatio: windowEntry.aspectRatio,
3661
3702
  bodyClassName: windowEntry.bodyClassName,
3662
3703
  border: windowEntry.border,
3663
3704
  className: windowEntry.className,
3664
3705
  closeable: windowEntry.closeable,
3665
3706
  draggable: windowEntry.draggable,
3707
+ icon: windowEntry.icon,
3666
3708
  maximizable: windowEntry.maximizable,
3709
+ minHeight: windowEntry.minHeight,
3710
+ minWidth: windowEntry.minWidth,
3667
3711
  minimizable: windowEntry.minimizable,
3668
3712
  maximized: windowEntry.maximized,
3669
3713
  minimized: windowEntry.minimized,
@@ -3694,6 +3738,7 @@ function NuWindowProvider({
3694
3738
  active: windowEntry.active,
3695
3739
  domain: windowEntry.domain,
3696
3740
  id: windowEntry.id,
3741
+ icon: windowEntry.icon,
3697
3742
  minimized: windowEntry.minimized,
3698
3743
  title: windowEntry.title
3699
3744
  })),