@launchdarkly/toolbar 0.8.0 → 0.9.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/js/index.js CHANGED
@@ -2657,23 +2657,56 @@ class FlagStateManager {
2657
2657
  this.listeners.clear();
2658
2658
  }
2659
2659
  }
2660
- const STORAGE_KEY = 'launchdarkly-toolbar-project';
2660
+ const TAB_ORDER = [
2661
+ 'flags',
2662
+ 'settings'
2663
+ ];
2664
+ const TOOLBAR_POSITIONS = [
2665
+ 'left',
2666
+ 'right'
2667
+ ];
2668
+ const TOOLBAR_STORAGE_KEYS = {
2669
+ POSITION: 'ld-toolbar-position',
2670
+ DISABLED: 'ld-toolbar-disabled',
2671
+ PROJECT: 'ld-toolbar-project'
2672
+ };
2673
+ function saveToolbarPosition(position) {
2674
+ try {
2675
+ localStorage.setItem(TOOLBAR_STORAGE_KEYS.POSITION, position);
2676
+ } catch (error) {
2677
+ console.warn('Failed to save toolbar position to localStorage:', error);
2678
+ }
2679
+ }
2680
+ function loadToolbarPosition() {
2681
+ try {
2682
+ const position = localStorage.getItem(TOOLBAR_STORAGE_KEYS.POSITION);
2683
+ return position && TOOLBAR_POSITIONS.includes(position) ? position : null;
2684
+ } catch (error) {
2685
+ console.warn('Failed to load toolbar position from localStorage:', error);
2686
+ return null;
2687
+ }
2688
+ }
2689
+ const STORAGE_KEY = TOOLBAR_STORAGE_KEYS.PROJECT;
2661
2690
  const LaunchDarklyToolbarContext = /*#__PURE__*/ createContext(null);
2662
2691
  const useToolbarContext = ()=>{
2663
2692
  const context = useContext(LaunchDarklyToolbarContext);
2664
2693
  if (!context) throw new Error('useToolbarContext must be used within LaunchDarklyToolbarProvider');
2665
2694
  return context;
2666
2695
  };
