ElementsPanel.tsx 5.83 KB
import React from "react";
import type { ElementLibraryCategory, ElementType } from "../../../types/labelTemplate";
import {
  elementLibraryCategoryPanelTitle,
  LABEL_EDITOR_FONT_FAMILY,
} from "../../../types/labelTemplate";

/** 左侧标签库:四类分组(与产品图一致);打印时输入项可带 config 以正确显示为 input */
const ELEMENT_CATEGORIES: {
  title: ElementLibraryCategory;
  subtitle?: string;
  items: { label: string; type: ElementType; config?: Record<string, unknown>; disabled?: boolean }[];
}[] = [
  {
    title: "Template",
    items: [
      { label: "Text", type: "TEXT_STATIC" },
      { label: "QR Code", type: "QRCODE" },
      { label: "Barcode", type: "BARCODE" },
      { label: "Blank Space", type: "BLANK" },
      { label: "Price", type: "TEXT_PRICE" },
      { label: "Image", type: "IMAGE" },
      { label: "Logo", type: "IMAGE" },
    ],
  },
  {
    title: "Label",
    items: [
      { label: "Label Name", type: "TEXT_PRODUCT" },
      { label: "Text", type: "TEXT_STATIC" },
      { label: "QR Code", type: "QRCODE" },
      { label: "Barcode", type: "BARCODE" },
      { label: "Nutrition Facts", type: "NUTRITION" },
      { label: "Price", type: "TEXT_PRICE" },
      { label: "Duration Date", type: "DATE" },
      { label: "Duration Time", type: "TIME" },
      { label: "Duration", type: "DURATION" },
      { label: "Image", type: "IMAGE" },
      { label: "Label Type", type: "TEXT_STATIC" },
      { label: "How-to", type: "TEXT_STATIC", disabled: true },
      { label: "Expiration Alert", type: "TEXT_STATIC", disabled: true },
    ],
  },
  {
    title: "Auto-generated",
    items: [
      { label: "Company", type: "TEXT_STATIC", config: { companyIncludeFields: ["address", "city", "state", "zip", "email"] } },
      { label: "Employee", type: "TEXT_STATIC" },
      { label: "Current Date", type: "DATE" },
      { label: "Current Time", type: "TIME" },
      { label: "Label ID", type: "TEXT_STATIC", config: { prefix: "LABEL ID: ", text: "LABEL ID: ", fontSize: 10, textAlign: "left" } },
    ],
  },
  {
    title: "Print input",
    items: [
      { label: "Text", type: "TEXT_STATIC", config: { inputType: "text" } },
      { label: "Weight", type: "WEIGHT" },
      { label: "Number", type: "TEXT_STATIC", config: { inputType: "number", text: "0" } },
      { label: "Date & Time", type: "DATE", config: { inputType: "datetime", format: "YYYY-MM-DD HH:mm" } },
      {
        label: "Multiple Options",
        type: "TEXT_STATIC",
        config: {
          inputType: "options",
          multipleOptionId: "",
          selectedOptionValues: [],
          text: "Text",
          fontFamily: LABEL_EDITOR_FONT_FAMILY,
          fontSize: 14,
          fontWeight: "normal",
          textAlign: "left",
        },
      },
    ],
  },
];

interface ElementsPanelProps {
  onAddElement: (
    type: ElementType,
    configOverride: Partial<Record<string, unknown>> | undefined,
    libraryCategory: ElementLibraryCategory,
    paletteItemLabel: string,
  ) => void;
  onOpenInstruction: () => void;
}

/** 各分组:无底色,蓝色细实线边框 */
function sectionSurfaceStyle(): React.CSSProperties {
  return {
    backgroundColor: "transparent",
    border: "1px solid #93c5fd",
    borderRadius: 8,
    padding: 8,
  };
}

/**
 * 左侧元素库。纵向滚动由父级(index 左侧列容器)的 overflow-y:auto 负责,
 * 避免预编译 CSS 缺少 min-h-0 / flex-1 时内层滚动高度为 0 导致底部橙色区被裁切。
 */
export function ElementsPanel({ onAddElement, onOpenInstruction }: ElementsPanelProps) {
  return (
    <div className="border-r border-slate-200 bg-slate-50">
      <div className="border-b border-[#93c5fd] bg-white px-2 py-2 text-sm font-semibold text-slate-800">
        Elements
      </div>
      <div className="space-y-3 p-2">
        {ELEMENT_CATEGORIES.map((cat) => (
          <div key={cat.title} style={sectionSurfaceStyle()}>
            <div className="px-2 py-1 text-xs font-semibold leading-snug text-gray-700">
              {elementLibraryCategoryPanelTitle(cat.title)}
            </div>
            {cat.subtitle ? (
              <div className="px-2 py-0.5 text-[10px] text-gray-400">{cat.subtitle}</div>
            ) : null}
            <div className="mt-0.5 grid grid-cols-2 gap-1">
              {cat.items.map((item, i) => {
                const isDisabled = item.disabled === true;
                return (
                <button
                  key={`${cat.title}-${item.label}-${i}`}
                  type="button"
                  disabled={isDisabled}
                  aria-disabled={isDisabled}
                  title={isDisabled ? "Not available in the US version" : undefined}
                  onClick={
                    isDisabled
                      ? undefined
                      : () => onAddElement(item.type, item.config, cat.title, item.label)
                  }
                  className={
                    isDisabled
                      ? "items-start justify-start whitespace-normal break-words rounded border border-transparent px-2 py-1.5 text-left text-xs leading-snug cursor-not-allowed text-gray-400 bg-gray-100/70 opacity-60"
                      : "items-start justify-start whitespace-normal break-words rounded border border-transparent px-2 py-1.5 text-left text-xs leading-snug hover:border-gray-200 hover:bg-gray-100"
                  }
                >
                  {item.label}
                </button>
                );
              })}
            </div>
          </div>
        ))}
        <div style={sectionSurfaceStyle()}>
          <button
            type="button"
            className="w-full rounded px-2 py-1.5 text-left text-xs font-medium text-gray-900 hover:bg-gray-100"
            onClick={onOpenInstruction}
          >
            Instruction
          </button>
        </div>
      </div>
    </div>
  );
}