@liiift-studio/mac-os9-ui 0.2.21 → 0.2.24

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.js CHANGED
@@ -716,7 +716,45 @@ const Tabs = ({ children, defaultActiveTab = 0, activeTab: controlledActiveTab,
716
716
  };
717
717
  Tabs.displayName = 'Tabs';
718
718
 
719
- var styles$6 = {"window":"Window-module_window","window--active":"Window-module_window--active","window--inactive":"Window-module_window--inactive","titleBar":"Window-module_titleBar","titleCenter":"Window-module_titleCenter","controls":"Window-module_controls","controlButton":"Window-module_controlButton","closeBox":"Window-module_closeBox","minimizeBox":"Window-module_minimizeBox","maximizeBox":"Window-module_maximizeBox","titleText":"Window-module_titleText","content":"Window-module_content","resizeHandle":"Window-module_resizeHandle"};
719
+ // Utility for merging CSS class names
720
+ // Filters out falsy values and joins valid class names with spaces
721
+ /**
722
+ * Merges multiple class names into a single string
723
+ * Filters out undefined, null, false, and empty strings
724
+ *
725
+ * @param classes - Class names to merge
726
+ * @returns Merged class name string
727
+ *
728
+ * @example
729
+ * ```ts
730
+ * mergeClasses('base', isActive && 'active', undefined, 'custom')
731
+ * // Returns: "base active custom"
732
+ * ```
733
+ */
734
+ const mergeClasses = (...classes) => {
735
+ return classes.filter(Boolean).join(' ');
736
+ };
737
+ /**
738
+ * Creates a class name builder function with a base class
739
+ * Useful for component-level class management
740
+ *
741
+ * @param baseClass - Base class name
742
+ * @returns Function that merges additional classes with base
743
+ *
744
+ * @example
745
+ * ```ts
746
+ * const cn = createClassBuilder('button');
747
+ * cn('primary', isDisabled && 'disabled')
748
+ * // Returns: "button primary disabled"
749
+ * ```
750
+ */
751
+ const createClassBuilder = (baseClass) => {
752
+ return (...additionalClasses) => {
753
+ return mergeClasses(baseClass, ...additionalClasses);
754
+ };
755
+ };
756
+
757
+ var styles$6 = {"window":"Window-module_window","window--active":"Window-module_window--active","window--inactive":"Window-module_window--inactive","window--draggable":"Window-module_window--draggable","titleBar":"Window-module_titleBar","titleCenter":"Window-module_titleCenter","titleBar--draggable":"Window-module_titleBar--draggable","titleBar--dragging":"Window-module_titleBar--dragging","controls":"Window-module_controls","controlButton":"Window-module_controlButton","closeBox":"Window-module_closeBox","minimizeBox":"Window-module_minimizeBox","maximizeBox":"Window-module_maximizeBox","titleText":"Window-module_titleText","content":"Window-module_content","resizeHandle":"Window-module_resizeHandle"};
720
758
 
721
759
  /**
722
760
  * Mac OS 9 style Window component
@@ -729,6 +767,7 @@ var styles$6 = {"window":"Window-module_window","window--active":"Window-module_
729
767
  * - Active/inactive states
730
768
  * - Composable with custom TitleBar component
731
769
  * - Flexible sizing
770
+ * - Draggable windows (optional) - drag by title bar
732
771
  *
733
772
  * @example
734
773
  * ```tsx
@@ -750,18 +789,100 @@ var styles$6 = {"window":"Window-module_window","window--active":"Window-module_
750
789
  * >
751
790
  * <p>Content</p>
752
791
  * </Window>
792
+ *
793
+ * // Draggable window (uncontrolled)
794
+ * <Window title="Draggable" draggable>
795
+ * <p>Drag me by the title bar!</p>
796
+ * </Window>
797
+ *
798
+ * // Draggable window with initial position
799
+ * <Window
800
+ * title="Positioned"
801
+ * draggable
802
+ * defaultPosition={{ x: 100, y: 100 }}
803
+ * >
804
+ * <p>Starts at a specific position</p>
805
+ * </Window>
806
+ *
807
+ * // Controlled draggable window
808
+ * const [pos, setPos] = useState({ x: 0, y: 0 });
809
+ * <Window
810
+ * title="Controlled"
811
+ * draggable
812
+ * position={pos}
813
+ * onPositionChange={setPos}
814
+ * >
815
+ * <p>Parent controls position</p>
816
+ * </Window>
753
817
  * ```
754
818
  */
755
- const Window = forwardRef(({ children, title, titleBar, active = true, width = 'auto', height = 'auto', className = '', contentClassName = '', showControls = true, onClose, onMinimize, onMaximize, onMouseEnter, resizable = false, }, ref) => {
819
+ const Window = forwardRef(({ children, title, titleBar, active = true, width = 'auto', height = 'auto', className = '', contentClassName = '', classes, showControls = true, onClose, onMinimize, onMaximize, onMouseEnter, resizable = false, draggable = false, defaultPosition, position: controlledPosition, onPositionChange, }, ref) => {
820
+ // Drag state management
821
+ const [internalPosition, setInternalPosition] = useState(defaultPosition || null);
822
+ const [isDragging, setIsDragging] = useState(false);
823
+ const [hasBeenDragged, setHasBeenDragged] = useState(!!(defaultPosition || controlledPosition));
824
+ const dragStartRef = useRef(null);
825
+ // Use controlled position if provided, otherwise use internal state
826
+ const currentPosition = controlledPosition || internalPosition;
827
+ // Handle mouse down on title bar to start dragging
828
+ const handleTitleBarMouseDown = useCallback((event) => {
829
+ if (!draggable)
830
+ return;
831
+ // Don't start drag if clicking on buttons
832
+ if (event.target.closest('button')) {
833
+ return;
834
+ }
835
+ event.preventDefault();
836
+ const windowElement = event.currentTarget.closest(`.${styles$6.window}`);
837
+ if (!windowElement)
838
+ return;
839
+ const rect = windowElement.getBoundingClientRect();
840
+ // Store drag start info
841
+ dragStartRef.current = {
842
+ x: event.clientX - rect.left,
843
+ y: event.clientY - rect.top,
844
+ };
845
+ setIsDragging(true);
846
+ }, [draggable]);
847
+ // Handle mouse move during drag
848
+ useEffect(() => {
849
+ if (!isDragging || !dragStartRef.current)
850
+ return;
851
+ const handleMouseMove = (event) => {
852
+ event.preventDefault();
853
+ if (!dragStartRef.current)
854
+ return;
855
+ const newPosition = {
856
+ x: event.clientX - dragStartRef.current.x,
857
+ y: event.clientY - dragStartRef.current.y,
858
+ };
859
+ // Update position
860
+ if (controlledPosition && onPositionChange) {
861
+ onPositionChange(newPosition);
862
+ }
863
+ else {
864
+ setInternalPosition(newPosition);
865
+ }
866
+ // Mark as dragged
867
+ if (!hasBeenDragged) {
868
+ setHasBeenDragged(true);
869
+ }
870
+ };
871
+ const handleMouseUp = () => {
872
+ setIsDragging(false);
873
+ dragStartRef.current = null;
874
+ };
875
+ document.addEventListener('mousemove', handleMouseMove);
876
+ document.addEventListener('mouseup', handleMouseUp);
877
+ return () => {
878
+ document.removeEventListener('mousemove', handleMouseMove);
879
+ document.removeEventListener('mouseup', handleMouseUp);
880
+ };
881
+ }, [isDragging, controlledPosition, onPositionChange, hasBeenDragged]);
756
882
  // Class names
757
- const windowClassNames = [
758
- styles$6.window,
759
- active ? styles$6['window--active'] : styles$6['window--inactive'],
760
- className,
761
- ]
762
- .filter(Boolean)
763
- .join(' ');
764
- const contentClassNames = [styles$6.content, contentClassName].filter(Boolean).join(' ');
883
+ const windowClassNames = mergeClasses(styles$6.window, active ? styles$6['window--active'] : styles$6['window--inactive'], draggable && hasBeenDragged && styles$6['window--draggable'], className, classes?.root);
884
+ const contentClassNames = mergeClasses(styles$6.content, contentClassName, classes?.content);
885
+ const titleBarClassNames = mergeClasses(styles$6.titleBar, draggable && styles$6['titleBar--draggable'], isDragging && styles$6['titleBar--dragging'], classes?.titleBar);
765
886
  // Window style
766
887
  const windowStyle = {};
767
888
  if (width !== 'auto') {
@@ -770,13 +891,19 @@ const Window = forwardRef(({ children, title, titleBar, active = true, width = '
770
891
  if (height !== 'auto') {
771
892
  windowStyle.height = typeof height === 'number' ? `${height}px` : height;
772
893
  }
894
+ // Apply position if draggable and has been dragged
895
+ if (draggable && hasBeenDragged && currentPosition) {
896
+ windowStyle.position = 'absolute';
897
+ windowStyle.left = `${currentPosition.x}px`;
898
+ windowStyle.top = `${currentPosition.y}px`;
899
+ }
773
900
  // Render title bar if title provided and no custom titleBar
774
901
  const renderTitleBar = () => {
775
902
  if (titleBar) {
776
903
  return titleBar;
777
904
  }
778
905
  if (title) {
779
- return (jsxs("div", { className: styles$6.titleBar, "data-numControls": [onClose, onMinimize, onMaximize].filter(Boolean).length, children: [showControls && (jsxs("div", { className: styles$6.controls, children: [onClose && (jsx("button", { type: "button", className: styles$6.controlButton, onClick: onClose, "aria-label": "Close", title: "Close", children: jsx("div", { className: styles$6.closeBox }) })), onMinimize && (jsx("button", { type: "button", className: styles$6.controlButton, onClick: onMinimize, "aria-label": "Minimize", title: "Minimize", children: jsx("div", { className: styles$6.minimizeBox }) })), onMaximize && (jsx("button", { type: "button", className: styles$6.controlButton, onClick: onMaximize, "aria-label": "Maximize", title: "Maximize", children: jsx("div", { className: styles$6.maximizeBox }) }))] })), jsxs("div", { className: styles$6.titleCenter, children: [jsxs("svg", { width: "132", height: "13", viewBox: "0 0 132 13", fill: "none", preserveAspectRatio: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsx("rect", { width: "130.517", height: "13", fill: "#DDDDDD" }), jsx("rect", { width: "1", height: "13", fill: "#EEEEEE" }), jsx("rect", { x: "130", width: "1", height: "13", fill: "#C5C5C5" }), jsx("rect", { y: "1", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "5", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "9", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "3", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "7", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "11", width: "131.268", height: "1", fill: "#999999" })] }), jsx("div", { className: `${styles$6.titleText} bold`, children: title }), jsxs("svg", { width: "132", height: "13", viewBox: "0 0 132 13", fill: "none", preserveAspectRatio: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsx("rect", { width: "130.517", height: "13", fill: "#DDDDDD" }), jsx("rect", { width: "1", height: "13", fill: "#EEEEEE" }), jsx("rect", { x: "130", width: "1", height: "13", fill: "#C5C5C5" }), jsx("rect", { y: "1", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "5", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "9", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "3", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "7", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "11", width: "131.268", height: "1", fill: "#999999" })] })] })] }));
906
+ return (jsxs("div", { className: titleBarClassNames, "data-numControls": [onClose, onMinimize, onMaximize].filter(Boolean).length, onMouseDown: handleTitleBarMouseDown, children: [showControls && (jsxs("div", { className: mergeClasses(styles$6.controls, classes?.controls), children: [onClose && (jsx("button", { type: "button", className: mergeClasses(styles$6.controlButton, classes?.controlButton), onClick: onClose, "aria-label": "Close", title: "Close", children: jsx("div", { className: styles$6.closeBox }) })), onMinimize && (jsx("button", { type: "button", className: mergeClasses(styles$6.controlButton, classes?.controlButton), onClick: onMinimize, "aria-label": "Minimize", title: "Minimize", children: jsx("div", { className: styles$6.minimizeBox }) })), onMaximize && (jsx("button", { type: "button", className: mergeClasses(styles$6.controlButton, classes?.controlButton), onClick: onMaximize, "aria-label": "Maximize", title: "Maximize", children: jsx("div", { className: styles$6.maximizeBox }) }))] })), jsxs("div", { className: styles$6.titleCenter, children: [jsxs("svg", { width: "132", height: "13", viewBox: "0 0 132 13", fill: "none", preserveAspectRatio: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsx("rect", { width: "130.517", height: "13", fill: "#DDDDDD" }), jsx("rect", { width: "1", height: "13", fill: "#EEEEEE" }), jsx("rect", { x: "130", width: "1", height: "13", fill: "#C5C5C5" }), jsx("rect", { y: "1", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "5", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "9", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "3", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "7", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "11", width: "131.268", height: "1", fill: "#999999" })] }), jsx("div", { className: mergeClasses(styles$6.titleText, classes?.titleText, 'bold'), children: title }), jsxs("svg", { width: "132", height: "13", viewBox: "0 0 132 13", fill: "none", preserveAspectRatio: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsx("rect", { width: "130.517", height: "13", fill: "#DDDDDD" }), jsx("rect", { width: "1", height: "13", fill: "#EEEEEE" }), jsx("rect", { x: "130", width: "1", height: "13", fill: "#C5C5C5" }), jsx("rect", { y: "1", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "5", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "9", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "3", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "7", width: "131.268", height: "1", fill: "#999999" }), jsx("rect", { y: "11", width: "131.268", height: "1", fill: "#999999" })] })] })] }));
780
907
  }
781
908
  return null;
782
909
  };
@@ -913,7 +1040,7 @@ const Dialog = forwardRef(({ open = false, onClose, closeOnBackdropClick = true,
913
1040
  });
914
1041
  Dialog.displayName = 'Dialog';
915
1042
 
916
- var styles$4 = {"menuBar":"MenuBar-module_menuBar","leftContent":"MenuBar-module_leftContent","menusContainer":"MenuBar-module_menusContainer","menuContainer":"MenuBar-module_menuContainer","rightContent":"MenuBar-module_rightContent","menuButton":"MenuBar-module_menuButton","menuButton--disabled":"MenuBar-module_menuButton--disabled","menuButton--open":"MenuBar-module_menuButton--open","dropdown":"MenuBar-module_dropdown"};
1043
+ var styles$4 = {"menuBar":"MenuBar-module_menuBar","leftContent":"MenuBar-module_leftContent","menusContainer":"MenuBar-module_menusContainer","menuContainer":"MenuBar-module_menuContainer","rightContent":"MenuBar-module_rightContent","menuButton":"MenuBar-module_menuButton","menuButton--disabled":"MenuBar-module_menuButton--disabled","menuButton--open":"MenuBar-module_menuButton--open","dropdown":"MenuBar-module_dropdown","dropdown--right":"MenuBar-module_dropdown--right"};
917
1044
 
918
1045
  /**
919
1046
  * Mac OS 9 style MenuBar component
@@ -970,41 +1097,56 @@ var styles$4 = {"menuBar":"MenuBar-module_menuBar","leftContent":"MenuBar-module
970
1097
  const MenuBar = forwardRef(({ menus, openMenuIndex, onMenuOpen, onMenuClose, className = '', dropdownClassName = '', leftContent, rightContent, }, ref) => {
971
1098
  const [menuBarElement, setMenuBarElement] = useState(null);
972
1099
  const [focusedIndex, setFocusedIndex] = useState(-1);
1100
+ const [internalOpenIndex, setInternalOpenIndex] = useState(undefined);
1101
+ const isControlled = openMenuIndex !== undefined;
1102
+ const activeOpenIndex = isControlled ? openMenuIndex : internalOpenIndex;
1103
+ const handleMenuOpenInternal = (index) => {
1104
+ if (!isControlled) {
1105
+ setInternalOpenIndex(index);
1106
+ }
1107
+ onMenuOpen?.(index);
1108
+ };
1109
+ const handleMenuCloseInternal = () => {
1110
+ if (!isControlled) {
1111
+ setInternalOpenIndex(undefined);
1112
+ }
1113
+ onMenuClose?.();
1114
+ };
973
1115
  // Handle click outside to close menu
974
1116
  useEffect(() => {
975
- if (openMenuIndex === undefined || !menuBarElement)
1117
+ if (activeOpenIndex === undefined || !menuBarElement)
976
1118
  return;
977
1119
  const handleClickOutside = (event) => {
978
1120
  if (menuBarElement && !menuBarElement.contains(event.target)) {
979
- onMenuClose?.();
1121
+ handleMenuCloseInternal();
980
1122
  }
981
1123
  };
982
1124
  document.addEventListener('mousedown', handleClickOutside);
983
1125
  return () => document.removeEventListener('mousedown', handleClickOutside);
984
- }, [openMenuIndex, onMenuClose, menuBarElement]);
1126
+ }, [activeOpenIndex, onMenuClose, menuBarElement, isControlled]);
985
1127
  // Handle Escape key to close menu
986
1128
  useEffect(() => {
987
- if (openMenuIndex === undefined)
1129
+ if (activeOpenIndex === undefined)
988
1130
  return;
989
1131
  const handleEscape = (event) => {
990
1132
  if (event.key === 'Escape') {
991
1133
  event.preventDefault();
992
- onMenuClose?.();
1134
+ handleMenuCloseInternal();
993
1135
  }
994
1136
  };
995
1137
  document.addEventListener('keydown', handleEscape);
996
1138
  return () => document.removeEventListener('keydown', handleEscape);
997
- }, [openMenuIndex, onMenuClose]);
1139
+ }, [activeOpenIndex, onMenuClose, isControlled]);
998
1140
  // Handle keyboard navigation
999
1141
  const handleKeyDown = useCallback((event) => {
1000
1142
  switch (event.key) {
1001
1143
  case 'ArrowLeft':
1002
1144
  event.preventDefault();
1003
- if (openMenuIndex !== undefined) {
1145
+ if (activeOpenIndex !== undefined) {
1004
1146
  // Move to previous menu
1005
- const prevIndex = openMenuIndex > 0 ? openMenuIndex - 1 : menus.length - 1;
1147
+ const prevIndex = activeOpenIndex > 0 ? activeOpenIndex - 1 : menus.length - 1;
1006
1148
  if (!menus[prevIndex]?.disabled) {
1007
- onMenuOpen?.(prevIndex);
1149
+ handleMenuOpenInternal(prevIndex);
1008
1150
  }
1009
1151
  }
1010
1152
  else if (focusedIndex > 0) {
@@ -1013,11 +1155,11 @@ const MenuBar = forwardRef(({ menus, openMenuIndex, onMenuOpen, onMenuClose, cla
1013
1155
  break;
1014
1156
  case 'ArrowRight':
1015
1157
  event.preventDefault();
1016
- if (openMenuIndex !== undefined) {
1158
+ if (activeOpenIndex !== undefined) {
1017
1159
  // Move to next menu
1018
- const nextIndex = openMenuIndex < menus.length - 1 ? openMenuIndex + 1 : 0;
1160
+ const nextIndex = activeOpenIndex < menus.length - 1 ? activeOpenIndex + 1 : 0;
1019
1161
  if (!menus[nextIndex]?.disabled) {
1020
- onMenuOpen?.(nextIndex);
1162
+ handleMenuOpenInternal(nextIndex);
1021
1163
  }
1022
1164
  }
1023
1165
  else if (focusedIndex < menus.length - 1) {
@@ -1026,18 +1168,18 @@ const MenuBar = forwardRef(({ menus, openMenuIndex, onMenuOpen, onMenuClose, cla
1026
1168
  break;
1027
1169
  case 'ArrowDown':
1028
1170
  event.preventDefault();
1029
- if (openMenuIndex === undefined && focusedIndex >= 0) {
1171
+ if (activeOpenIndex === undefined && focusedIndex >= 0) {
1030
1172
  // Open the focused menu (only if it's a dropdown)
1031
1173
  const menu = menus[focusedIndex];
1032
1174
  if (!menu?.disabled && menu?.type !== 'link') {
1033
- onMenuOpen?.(focusedIndex);
1175
+ handleMenuOpenInternal(focusedIndex);
1034
1176
  }
1035
1177
  }
1036
1178
  break;
1037
1179
  case 'Enter':
1038
1180
  case ' ':
1039
1181
  event.preventDefault();
1040
- if (openMenuIndex === undefined && focusedIndex >= 0) {
1182
+ if (activeOpenIndex === undefined && focusedIndex >= 0) {
1041
1183
  const menu = menus[focusedIndex];
1042
1184
  if (!menu?.disabled) {
1043
1185
  if (menu.type === 'link') {
@@ -1046,13 +1188,13 @@ const MenuBar = forwardRef(({ menus, openMenuIndex, onMenuOpen, onMenuClose, cla
1046
1188
  }
1047
1189
  else {
1048
1190
  // Open the focused dropdown menu
1049
- onMenuOpen?.(focusedIndex);
1191
+ handleMenuOpenInternal(focusedIndex);
1050
1192
  }
1051
1193
  }
1052
1194
  }
1053
1195
  break;
1054
1196
  }
1055
- }, [openMenuIndex, focusedIndex, menus, onMenuOpen, onMenuClose]);
1197
+ }, [activeOpenIndex, focusedIndex, menus, onMenuOpen, onMenuClose, isControlled]);
1056
1198
  // Handle menu button click
1057
1199
  const handleMenuClick = (index) => {
1058
1200
  const menu = menus[index];
@@ -1063,13 +1205,13 @@ const MenuBar = forwardRef(({ menus, openMenuIndex, onMenuOpen, onMenuClose, cla
1063
1205
  menu.onClick?.();
1064
1206
  return;
1065
1207
  }
1066
- if (openMenuIndex === index) {
1208
+ if (activeOpenIndex === index) {
1067
1209
  // Clicking the same menu closes it
1068
- onMenuClose?.();
1210
+ handleMenuCloseInternal();
1069
1211
  }
1070
1212
  else {
1071
1213
  // Open the clicked menu
1072
- onMenuOpen?.(index);
1214
+ handleMenuOpenInternal(index);
1073
1215
  }
1074
1216
  };
1075
1217
  // Class names
@@ -1086,7 +1228,7 @@ const MenuBar = forwardRef(({ menus, openMenuIndex, onMenuOpen, onMenuClose, cla
1086
1228
  }
1087
1229
  }, [ref]);
1088
1230
  return (jsxs("div", { ref: handleRef, className: menuBarClassNames, role: "menubar", onKeyDown: handleKeyDown, children: [leftContent && (jsx("div", { className: styles$4.leftContent, children: leftContent })), jsx("div", { className: styles$4.menusContainer, children: menus.map((menu, index) => {
1089
- const isOpen = openMenuIndex === index;
1231
+ const isOpen = activeOpenIndex === index;
1090
1232
  const isDropdown = menu.type !== 'link';
1091
1233
  const menuButtonClassNames = [
1092
1234
  styles$4.menuButton,
@@ -1105,14 +1247,14 @@ const MenuBar = forwardRef(({ menus, openMenuIndex, onMenuOpen, onMenuClose, cla
1105
1247
  }, onFocus: () => setFocusedIndex(index), onBlur: () => setFocusedIndex(-1), "aria-disabled": menu.disabled, children: jsx("h3", { children: menu.label }) }) }, index));
1106
1248
  }
1107
1249
  // Standard dropdown menu or link without href
1108
- return (jsxs("div", { className: styles$4.menuContainer, children: [jsx("button", { type: "button", className: menuButtonClassNames, onClick: () => handleMenuClick(index), onFocus: () => setFocusedIndex(index), onBlur: () => setFocusedIndex(-1), disabled: menu.disabled, "aria-haspopup": isDropdown ? 'true' : undefined, "aria-expanded": isOpen, "aria-disabled": menu.disabled, children: menu.label }), isOpen && isDropdown && menu.items && (jsx("div", { className: dropdownClassNames, role: "menu", children: menu.items }))] }, index));
1250
+ return (jsxs("div", { className: styles$4.menuContainer, children: [jsx("button", { type: "button", className: menuButtonClassNames, onClick: () => handleMenuClick(index), onFocus: () => setFocusedIndex(index), onBlur: () => setFocusedIndex(-1), disabled: menu.disabled, "aria-haspopup": isDropdown ? 'true' : undefined, "aria-expanded": isOpen, "aria-disabled": menu.disabled, children: jsx("h3", { children: menu.label }) }), isOpen && isDropdown && menu.items && (jsx("div", { className: dropdownClassNames, role: "menu", children: menu.items }))] }, index));
1109
1251
  }) }), rightContent && (jsx("div", { className: styles$4.rightContent, children: Array.isArray(rightContent)
1110
1252
  ? rightContent.map((item, index) => (jsx(React.Fragment, { children: item }, index)))
1111
1253
  : rightContent }))] }));
1112
1254
  });
1113
1255
  MenuBar.displayName = 'MenuBar';
1114
1256
 
1115
- var styles$3 = {"menuItem":"MenuItem-module_menuItem","menuItem--disabled":"MenuItem-module_menuItem--disabled","menuItem--selected":"MenuItem-module_menuItem--selected","menuItem--separator":"MenuItem-module_menuItem--separator","checkmark":"MenuItem-module_checkmark","icon":"MenuItem-module_icon","label":"MenuItem-module_label","shortcut":"MenuItem-module_shortcut","submenuArrow":"MenuItem-module_submenuArrow","separatorLine":"MenuItem-module_separatorLine"};
1257
+ var styles$3 = {"menuItem":"MenuItem-module_menuItem","menuItem--disabled":"MenuItem-module_menuItem--disabled","menuItem--selected":"MenuItem-module_menuItem--selected","menuItem--separator":"MenuItem-module_menuItem--separator","checkmark":"MenuItem-module_checkmark","icon":"MenuItem-module_icon","label":"MenuItem-module_label","shortcut":"MenuItem-module_shortcut","submenuArrow":"MenuItem-module_submenuArrow","submenu":"MenuItem-module_submenu","separatorLine":"MenuItem-module_separatorLine"};
1116
1258
 
1117
1259
  /**
1118
1260
  * Mac OS 9 style MenuItem component
@@ -1151,7 +1293,9 @@ var styles$3 = {"menuItem":"MenuItem-module_menuItem","menuItem--disabled":"Menu
1151
1293
  * <MenuItem label="Recent Files" hasSubmenu />
1152
1294
  * ```
1153
1295
  */
1154
- const MenuItem = forwardRef(({ label, shortcut, disabled = false, selected = false, separator = false, checked = false, icon, onClick, onFocus, onBlur, className = '', hasSubmenu = false, }, ref) => {
1296
+ const MenuItem = forwardRef(({ label, shortcut, disabled = false, selected = false, separator = false, checked = false, icon, onClick, onFocus, onBlur, className = '', hasSubmenu = false, items, }, ref) => {
1297
+ const [isSubmenuOpen, setIsSubmenuOpen] = useState(false);
1298
+ const effectiveHasSubmenu = hasSubmenu || !!items;
1155
1299
  // Class names
1156
1300
  const menuItemClassNames = [
1157
1301
  styles$3.menuItem,
@@ -1170,10 +1314,66 @@ const MenuItem = forwardRef(({ label, shortcut, disabled = false, selected = fal
1170
1314
  }
1171
1315
  onClick?.(event);
1172
1316
  };
1173
- return (jsxs(Fragment, { children: [jsxs("button", { ref: ref, type: "button", className: menuItemClassNames, onClick: handleClick, onFocus: onFocus, onBlur: onBlur, disabled: disabled, role: "menuitem", "aria-disabled": disabled, "aria-checked": checked ? 'true' : undefined, children: [jsx("span", { className: styles$3.checkmark, children: checked && '✓' }), icon && jsx("span", { className: styles$3.icon, children: icon }), jsx("span", { className: styles$3.label, children: label }), shortcut && jsx("span", { className: styles$3.shortcut, children: shortcut }), hasSubmenu && jsx("span", { className: styles$3.submenuArrow, children: "\u25B6" })] }), separator && jsx("div", { className: styles$3.separatorLine, role: "separator" })] }));
1317
+ return (jsxs("div", { className: styles$3.menuItemContainer, onMouseEnter: () => setIsSubmenuOpen(true), onMouseLeave: () => setIsSubmenuOpen(false), style: { position: 'relative', width: '100%' }, children: [jsxs("button", { ref: ref, type: "button", className: menuItemClassNames, onClick: handleClick, onFocus: onFocus, onBlur: onBlur, disabled: disabled, role: "menuitem", "aria-disabled": disabled, "aria-checked": checked ? 'true' : undefined, children: [jsx("span", { className: styles$3.checkmark, children: checked && '✓' }), icon && jsx("span", { className: styles$3.icon, children: icon }), jsx("span", { className: styles$3.label, children: label }), shortcut && jsx("span", { className: styles$3.shortcut, children: shortcut }), effectiveHasSubmenu && jsx("span", { className: styles$3.submenuArrow, children: "\u25B6" })] }), items && isSubmenuOpen && (jsx("div", { className: styles$3.submenu, role: "menu", children: items })), separator && jsx("div", { className: styles$3.separatorLine, role: "separator" })] }));
1174
1318
  });
1175
1319
  MenuItem.displayName = 'MenuItem';
1176
1320
 
1321
+ /**
1322
+ * Mac OS 9 style MenuDropdown component
1323
+ *
1324
+ * A standalone dropdown menu that shares the styling of the MenuBar.
1325
+ * Useful for placing menus in the status area (rightContent) or other parts of the app.
1326
+ */
1327
+ const MenuDropdown = ({ label, items, disabled = false, className = '', dropdownClassName = '', align = 'left', }) => {
1328
+ const [isOpen, setIsOpen] = useState(false);
1329
+ const containerRef = useRef(null);
1330
+ // Handle click outside to close menu
1331
+ useEffect(() => {
1332
+ if (!isOpen)
1333
+ return;
1334
+ const handleClickOutside = (event) => {
1335
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
1336
+ setIsOpen(false);
1337
+ }
1338
+ };
1339
+ document.addEventListener('mousedown', handleClickOutside);
1340
+ return () => document.removeEventListener('mousedown', handleClickOutside);
1341
+ }, [isOpen]);
1342
+ // Handle Escape key to close menu
1343
+ useEffect(() => {
1344
+ if (!isOpen)
1345
+ return;
1346
+ const handleEscape = (event) => {
1347
+ if (event.key === 'Escape') {
1348
+ event.preventDefault();
1349
+ setIsOpen(false);
1350
+ }
1351
+ };
1352
+ document.addEventListener('keydown', handleEscape);
1353
+ return () => document.removeEventListener('keydown', handleEscape);
1354
+ }, [isOpen]);
1355
+ const handleToggle = () => {
1356
+ if (!disabled) {
1357
+ setIsOpen(!isOpen);
1358
+ }
1359
+ };
1360
+ const menuContainerClassNames = [
1361
+ styles$4.menuContainer,
1362
+ className
1363
+ ].filter(Boolean).join(' ');
1364
+ const menuButtonClassNames = [
1365
+ styles$4.menuButton,
1366
+ isOpen ? styles$4['menuButton--open'] : '',
1367
+ disabled ? styles$4['menuButton--disabled'] : '',
1368
+ ].filter(Boolean).join(' ');
1369
+ const dropdownClassNames = [
1370
+ styles$4.dropdown,
1371
+ align === 'right' ? styles$4['dropdown--right'] : '',
1372
+ dropdownClassName
1373
+ ].filter(Boolean).join(' ');
1374
+ return (jsxs("div", { ref: containerRef, className: menuContainerClassNames, children: [jsx("button", { type: "button", className: menuButtonClassNames, onClick: handleToggle, disabled: disabled, "aria-haspopup": "true", "aria-expanded": isOpen, "aria-disabled": disabled, children: typeof label === 'string' ? jsx("h3", { children: label }) : label }), isOpen && (jsx("div", { className: dropdownClassNames, role: "menu", onClick: () => setIsOpen(false), children: items }))] }));
1375
+ };
1376
+
1177
1377
  var styles$2 = {"scrollbar":"Scrollbar-module_scrollbar","scrollbar--vertical":"Scrollbar-module_scrollbar--vertical","scrollbar--horizontal":"Scrollbar-module_scrollbar--horizontal","scrollbar--disabled":"Scrollbar-module_scrollbar--disabled","arrow":"Scrollbar-module_arrow","arrowIcon":"Scrollbar-module_arrowIcon","arrow--start":"Scrollbar-module_arrow--start","arrow--end":"Scrollbar-module_arrow--end","track":"Scrollbar-module_track","thumb":"Scrollbar-module_thumb"};
1178
1378
 
1179
1379
  // Scrollbar component - Mac OS 9 style
@@ -1309,11 +1509,13 @@ var styles$1 = {"listView":"ListView-module_listView","header":"ListView-module_
1309
1509
  * />
1310
1510
  * ```
1311
1511
  */
1312
- const ListView = forwardRef(({ columns, items, selectedIds = [], onSelectionChange, onItemOpen, onItemMouseEnter, onSort, className = '', height = 'auto', }, ref) => {
1512
+ const ListView = forwardRef(({ columns, items, selectedIds = [], onSelectionChange, onItemOpen, onItemMouseEnter, onItemMouseLeave, onSort, className = '', height = 'auto', classes, renderRow, renderCell, renderHeaderCell, onCellClick, onCellMouseEnter, onCellMouseLeave, }, ref) => {
1313
1513
  const [sortColumn, setSortColumn] = useState(null);
1314
1514
  const [sortDirection, setSortDirection] = useState('asc');
1515
+ const [hoveredRow, setHoveredRow] = useState(null);
1516
+ const [hoveredCell, setHoveredCell] = useState(null);
1315
1517
  // Class names
1316
- const classNames = [styles$1.listView, className].filter(Boolean).join(' ');
1518
+ const classNames = mergeClasses(styles$1.listView, className, classes?.root);
1317
1519
  // Handle column header click
1318
1520
  const handleColumnClick = useCallback((columnKey, sortable = true) => {
1319
1521
  if (!sortable || !onSort)
@@ -1358,7 +1560,7 @@ const ListView = forwardRef(({ columns, items, selectedIds = [], onSelectionChan
1358
1560
  }
1359
1561
  }, [onItemOpen]);
1360
1562
  // Handle row mouse enter
1361
- const handleRowMouseEnter = useCallback((item) => {
1563
+ useCallback((item) => {
1362
1564
  if (onItemMouseEnter) {
1363
1565
  onItemMouseEnter(item);
1364
1566
  }
@@ -1368,17 +1570,108 @@ const ListView = forwardRef(({ columns, items, selectedIds = [], onSelectionChan
1368
1570
  if (height !== 'auto') {
1369
1571
  containerStyle.height = typeof height === 'number' ? `${height}px` : height;
1370
1572
  }
1371
- return (jsxs("div", { ref: ref, className: classNames, style: containerStyle, children: [jsx("div", { className: styles$1.header, children: columns.map((column) => (jsxs("div", { className: `${styles$1.headerCell} ${column.sortable !== false ? styles$1.sortable : ''}`, style: {
1372
- width: typeof column.width === 'number'
1373
- ? `${column.width}px`
1374
- : column.width,
1375
- }, onClick: () => handleColumnClick(column.key, column.sortable), children: [column.label, sortColumn === column.key && (jsx("span", { className: styles$1.sortIndicator, children: sortDirection === 'asc' ? '▲' : '▼' }))] }, column.key))) }), jsx("div", { className: styles$1.body, children: items.map((item) => {
1573
+ return (jsxs("div", { ref: ref, className: classNames, style: containerStyle, children: [jsx("div", { className: mergeClasses(styles$1.header, classes?.header), children: columns.map((column) => {
1574
+ const isSorted = sortColumn === column.key;
1575
+ const headerState = {
1576
+ isSorted,
1577
+ sortDirection: isSorted ? sortDirection : undefined,
1578
+ };
1579
+ const headerDefaultProps = {
1580
+ key: column.key,
1581
+ className: mergeClasses(styles$1.headerCell, column.sortable !== false && styles$1.sortable, classes?.headerCell),
1582
+ style: {
1583
+ width: typeof column.width === 'number'
1584
+ ? `${column.width}px`
1585
+ : column.width,
1586
+ },
1587
+ onClick: () => handleColumnClick(column.key, column.sortable),
1588
+ 'data-column': column.key,
1589
+ 'data-sortable': column.sortable !== false,
1590
+ ...(isSorted && {
1591
+ 'data-sorted': true,
1592
+ 'data-sort-direction': sortDirection,
1593
+ }),
1594
+ };
1595
+ // Use custom render or default
1596
+ if (renderHeaderCell) {
1597
+ return renderHeaderCell(column, headerState, headerDefaultProps);
1598
+ }
1599
+ // Default header cell rendering
1600
+ return (jsxs("div", { ...headerDefaultProps, children: [column.label, isSorted && (jsx("span", { className: styles$1.sortIndicator, children: sortDirection === 'asc' ? '▲' : '▼' }))] }));
1601
+ }) }), jsx("div", { className: mergeClasses(styles$1.body, classes?.body), children: items.map((item, rowIndex) => {
1376
1602
  const isSelected = selectedIds.includes(item.id);
1377
- return (jsx("div", { className: `${styles$1.row} ${isSelected ? styles$1.selected : ''}`, onClick: (e) => handleRowClick(item.id, e), onDoubleClick: () => handleRowDoubleClick(item), onMouseEnter: () => handleRowMouseEnter(item), children: columns.map((column, index) => (jsxs("div", { className: styles$1.cell, style: {
1378
- width: typeof column.width === 'number'
1379
- ? `${column.width}px`
1380
- : column.width,
1381
- }, children: [index === 0 && item.icon && (jsx("span", { className: styles$1.icon, children: item.icon })), item[column.key]] }, column.key))) }, item.id));
1603
+ const isHovered = hoveredRow === item.id;
1604
+ const rowState = {
1605
+ isSelected,
1606
+ isHovered,
1607
+ index: rowIndex,
1608
+ };
1609
+ const rowDefaultProps = {
1610
+ key: item.id,
1611
+ className: mergeClasses(styles$1.row, isSelected && styles$1.selected, classes?.row),
1612
+ onClick: (e) => handleRowClick(item.id, e),
1613
+ onDoubleClick: () => handleRowDoubleClick(item),
1614
+ onMouseEnter: () => {
1615
+ setHoveredRow(item.id);
1616
+ onItemMouseEnter?.(item);
1617
+ },
1618
+ onMouseLeave: () => {
1619
+ setHoveredRow(null);
1620
+ setHoveredCell(null);
1621
+ onItemMouseLeave?.(item);
1622
+ },
1623
+ 'data-selected': isSelected,
1624
+ 'data-index': rowIndex,
1625
+ 'data-item-id': item.id,
1626
+ };
1627
+ // Use custom row render or default
1628
+ if (renderRow) {
1629
+ return renderRow(item, rowState, rowDefaultProps);
1630
+ }
1631
+ // Default row rendering
1632
+ return (jsx("div", { ...rowDefaultProps, children: columns.map((column, columnIndex) => {
1633
+ const value = item[column.key];
1634
+ const isCellHovered = hoveredCell?.rowId === item.id &&
1635
+ hoveredCell?.columnKey === column.key;
1636
+ const cellState = {
1637
+ isHovered: isCellHovered,
1638
+ isRowSelected: isSelected,
1639
+ columnIndex,
1640
+ rowIndex,
1641
+ };
1642
+ // Cell event handlers
1643
+ const handleCellClick = (e) => {
1644
+ if (onCellClick) {
1645
+ onCellClick(item, column, e);
1646
+ }
1647
+ };
1648
+ const handleCellMouseEnter = () => {
1649
+ setHoveredCell({ rowId: item.id, columnKey: column.key });
1650
+ if (onCellMouseEnter) {
1651
+ onCellMouseEnter(item, column);
1652
+ }
1653
+ };
1654
+ const handleCellMouseLeave = () => {
1655
+ setHoveredCell(null);
1656
+ if (onCellMouseLeave) {
1657
+ onCellMouseLeave(item, column);
1658
+ }
1659
+ };
1660
+ // Use custom cell render or default
1661
+ if (renderCell) {
1662
+ return (jsx("div", { className: mergeClasses(styles$1.cell, classes?.cell), style: {
1663
+ width: typeof column.width === 'number'
1664
+ ? `${column.width}px`
1665
+ : column.width,
1666
+ }, "data-column": column.key, "data-hovered": isCellHovered, onClick: handleCellClick, onMouseEnter: handleCellMouseEnter, onMouseLeave: handleCellMouseLeave, children: renderCell(value, item, column, cellState) }, column.key));
1667
+ }
1668
+ // Default cell rendering
1669
+ return (jsxs("div", { className: mergeClasses(styles$1.cell, classes?.cell), style: {
1670
+ width: typeof column.width === 'number'
1671
+ ? `${column.width}px`
1672
+ : column.width,
1673
+ }, "data-column": column.key, "data-hovered": isCellHovered, onClick: handleCellClick, onMouseEnter: handleCellMouseEnter, onMouseLeave: handleCellMouseLeave, children: [columnIndex === 0 && item.icon && (jsx("span", { className: styles$1.icon, children: item.icon })), value] }, column.key));
1674
+ }) }));
1382
1675
  }) })] }));
1383
1676
  });
1384
1677
  ListView.displayName = 'ListView';
@@ -1395,6 +1688,7 @@ var styles = {"folderListContent":"FolderList-module_folderListContent","listVie
1395
1688
  *
1396
1689
  * @example
1397
1690
  * ```tsx
1691
+ * // Basic folder list
1398
1692
  * <FolderList
1399
1693
  * title="My Documents"
1400
1694
  * items={[
@@ -1404,8 +1698,14 @@ var styles = {"folderListContent":"FolderList-module_folderListContent","listVie
1404
1698
  * selectedIds={['1']}
1405
1699
  * onSelectionChange={(ids) => console.log('Selected:', ids)}
1406
1700
  * onItemOpen={(item) => console.log('Open:', item.name)}
1407
- * onItemMouseEnter={(item) => console.log('Hovering:', item.name)}
1408
- * onMouseEnter={(e) => console.log('Mouse entered folder list')}
1701
+ * />
1702
+ *
1703
+ * // Draggable folder list
1704
+ * <FolderList
1705
+ * title="My Documents"
1706
+ * items={items}
1707
+ * draggable
1708
+ * defaultPosition={{ x: 100, y: 100 }}
1409
1709
  * />
1410
1710
  * ```
1411
1711
  */
@@ -1413,9 +1713,18 @@ const FolderList = forwardRef(({ columns = [
1413
1713
  { key: 'name', label: 'Name', width: '40%' },
1414
1714
  { key: 'modified', label: 'Date Modified', width: '30%' },
1415
1715
  { key: 'size', label: 'Size', width: '30%' },
1416
- ], items, selectedIds, onSelectionChange, onItemOpen, onItemMouseEnter, onSort, onMouseEnter, listHeight = 400, ...windowProps }, ref) => {
1716
+ ], items, selectedIds, onSelectionChange, onItemOpen, onItemMouseEnter, onItemMouseLeave, onSort, onMouseEnter, listHeight = 400, classes, renderRow, renderCell, renderHeaderCell, onCellClick, onCellMouseEnter, onCellMouseLeave, ...windowProps }, ref) => {
1717
+ // Build ListView classes from FolderList classes
1718
+ const listViewClasses = classes ? {
1719
+ root: classes.listView,
1720
+ header: classes.header,
1721
+ headerCell: classes.headerCell,
1722
+ body: classes.body,
1723
+ row: classes.row,
1724
+ cell: classes.cell,
1725
+ } : undefined;
1417
1726
  // Window content with ListView
1418
- return (jsx(Window, { ref: ref, contentClassName: styles.folderListContent, onMouseEnter: onMouseEnter, ...windowProps, children: jsx(ListView, { columns: columns, items: items, selectedIds: selectedIds, onSelectionChange: onSelectionChange, onItemOpen: onItemOpen, onItemMouseEnter: onItemMouseEnter, onSort: onSort, height: listHeight, className: styles.listView }) }));
1727
+ return (jsx(Window, { ref: ref, contentClassName: styles.folderListContent, onMouseEnter: onMouseEnter, className: classes?.root, ...windowProps, children: jsx(ListView, { columns: columns, items: items, selectedIds: selectedIds, onSelectionChange: onSelectionChange, onItemOpen: onItemOpen, onItemMouseEnter: onItemMouseEnter, onItemMouseLeave: onItemMouseLeave, onSort: onSort, height: listHeight, className: styles.listView, classes: listViewClasses, renderRow: renderRow, renderCell: renderCell, renderHeaderCell: renderHeaderCell, onCellClick: onCellClick, onCellMouseEnter: onCellMouseEnter, onCellMouseLeave: onCellMouseLeave }) }));
1419
1728
  });
1420
1729
  FolderList.displayName = 'FolderList';
1421
1730
 
@@ -1637,5 +1946,5 @@ const tokens = {
1637
1946
  transitions,
1638
1947
  };
1639
1948
 
1640
- export { Button, Checkbox, Dialog, DividerIcon, FolderList, Icon, IconButton, IconLibrary, ListView, MenuBar, MenuItem, Radio, Scrollbar, Select, TabPanel, Tabs, TextField, Window, borders, colors, shadows, spacing, tokens, transitions, typography, zIndex };
1949
+ export { Button, Checkbox, Dialog, DividerIcon, FolderList, Icon, IconButton, IconLibrary, ListView, MenuBar, MenuDropdown, MenuItem, Radio, Scrollbar, Select, TabPanel, Tabs, TextField, Window, borders, colors, createClassBuilder, mergeClasses, shadows, spacing, tokens, transitions, typography, zIndex };
1641
1950
  //# sourceMappingURL=index.js.map