All files / webview-app/src/components Palette.tsx

62.96% Statements 17/27
50% Branches 4/8
55.55% Functions 5/9
65.38% Lines 17/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161                                                                          1x                               1x             1x         1x     1x       1x 1x               4x 4x   4x 1x 1x           4x           4x                                                           8x                 68x   68x                                          
import {
  Braces,
  ChevronDown,
  ChevronRight,
  Circle,
  Component,
  Hash,
  LayoutPanelLeft,
  List,
  ListTree,
  Loader,
  Minus,
  PanelTop,
  Rows3,
  SlidersHorizontal,
  Square,
  SquareMenu,
  Type,
  Wrench,
} from "lucide-react";
import { useEffect, useId, useState } from "react";
 
import { toSwingTypeLabel } from "@/lib/swingTypeLabels";
import { cn } from "@/lib/utils";
 
interface PaletteItem {
  id: string;
  name: string;
  icon: typeof Component;
}
 
interface PaletteSection {
  id: string;
  name: string;
  items: PaletteItem[];
}
 
const COMPONENT_ITEMS: PaletteItem[] = [
  { id: "JPanel", name: toSwingTypeLabel("JPanel"), icon: LayoutPanelLeft },
  { id: "JButton", name: toSwingTypeLabel("JButton"), icon: Square },
  { id: "JLabel", name: toSwingTypeLabel("JLabel"), icon: Type },
  { id: "JTextField", name: toSwingTypeLabel("JTextField"), icon: Braces },
  { id: "JTextArea", name: toSwingTypeLabel("JTextArea"), icon: PanelTop },
  { id: "JCheckBox", name: toSwingTypeLabel("JCheckBox"), icon: Component },
  { id: "JRadioButton", name: toSwingTypeLabel("JRadioButton"), icon: Circle },
  { id: "JComboBox", name: toSwingTypeLabel("JComboBox"), icon: ChevronDown },
  { id: "JList", name: toSwingTypeLabel("JList"), icon: List },
  { id: "JProgressBar", name: toSwingTypeLabel("JProgressBar"), icon: Loader },
  { id: "JSlider", name: toSwingTypeLabel("JSlider"), icon: SlidersHorizontal },
  { id: "JSpinner", name: toSwingTypeLabel("JSpinner"), icon: Hash },
  { id: "JSeparator", name: toSwingTypeLabel("JSeparator"), icon: Minus },
];
 
const CONTAINER_ITEMS: PaletteItem[] = [
  { id: "JMenuBar", name: toSwingTypeLabel("JMenuBar"), icon: Rows3 },
  { id: "JMenu", name: toSwingTypeLabel("JMenu"), icon: SquareMenu },
  { id: "JMenuItem", name: toSwingTypeLabel("JMenuItem"), icon: ListTree },
  { id: "JToolBar", name: toSwingTypeLabel("JToolBar"), icon: Wrench },
];
 
const PALETTE_SECTIONS: PaletteSection[] = [
  { id: "components", name: "Components", items: COMPONENT_ITEMS },
  { id: "containers", name: "Containers", items: CONTAINER_ITEMS },
];
 
const PALETTE_COLLAPSED_STORAGE_KEY = "swing-gui-builder.sidebar.palette-collapsed";
 
function getInitialPaletteCollapsedState(): boolean {
  Iif (typeof window === "undefined") {
    return true;
  }
 
  try {
    return window.localStorage.getItem(PALETTE_COLLAPSED_STORAGE_KEY) === "true";
  } catch (error) {
    console.warn("[Palette] Failed to read persisted collapse state", error);
    return true;
  }
}
 
export function Palette() {
  const [isCollapsed, setIsCollapsed] = useState(getInitialPaletteCollapsedState);
  const contentId = useId();
 
  useEffect(() => {
    try {
      window.localStorage.setItem(PALETTE_COLLAPSED_STORAGE_KEY, String(isCollapsed));
    } catch (error) {
      console.warn("[Palette] Failed to persist collapse state", error);
    }
  }, [isCollapsed]);
 
  const handleDragStart = (event: React.DragEvent<HTMLLIElement>, componentType: string) => {
    event.dataTransfer.effectAllowed = "copy";
    event.dataTransfer.setData("text/plain", componentType);
    event.dataTransfer.setData("application/x-swing-component", componentType);
  };
 
  return (
    <section
      className={cn("flex min-h-0 flex-col overflow-hidden", isCollapsed ? "flex-none" : "flex-1")}
      aria-label="Swing component palette"
    >
      <header className="flex items-center justify-between border-b border-vscode-panel-border px-4 py-3">
        <h2 className="text-sm font-semibold">Palette</h2>
        <button
          type="button"
          className="inline-flex size-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
          onClick={() => setIsCollapsed((previous) => !previous)}
          aria-controls={contentId}
          aria-expanded={!isCollapsed}
          aria-label={isCollapsed ? "Expand palette section" : "Collapse palette section"}
        >
          {isCollapsed ? (
            <ChevronRight className="size-4" aria-hidden="true" />
          ) : (
            <ChevronDown className="size-4" aria-hidden="true" />
          )}
        </button>
      </header>
 
      <div
        id={contentId}
        hidden={isCollapsed}
        className="min-h-0 flex-1 space-y-4 overflow-y-auto p-2"
        aria-label="Draggable Swing components"
      >
        {PALETTE_SECTIONS.map((section) => (
          <div key={section.id} className="space-y-1">
            <h3 className="px-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
              {section.name}
            </h3>
            <ul
              className="space-y-1"
              aria-label={`Draggable ${section.name.toLowerCase()} components`}
            >
              {section.items.map((item) => {
                const Icon = item.icon;
 
                return (
                  <li
                    key={item.id}
                    draggable
                    onDragStart={(event) => handleDragStart(event, item.id)}
                    className="flex cursor-grab select-none items-center gap-2 rounded-md border border-transparent px-2 py-2 text-sm hover:border-vscode-panel-border hover:bg-accent active:cursor-grabbing"
                    aria-label={`Drag ${item.name}`}
                    title={`Drag ${item.name} to canvas`}
                  >
                    <Icon className="size-4 text-muted-foreground" aria-hidden="true" />
                    <span>{item.name}</span>
                  </li>
                );
              })}
            </ul>
          </div>
        ))}
      </div>
    </section>
  );
}