2667
- const LaunchDarklyToolbarProvider = ({ children, config })=>{
2668
- const [toolbarState, setToolbarState] = useState({
2669
- flags: {},
2670
- connectionStatus: 'disconnected',
2671
- lastSyncTime: 0,
2672
- isLoading: true,
2673
- error: null,
2674
- sourceEnvironmentKey: null,
2675
- availableProjects: [],
2676
- currentProjectKey: null
2696
+ const LaunchDarklyToolbarProvider = ({ children, config, initialPosition })=>{
2697
+ const [toolbarState, setToolbarState] = useState(()=>{
2698
+ const savedPosition = loadToolbarPosition();
2699
+ return {
2700
+ flags: {},
2701
+ connectionStatus: 'disconnected',
2702
+ lastSyncTime: 0,
2703
+ isLoading: true,
2704
+ error: null,
2705
+ sourceEnvironmentKey: null,
2706
+ availableProjects: [],
2707
+ currentProjectKey: null,
2708
+ position: savedPosition || initialPosition || 'right'
2709
+ };
2677
2710
  });
2678
2711
  const devServerClient = useMemo(()=>new DevServerClient(config.devServerUrl, config.projectKey), [
2679
2712
  config.devServerUrl,
@@ -2965,20 +2998,29 @@ const LaunchDarklyToolbarProvider = ({ children, config })=>{
2965
2998
  flagStateManager,
2966
2999
  toolbarState.availableProjects
2967
3000
  ]);
3001
+ const handlePositionChange = useCallback((newPosition)=>{
3002
+ setToolbarState((prev)=>({
3003
+ ...prev,
3004
+ position: newPosition
3005
+ }));
3006
+ saveToolbarPosition(newPosition);
3007
+ }, []);
2968
3008
  const value = useMemo(()=>({
2969
3009
  state: toolbarState,
2970
3010
  setOverride,
2971
3011
  clearOverride,
2972
3012
  clearAllOverrides,
2973
3013
  refresh,
2974
- switchProject
3014
+ switchProject,
3015
+ handlePositionChange
2975
3016
  }), [
2976
3017
  toolbarState,
2977
3018
  setOverride,
2978
3019
  clearOverride,
2979
3020
  clearAllOverrides,
2980
3021
  refresh,
2981
- switchProject
3022
+ switchProject,
3023
+ handlePositionChange
2982
3024
  ]);
2983
3025
  return /*#__PURE__*/ jsx(LaunchDarklyToolbarContext.Provider, {
2984
3026
  value: value,
@@ -3631,6 +3673,45 @@ function ProjectSelector(props) {
3631
3673
  ]
3632
3674
  });
3633
3675
  }
3676
+ function PositionSelector(props) {
3677
+ const { currentPosition, onPositionChange } = props;
3678
+ function getPositionsDisplayName(position) {
3679
+ return position.charAt(0).toUpperCase() + position.slice(1);
3680
+ }
3681
+ const handlePositionSelect = (key)=>{
3682
+ if (key && 'string' == typeof key) {
3683
+ const position = key;
3684
+ if (position !== currentPosition) onPositionChange(position);
3685
+ }
3686
+ };
3687
+ return /*#__PURE__*/ jsxs(Select, {
3688
+ selectedKey: currentPosition,
3689
+ onSelectionChange: handlePositionSelect,
3690
+ "aria-label": "Select toolbar position",
3691
+ placeholder: "Select position",
3692
+ "data-theme": "dark",
3693
+ className: SettingsTab_css_select,
3694
+ children: [
3695
+ /*#__PURE__*/ jsxs(Button, {
3696
+ children: [
3697
+ /*#__PURE__*/ jsx(SelectValue, {}),
3698
+ /*#__PURE__*/ jsx(ChevronDownIcon, {
3699
+ className: SettingsTab_css_icon
3700
+ })
3701
+ ]
3702
+ }),
3703
+ /*#__PURE__*/ jsx(Popover, {
3704
+ "data-theme": "dark",
3705
+ children: /*#__PURE__*/ jsx(ListBox, {
3706
+ children: TOOLBAR_POSITIONS.map((position)=>/*#__PURE__*/ jsx(ListBoxItem, {
3707
+ id: position,
3708
+ children: getPositionsDisplayName(position)
3709
+ }, position))
3710
+ })
3711
+ })
3712
+ ]
3713
+ });
3714
+ }
3634
3715
  function ConnectionStatusDisplay(props) {
3635
3716
  const { status } = props;
3636
3717
  const getStatusText = ()=>{
@@ -3657,8 +3738,9 @@ function ConnectionStatusDisplay(props) {
3657
3738
  });
3658
3739
  }
3659
3740
  function SettingsTabContent() {
3660
- const { state, switchProject } = useToolbarContext();
3741
+ const { state, switchProject, handlePositionChange } = useToolbarContext();
3661
3742
  const { searchTerm } = useSearchContext();
3743
+ const position = state.position;
3662
3744
  const handleProjectSwitch = async (projectKey)=>{
3663
3745
  try {
3664
3746
  await switchProject(projectKey);
@@ -3666,6 +3748,9 @@ function SettingsTabContent() {
3666
3748
  console.error('Failed to switch project:', error);
3667
3749
  }
3668
3750
  };
3751
+ const handlePositionSelect = (newPosition)=>{
3752
+ handlePositionChange(newPosition);
3753
+ };
3669
3754
  const settingsGroups = [
3670
3755
  {
3671
3756
  title: 'Configuration',
@@ -3676,6 +3761,12 @@ function SettingsTabContent() {
3676
3761
  icon: 'folder',
3677
3762
  isProjectSelector: true
3678
3763
  },
3764
+ {
3765
+ id: 'position',
3766
+ name: 'Position',
3767
+ icon: 'move',
3768
+ isPositionSelector: true
3769
+ },
3679
3770
  {
3680
3771
  id: 'environment',
3681
3772
  name: 'Environment',
@@ -3745,6 +3836,9 @@ function SettingsTabContent() {
3745
3836
  currentProject: state.currentProjectKey,
3746
3837
  onProjectChange: handleProjectSwitch,
3747
3838
  isLoading: state.isLoading
3839
+ }) : item.isPositionSelector ? /*#__PURE__*/ jsx(PositionSelector, {
3840
+ currentPosition: position,
3841
+ onPositionChange: handlePositionSelect
3748
3842
  }) : /*#__PURE__*/ jsx("span", {
3749
3843
  className: settingValue,
3750
3844
  children: item.value
@@ -3922,10 +4016,31 @@ function ExpandedToolbarContent(props) {
3922
4016
  ]
3923
4017
  }, "toolbar-content");
3924
4018
  }
3925
- const TAB_ORDER = [
3926
- 'flags',
3927
- 'settings'
3928
- ];
4019
+ function useKeyPressed(targetKey) {
4020
+ const [keyPressed, setKeyPressed] = useState(false);
4021
+ useEffect(()=>{
4022
+ const downHandler = (event)=>{
4023
+ if (event.key === targetKey) setKeyPressed(true);
4024
+ };
4025
+ const upHandler = (event)=>{
4026
+ if (event.key === targetKey) setKeyPressed(false);
4027
+ };
4028
+ const blurHandler = ()=>{
4029
+ setKeyPressed(false);
4030
+ };
4031
+ window.addEventListener('keydown', downHandler);
4032
+ window.addEventListener('keyup', upHandler);
4033
+ window.addEventListener('blur', blurHandler);
4034
+ return ()=>{
4035
+ window.removeEventListener('keydown', downHandler);
4036
+ window.removeEventListener('keyup', upHandler);
4037
+ window.removeEventListener('blur', blurHandler);
4038
+ };
4039
+ }, [
4040
+ targetKey
4041
+ ]);
4042
+ return keyPressed;
4043
+ }
3929
4044
  function useToolbarState() {
3930
4045
  const [isExpanded, setIsExpanded] = useState(false);
3931
4046
  const [isHovered, setIsHovered] = useState(false);
@@ -3935,10 +4050,14 @@ function useToolbarState() {
3935
4050
  const [searchIsExpanded, setSearchIsExpanded] = useState(false);
3936
4051
  const hasBeenExpandedRef = useRef(false);
3937
4052
  const toolbarRef = useRef(null);
4053
+ const isMetaPressed = useKeyPressed('Meta');
4054
+ const isControlPressed = useKeyPressed('Control');
4055
+ const isDragModifierPressed = isMetaPressed || isControlPressed;
3938
4056
  const { setSearchTerm } = useSearchContext();
3939
- const showFullToolbar = useMemo(()=>isExpanded || isHovered && !isExpanded, [
4057
+ const showFullToolbar = useMemo(()=>isExpanded || isHovered && !isExpanded && !isDragModifierPressed, [
3940
4058
  isExpanded,
3941
- isHovered
4059
+ isHovered,
4060
+ isDragModifierPressed
3942
4061
  ]);
3943
4062
  const slideDirection = useMemo(()=>{
3944
4063
  if (!activeTab || !previousTab) return 1;
@@ -4012,6 +4131,7 @@ function useToolbarState() {
4012
4131
  showFullToolbar,
4013
4132
  slideDirection,
4014
4133
  hasBeenExpanded: hasBeenExpandedRef.current,
4134
+ isDragModifierPressed,
4015
4135
  toolbarRef,
4016
4136
  handleTabChange,
4017
4137
  handleMouseEnter,
@@ -4051,7 +4171,7 @@ function useToolbarAnimations(props) {
4051
4171
  handleAnimationComplete
4052
4172
  };
4053
4173
  }
4054
- const useToolbarVisibility_STORAGE_KEY = 'ld-toolbar-disabled';
4174
+ const useToolbarVisibility_STORAGE_KEY = TOOLBAR_STORAGE_KEYS.DISABLED;
4055
4175
  function useToolbarVisibility() {
4056
4176
  const [isDisabled, setIsDisabled] = useState(()=>{
4057
4177
  if ('undefined' == typeof window) return true;
@@ -4097,22 +4217,90 @@ function useToolbarVisibility() {
4097
4217
  }, []);
4098
4218
  return !isDisabled;
4099
4219
  }
4100
- function LdToolbar(props) {
4101
- const { position = 'right' } = props;
4220
+ function useToolbarDrag({ enabled, onDragEnd, elementRef }) {
4221
+ const handleMouseDown = useCallback((event)=>{
4222
+ if (!enabled || !elementRef.current) return;
4223
+ event.preventDefault();
4224
+ const startPosition = {
4225
+ x: event.clientX,
4226
+ y: event.clientY
4227
+ };
4228
+ const boundingRect = elementRef.current.getBoundingClientRect();
4229
+ const initialPosition = {
4230
+ x: boundingRect.left,
4231
+ y: boundingRect.top
4232
+ };
4233
+ const updateElementPosition = (mouseEvent)=>{
4234
+ if (!elementRef.current) return;
4235
+ const offset = {
4236
+ x: mouseEvent.clientX - startPosition.x,
4237
+ y: mouseEvent.clientY - startPosition.y
4238
+ };
4239
+ const newPosition = {
4240
+ x: initialPosition.x + offset.x,
4241
+ y: initialPosition.y + offset.y
4242
+ };
4243
+ elementRef.current.style.left = `${newPosition.x}px`;
4244
+ elementRef.current.style.top = `${newPosition.y}px`;
4245
+ };
4246
+ const resetElementStyles = ()=>{
4247
+ if (elementRef.current) {
4248
+ elementRef.current.style.left = '';
4249
+ elementRef.current.style.top = '';
4250
+ elementRef.current.style.right = '';
4251
+ elementRef.current.style.bottom = '';
4252
+ elementRef.current.style.transform = '';
4253
+ elementRef.current.style.zIndex = '';
4254
+ }
4255
+ };
4256
+ const handleDragComplete = (upEvent)=>{
4257
+ document.removeEventListener('mousemove', updateElementPosition);
4258
+ document.removeEventListener('mouseup', handleDragComplete);
4259
+ resetElementStyles();
4260
+ onDragEnd(upEvent.clientX);
4261
+ };
4262
+ document.addEventListener('mousemove', updateElementPosition);
4263
+ document.addEventListener('mouseup', handleDragComplete);
4264
+ }, [
4265
+ enabled,
4266
+ onDragEnd,
4267
+ elementRef
4268
+ ]);
4269
+ return {
4270
+ handleMouseDown
4271
+ };
4272
+ }
4273
+ function LdToolbar() {
4102
4274
  const { searchTerm } = useSearchContext();
4275
+ const { state, handlePositionChange } = useToolbarContext();
4103
4276
  const toolbarState = useToolbarState();
4104
- const { isExpanded, activeTab, slideDirection, searchIsExpanded, showFullToolbar, hasBeenExpanded, toolbarRef, handleTabChange, handleMouseEnter, handleMouseLeave, handleClose, handleSearch, setSearchIsExpanded, setIsAnimating } = toolbarState;
4277
+ const position = state.position;
4278
+ const { isExpanded, activeTab, slideDirection, searchIsExpanded, showFullToolbar, hasBeenExpanded, toolbarRef, handleTabChange, handleMouseEnter, handleMouseLeave, handleClose, handleSearch, setSearchIsExpanded, setIsAnimating, isHovered, isDragModifierPressed } = toolbarState;
4105
4279
  const toolbarAnimations = useToolbarAnimations({
4106
4280
  showFullToolbar,
4107
- isHovered: toolbarState.isHovered,
4281
+ isHovered,
4108
4282
  setIsAnimating
4109
4283
  });
4110
4284
  const { containerAnimations, animationConfig, handleAnimationStart, handleAnimationComplete } = toolbarAnimations;
4285
+ const isDragEnabled = !showFullToolbar && isHovered && isDragModifierPressed;
4286
+ const handleDragEnd = useCallback((clientX)=>{
4287
+ const screenWidth = window.innerWidth;
4288
+ const newPosition = clientX < screenWidth / 2 ? 'left' : 'right';
4289
+ handlePositionChange(newPosition);
4290
+ }, [
4291
+ handlePositionChange
4292
+ ]);
4293
+ const { handleMouseDown } = useToolbarDrag({
4294
+ enabled: isDragEnabled,
4295
+ onDragEnd: handleDragEnd,
4296
+ elementRef: toolbarRef
4297
+ });
4111
4298
  return /*#__PURE__*/ jsxs(motion.div, {
4112
4299
  ref: toolbarRef,
4113
4300
  className: `${toolbarContainer} ${'left' === position ? positionLeft : positionRight} ${showFullToolbar ? toolbarExpanded : toolbarCircle}`,
4114
4301
  onMouseEnter: handleMouseEnter,
4115
4302
  onMouseLeave: handleMouseLeave,
4303
+ onMouseDown: handleMouseDown,
4116
4304
  initial: false,
4117
4305
  animate: containerAnimations,
4118
4306
  transition: animationConfig,
@@ -4153,10 +4341,9 @@ function LaunchDarklyToolbar(props) {
4153
4341
  devServerUrl,
4154
4342
  pollIntervalInMs
4155
4343
  },
4344
+ initialPosition: position,
4156
4345
  children: /*#__PURE__*/ jsx(SearchProvider, {
4157
- children: /*#__PURE__*/ jsx(LdToolbar, {
4158
- position: position
4159
- })
4346
+ children: /*#__PURE__*/ jsx(LdToolbar, {})
4160
4347
  })
4161
4348
  });
4162
4349
  }
@@ -1,10 +1,9 @@
1
- export interface LdToolbarProps {
2
- position?: 'left' | 'right';
3
- }
4
- export declare function LdToolbar(props: LdToolbarProps): import("react/jsx-runtime").JSX.Element;
5
- export interface LaunchDarklyToolbarProps extends LdToolbarProps {
1
+ import { ToolbarPosition } from './types/toolbar';
2
+ export declare function LdToolbar(): import("react/jsx-runtime").JSX.Element;
3
+ export interface LaunchDarklyToolbarProps {
6
4
  devServerUrl?: string;
7
5
  projectKey?: string;
8
6
  pollIntervalInMs?: number;
7
+ position?: ToolbarPosition;
9
8
  }
10
9
  export declare function LaunchDarklyToolbar(props: LaunchDarklyToolbarProps): import("react/jsx-runtime").JSX.Element | null;
@@ -1,20 +1,24 @@
1
1
  import React from 'react';
2
2
  import { LdToolbarConfig, ToolbarState } from '../../../types/devServer';
3
+ import { ToolbarPosition } from '../types/toolbar';
3
4
  interface LaunchDarklyToolbarContextValue {
4
5
  state: ToolbarState & {
5
6
  availableProjects: string[];
6
7
  currentProjectKey: string | null;
8
+ position: ToolbarPosition;
7
9
  };
8
10
  setOverride: (flagKey: string, value: any) => Promise<void>;
9
11
  clearOverride: (flagKey: string) => Promise<void>;
10
12
  clearAllOverrides: () => Promise<void>;
11
13
  refresh: () => Promise<void>;
12
14
  switchProject: (projectKey: string) => Promise<void>;
15
+ handlePositionChange: (position: ToolbarPosition) => void;
13
16
  }
14
17
  export declare const useToolbarContext: () => LaunchDarklyToolbarContextValue;
15
18
  export interface LaunchDarklyToolbarProviderProps {
16
19
  children: React.ReactNode;
17
20
  config: LdToolbarConfig;
21
+ initialPosition?: ToolbarPosition;
18
22
  }
19
23
  export declare const LaunchDarklyToolbarProvider: React.FC<LaunchDarklyToolbarProviderProps>;
20
24
  export {};
@@ -1,3 +1,5 @@
1
1
  export { useToolbarState } from './useToolbarState';
2
2
  export { useToolbarAnimations } from './useToolbarAnimations';
3
3
  export { useToolbarVisibility } from './useToolbarVisibility';
4
+ export { useToolbarDrag } from './useToolbarDrag';
5
+ export { useKeyPressed } from './useKeyPressed';
@@ -0,0 +1 @@
1
+ export declare function useKeyPressed(targetKey: string): boolean;
@@ -0,0 +1,10 @@
1
+ interface UseToolbarDragOptions {
2
+ enabled: boolean;
3
+ onDragEnd: (clientX: number) => void;
4
+ elementRef: React.RefObject<HTMLDivElement | null>;
5
+ }
6
+ interface UseToolbarDragReturn {
7
+ handleMouseDown: (event: React.MouseEvent) => void;
8
+ }
9
+ export declare function useToolbarDrag({ enabled, onDragEnd, elementRef }: UseToolbarDragOptions): UseToolbarDragReturn;
10
+ export {};
@@ -10,6 +10,7 @@ export interface UseToolbarStateReturn {
10
10
  showFullToolbar: boolean;
11
11
  slideDirection: number;
12
12
  hasBeenExpanded: boolean;
13
+ isDragModifierPressed: boolean;
13
14
  toolbarRef: React.RefObject<HTMLDivElement | null>;
14
15
  handleTabChange: (tabId: string) => void;
15
16
  handleMouseEnter: () => void;
@@ -10,3 +10,5 @@ export interface FeatureFlag {
10
10
  lastModified?: Date;
11
11
  environment?: string;
12
12
  }
13
+ export declare const TOOLBAR_POSITIONS: readonly ["left", "right"];
14
+ export type ToolbarPosition = (typeof TOOLBAR_POSITIONS)[number];
@@ -0,0 +1,8 @@
1
+ import { ToolbarPosition } from '../types/toolbar';
2
+ export declare const TOOLBAR_STORAGE_KEYS: {
3
+ readonly POSITION: "ld-toolbar-position";
4
+ readonly DISABLED: "ld-toolbar-disabled";
5
+ readonly PROJECT: "ld-toolbar-project";
6
+ };
7
+ export declare function saveToolbarPosition(position: ToolbarPosition): void;
8
+ export declare function loadToolbarPosition(): ToolbarPosition | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@launchdarkly/toolbar",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "A React component that provides a developer-friendly toolbar for interacting with LaunchDarkly during development",
5
5
  "keywords": [
6
6
  "launchdarkly",