primitives.jsx 6.66 KB
// Form primitives shared across screens. Tight, dense, faithful enterprise look.

const fieldStyles = {
  row: { display: "flex", alignItems: "center", gap: 0, minWidth: 0 },
  label: {
    width: 88, flex: "none", textAlign: "right", paddingRight: 8,
    color: "var(--text)", fontSize: 12, lineHeight: "26px",
    whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
  },
  labelReq: { color: "var(--required)" },
  control: {
    flex: 1, minWidth: 0, height: "var(--input-h)",
    border: "1px solid var(--border-input)",
    background: "var(--bg-input)",
    padding: "0 6px", fontSize: 12, color: "var(--text)",
    borderRadius: 0,
  },
  controlDisabled: { background: "var(--bg-disabled)", color: "var(--text-muted)" },
  controlReq: { background: "#fff8e1" }, // matches reference: required cells get a yellow tint
};

const Field = ({ label, required, children, labelWidth, span = 1, valign = "center" }) => (
  <div style={{ display: "flex", alignItems: valign === "top" ? "flex-start" : "center", gridColumn: `span ${span}`, minWidth: 0 }}>
    <div
      style={{
        ...fieldStyles.label,
        width: labelWidth || fieldStyles.label.width,
        ...(required ? { color: "var(--required)" } : {}),
      }}
      title={label}
    >
      {required ? <span style={{ marginRight: 2 }}>*</span> : null}
      {label}:
    </div>
    {children}
  </div>
);

const Input = ({ value, onChange, disabled, required, placeholder, mono, style }) => (
  <input
    type="text"
    value={value ?? ""}
    onChange={onChange ? (e) => onChange(e.target.value) : undefined}
    disabled={disabled}
    placeholder={placeholder}
    style={{
      ...fieldStyles.control,
      ...(disabled ? fieldStyles.controlDisabled : {}),
      ...(required && !disabled ? fieldStyles.controlReq : {}),
      ...(mono ? { fontFamily: '"JetBrains Mono", Menlo, Consolas, monospace', fontSize: 11 } : {}),
      ...style,
    }}
  />
);

const Select = ({ value, onChange, options, disabled, required, style }) => (
  <div style={{ position: "relative", flex: 1, minWidth: 0 }}>
    <select
      value={value ?? ""}
      onChange={onChange ? (e) => onChange(e.target.value) : undefined}
      disabled={disabled}
      style={{
        ...fieldStyles.control,
        ...(disabled ? fieldStyles.controlDisabled : {}),
        ...(required && !disabled ? fieldStyles.controlReq : {}),
        appearance: "none", paddingRight: 22, width: "100%",
        ...style,
      }}
    >
      <option value="" />
      {options.map((o) => {
        const v = typeof o === "string" ? o : o.value;
        const l = typeof o === "string" ? o : o.label;
        return <option key={v} value={v}>{l}</option>;
      })}
    </select>
    <div style={{ position: "absolute", right: 6, top: "50%", transform: "translateY(-50%)", color: "var(--text-faint)", pointerEvents: "none" }}>
      <Ic.chevronDown size={10} />
    </div>
  </div>
);

const Checkbox = ({ checked, onChange, disabled, label, size = 13 }) => (
  <label style={{ display: "inline-flex", alignItems: "center", gap: 4, cursor: disabled ? "not-allowed" : "pointer", userSelect: "none", color: "var(--text)", fontSize: 12 }}>
    <span
      onClick={disabled ? undefined : () => onChange && onChange(!checked)}
      style={{
        width: size, height: size, border: "1px solid var(--border-strong)",
        background: checked ? "var(--accent)" : "#fff",
        borderColor: checked ? "var(--accent)" : "var(--border-strong)",
        display: "inline-flex", alignItems: "center", justifyContent: "center",
        cursor: disabled ? "not-allowed" : "pointer", flex: "none",
        opacity: disabled ? 0.6 : 1,
      }}
    >
      {checked ? (
        <svg width={size - 4} height={size - 4} viewBox="0 0 10 10" fill="none">
          <path d="M2 5l2 2 4-4" stroke="#fff" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      ) : null}
    </span>
    {label != null ? <span>{label}</span> : null}
  </label>
);

// Top-bar style (light blue) toolbar button — used in module config screen
const ToolbarBtnLight = ({ children, icon, onClick, disabled, primary, danger, success }) => {
  const [hover, setHover] = React.useState(false);
  let bg = "#fff", border = "var(--border-input)", color = "var(--text)";
  if (primary) { bg = hover ? "#3a9ae8" : "#5cabe8"; border = "#3a8ad6"; color = "#fff"; }
  else if (danger) { bg = hover ? "#e85a5a" : "#f07070"; border = "#d04141"; color = "#fff"; }
  else if (success) { bg = hover ? "#38b777" : "#4cc488"; border = "#27a567"; color = "#fff"; }
  else if (hover && !disabled) { bg = "var(--accent-soft)"; border = "var(--accent)"; }
  return (
    <button
      onClick={disabled ? undefined : onClick}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      disabled={disabled}
      style={{
        height: "var(--tb-h)", padding: "0 10px",
        display: "inline-flex", alignItems: "center", gap: 4,
        background: disabled ? "var(--bg-disabled)" : bg,
        border: `1px solid ${disabled ? "var(--border)" : border}`,
        color: disabled ? "var(--text-faint)" : color,
        cursor: disabled ? "not-allowed" : "pointer",
        fontSize: 12, fontWeight: primary || danger || success ? 500 : 400,
        borderRadius: 2, whiteSpace: "nowrap",
      }}
    >
      {icon ? <span style={{ display: "inline-flex" }}>{icon}</span> : null}
      {children}
    </button>
  );
};

// Dark-band toolbar button (used in user detail, matches the dark sub-toolbar in references)
const ToolbarBtnDark = ({ children, icon, onClick, disabled, danger }) => {
  const [hover, setHover] = React.useState(false);
  return (
    <button
      onClick={disabled ? undefined : onClick}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      disabled={disabled}
      style={{
        height: 26, padding: "0 10px",
        display: "inline-flex", alignItems: "center", gap: 4,
        background: hover && !disabled ? "rgba(255,255,255,0.08)" : "transparent",
        border: "1px solid transparent",
        color: disabled ? "var(--text-on-dark-muted)" : danger && hover ? "#ff8b8b" : "var(--text-on-dark)",
        cursor: disabled ? "not-allowed" : "pointer",
        fontSize: 12, borderRadius: 2, whiteSpace: "nowrap",
      }}
    >
      {icon ? <span style={{ display: "inline-flex" }}>{icon}</span> : null}
      {children}
    </button>
  );
};

const Divider = ({ vertical }) =>
  vertical
    ? <span style={{ width: 1, height: 18, background: "rgba(255,255,255,0.12)", margin: "0 4px" }} />
    : <div style={{ height: 1, background: "var(--border)", margin: "4px 0" }} />;

Object.assign(window, {
  Field, Input, Select, Checkbox, ToolbarBtnLight, ToolbarBtnDark, Divider, fieldStyles,
});